A typical Windows developer question: “Why is it still not here
< LINUX>
?”. Whether it’s powerful scrolling
less
or the usual
grep
or
sed
tools, Windows developers want easy access to these commands in everyday work.
The Windows Subsystem for Linux (WSL) has taken a huge step forward in this regard. It allows you to call Linux commands from Windows, proxing them through
wsl.exe
(for example,
wsl ls
). Although this is a significant improvement, this option suffers from a number of disadvantages.
- The ubiquitous addition of
wsl
tiring and unnatural.
- Windows paths in arguments do not always work, because backslashes are interpreted as escape characters, not directory separators.
- Windows paths in arguments do not translate to the corresponding mount point in WSL.
- The default settings in WSL profiles with aliases and environment variables are not taken into account.
- Linux path completion is not supported.
- Command completion is not supported.
- Argument completion is not supported.
As a result, Linux commands are perceived under Windows as second-class citizens - and they are harder to use than native teams. To equalize their rights, you need to solve these problems.
PowerShell Shells
Using PowerShell function wrappers, we can add command completion and eliminate the need for
wsl
prefixes by translating Windows paths to WSL paths. Basic requirements for shells:
- Each Linux command must have one shell of the function with the same name.
- The shell must recognize the Windows paths passed as arguments and convert them to WSL paths.
- The shell should call
wsl
with the appropriate Linux command on any pipeline input and passing any command line arguments passed to the function.
Since this template can be applied to any command, we can abstract the definition of these shells and dynamically generate them from the list of commands to import.
# The commands to import. $commands = "awk", "emacs", "grep", "head", "less", "ls", "man", "sed", "seq", "ssh", "tail", "vim" # Register a function for each command. $commands | ForEach-Object { Invoke-Expression @" Remove-Alias $_ -Force -ErrorAction Ignore function global:$_() { for (`$i = 0; `$i -lt `$args.Count; `$i++) { # If a path is absolute with a qualifier (eg C:), run it through wslpath to map it to the appropriate mount point. if (Split-Path `$args[`$i] -IsAbsolute -ErrorAction Ignore) { `$args[`$i] = Format-WslArgument (wsl.exe wslpath (`$args[`$i] -replace "\\", "/")) # If a path is relative, the current working directory will be translated to an appropriate mount point, so just format it. } elseif (Test-Path `$args[`$i] -ErrorAction Ignore) { `$args[`$i] = Format-WslArgument (`$args[`$i] -replace "\\", "/") } } if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ (`$args -split ' ') } else { wsl.exe $_ (`$args -split ' ') } } "@ }
The
$command
list defines the commands to import. Then we dynamically generate a function wrapper for each of them using the
Invoke-Expression
command (first removing any aliases that will conflict with the function).
The function iterates over the command line arguments, determines the Windows paths using the
Split-Path
and
Test-Path
commands, and then converts these paths to WSL paths. We run the paths through the helper function
Format-WslArgument
, which we define later. It escapes special characters, such as spaces and brackets, which would otherwise be misinterpreted.
Finally, we
wsl
wsl the
wsl
input and any command line arguments.
Using these wrappers, you can call your favorite Linux commands in a more natural way without adding the
wsl
prefix and without worrying about how the paths are converted:
-
man bash
-
less -i $profile.CurrentUserAllHosts
-
ls -Al C:\Windows\ | less
-
grep -Ein error *.log
-
tail -f *.log
The basic command set is shown here, but you can create a shell for any Linux command by simply adding it to the list. If you add this code to your PowerShell
profile , these commands will be available to you in every PowerShell session, as will the native commands!
Default options
On Linux, it is customary to define aliases and / or environment variables in profiles (login profile), setting default parameters for frequently used commands (for example,
alias ls=ls -AFh
or
export LESS=-i
). One of the disadvantages of proxying through the non-interactive
wsl.exe
shell is that profiles are not loaded, therefore these options are not available by default (i.e.,
ls
in WSL and
wsl ls
will behave differently with the alias defined above).
PowerShell provides
$ PSDefaultParameterValues , a standard mechanism for defining default parameters, but only for cmdlets and advanced functions. Of course, you can make advanced functions from our shells, but this introduces unnecessary complications (for example, PowerShell maps partial parameter names (for example,
-a
-ArgumentList
to
-ArgumentList
), which will conflict with Linux commands that accept partial names as arguments), and the syntax for defining default values will not be the most suitable (for defining default arguments, the parameter name in the key is required, and not just the command name).
However, with a slight modification to our shells, we can implement a model similar to
$PSDefaultParameterValues
and enable default options for Linux commands!
function global:$_() { … `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "")[`$WslDefaultParameterValues.Disabled -eq `$true] if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ') } else { wsl.exe $_ `$defaultArgs (`$args -split ' ') } }
By
$WslDefaultParameterValues
to the command line, we send the parameters through
wsl.exe
. The following shows how to add instructions to a PowerShell profile to configure default settings. Now we can do it!
$WslDefaultParameterValues["grep"] = "-E" $WslDefaultParameterValues["less"] = "-i" $WslDefaultParameterValues["ls"] = "-AFh --group-directories-first"
Since parameters are modeled after
$PSDefaultParameterValues
, you can
easily turn them off temporarily by setting the
"Disabled"
key to
$true
. An additional advantage of a separate hash table is the ability to disable
$WslDefaultParameterValues
separately from
$PSDefaultParameterValues
.
Argument Completion
PowerShell allows registering argument terminators using the
Register-ArgumentCompleter
command. Bash has powerful
programmable completion tools . WSL allows you to call bash from PowerShell. If we can register the argument terminators for our PowerShell function wrappers and call bash to create the terminations, then we get the full completion of the arguments with the same precision as in bash itself!
# Register an ArgumentCompleter that shims bash's programmable completion. Register-ArgumentCompleter -CommandName $commands -ScriptBlock { param($wordToComplete, $commandAst, $cursorPosition) # Map the command to the appropriate bash completion function. $F = switch ($commandAst.CommandElements[0].Value) { {$_ -in "awk", "grep", "head", "less", "ls", "sed", "seq", "tail"} { "_longopt" break } "man" { "_man" break } "ssh" { "_ssh" break } Default { "_minimal" break } } # Populate bash programmable completion variables. $COMP_LINE = "`"$commandAst`"" $COMP_WORDS = "('$($commandAst.CommandElements.Extent.Text -join "' '")')" -replace "''", "'" for ($i = 1; $i -lt $commandAst.CommandElements.Count; $i++) { $extent = $commandAst.CommandElements[$i].Extent if ($cursorPosition -lt $extent.EndColumnNumber) { # The cursor is in the middle of a word to complete. $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text $COMP_CWORD = $i break } elseif ($cursorPosition -eq $extent.EndColumnNumber) { # The cursor is immediately after the current word. $previousWord = $extent.Text $COMP_CWORD = $i + 1 break } elseif ($cursorPosition -lt $extent.StartColumnNumber) { # The cursor is within whitespace between the previous and current words. $previousWord = $commandAst.CommandElements[$i - 1].Extent.Text $COMP_CWORD = $i break } elseif ($i -eq $commandAst.CommandElements.Count - 1 -and $cursorPosition -gt $extent.EndColumnNumber) { # The cursor is within whitespace at the end of the line. $previousWord = $extent.Text $COMP_CWORD = $i + 1 break } } # Repopulate bash programmable completion variables for scenarios like '/mnt/c/Program Files'/<TAB> where <TAB> should continue completing the quoted path. $currentExtent = $commandAst.CommandElements[$COMP_CWORD].Extent $previousExtent = $commandAst.CommandElements[$COMP_CWORD - 1].Extent if ($currentExtent.Text -like "/*" -and $currentExtent.StartColumnNumber -eq $previousExtent.EndColumnNumber) { $COMP_LINE = $COMP_LINE -replace "$($previousExtent.Text)$($currentExtent.Text)", $wordToComplete $COMP_WORDS = $COMP_WORDS -replace "$($previousExtent.Text) '$($currentExtent.Text)'", $wordToComplete $previousWord = $commandAst.CommandElements[$COMP_CWORD - 2].Extent.Text $COMP_CWORD -= 1 } # Build the command to pass to WSL. $command = $commandAst.CommandElements[0].Value $bashCompletion = ". /usr/share/bash-completion/bash_completion 2> /dev/null" $commandCompletion = ". /usr/share/bash-completion/completions/$command 2> /dev/null" $COMPINPUT = "COMP_LINE=$COMP_LINE; COMP_WORDS=$COMP_WORDS; COMP_CWORD=$COMP_CWORD; COMP_POINT=$cursorPosition" $COMPGEN = "bind `"set completion-ignore-case on`" 2> /dev/null; $F `"$command`" `"$wordToComplete`" `"$previousWord`" 2> /dev/null" $COMPREPLY = "IFS=`$'\n'; echo `"`${COMPREPLY[*]}`"" $commandLine = "$bashCompletion; $commandCompletion; $COMPINPUT; $COMPGEN; $COMPREPLY" -split ' ' # Invoke bash completion and return CompletionResults. $previousCompletionText = "" (wsl.exe $commandLine) -split '\n' | Sort-Object -Unique -CaseSensitive | ForEach-Object { if ($wordToComplete -match "(.*=).*") { $completionText = Format-WslArgument ($Matches[1] + $_) $true $listItemText = $_ } else { $completionText = Format-WslArgument $_ $true $listItemText = $completionText } if ($completionText -eq $previousCompletionText) { # Differentiate completions that differ only by case otherwise PowerShell will view them as duplicate. $listItemText += ' ' } $previousCompletionText = $completionText [System.Management.Automation.CompletionResult]::new($completionText, $listItemText, 'ParameterName', $completionText) } } # Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted. function global:Format-WslArgument([string]$arg, [bool]$interactive) { if ($interactive -and $arg.Contains(" ")) { return "'$arg'" } else { return ($arg -replace " ", "\ ") -replace "([()|])", ('\$1', '`$1')[$interactive] } }
The code is a little tight without understanding some of the bash internals, but basically we do the following:
- We register the argument finalizer for all our function wrappers by passing the list of
$commands
to the -CommandName
parameter for Register-ArgumentCompleter
.
- We map each command to the shell function that bash uses for autocompletion (bash uses
$F
to define autocompletion specifications, short for complete -F <FUNCTION>
).
- Convert PowerShell arguments
$wordToComplete
, $commandAst
and $cursorPosition
to the format expected by bash completion functions according to bash programmable completion specifications.
- We compose the command line for transferring to
wsl.exe
, which ensures the correct environment setting, calls the appropriate autocompletion function and displays the results with line breaks.
- Then we call
wsl
with the command line, separate the output with line separators and generate CompletionResults
for each, sorting them and escaping characters such as spaces and brackets that would otherwise be misinterpreted.
As a result, our Linux command shells will use exactly the same autocompletion as in bash! For example:
-
ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>
Each autocompletion supplies values specific to the previous argument by reading configuration data, such as known hosts, from WSL!
<TAB>
will cycle through the parameters.
<Ctrl + >
will show all available options.
Also, since bash autocomplete now works with us, you can autocomplete Linux paths directly in PowerShell!
-
less /etc/<TAB>
-
ls /usr/share/<TAB>
-
vim ~/.bash<TAB>
In cases where bash completion does not produce any results, PowerShell reverts to the default system with Windows paths. Thus, in practice, you can simultaneously use those and other ways at your discretion.
Conclusion
With PowerShell and WSL, we can integrate Linux commands into Windows as native applications. There is no need to look for Win32 builds or Linux utilities or interrupt the workflow by switching to the Linux shell. Just
install WSL , configure
your PowerShell profile, and
list the commands you want to import ! The rich autocompletion for command and path parameters for Linux and Windows files is a functionality that even today does not have in native Windows commands.
The full source code described above, as well as additional recommendations for including it in the workflow are available
here .
Which Linux commands do you find most useful? What other familiar things are missing when working on Windows? Write in the comments or
on GitHub !