We integrate Linux commands in Windows using PowerShell and WSL

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.





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:





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:





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:





As a result, our Linux command shells will use exactly the same autocompletion as in bash! For example:





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!





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 !



All Articles