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 wsltiring 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 wslwith 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 $commandsto the-CommandNameparameter forRegister-ArgumentCompleter.
 
- We map each command to the shell function that bash uses for autocompletion (bash uses $Fto define autocompletion specifications, short forcomplete -F <FUNCTION>).
 
- Convert PowerShell arguments $wordToComplete,$commandAstand$cursorPositionto 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 wslwith the command line, separate the output with line separators and generateCompletionResultsfor 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 !