使用 PowerShell 和 WSL 將 Linux 命令整合到 Windows 中

Windows 開發人員的典型問題是:「為什麼仍然沒有 <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?。 無論是強力的滑動 less 或熟悉的工具 grepsed,Windows 開發人員希望在日常工作中輕鬆存取這些命令。

適用於 Linux 的 Windows 子系統 (WSL) 在這方面取得了巨大的進步。 它允許您透過代理從 Windows 呼叫 Linux 命令 wsl.exe (例如, wsl ls)。 儘管這是一個顯著的改進,但該選項存在許多缺點。

  • 無所不在的添加 wsl 乏味且不自然。
  • 參數中的 Windows 路徑並不總是有效,因為反斜線被解釋為轉義字元而不是目錄分隔符號。
  • 參數中的 Windows 路徑不會轉換為 WSL 中對應的掛載點。
  • 具有別名和環境變數的 WSL 設定檔不遵循預設設定。
  • 不支援 Linux 路徑補全。
  • 不支援命令完成。
  • 不支援參數完成。

因此,Linux 命令在 Windows 下被視為二等公民,並且比本機命令更難使用。 要實現權利平等,就必須解決所列問題。

PowerShell 函數包裝器

使用 PowerShell 函數包裝器,我們可以添加命令完成並消除對前綴的需要 wsl,將 Windows 路徑轉換為 WSL 路徑。 外殼的基本要求:

  • 對於每一個 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 命令新增至清單即可為任何 Linux 命令建立 shell。 如果您將此程式碼新增至您的 輪廓 PowerShell,這些命令將在每個 PowerShell 會話中可供您使用,就像本機命令一樣!

預設設定

在 Linux 中,通常在登入設定檔中定義別名和/或環境變量,為常用命令設定預設參數(例如, alias ls=ls -AFhexport LESS=-i)。 透過非互動式 shell 進行代理的缺點之一 wsl.exe - 配置檔案未加載,因此預設情況下這些選項不可用(即 ls 在 WSL 和 wsl 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 在參數 -CommandNameRegister-ArgumentCompleter.
  • 我們將每個命令映射到 bash 用於自動完成的 shell 函數(為了定義自動完成規範,bash 使用 $F,縮寫為 complete -F <FUNCTION>).
  • 轉換 PowerShell 參數 $wordToComplete, $commandAst и $cursorPosition 根據規格轉換為 bash 自動補全函數所期望的格式 可程式自動完成 猛擊。
  • 我們寫一個命令列來傳輸到 wsl.exe,它確保環境設定正確,呼叫適當的自動完成函數,並以逐行方式輸出結果。
  • 然後我們打電話 wsl 使用命令行,我們用行分隔符號分隔輸出並為每個生成 CompletionResults,對它們進行排序並轉義空格和括號等可能會被誤解的字元。

因此,我們的 Linux 命令 shell 將使用與 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 上!

來源: www.habr.com

添加評論