PythonでBashコマンドを実行する 質問する

PythonでBashコマンドを実行する 質問する

ローカルマシンで、次の行を含むPythonスクリプトを実行します。

bashCommand = "cwm --rdf test.rdf --ntriples > test.nt"
os.system(bashCommand)

これは問題なく動作します。

次に、同じコードをサーバー上で実行すると、次のエラーメッセージが表示されます。

'import site' failed; use -v for traceback
Traceback (most recent call last):
File "/usr/bin/cwm", line 48, in <module>
from swap import  diag
ImportError: No module named swap

そこで私がやったことは、 を挿入して、print bashCommandを実行する前にターミナルにコマンドを出力することでしたos.system()

もちろん、再びエラーが発生します (原因はos.system(bashCommand)) が、そのエラーが発生する前に、ターミナルにコマンドが出力されます。次に、その出力をコピーしてターミナルにコピーして貼り付け、Enter キーを押すと、正常に動作します...

何が起こっているのか誰か分かりますか?

ベストアンサー1

ここでの以前の回答を少し詳しく説明すると、一般的に見落とされがちな詳細がいくつかあります。

  • より好み、subprocess.run()よりsubprocess.check_call()友達よりsubprocess.call()よりよりsubprocess.Popen()よりos.system()よりos.popen()
  • を理解し、おそらく使用しtext=Trueますuniversal_newlines=True
  • shell=Trueorの意味shell=Falseと、それが引用符をどのように変更するか、およびシェルの便利な機能の可用性について理解します。
  • shとBashの違いを理解する
  • サブプロセスが親プロセスから分離され、通常は親プロセスを変更できないことを理解します。
  • Python インタープリターを Python のサブプロセスとして実行することは避けてください。

これらのトピックについては、以下でさらに詳しく説明します。

好むsubprocess.run()subprocess.check_call()

このsubprocess.Popen()関数は低レベルの便利な関数ですが、正しく使用するのは難しいため、複数行のコードをコピー/貼り付けすることになります。便利なことに、これらのコードは、さまざまな目的のための高レベルのラッパー関数のセットとして標準ライブラリに既に存在しており、以下で詳しく説明します。

以下は、ドキュメンテーション:

サブプロセスを呼び出すための推奨されるアプローチは、run()処理可能なすべてのユースケースに対して関数を使用することです。より高度なユースケースの場合は、基礎となるPopenインターフェースを直接使用できます。

残念ながら、これらのラッパー関数の可用性は Python のバージョンによって異なります。

  • subprocess.run()Python 3.5 で正式に導入されました。これは、以下のすべてを置き換えることを目的としています。
  • subprocess.check_output()Python 2.7 / 3.1で導入されました。基本的にはsubprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
  • subprocess.check_call()Python 2.5で導入されました。基本的にはsubprocess.run(..., check=True)
  • subprocess.call()Python 2.4で元のモジュールsubprocessペップ324)。基本的にはsubprocess.run(...).returncode

高レベルAPIとsubprocess.Popen()

リファクタリングされ拡張された関数は、subprocess.run()置き換えられる古いレガシー関数よりも論理的で多用途です。CompletedProcess終了ステータス、標準出力、および終了したサブプロセスからその他のいくつかの結果とステータス インジケーターを取得できるさまざまなメソッドを持つオブジェクト。

subprocess.run()単にプログラムを実行してPythonに制御を返すだけであれば、これが最適な方法です。より複雑なシナリオ(バックグラウンドプロセス、Pythonの親プログラムとの対話型I/Oなど)では、subprocess.Popen()すべての配管を自分で使用して管理する必要があります。これには、すべての可動部分についてかなり複雑な理解が必要であり、軽々しく取り組むべきではありません。より単純なPopen物体サブプロセスの残りの存続期間中、コードから管理する必要がある (まだ実行中の可能性がある) プロセスを表します。

