我们使用PowerShell和WSL在Windows中集成Linux命令

Windows开发人员经常遇到的一个问题:“为什么在这里< LINUX>仍然没有?”。 无论是功能强大的less滚动功能还是常用的grepsed工具,Windows开发人员都希望在日常工作中轻松访问这些命令。

在这方面,Windows Linux子系统(WSL)向前迈出了一大步。 它允许您从Windows调用Linux命令,并通过wsl.exe (例如wsl ls )对它们进行wsl.exe 。 尽管这是一个重大的改进,但是此选项有许多缺点。

  • wsl的普遍添加很累,而且不自然。
  • 参数中的Windows路径并非始终有效,因为反斜杠被解释为转义符,而不是目录分隔符。
  • 参数中的Windows路径不会转换为WSL中的相应安装点。
  • WSL概要文件中带有别名和环境变量的默认设置未考虑在内。
  • 不支持Linux路径完成。
  • 不支持命令完成。
  • 不支持参数完成。

结果,Linux命令在Windows下被视为二等公民-与本地团队相比,它们更难使用。 为了使他们的权利平等,您需要解决这些问题。

PowerShell外壳


使用PowerShell函数包装器,我们可以通过将Windows路径转换为WSL路径来添加命令完成功能并消除对wsl前缀的需要。 外壳的基本要求:

  • 每个Linux命令必须具有一个具有相同名称的函数外壳。
  • 外壳程序必须识别作为参数传递的Windows路径,并将其转换为WSL路径。
  • 外壳程序应使用适当的Linux命令对任何管道输入调用wsl ,并将传递给函数的任何命令行参数传递给该函数。

由于此模板可以应用于任何命令,因此我们可以抽象化这些shell的定义,并从要导入的命令列表中动态生成它们。

 # 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 ' ') } } "@ } 

$command列表定义要导入的命令。 然后,我们使用Invoke-Expression命令为它们中的每一个动态生成一个函数包装器(首先删除将与该函数冲突的任何别名)。

该函数遍历命令行参数,使用Split-PathTest-Path命令确定Windows路径,然后将这些路径转换为WSL路径。 我们通过帮助函数Format-WslArgument来运行路径,稍后将对其进行定义。 它转义了特殊字符,例如空格和方括号,否则会被误解。

最后,我们将wsl输入和任何命令行参数传递给wsl

使用这些包装器,您可以更自然的方式调用自己喜欢的Linux命令,而无需添加wsl前缀,也不必担心路径如何转换:

  • man bash
  • less -i $profile.CurrentUserAllHosts
  • ls -Al C:\Windows\ | less
  • grep -Ein error *.log
  • tail -f *.log

基本的命令集显示在这里,但是您可以通过将任何Linux命令添加到列表中来为任何Linux命令创建一个shell。 如果将此代码添加到PowerShell 配置文件中 ,这些命令将在每个PowerShell会话中对您可用,本机命令也将可用!

默认选项


在Linux上,习惯上在配置文件(登录配置文件)中定义别名和/或环境变量,为常用命令设置默认参数(例如, alias ls=ls -AFhexport LESS=-i )。 通过非交互式wsl.exe shell进行代理的缺点之一是未加载配置文件,因此默认情况下这些选项不可用(即,WSL中的wsl lswsl ls行为与上面定义的别名不同)。

PowerShell提供了$ PSDefaultParameterValues ,这是用于定义默认参数的标准机制,但仅适用于cmdlet和高级功能。 当然,您可以从我们的Shell中执行高级功能,但这会带来不必要的复杂性(例如,PowerShell匹配部分参数名称(例如, -a对应于-ArgumentList ),这将与接受部分名称作为参数的Linux命令冲突),并且定义默认值的语法将不是最合适的(对于定义默认参数,键中的参数名称是必需的,而不仅仅是命令名称)。

但是,只要对我们的shell稍加修改,我们就可以实现类似于$PSDefaultParameterValues的模型,并为Linux命令启用默认选项!

 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 ' ') } } 

通过$WslDefaultParameterValues到命令行,我们通过wsl.exe发送参数。 下面显示了如何向PowerShell配置文件添加指令以配置默认设置。 现在我们可以做到!

 $WslDefaultParameterValues["grep"] = "-E" $WslDefaultParameterValues["less"] = "-i" $WslDefaultParameterValues["ls"] = "-AFh --group-directories-first" 

由于参数是在$PSDefaultParameterValues之后建模的,因此可以通过将"Disabled"键设置为$true 轻松地暂时将其关闭 。 单独的哈希表的另一个优点是能够与$WslDefaultParameterValues分别禁用$PSDefaultParameterValues

参数完成


PowerShell允许使用Register-ArgumentCompleter命令注册参数终止符。 Bash具有功能强大的可编程完成工具 。 WSL允许您从PowerShell调用bash。 如果我们可以为PowerShell函数包装器注册参数终止符并调用bash来创建终止符,那么我们将以与bash本身相同的精度获得参数的完整完成!

 # 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] } } 

在不了解某些bash内部原理的情况下,代码有些紧,但是基本上,我们执行以下操作:

  • 通过将$commands列表传递给Register-ArgumentCompleter-CommandName参数,我们为所有函数包装器注册参数终结器。
  • 我们将每个命令映射到bash用于自动完成的shell函数(bash使用$F定义自动完成规范,是complete -F <FUNCTION>缩写)。
  • 根据bash 可编程完成规范,将PowerShell参数$wordToComplete$commandAst$cursorPosition转换为bash完成函数期望的格式。
  • 我们wsl.exe用于传输到wsl.exe的命令行,以确保正确的环境设置,调用适当的自动完成功能并显示带有换行符的结果。
  • 然后,我们使用命令行调用wsl ,用行分隔符分隔输出,并为每个分隔符生成CompletionResults ,对它们进行排序,并转义诸如空格和方括号之类的字符,否则这些字符将被误解。

结果,我们的Linux命令外壳将使用与bash中完全相同的自动完成功能! 例如:

  • ssh -c <TAB> -J <TAB> -m <TAB> -O <TAB> -o <TAB> -Q <TAB> -w <TAB> -b <TAB>

每个自动补全都提供特定于前一个参数的值,从WSL中读取配置数据,例如已知主机!

<TAB>将循环显示参数。 <Ctrl + >将显示所有可用选项。

另外,由于bash自动完成现在可以与我们一起使用,因此您可以直接在PowerShell中自动完成Linux路径!

  • less /etc/<TAB>
  • ls /usr/share/<TAB>
  • vim ~/.bash<TAB>

如果bash自动补全失败,PowerShell将恢复为具有Windows路径的默认系统。 因此,实际上,您可以根据自己的判断同时使用这些方式和其他方式。

结论


使用PowerShell和WSL,我们可以将Linux命令作为本机应用程序集成到Windows中。 无需寻找Win32构建或Linux实用程序,也无需通过切换到Linux Shell来中断工作流程。 只需安装WSL ,配置PowerShell配置文件列出要导入的命令 ! Linux和Windows文件的命令和路径参数丰富的自动补全功能是当今本机Windows命令甚至都没有的功能。

此处提供上述完整源代码以及将其包括在工作流程中的其他建议。

您发现哪个Linux命令最有用? 在Windows上工作时,还缺少哪些其他熟悉的东西? 写评论或在GitHub上

Source: https://habr.com/ru/post/zh-CN469257/


All Articles