团队整合 Linux в Windows 使用 PowerShell 和 WSL

开发人员经常会问的问题 Windows为什么这里什么都没有? <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?。 无论是强力的滑动 less 或者熟悉的工具 grep или sed,开发者 Windows 希望在日常工作中能够轻松访问这些命令。

子系统 Windows 为 Linux (WSL) 在这方面已经取得了巨大的进步。它允许你调用命令。 Linux из Windows通过代理它们 wsl.exe (例如, wsl ls)。 尽管这是一个显着的改进,但该选项存在许多缺点。

  • 无处不在的添加 wsl 乏味且不自然。
  • 方式 Windows 在参数中使用反斜杠并不总是有效,因为反斜杠会被解释为转义字符,而不是目录分隔符。
  • 方式 Windows 参数中的内容不会转换为 WSL 中相应的挂载点。
  • 具有别名和环境变量的 WSL 配置文件不遵循默认设置。
  • 不支持路径自动补全 Linux.
  • 不支持命令完成。
  • 不支持参数完成。

由于团队 Linux 被认为是 Windows 他们就像二等公民——而且比他们的主队更难使用。为了保障他们的权利平等,这些问题必须得到解决。

PowerShell 函数包装器

使用 PowerShell 函数包装器,我们可以添加命令完成并消除对前缀的需要 wsl广播路径 Windows 关于 WSL 路径。shell 的基本要求:

  • 对于每个团队 Linux 必须存在一个同名的函数包装器。
  • shell 必须能够识别路径 Windows将其作为参数传递,并将其转换为 WSL 路径。
  • shell 应该调用 wsl 使用适当的命令 Linux 可以向任何管道输入传递任何传递给该函数的命令行参数。

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

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

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

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

最后,我们传达 wsl 管道输入和任何命令行参数。

借助这些包装器,您可以调用您喜欢的命令。 Linux 以更自然的方式,无需添加前缀 wsl 并且不用担心路径如何转换:

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

这里展示了一组基本命令,但你可以为任何命令创建包装器。 Linux只需将其添加到列表中即可。如果您将此代码添加到您的 轮廓 PowerShell,这些命令将在每个 PowerShell 会话中可供您使用,就像本机命令一样!

默认设置

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

PowerShell 提供 $PSDefaultParameterValues,一种用于定义默认参数的标准机制,但仅适用于 cmdlet 和高级函数。 当然,我们可以在 shell 中创建高级函数,但这会带来不必要的复杂性(例如,PowerShell 会关联部分参数名称(例如, -a 相关于 -ArgumentList这将与命令冲突 Linux(接受部分名称作为参数),并且定义默认值的语法不是最合适的(定义默认参数需要 switch 中的参数名称,而不仅仅是命令名称)。

但是,通过对 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 在参数中 -CommandNameRegister-ArgumentCompleter.
  • 我们将每个命令映射到 bash 用于自动完成的 shell 函数(为了定义自动完成规范,bash 使用 $F,缩写为 complete -F <FUNCTION>).
  • 转换 PowerShell 参数 $wordToComplete, $commandAst и $cursorPosition 根据规范转换为 bash 自动补全函数所期望的格式 可编程自动完成 猛击。
  • 我们编写一个命令行来传输到 wsl.exe,它确保环境设置正确,调用适当的自动完成函数,并以逐行方式输出结果。
  • 然后我们打电话 wsl 使用命令行,我们用行分隔符分隔输出并为每个生成 CompletionResults,对它们进行排序并转义空格和括号等可能会被误解的字符。

因此,我们的命令行 shell Linux 将使用与 bash 完全相同的自动补全功能!例如:

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

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

<TAB> 将循环浏览参数。 <Ctrl + пробел> 将显示所有可用选项。

另外,由于我们现在已经实现了 bash 自动补全功能,您也可以自动补全路径。 Linux 直接在 PowerShell 中运行!

  • 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上!

来源: habr.com

为具有 DDoS 保护、VPS VDS 服务器的站点购买可靠的主机 🔥 购买具备 DDoS 防护的可靠网站托管服务,包括 VPS 和 VDS 服务器 | ProHoster