ネイティブコマンドとの連携改善

PowerShellはWindowsのシステム向けシェルスクリプトであると同時に、管理用のインタラクティブシェルでもある。これまでのWindowsのネイティブコマンドもシームレスに使えるような工夫も織り込まれている。しかし、いくつかのコマンドとは相性が悪い。特に引数の処理で問題が出ることが多いのだ。

現在のWindows 11やWindows 10ではWSL2を使ってLinuxのコマンドを実行することができる。PowerShellはLinux系のシェルと似たような構文に対応しているため、PowerShellを使っている状態でWSL2経由でLinuxのコマンドを実行することができる。しかし、いくつかの点で使い物にならない問題がある。引数の処理が求めているものに対応していないのだ。

PowerShellでは入力されたコマンドラインは、専用のパーサによって分析された後で処理される。この処理がLinuxのコマンドが求めているものに対応していない部分があるのだ。具体的には、引数にダブルクォーテーションそのものを含ませることができず、そして、空という引数を指定することもできない。これはWindowsにデフォルト搭載されているネイティブコマンドや、Windowsでデフォルト使用できるスクリプトなどを使う場合にはあまり問題にならないが、Linuxコマンドや、Linux由来の移植されたネイティブコマンドなどを実行する場合に問題となる。

PowerShellが問題だったのは、この引数パース部分を自分で回避することがほとんど現実的な方法ではできなかったという点にある。工夫で回避できるならいくらでもやるのだが、結構深い部分の機能なので、それを回避してまでなんとか実現するとなると、かなりトリッキーな方法を使わなければならず、ほとんど現実的とは言えなかったのだ。

ネイティブコマンド向けの引数処理を改善

ここで注目されるのが、PowerShell 7.2に導入された「PSNativeCommandArgumentPassing」という機能だ。この機能を使うと引数の処理方法を切り替えることができる。まだ実験段階という位置付けなのだが、将来的には現在のWindows向けのパース処理と、Linuxネイティブコマンドとの連携を問題なく処理するパース処理の双方が統合されたような機能が有効になるはずだ。

PowerShell 7.2でこの機能を有効化するには、次のようにコマンドレットを実行して実験機能を有効化する必要がある。

PSNativeCommandArgumentPassing

Enable-ExperimentalFeature -Name PSNativeCommandArgumentPassing

なお、この機能は有効化した後にPowerShellを再起動しないと設定が反映されない点に注意が必要だ。

引数のパース処理方法は「$PSNativeCommandArgumentPassing」という変数で指定することになっている。執筆時点で指定できるのは"Legacy"、"Standard"、"Windows"のいずれかだ。将来的には"Windows"か"Standard"がデフォルトになるんじゃないかと思う。

まず、次の処理を見てみよう。"Legacy"はこれまでの、そして現在のPowerShellの処理だ。引数に指定されているダブルクォーテーションはダブルクォーテーションとしては保持されない。ダブルクォーテーションが抜かれた状態が分析結果となっている。

ダブルクォーテーションを含む引数の分析 - Legacy

PS C:\Users\daichi> $PSNativeCommandArgumentPassing = "Legacy"
PS C:\Users\daichi>
PS C:\Users\daichi> $v1 = 'a" "b'
PS C:\Users\daichi> testexe -echoargs $v1 'a " " b' a" "b
Arg 0 is <a b>
Arg 1 is <a >
Arg 2 is < b>
Arg 3 is <a b>
PS C:\Users\daichi>

今度は$PSNativeCommandArgumentPassingに"Standard"を設定して同じ処理を実行してみよう。次のような結果が得られる。

ダブルクォーテーションを含む引数の分析 - Standard

PS C:\Users\daichi> $PSNativeCommandArgumentPassing = "Standard"
PS C:\Users\daichi> 
PS C:\Users\daichi> $v1 = 'a" "b'
PS C:\Users\daichi> testexe -echoargs $v1 'a " " b' a" "b
Arg 0 is <a" "b>
Arg 1 is <a " " b>
Arg 2 is <a b>
PS C:\Users\daichi>

こちらはクォートされたダブルクォーテーションがそのままダブルクォーテーションとして保持されていることがわかる。この動作はLinuxのシェルでは一般的なものであり、Linuxコマンドでも必要となるものだ。この指定ができるようになったことは大きな進歩と言える。

次は空の引数を調べてみよう。これまでのPowerShellではコマンドに対して空の引数を与えるといったことはできない。次のように空の引数はなかったことになる。

空の引数は指定することができない - Legacy

PS C:\Users\daichi> $PSNativeCommandArgumentPassing = "Legacy"
PS C:\Users\daichi> testexe -echoargs '' a b ''
Arg 0 is <a>
Arg 1 is <b>
PS C:\Users\daichi>

これを$PSNativeCommandArgumentPassingに"Standard"を設定して実行してみよう。次のように空の引数が空の引数として処理されることを確認することができる。

空の引数を指定することができる - Standard

PS C:\Users\daichi> $PSNativeCommandArgumentPassing = "Standard"
PS C:\Users\daichi> testexe -echoargs '' a b ''
Arg 0 is <>
Arg 1 is <a>
Arg 2 is <b>
Arg 3 is <>
PS C:\Users\daichi>