おそらく、subprocess.Popen()単にプロセスを作成するだけであることを強調しておく必要があります。そのままにしておくと、Python と並行してサブプロセスが実行されることになります。つまり、「バックグラウンド」プロセスです。入力や出力を行う必要がなく、その他の調整も必要ない場合は、Python プログラムと並行して有用な作業を行うことができます。

避けos.system()os.popen()

永遠の昔から(正確にはPython 2.5以来)osモジュールドキュメントsubprocessには、 を優先する推奨事項が含まれていますos.system():

このsubprocessモジュールは、新しいプロセスを生成してその結果を取得するためのより強力な機能を提供します。この関数を使用するよりも、そのモジュールを使用することをお勧めします。

問題はsystem()、明らかにシステムに依存しており、サブプロセスと対話する方法が提供されていないことです。 単に実行され、標準出力と標準エラーは Python の範囲外です。 Python が受け取る唯一の情報は、コマンドの終了ステータスです (ゼロは成功を意味しますが、ゼロ以外の値の意味も多少システムに依存します)。

ペップ324(すでに上で述べたように) には、なぜos.system問題なのか、そしてsubprocessそれらの問題をどのように解決しようとしているのかという、より詳細な根拠が含まれています。

os.popen()かつてはさらに強くお勧めしません:

バージョン 2.6 以降では非推奨:この関数は廃止されました。subprocessモジュールを使用してください。

ただし、Python 3 ではいつからか、 を単純に使用するように再実装されsubprocesssubprocess.Popen()詳細についてはドキュメントにリダイレクトされるようになりました。

理解し、普段から使うcheck=True

subprocess.call()また、には と同じ制限が多数あることにも気づくでしょう。通常の使用では、一般的に、とos.system()を実行してプロセスが正常に終了したかどうかを確認する必要があります(後者は、終了したサブプロセスの標準出力も返します)。同様に、サブプロセスがエラー ステータスを返すことを明示的に許可する必要がある場合を除き、通常は をとともに使用する必要があります。subprocess.check_call()subprocess.check_output()check=Truesubprocess.run()

実際には、check=Trueまたはを使用するとsubprocess.check_*、PythonはCalledProcessError例外サブプロセスがゼロ以外の終了ステータスを返す場合。

よくあるエラーは、サブプロセスが失敗した場合に下流のコードが失敗して驚いてしまうことsubprocess.run()です。check=True

一方、 と の一般的な問題は、check_call()これらの関数を盲目的に使用したユーザーが、一致が見つからなかったcheck_output()ときなど、例外が発生したときに驚くことでした。(いずれにしても、以下に概説するように、ネイティブ Python コードに置き換える必要があります。)grepgrep

結局のところ、シェル コマンドがどのように終了コードを返すか、どのような条件下でゼロ以外の (エラー) 終了コードを返すかを理解し、それをどのように処理するかを意識的に決定する必要があります。

text=Trueakaを理解し、おそらく使用するuniversal_newlines=True

Python 3 以降、Python 内部の文字列は Unicode 文字列です。ただし、サブプロセスが Unicode 出力または文字列を生成するという保証はありません。

(違いがすぐには分からない場合は、ネッド・バッチェルダーの実用的なUnicode読むことが推奨されます (義務ではないにしても)。リンクの後ろに 36 分間のビデオ プレゼンテーションがありますので、そちらをご利用ください。ただし、ページを自分で読む方がおそらく大幅に短い時間で済みます。

Python は、根本的に、bytesバッファを取得して何らかの方法で解釈する必要があります。バイナリ データの塊が含まれている場合、 Unicode 文字列にデコードしないでください。これは、エラーが発生しやすく、バグを誘発する動作であるためです。これは、エンコードされたテキストとバイナリ データを適切に区別する方法が存在する前に、多くの Python 2 スクリプトを悩ませていた厄介な動作とまったく同じです。

を使用するとtext=True、実際にはシステムのデフォルト エンコーディングでテキスト データが返されることを期待しており、Python の能力の範囲内で Python (Unicode) 文字列にデコードする必要があることを Python に伝えます (通常、Windows を除く、比較的新しいシステムでは UTF-8 です)。

それが要求したものでない場合、Python は文字列と文字列bytesに文字列を返すだけです。おそらく、後になって、それが結局はテキスト文字列であり、そのエンコーディングがわかっていることが分かるでしょう。その後、それらをデコードできます。stdoutstderr

normal = subprocess.run([external, arg],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    check=True,
    text=True)
print(normal.stdout)

convoluted = subprocess.run([external, arg],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    check=True)
# You have to know (or guess) the encoding
print(convoluted.stdout.decode('utf-8'))

textPython 3.7では、以前は多少誤解を招くような名前だったキーワード引数の、より短く、より説明的でわかりやすいエイリアスが導入されましたuniversal_newlines

理解するshell=Truevsshell=False

shell=Trueシェルに単一の文字列を渡すと、シェルがそれを処理します。

シェルをバイパスしてshell=False、引数のリストを OS に渡します。

シェルがない場合、プロセスを保存してかなり多くの隠れた複雑さがあり、バグやセキュリティ上の問題が潜んでいる可能性もあります。

一方、シェルがない場合、リダイレクト、ワイルドカード拡張、ジョブ制御、その他多数のシェル機能が利用できません。

よくある間違いはshell=True、 を使用した後も Python にトークンのリストを渡したり、その逆を行ったりすることです。これは場合によってはうまく機能しますが、実際には定義が不十分で、興味深い方法で機能しなくなる可能性があります。

# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short stackoverflow.com')

# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'stackoverflow.com'],
    shell=True)

# XXX DEFINITELY AVOID THIS
pathological = subprocess.run(['dig +short stackoverflow.com'],
    shell=True)

correct = subprocess.run(['dig', '+short', 'stackoverflow.com'],
    # Probably don't forget these, too
    check=True, text=True)

# XXX Probably better avoid shell=True
# but this is nominally correct
fixed_but_fugly = subprocess.run('dig +short stackoverflow.com',
    shell=True,
    # Probably don't forget these, too
    check=True, text=True)

「でも、私の場合はうまくいきます」というよくある反論は、どのような状況でうまくいかなくなるかを正確に理解していない限り、有効な反論にはなりません。

簡単にまとめると、正しい使い方は次のようになります

subprocess.run("string for 'the shell' to parse", shell=True)
# or
subprocess.run(["list", "of", "tokenized strings"]) # shell=False

シェルを避けたいが、文字列をトークンのリストに解析する方法が面倒だったり、よくわからない場合は、shlex.split()が代わりにこれを実行できることに留意してください。

subprocess.run(shlex.split("no string for 'the shell' to parse"))  # shell=False
# equivalent to
# subprocess.run(["no", "string", "for", "the shell", "to", "parse"])

通常の方法split()は引用符を保持しないため、ここでは機能しません。上記の例では、が"the shell"単一の文字列であることに注意してください。

リファクタリングの例

多くの場合、シェルの機能はネイティブの Python コードに置き換えることができます。単純な Awk またはsedスクリプトは、おそらく Python に翻訳するだけでしょう。

これを部分的に説明するために、多くのシェルの機能を含む典型的だが少しばかげた例を示します。

cmd = '''while read -r x;
   do ping -c 3 "$x" | grep 'min/avg/max'
   done <hosts.txt'''

# Trivial but horrible
results = subprocess.run(
    cmd, shell=True, universal_newlines=True, check=True)
print(results.stdout)

# Reimplement with shell=False
with open('hosts.txt') as hosts:
    for host in hosts:
        host = host.rstrip('\n')  # drop newline
        ping = subprocess.run(
             ['ping', '-c', '3', host],
             text=True,
             stdout=subprocess.PIPE,
             check=True)
        for line in ping.stdout.split('\n'):
             if 'min/avg/max' in line:
                 print('{}: {}'.format(host, line))

ここで注意すべき点がいくつかあります:

  • を使用するとshell=False、シェルが文字列を囲むのに必要な引用符は必要ありません。とにかく引用符を付けると、おそらくエラーになります。
  • 多くの場合、サブプロセスで実行するコードはできるだけ少なくするのが理にかなっています。これにより、Python コード内からの実行をより細かく制御できるようになります。
  • そうは言っても、複雑なシェル パイプラインを Python で再実装するのは面倒で、難しい場合もあります。

リファクタリングされたコードは、シェルが非常に簡潔な構文で、良くも悪くも実際にどれだけ多くのことをしてくれるかを示しています。Python では、明示的の方が暗黙的よりも優れていると言われていますが、Python コードはかなり冗長、実際よりも複雑に見えます。一方、シェル コマンド出力にホスト名を簡単に含めることができるという機能強化によって簡単に例示されるように、他の作業の途中で制御を奪取できるポイントが数多くあります。(シェルでこれを行うのも決して難しいことではありませんが、別の逸脱とおそらく別のプロセスが必要になります。)

一般的なシェル構成

完全を期すために、これらのシェルの機能のいくつかについて簡単に説明し、それらをネイティブの Python 機能で置き換える方法についていくつか説明します。

  • glob.glob()グロブ、つまりワイルドカード拡張は、 または のような単純な Python 文字列比較で置き換えることができます。Bash には、中括弧拡張やチルダ拡張 (ホーム ディレクトリに拡張され、より一般的には他のユーザーのホーム ディレクトリに拡張されます)など、for file in os.listdir('.'): if not file.endswith('.png'): continueさまざまな拡張機能があります。.{png,jpg}{1..100}~~account
  • $SHELLやのようなシェル変数は、 $my_exported_varPython 変数に簡単に置き換えられることがあります。エクスポートされたシェル変数は、例えば として使用できますos.environ['SHELL']( の意味はexport、変数をサブプロセスで使用できるようにすることです。サブプロセスで使用できない変数は、シェルのサブプロセスとして実行されている Python では明らかに使用できません。その逆も同様です。 methodsenv=のキーワード引数を使用するsubprocessと、サブプロセスの環境を辞書として定義できるため、これは Python 変数をサブプロセスで表示できるようにする 1 つの方法です)。 では、引用shell=False符を削除する方法を理解する必要があります。たとえば、 は、ディレクトリ名を引用符で囲まないcd "$HOME"と同等ですos.chdir(os.environ['HOME'])。( は多くの場合、cd役に立たなかったり必要でなかったりするため、多くの初心者は変数を囲む二重引用符を省略して、そのままにしています。ある日まで...
  • リダイレクトを使用すると、 を標準入力としてファイルから読み取り、標準出力をファイルに書き込むことができます。 は書き込み用と読み取り用にgrep 'foo' <inputfile >outputfile開き、その内容を に標準入力として渡します。の標準出力は に出力されます。 これをネイティブ Python コードに置き換えるのは、一般的に難しくありません。outputfileinputfilegrepoutputfile
  • パイプラインはリダイレクトの一種です。echo foo | nlは2つのサブプロセスを実行し、 の標準出力はechoの標準入力ですnl(OSレベルでは、Unix系システムでは、これは単一のファイルハンドルです)。パイプラインの片方または両方の端をネイティブPythonコードに置き換えることができない場合は、特にパイプラインに2つまたは3つ以上のプロセスがある場合は、シェルの使用を検討してください(ただし、pipesPython標準ライブラリのモジュールまたは、より現代的で多用途なサードパーティの競合製品も多数あります。
  • ジョブ制御を使用すると、ジョブを中断したり、バックグラウンドで実行したり、フォアグラウンドに戻したりすることができます。プロセスを停止および続行するための基本的な Unix シグナルは、もちろん Python でも使用できます。ただし、ジョブはシェル内の高レベルの抽象化であり、プロセス グループなどが含まれるため、Python からこのような操作を実行する場合は、これを理解する必要があります。
  • シェル内での引用符の使用は、すべてが基本的に文字列であることを理解しないと、混乱を招く可能性があります。ls -l /は と同等ですが'ls' '-l' '/'、リテラルを引用符で囲むことは完全にオプションです。 シェルのメタ文字を含む引用符で囲まれていない文字列は、パラメータ展開、空白のトークン化、およびワイルドカード展開の対象となります。二重引用符は、空白のトークン化とワイルドカード展開を防止しますが、パラメータ展開 (変数置換、コマンド置換、およびバックスラッシュ処理) は許可します。 これは理論上は単純ですが、特に解釈のレイヤーが複数ある場合 (たとえば、リモート シェル コマンド) は混乱を招く可能性があります。

shとBashの違いを理解する

subprocess明示的に指定しない限り、シェルコマンドは で実行されます/bin/sh(もちろんWindowsでは変数の値が使用されますCOMSPEC)。つまり、[[配列などのさまざまな Bash 独自の機能利用できません。

Bash のみの構文を使用する必要がある場合は、シェルへのパスを次のように渡すことができますexecutable='/bin/bash'(もちろん、Bash が別の場所にインストールされている場合は、パスを調整する必要があります)。

subprocess.run('''
    # This for loop syntax is Bash only
    for((i=1;i<=$#;i++)); do
        # Arrays are Bash-only
        array[i]+=123
    done''',
    shell=True, check=True,
    executable='/bin/bash')

Aはsubprocess親とは別物であり、変更することはできない

よくある間違いとしては、次のようなことです

subprocess.run('cd /tmp', shell=True)
subprocess.run('pwd', shell=True)  # Oops, doesn't print /tmp

最初のサブプロセスが環境変数を設定しようとした場合も同じことが起こりますが、もちろん、別のサブプロセスを実行すると環境変数は消えてしまいます。

子プロセスは Python とは完全に独立して実行され、終了すると、Python はそれが何を行ったかを把握できません (子プロセスの終了ステータスと出力から推測できる漠然とした指標は別として)。子は通常、親の環境を変更できません。つまり、変数を設定したり、作業ディレクトリを変更したり、言い換えれば、親の協力なしに親と通信したりすることはできません。

この特定のケースでの即時の修正は、両方のコマンドを単一のサブプロセスで実行することです。

subprocess.run('cd /tmp; pwd', shell=True)

ただし、この特定の使用例はあまり役に立ちません。代わりに、cwdキーワード引数を使用するか、os.chdir()サブプロセスを実行する前に実行してください。同様に、変数を設定する場合は、現在のプロセス(およびその子プロセス)の環境を次のように操作できます。

os.environ['foo'] = 'bar'

または、子プロセスに環境設定を渡すには、

subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})

(明らかなリファクタリングは言うまでもありませんsubprocess.run(['echo', 'bar'])が、echoもちろん、そもそもサブプロセスで実行するものとしては不適切な例です)。

PythonからPythonを実行しないでください

これは少し疑わしいアドバイスです。Python スクリプトから Python インタープリターをサブプロセスとして実行することが理にかなっている、あるいは絶対的な要件である状況は確かにあります。しかし、多くの場合、正しいアプローチは、単にimport他の Python モジュールを呼び出しスクリプトに組み込み、その関数を直接呼び出すことです。

他のPythonスクリプトがあなたの管理下にあり、モジュールではない場合は、それを一つに変える(この回答はすでに長すぎるので、ここでは詳細には触れません。)

並列処理が必要な場合は、Python関数をサブプロセスで実行することができます。multiprocessingモジュール。もありますthreading複数のタスクを単一のプロセスで実行する(より軽量で制御性も高いが、プロセス内のスレッドが密に結合され、単一のスレッドにバインドされるという制約も大きい)。ギル

おすすめ記事