A common question from Windows developers is: "Why is there still no <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?». Будь то мощное пролистывание less or familiar tools grep or sed, Windows developers want easy access to these commands in their daily work.
has made significant strides in this regard. It allows calling Linux commands from Windows by proxying them through wsl.exe (for example, wsl ls). Although this is a significant improvement, this option suffers from several drawbacks.
- The widespread addition of
wslis tedious and unnatural. - Windows paths in the arguments do not always work because backslashes are interpreted as escape characters rather than directory separators.
- Windows paths in the arguments are not translated to the corresponding mount point in WSL.
- Default parameters in WSL profiles with aliases and environment variables are not considered.
- Linux path completion is not supported.
- Command completion is not supported.
- Argument completion is not supported.
As a result, Linux commands are perceived in Windows as second-class citizens — making them harder to use than native commands. To level the playing field, these issues need to be addressed.
PowerShell Function Shells
With PowerShell function shells, we can add command autocompletion and eliminate the need for prefixes wsl, translating Windows paths into WSL paths. The main requirements for shells are:
- For each Linux command, there should be one function shell with the same name.
- The shell must recognize Windows paths passed as arguments and convert them to WSL paths.
- The shell must invoke
wslwith the corresponding Linux command for any pipeline input and passing any command-line arguments given to the function.
Since this pattern can be applied to any command, we can abstract the definition of these shells and dynamically generate them from a list of commands for 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 (e.g. 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 ' ')
}
}
"@
} List $command defines the commands for import. Then we dynamically generate a function wrapper for each of them using the command Invoke-Expression (first removing any aliases that would conflict with the function).
The function iterates over the command-line arguments, identifying Windows paths using the commands Split-Path and Test-Path, and then transforms these paths into WSL paths. We run the paths through a helper function Format-WslArgument, which we will define later. It escapes special characters like spaces and parentheses that would otherwise be misinterpreted.
Finally, we pass wsl the pipeline input and any command-line arguments.
With such wrappers, you can invoke your favorite Linux commands in a more natural way, without adding a prefix wsl and without worrying about how paths are transformed:
man bashless -i $profile.CurrentUserAllHostsls -Al C:Windows | lessgrep -Ein error *.logtail -f *.log
Here is a basic set of commands, but you can create a wrapper for any Linux command simply by adding it to the list. If you add this code to your PowerShell, these commands will be available to you in every PowerShell session, just like native commands!
Default parameters
In Linux, it's common to define aliases and/or environment variables in profiles (login profiles), setting default parameters for frequently used commands (e.g., alias ls=ls -AFh or export LESS=-i). One downside of proxying through a non-interactive shell wsl.exe is that profiles do not load, so these default parameters are not available (i.e., ls in WSL and wsl ls will behave differently with the alias defined above).
PowerShell provides , a standard mechanism for defining default parameters, but only for cmdlets and advanced functions. Of course, we can make our shells advanced functions, but this adds unnecessary complexity (for example, PowerShell matches partial parameter names (e.g., -a matches with -ArgumentList), which will conflict with Linux commands that accept partial names as arguments), and the syntax for defining default values is not the most suitable (to define default arguments requires the parameter name in the key, not just the command name).
However, with a slight modification to our shells, we can implement a model similar to $PSDefaultParameterValues, and include default parameters 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 ' ')
}
} Passing $WslDefaultParameterValues in the command line, we send parameters through wsl.exe. Below is how to add instructions to the PowerShell profile to set default parameters. Now we can do it!
$WslDefaultParameterValues["grep"] = "-E"
$WslDefaultParameterValues["less"] = "-i"
$WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" Since the parameters are modeled after $PSDefaultParameterValues, you can temporarily by setting the key "Disabled" to $true. An additional benefit of a separate hash table is the ability to disable $WslDefaultParameterValues independently from $PSDefaultParameterValues.
Argument completion
PowerShell allows you to register argument completers using the command Register-ArgumentCompleter. Bash has powerful WSL allows you to call bash from PowerShell. If we can register argument completers for our PowerShell function shells and call bash to create completions, we will achieve complete argument completion with the same accuracy 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 bit dense without understanding some internal bash functions, but mostly we are doing the following:
- Registering an argument completer for all our function wrappers, passing the list
$commandsto the parameter-CommandNameforRegister-ArgumentCompleter. - Mapping each command to the shell function that bash uses for completion (to determine completion specifications in bash, the
$F, short forcomplete -F). - Transforming PowerShell arguments
$wordToComplete,$commandAstand$cursorPositioninto the format expected by bash completion functions according to the specifications of bash. - Assembling the command line to pass to
wsl.exe, which sets up the environment correctly, calls the appropriate completion function, and outputs results line by line. - Then we call
wslwith the command line, split the output by line breaks and generate CompletionResults for each, sorting them and escaping characters such as spaces and parentheses that would otherwise be misinterpreted.In the end, our Linux command shells will use exactly the same completion as in bash! For example:
ssh -c -J -m -O -o -Q -w -b
ssh -c -J -m -O -o -Q -w -b
Each auto-completion provides values specific to the previous argument by reading configuration data, such as known hosts, from WSL!
<TAB> will cyclically cycle through the parameters. <Ctrl + пробел> will show all available options.
Additionally, since we now have bash auto-completion working, you can auto-complete Linux paths directly in PowerShell!
less /etc/ls /usr/share/vim ~/ .bash
In cases where bash auto-completion does not yield any results, PowerShell falls back to the default system with Windows paths. Thus, you can practically use both paths at your discretion.
Conclusion
With PowerShell and WSL, we can integrate Linux commands into Windows as if they were native applications. There’s no need to search for Win32 builds or Linux utilities or interrupt your workflow by switching to the Linux shell. Just , configure and ! Rich auto-completion for command parameters and file paths in both Linux and Windows is functionality that is not even available in native Windows commands today.
The complete source code described above, along with additional recommendations for integrating it into your workflow, is available .
Which Linux commands do you find most useful? What other familiar tools do you miss while working in Windows? Leave your comments or !
Source: habr.com