これもとても大切な改善だ。Linuxコマンドでは引数に空引数を指定したいこともある。これができるようになると、今まで使えなかったコマンドを利用できるようになる。

なお、引数のパース処理に関してはTrace-Commandコマンドレットを使ってトレースすることができる。気になる場合にはトレースしてみよう。

分析パーストレース - Legacy

PS C:\Users\daichi> $PSNativeCommandArgumentPassing = "Legacy"
PS C:\Users\daichi>
PS C:\Users\daichi> $v1 = 'a" "b'
PS C:\Users\daichi> Trace-Command -PSHost -Name ParameterBinding { testexe -echoargs $v1 'a " " b' a" "b }
DEBUG: 2021-11-25 14:40:17.5534 ParameterBinding Information: 0 : BIND NAMED native application line args [C:\Users\daichi\Documents\PowerShell-src\test\tools\TestExe\bin\Debug\net6.0\testexe.exe]
DEBUG: 2021-11-25 14:40:17.5565 ParameterBinding Information: 0 :     BIND argument [-echoargs a" "b "a " " b" "a b"]
DEBUG: 2021-11-25 14:40:17.5616 ParameterBinding Information: 0 : CALLING BeginProcessing
Arg 0 is <a b>
Arg 1 is <a >
Arg 2 is < b>
Arg 3 is <a b>
PS C:\Users\daichi>

分析パーストレース - Standard

PS C:\Users\daichi> $PSNativeCommandArgumentPassing = "Standard"
PS C:\Users\daichi>
PS C:\Users\daichi> $v1 = 'a" "b'
PS C:\Users\daichi> Trace-Command -PSHost -Name ParameterBinding { testexe -echoargs $v1 'a " " b' a" "b }
DEBUG: 2021-11-25 14:40:53.3574 ParameterBinding Information: 0 : BIND NAMED native application line args [C:\Users\daichi\Documents\PowerShell-src\test\tools\TestExe\bin\Debug\net6.0\testexe.exe]
DEBUG: 2021-11-25 14:40:53.3576 ParameterBinding Information: 0 :     BIND cmd line arg [-echoargs] to position [0]
DEBUG: 2021-11-25 14:40:53.3577 ParameterBinding Information: 0 :     BIND cmd line arg [a" "b] to position [1]
DEBUG: 2021-11-25 14:40:53.3578 ParameterBinding Information: 0 :     BIND cmd line arg [a " " b] to position [2]
DEBUG: 2021-11-25 14:40:53.3581 ParameterBinding Information: 0 :     BIND cmd line arg [a b] to position [3]
DEBUG: 2021-11-25 14:40:53.3608 ParameterBinding Information: 0 : CALLING BeginProcessing
Arg 0 is <a" "b>
Arg 1 is <a " " b>
Arg 2 is <a b>
PS C:\Users\daichi>

ダブルクォーテーションのクォート処理が通るようになることと、空引数が指定できるようになるのはとても大きい。

なお、$PSNativeArgumentPassingが"Windows"に設定されていると、次のコマンドやスクリプトが実行されるときには引数解析処理が自動的にLegacyに切り替わるようになる。

  • cmd.exe
  • cscript.exe
  • wscript.exe
  • 拡張子.bat
  • 拡張子.cmd
  • 拡張子.js
  • 拡張子.vbs
  • 拡張子.wsf

互換性を維持しつつ新しい利便性にも対応する方法としては、「$PSNativeArgumentPassing = "Windows"」はなかなか合理的な判断のように思える。今後どの段階でデフォルトの機能になるかはわからないが、そのときが楽しみだ。現時点でこの機能が必要な場合には「PSNativeCommandArgumentPassing」を有効化して使ってみよう。

testexeツールのインストール方法

本稿で使っている「testexe」というコマンドは、PowerShellのソースコードに含まれている「test/tools/TestExe」というユーティリティだ。ソースコードとビルド用のプロジェクト設定は次にページに掲載されている。

ビルドして使う場合、「PowerShell/PowerShell」ごとクローンして、Visual Studio Codeでビルドするのが簡単だろう。

PowerShellのソースコードをクローン

git clone git@github.com:PowerShell/PowerShell.git

Visual Studio CodeでPowerShellのソースコードをクローンしたら、「test/tools/TestExeをVisual Studio Code」でオープンして関連する拡張機能と.NET SDKをインストールし、次のようにビルドを行えばよい。

  • test/tools/TestExeをビルド

    test/tools/TestExeをビルド

ビルドすると次のように「testexe.exe」という実行ファイルが生成される(test\tools\TestExe\bin\Debug\net6.0\testexe.exe)。

  • ビルドで生成されたtestexe.exe実行ファイル

    ビルドで生成されたtestexe.exe実行ファイル

これを利用すればよい。環境変数PATHに追加するのは面倒なので、「$PROFILE」に次のような感じでエイリアス設定を追加しておくと良いだろう。

エイリアスを設定してtestexe.exeを利用できるようにする

Set-Alias -Name testexe -Value "C:\Users\ユーザー名\クローンした場所\PowerShell\test\tools\TestExe\bin\Debug\net6.0\testexe.exe"

testext.exeはいろいろと便利なツールなので、もし使ったことがなければ一度使ってみてはいかがだろうか。