。
Mike
9月26日のことです。 2019年
Windows開発者によくある質問が、”なぜWindowsにはまだ<INSERT FAVORITE LINUX COMMAND HERE>
less
grep
sed
のようなおなじみのコマンドを使いたいと思っていても、Windows 開発者は、コア ワークフローの一部としてこれらのコマンドに簡単にアクセスできることを望んでいます。
Windows Subsystem for Linux (WSL) は、開発者が wsl.exe
wsl ls
) を通してプロキシすることにより、Windows から Linux コマンドを呼び出すことを可能にし、ここで大きな前進を遂げました。 大幅な改善ではありますが、このエクスペリエンスはいくつかの点で不足しています。
- コマンドを
wsl
でプレフィックスするのは面倒で不自然です - 引数として渡された Windows パスは、バックスラッシュがディレクトリの区切りではなくエスケープ文字として解釈されるため、しばしば解決されません
- 引数として渡された Windows パスは、WSL 内で適切な
- WSL のログイン プロファイルでエイリアスや環境変数を使って定義されたデフォルトのパラメータは尊重されません
- Linux のパスの補完はサポートされていません
- コマンドの補完はサポートされていません
- 引数の補完はサポートされていません
これらの欠点の結果、Linux のコマンドは Windows の二級市民のように感じられ、操作が困難になります。その結果、Linux コマンドは Windows の二級市民のように感じられ、必要以上に使いにくくなっています。
PowerShell 関数 ラッパー
コマンドに wsl
というプレフィックスを付ける必要をなくし、Windows パスから WSL パスへの変換を処理し、PowerShell 関数ラッパーでコマンド補完をサポートすることができます。 ラッパーの基本的な要件は以下の通りです。
- Linux コマンドごとに、コマンドと同じ名前の関数ラッパーが 1 つ必要です
- ラッパーは、引数として渡された Windows パスを認識し、WSL パスに変換します
- ラッパーは、対応する Linux コマンドで
wsl
を起動します。 パイプラインの入力をすべてパイプし、関数に渡されたすべてのコマンド ライン引数を渡します
このテンプレートは任意のコマンドに適用できるため、これらのラッパーの定義を抽象化し、インポートするコマンドのリストから動的に生成することができます。
# 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 Ignorefunction 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 -IsAbsolute -ErrorAction Ignore) { `$args = Format-WslArgument (wsl.exe wslpath (`$args -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 -ErrorAction Ignore) { `$args = Format-WslArgument (`$args -replace "\\", "/") } } if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ (`$args -split ' ') } else { wsl.exe $_ (`$args -split ' ') }}"@}
$command
リストには、インポートするコマンドを定義します。
この関数は、コマンド ライン引数をループし、Split-Path
Test-Path
コマンドを使用して Windows パスを識別し、それらのパスを WSL パスに変換します。 パスは、後で定義する Format-WslArgument
というヘルパー関数で実行され、スペースや括弧などの誤って解釈される可能性のある特殊文字をエスケープします。
これらの関数ラッパーを使用することで、お気に入りの Linux コマンドを、wsl
でプレフィックスを付けたり、Windows のパスが 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
wsl.exe
wsl.exe
)。
PowerShell では、デフォルトのパラメーター値を定義する標準的なメカニズムである $PSDefaultParameterValues
を提供していますが、これはコマンドレットと高度な関数に対してのみです。 私たちの関数ラッパーを高度な関数に変更することは可能ですが、複雑な問題が発生します (たとえば、PowerShell はパラメータの部分的な名前を照合しますが (たとえば -ArgumentList
に照合します)、これは部分的な名前を引数として受け入れる Linux コマンドと競合します)。また、デフォルト値を定義するための構文は、このシナリオでは理想的ではありません (デフォルトの引数を定義するためのキーに、コマンド名だけでなくパラメータ名を必要とします)。
関数ラッパーを少し変更するだけで、$PSDefaultParameterValues
に似たモデルを導入し、Linux コマンドのデフォルト パラメータを有効にすることができます。
function global:$_() { … `$defaultArgs = ((`$WslDefaultParameterValues.$_ -split ' '), "") if (`$input.MoveNext()) { `$input.Reset() `$input | wsl.exe $_ `$defaultArgs (`$args -split ' ') } else { wsl.exe $_ `$defaultArgs (`$args -split ' ') }}
$WslDefaultParameterValues
wsl.exe
を渡すことで、PowerShell プロファイルに以下のようなステートメントを追加して、デフォルト パラメーターを構成することができます。
$WslDefaultParameterValues = "-E"$WslDefaultParameterValues = "-i"$WslDefaultParameterValues = "-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.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.Extent if ($cursorPosition -lt $extent.EndColumnNumber) { # The cursor is in the middle of a word to complete. $previousWord = $commandAst.CommandElements.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.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.Extent $previousExtent = $commandAst.CommandElements.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.Extent.Text $COMP_CWORD -= 1 } # Build the command to pass to WSL. $command = $commandAst.CommandElements.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 + $_) $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 ::new($completionText, $listItemText, 'ParameterName', $completionText) }}# Helper function to escape characters in arguments passed to WSL that would otherwise be misinterpreted.function global:Format-WslArgument($arg, $interactive) { if ($interactive -and $arg.Contains(" ")) { return "'$arg'" } else { return ($arg -replace " ", "\ ") -replace "()", ('\$1', '`$1') }}
コードは、bash の内部構造を理解していないと少し難しいですが、基本的には次のとおりです。
-
$commands
-CommandName
Register-ArgumentCompleter
- 各コマンドを、bash が使用するシェル関数にマッピングします。 を、bashが補完するために使用するシェル関数(
$F
は、bashで補完仕様を定義するために使用されるcomplete -F <FUNCTION>
にちなんで命名されています)にマッピングします - PowerShellの
$wordToComplete
$commandAst
$cursorPosition
の引数を、bash のプログラム可能な補完仕様に従って、bash の補完関数で期待される形式に変換します - 補完環境が正しくセットアップされるように、
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 + Space>
は、利用可能なすべてのオプションを表示します。
さらに、bashの補完が担当するようになったので、PowerShell内でLinuxのパスを直接解決することができます。
ls /usr/share/<TAB>
vim ~/.bash<TAB>
bash 補完が結果を返さない場合、PowerShell はデフォルトに戻ります。 PowerShell は、Windows のパスを解決するデフォルトの補完機能にフォールバックし、Linux のパスと Windows のパスの両方を自由に解決できるようになります。
結論
PowerShell と WSL を使用すると、Linux コマンドをネイティブ アプリケーションのように Windows に統合することができます。 Linux ユーティリティの Win32 ビルドを探しまわったり、Linux シェルにドロップするためにワークフローを中断する必要はありません。 WSLをインストールし、PowerShellプロファイルを設定し、インポートしたいコマンドをリストアップするだけです。
上記の完全なソース コードと、ワークフローに組み込むための追加のガイダンスは、https://github.com/mikebattista/PowerShell-WSL-Interopで入手できます。
開発者のワークフローの他の部分で、Windows に欠けていると思うものは何ですか?
Mike Battista
シニア プログラム マネージャー。 Windows Developer Platform
フォローする