Skip to content

Instantly share code, notes, and snippets.

@hashchange
Last active August 11, 2022 17:51
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hashchange/a6e8caf919caeac47b2758a4b807bca9 to your computer and use it in GitHub Desktop.
Save hashchange/a6e8caf919caeac47b2758a4b807bca9 to your computer and use it in GitHub Desktop.
Safely converts a Windows path to a WSL (Linux) path. Does not suffer from the limitations of `wslpath -u`.
#!/usr/bin/env bash
# Script name
PROGNAME="$(basename "$BASH_SOURCE")"
if [[ "$1" == '--version' || "$1" == '-v' ]]; then
fmt -s <<- VERSION_TEXT
$PROGNAME 1.0.1
(c) 2022 Michael Heim
License: MIT
VERSION_TEXT
exit 0
elif [[ "$1" == '--help' || "$1" == '-h' ]]; then
fmt -s <<- HELP_TEXT
Safely converts a Windows path to a WSL (Linux) path. The Windows path can be provided in conventional format (e.g. C:\\Users\\foo), or in UNC format for locations within WSL (e.g. \\\\wsl$\\Ubuntu\\home\\foo). A trailing slash is removed by the conversion.
If a path argument is not provided, input is read from standard input instead (so the command can be used in a pipe).
With the -a option, a relative path is converted to an absolute path. A path which is already absolute is not affected.
Input
=====
Any valid path is converted. Such a path may point to a location in the Windows file system or to a location within the Linux file system of WSL (which would be provided as a UNC path, e.g. \\\\wsl$\\Ubuntu\\...).
A path in Linux format (e.g. /home/foo, /mnt/c/windows) is returned unchanged, except that a trailing slash is removed. The -a option expands a relative path, as expected.
The conversion is entirely string-based. The path does not have to exist. Specifically, the command also works for drives which are not mounted in WSL, unlike the built-in wslpath utility.
Mixed path separators
=====================
A path containing both forward slashes and backslashes is interpreted as a Windows path, unless it begins with a single forward slash (which only makes sense for a Linux path, as Windows paths never start with a single backslash).
The distinction between a Windows and Linux path usually doesn't matter, though. Absolute paths are unambiguous because of the way they begin. Any accidental backslashes in an absolute Linux path are corrected to forward slashes. Relative paths are unaffected, too. They point to the same location either way.
Usage:
$PROGNAME [options] path
... | $PROGNAME [options]
Options:
-a Convert a relative path to an absolute path.
-v, --version Show version and license.
-h, --help Show help.
Conversion examples:
D:\\Foo\\Bar Baz\\.quux\\file.txt => /mnt/d/Foo/Bar Baz/.quux/file.txt
D:\\Foo\\Bar Baz\\.quux\\ => /mnt/d/Foo/Bar Baz/.quux
D:\\Foo\\Bar Baz\\.quux => /mnt/d/Foo/Bar Baz/.quux
\\\\wsl$\\Ubuntu\\usr\\local\\bin\\ => /usr/local/bin
/usr/local/bin/ => /usr/local/bin
/usr/local/bin/command.sh => /usr/local/bin/command.sh
"~\\foo" => ~/foo
"~/foo" => ~/foo
~/foo => /home/[user]/foo (*)
bar => bar
.\\bar => ./bar
.\\bar\\ => ./bar
./bar/ => ./bar
(*) as a result of shell expansion
With the -a flag:
bar\\baz (assuming cwd /mnt/d/Foo) => /mnt/d/Foo/bar/baz
bar (assuming cwd /mnt/d/Foo) => /mnt/d/Foo/bar
bar (assuming cwd /home/m/foo) => /home/m/foo/bar
.\\bar (assuming cwd /home/m/foo) => /home/m/foo/bar
"~\\bar" (assuming cwd /mnt/d/Foo) => /mnt/d/Foo/~/bar
~/bar (assuming cwd /mnt/d/Foo) => /home/[user]/bar (*)
~\\bar (assuming cwd /mnt/d/Foo) => /mnt/d/Foo/~/bar (**)
"~/bar" (assuming cwd /mnt/d/Foo) => /mnt/d/Foo/~/bar (***)
/usr/local/bin/ => /usr/local/bin
(*) as a result of shell expansion
(**) ~ followed by \\, rather than /, is not expanded by the shell
(***) quotes prevent shell expansion of ~
$PROGNAME differs from the built-in wslpath utility in several respects: \`wslpath -u\`
- throws an error if the drive of the input path does not exist
- throws an error if the drive of the input path is not mounted in WSL
- does not recognize input in the form of an absolute Linux path.
It is treated as a subdirectory of the current working directory.
Limitations:
Input paths do not have to exist, but they are expected to be valid paths and do not pass an additional sanity check. Invalid paths may lead to unexpected output, rather than an error.
HELP_TEXT
exit 0
fi
fatal_error() { echo "$PROGNAME: $1" >&2; exit 1; }
# Checks if a path is absolute and in Windows format. Ie, it begins with
# [Drive letter]:\ or \\[UNC host name].
is_abs_path_in_windows_format() { [[ "$1" =~ ^[a-zA-Z]:(\\|$)|^\\\\[a-zA-Z] ]]; }
# Checks if a path is in Windows format. In ambiguous cases, the path is
# assumed to be in Windows format.
#
# - If the path does not contain any path separator, it is considered to be a
# Windows path.
# - If the path contains a backslash, it is treated as a Windows path (even if
# forward slashes are present, too).
# - But there is an exception: If the path begins with a single forward slash,
# it is considered to be a Linux path, even if backslashes are present
# (because a path beginning with a single directory separator does not make
# sense in Windows).
#
# NB These rules are somewhat different from the `is_in_windows_format()`
# function in `wsl-windows-path`.
is_in_windows_format() { [[ ! "$1" =~ [\\/] ]] || { [[ "$1" =~ \\ ]] && [[ ! "$1" =~ ^/[^/\\]+ ]]; }; }
# Checks if a WSL path points to a location in the Windows file system. Ie, it
# begins with /mnt/[drive letter]. Expects an absolute path. If necessary,
# resolve it with `realpath -m` first.
is_in_windows_filesystem() { [[ "$1" =~ ^/mnt/[a-zA-Z] ]]; }
# Option default values
to_absolute_path=false
while getopts ":a" option; do
case $option in
a)
to_absolute_path=true
;;
\?)
fatal_error "Option '-$OPTARG' is invalid."
;;
:)
fatal_error "The argument for option '-$OPTARG' is missing."
;;
esac
done
# After removing options from the arguments, get the input path from the
# remaining argument or, if there is none, from stdin (ie, from a pipe). See
# - https://stackoverflow.com/a/35512655/508355
# - https://stackoverflow.com/a/36432966/508355
# - https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion
#
# NB Multi-line input may come from stdin (or a redirected file). Therefore,
# multiple paths are handled from here on out, separated by newlines - one path
# per line.
shift $(($OPTIND - 1))
paths="${1:-$(</dev/stdin)}"
# Clean-up: Removing \r characters which may be left over from calls to Windows
# utilities.
paths="$(tr -d '\r' <<<"$paths")"
[ -z "$paths" ] && fatal_error "Missing argument. No path provided."
while IFS= read -r path; do
[ -z "$path" ] && fatal_error "Missing argument in input from pipe/stdin. No path provided."
if is_in_windows_format "$path"; then
if is_abs_path_in_windows_format "$path"; then
# - sed expression #1: if line begins with the UNC path prefix for a local Linux path
# (\\wsl$\[distro name]), remove it
# - sed expression #2: if line begins with a letter and colon (Windows drive), prepend
# it with /mnt/, convert the drive letter to lower case, and remove the colon.
# - sed expression #3: convert all backslashes to forward slashes
converted="$(sed -r -e 's_^\\\\wsl\$\\[^\\]+__' -e 's_^([a-zA-Z]):_/mnt/\L\1_' -e 's_\\_/_g' <<<"$path")" || exit $?
else
# Relative path in Windows format. Convert backslashes to forward slashes.
converted="$(tr '\\' '/' <<<"$path")" || exit $?
if $to_absolute_path; then
# If an absolute path has to be returned, resolve the path with realpath
converted="$(realpath -m "$converted")" || fatal_error "Error while processing the path \"${path//\\/\\\\}\"."
fi
fi
else
# Path in Linux format. Normalize accidental backslashes to forward
# slashes, otherwise input is left as-is (unless it should be converted
# to an absolute path).
converted="$(tr '\\' '/' <<<"$path")" || exit $?
if $to_absolute_path; then
converted="$(realpath -m "$converted")" || fatal_error "Error while processing the path \"${path//\\/\\\\}\"."
fi
fi
# Remove a trailing slash
converted="$(sed -r -e 's_(.)/$_\1_' <<<"$converted")" || exit $?
echo "$converted"
done <<<"$paths"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment