PowerShell と WSL を使用して Linux コマンドを Windows に統合する

Windows 開発者からの典型的な質問は次のとおりです。 <ВСТАВЬТЕ ТУТ ЛЮБИМУЮ КОМАНДУ LINUX>?。 強力なスワイプかどうか less または使い慣れたツール grep または sed, Windows 開発者は、日常の作業でこれらのコマンドに簡単にアクセスできることを望んでいます。

Linux 用 Windows サブシステム (WSL) この点で大きな前進を遂げました。 Linux コマンドをプロキシ経由で Wi​​ndows から呼び出すことができます。 wsl.exe (たとえば、 wsl ls)。 これは大幅な改善ですが、このオプションには多くの欠点があります。

  • ユビキタスな追加 wsl 退屈で不自然。
  • バックスラッシュはディレクトリ区切り文字ではなくエスケープ文字として解釈されるため、引数内の Windows パスは常に機能するとは限りません。
  • 引数内の Windows パスは、WSL の対応するマウント ポイントに変換されません。
  • デフォルト設定は、エイリアスと環境変数を含む WSL プロファイルでは尊重されません。
  • Linux のパス補完はサポートされていません。
  • コマンド補完はサポートされていません。
  • 引数の補完はサポートされていません。

その結果、Linux コマンドは Windows では二級市民のように扱われ、ネイティブ コマンドよりも使用が難しくなります。 彼らの権利を平等にするためには、列挙された問題を解決する必要があります。

PowerShell 関数ラッパー

PowerShell 関数ラッパーを使用すると、コマンド補完を追加し、プレフィックスの必要性を排除できます。 wsl、Windows パスを WSL パスに変換します。 シェルの基本要件:

  • Linux コマンドごとに、同じ名前の関数ラッパーが XNUMX つ必要です。
  • シェルは、引数として渡された Windows パスを認識し、それを WSL パスに変換する必要があります。
  • シェルは呼び出す必要があります 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)。 非対話型シェルを介したプロキシの欠点の XNUMX つは、 wsl.exe - プロファイルがロードされていないため、これらのオプションはデフォルトでは利用できません(つまり、 ls WSLと wsl ls 上記で定義したエイリアスでは動作が異なります)。

PowerShell が提供するもの $PSDefaultParameterValues、デフォルトのパラメーターを定義するための標準メカニズムですが、コマンドレットと高度な機能のみを対象としています。 もちろん、シェルから高度な関数を作成することもできますが、これにより不必要な複雑さが生じます (たとえば、PowerShell は部分的なパラメーター名を関連付けます (たとえば、 -a と相関する -ArgumentList)、部分的な名前を引数として受け取る Linux コマンドと競合します)、デフォルトを定義するための構文は最適ではありません(デフォルトの引数を定義するには、コマンド名だけでなくキー内のパラメータ名が必要です)。

ただし、シェルにわずかな変更を加えることで、次のようなモデルを実装できます。 $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。 バッシュには強力な機能があります プログラム可能なオートコンプリート ツール。 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 パラメータで -CommandName のために Register-ArgumentCompleter.
  • 各コマンドを bash がオートコンプリートに使用するシェル関数にマップします (オートコンプリートの仕様を定義するために、bash は $F、の略語 complete -F <FUNCTION>).
  • PowerShell の引数の変換 $wordToComplete, $commandAst и $cursorPosition 仕様に従って bash のオートコンプリート機能が期待する形式に変換します プログラム可能なオートコンプリート bash。
  • に転送するコマンドラインを作成します。 wsl.exeこれにより、環境が正しく設定されていることを確認し、適切なオートコンプリート関数を呼び出して、結果を XNUMX 行ずつ出力します。
  • それから電話します 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 シェルに移動してワークフローを中断したりする必要はありません。 ただ WSLをインストールする、 構成、設定 PowerShell プロファイル и インポートするコマンドをリストします! Linux および Windows のコマンド パラメータとファイル パスの豊富なオートコンプリートは、現在ネイティブの Windows コマンドでも利用できない機能です。

上記の完全なソース コードと、それをワークフローに組み込むための追加のガイドラインが利用可能です。 ここで.

どの Linux コマンドが最も便利だと思いますか? Windows で作業するときに他に欠けている一般的なものは何ですか? コメントに記入するか、 GitHubで!

出所: habr.com

コメントを追加します