2

I'm trying to write something for Bash on the Windows System for Linux that converts ~ to your Windows user directory when passed through a winpath function. So far, I'm able to retrieve the Windows directory, and convert it to a Unix path, and I'm also able to find the part after /home/[username]/ that gets output by ~. Where I'm running in to trouble is in concatenating these two.

I've got two variables, like so:

$target_path=/home/jacob/Repositories
$user_path=/mnt/c/Users/Jacob

(In reality I'm retrieving them programmatically, but I don't think that makes a difference.)

I then remove /home/jacob from $target_path, leaving it as just /Repositories.

The goal is to combine $user_path with the modified $target_path, so it outputs as:

/mnt/c/Users/Jacob/Repositories

To do this, I'm simply doing:

target_path=$user_path$target_path

But what's happening, for some reason, is it's outputting as:

/RepositoriesJacob

This doesn't make any sense, because when I output $user_path on it's own, it's correct, and when I output $target_path on it's own, it's also correct. So it's something in combining these two that's messing it up.

The relevant lines are 8-20 in this gist (full code pasted below).

winpath() {
    # get the Windows user path
    user_path=$(/mnt/c/Windows/System32/cmd.exe /C echo %HOMEDRIVE%%HOMEPATH%)

    # expand the specified path
    target_path=$(readlink -f $1)

    # change ~ to $user_path (WIP)
    if grep -q "^/home/" <<< $target_path; then
        # convert Windows-style user path to Unix-style (i.e. from C:\Users\[username] to /mnt/c/Users/[username])
        temp_user_path=$(echo "$user_path" | sed -e 's|\\|/|g' -e 's|^\([A-Za-z]\)\:/\(.*\)|/mnt/\L\1\E/\2|')
        # remove /home/[username]/ from $target_path
        target_path=$(echo "$target_path" | sed -e 's|^/home/\(.*\)/\(.*\)|/\2|')
        # output $temp_user_path for debugging
        echo $temp_user_path # correctly outputs
        # output $target_path for debugging
        echo $target_path # correctly outputs
        # combine the variables
        echo $temp_user_path$target_path # DOES NOT correctly output (?)
    fi

    # check if a Windows path is getting parsed
    if grep -q "^/mnt/[a-z]/" <<< $target_path; then
        # swap /mnt/[a-z]/ with [A-Z]:/ and / with \
        echo $(echo "$target_path" | sed -e 's|^\(/mnt/\([a-z]\)/\)\(.*\)|\U\2:\\\E\3|' -e 's|/|\\|g')
    else
        # return the user's home directory if a Unix path was parsed
        echo $user_path
    fi
}

EDIT: Okay, so this is weird... Trying this on a Mac, it works fine. Maybe it's some bug with WSL?

EDIT 2: After further testing, it looks like it's something to do with combining the two sed outputs. If I type out the strings as variables and try combining them, it works just fine. Hmm.

  • Maybe it's something to do with Windows line endings (carriage returns)? – steeldriver Mar 31 '17 at 14:37
  • @steeldriver Maybe! I just tried setting the user path as as string and combining it with $target_path, that worked just fine. Any suggestions on how to correct for that? – JacobTheDev Mar 31 '17 at 14:41
  • Got it :) Posting an answer. – JacobTheDev Mar 31 '17 at 15:01
2

Yeah, the extra carriage return is your problem.

Process substitution removes the final newline (line feed) from the output of the process, but in case cmd.exe outputs a CR-LF pair, the carriage return stays at the end of user_path. When printed, the CR causes the output to go back to the beginning of the line when printed. Repositories is the same length as /mnt/c/Users so the following slash aligns in a somewhat logical place (instead if looking like any word was broken).

You could remove a trailing CR inside Bash with ${user_path%$'\r'}. (${var%pattern} removes a suffix matching the pattern from the variable)

Also, I think your second sed ('s|^/home/\(.*\)/\(.*\)|/\2|') is a bit too zealous, the first .* matches the longest string it can, so /home/user/foo/bar would turn into just /bar instead of /foo/bar. You could do that with the parameter expansions too: ${target_path#/home/*/}. With one # it removes the shortest matching prefix.

  • Thanks, I'll look in to modifying my code with some of your suggestions :) – JacobTheDev Apr 3 '17 at 16:56
  • Adjusted my sed as recommend, works great! – JacobTheDev Apr 6 '17 at 17:52
1

Thanks to the help of @steeldriver, I was able to figure this out! Strangely, it was an issue with Windows line endings, even though there was only one line being output by CMD. The solution was to convert the CMD output to Unix style, and that solved the issue! Here's my final code:

winpath() {
    # get the Windows user path, convert to Unix line endings
    user_path=$(echo "$(/mnt/c/Windows/System32/cmd.exe /C echo %HOMEDRIVE%%HOMEPATH%)" | tr -d "\r")

    # expand the specified path
    target_path=$(readlink -f $1)

    # change ~ to $user_path
    if grep -q "^/home/" <<< $target_path; then
        temp_user_path=$(echo "$user_path" | sed -e 's|\\|/|g' -e 's|^\([A-Za-z]\)\:/\(.*\)|/mnt/\L\1\E/\2|' -e 's|^M$||')

        # if there was something after ~, add it to the end of the $user_path
        if grep -q "^/home/\(.*\)/\(.*\)" <<< $target_path; then
            target_path=$temp_user_path$(echo "$target_path" | sed -e 's|^/home/*/\(.*\)|/\2|')
        # if there was nothing after ~, $target_path is $user_path
        else
            target_path=$temp_user_path
        fi
    fi

    # check if a Windows path is getting parsed
    if grep -q "^/mnt/[a-z]" <<< $target_path; then
        # swap /mnt/[a-z] with [A-Z]: and / with \
        echo $(echo "$target_path" | sed -e 's|^\(/mnt/\([a-z]\)\)\(.*\)|\U\2:\E\3|' -e 's|/|\\|g')
    else
        # return the user's home directory if a Unix path was parsed
        echo $user_path
    fi
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.