shell=True を指定した subprocess.Popen() が Linux と Windows で動作が異なるのはなぜですか?
-
12-09-2019 - |
質問
使用するとき subprocess.Popen(args, shell=True)
走る "gcc --version
" (単なる例として)、Windows では次のようになります。
>>> from subprocess import Popen
>>> Popen(['gcc', '--version'], shell=True)
gcc (GCC) 3.4.5 (mingw-vista special r3) ...
したがって、期待どおりのバージョンがうまく出力されます。しかし、Linux では次のようになります。
>>> from subprocess import Popen
>>> Popen(['gcc', '--version'], shell=True)
gcc: no input files
gcc が受信していないため、 --version
オプション。
ドキュメントには、Windows で引数に何が起こるべきかが正確に指定されていませんが、Unix では次のように書かれています。 「args がシーケンスの場合、最初の項目はコマンド文字列を指定し、追加の項目は追加のシェル引数として扱われます。」 私の意見では、Windows の方法の方が優れています。 Popen(arglist)
と同じように呼び出します Popen(arglist, shell=True)
もの。
ここで Windows と Linux に違いがあるのはなぜでしょうか?
解決
実際、Windowsでは、それは使用します cmd.exe
いつ shell=True
- 先頭に追加されます cmd.exe /c
(実際には調べます COMSPEC
環境変数ですが、デフォルトでは cmd.exe
存在しない場合) をシェル引数に渡します。(Windows 95/98 では中間の w9xpopen
実際にコマンドを起動するプログラム)。
したがって、この奇妙な実装は実際には UNIX
1 つは次のことを行います (各スペースが異なる引数を区切ります)。
/bin/sh -c gcc --version
正しい実装 (少なくとも Linux では) は次のようになります。
/bin/sh -c "gcc --version" gcc --version
これにより、引用符で囲まれたパラメータからコマンド文字列が設定され、他のパラメータが正常に渡されるためです。
から sh
manページのセクション -c
:
Read commands from the command_string operand instead of from the standard input. Special parameter 0 will be set from the command_name operand and the positional parameters ($1, $2, etc.) set from the remaining argument operands.
このパッチは非常に簡単に問題を解決できるようです。
--- subprocess.py.orig 2009-04-19 04:43:42.000000000 +0200
+++ subprocess.py 2009-08-10 13:08:48.000000000 +0200
@@ -990,7 +990,7 @@
args = list(args)
if shell:
- args = ["/bin/sh", "-c"] + args
+ args = ["/bin/sh", "-c"] + [" ".join(args)] + args
if executable is None:
executable = args[0]
他のヒント
subprocess.py ソースから:
UNIX の場合、shell=True の場合:argsが文字列である場合、シェルを介して実行するコマンド文字列を指定します。ARGSがシーケンスである場合、最初の項目はコマンド文字列を指定し、追加のアイテムは追加のシェル引数として扱われます。
Windows の場合:Popenクラスは、CreateProcess()を使用して、文字列で動作する子プログラムを実行します。ARGSがシーケンスである場合、List2Cmdlineメソッドを使用して文字列に変換されます。すべてのMS Windowsアプリケーションがコマンドラインを同じ方法で解釈するわけではないことに注意してください。List2Cmdlineは、MS Cランタイムと同じルールを使用してアプリケーション用に設計されています。
これは理由の答えではなく、期待どおりの動作が見られることを明らかにするだけです。
おそらく「理由」は、UNIX のようなシステムでは、コマンド引数が実際にアプリケーションに渡されるためです ( exec*
呼び出しのファミリー) を文字列の配列として扱います。言い換えれば、呼び出しプロセスが各コマンド ライン引数に何を入れるかを決定します。一方、シェルを使用するように指示した場合、呼び出しプロセスは実際には、実行するシェルに 1 つのコマンド ライン引数を渡すことしかできません。実行するコマンド ライン全体、実行可能ファイル名および引数を 1 つの文字列として指定します。
しかし、Windows では、(上記のドキュメントによると) コマンド ライン全体が 1 つの文字列として子プロセスに渡されます。見てみると、 プロセスの作成 API ドキュメントを参照すると、すべてのコマンド ライン引数が 1 つの大きな文字列に連結されることが期待されていることがわかります (したがって、 list2cmdline
).
さらに、UNIX のようなシステムでは実際に は 便利な機能を備えたシェルなので、違いのもう 1 つの理由は Windows 上にあるのではないかと思われます。 shell=True
何も行わないため、ご覧のとおりに動作しています。2 つのシステムを同じように動作させる唯一の方法は、単にすべてのコマンド ライン引数を削除することです。 shell=True
Windows 上で。
UNIX の動作の理由 shell=True
引用するということです。シェル コマンドを作成するときはスペースで区切られるため、いくつかの引数を引用符で囲む必要があります。
cp "My File" "New Location"
これは私たちの議論の際に問題を引き起こします 含む 引用符、エスケープが必要:
grep -r "\"hello\"" .
時々私たちは得ることができます ひどい状況 どこ \
も逃げなければなりません!
もちろん、本当の問題は、私たちが使おうとしていることです。 1つ 指定する文字列 複数 文字列。システム コマンドを呼び出すとき、ほとんどのプログラミング言語では、最初に複数の文字列を送信できるようにすることでこれを回避します。
Popen(['cp', 'My File', 'New Location'])
Popen(['grep', '-r', '"hello"'])
場合によっては、「生の」シェル コマンドを実行すると便利な場合があります。たとえば、シェル スクリプトや Web サイトから何かをコピーアンドペーストするときに、ひどいエスケープをすべて手動で変換したくない場合です。だからこそ、 shell=True
オプションが存在します:
Popen(['cp "My File" "New Location"'], shell=True)
Popen(['grep -r "\"hello\"" .'], shell=True)
私は Windows に詳しくないので、Windows の動作がどのように、またなぜ異なるのかわかりません。