Python の subprocess でシェルコマンドを実行する基本

Python スクリプトから外部のシェルコマンドを実行したい場面は意外と多い。ファイルの一括処理、システム情報の取得、他のプログラムとの連携など、Python だけで完結しないタスクでは外部コマンドの力を借りることになる。標準ライブラリの subprocess モジュールは、こうした外部プロセスの実行を安全かつ柔軟に行うための仕組みを提供している。

subprocess.run の基本

subprocess.run() は Python 3.5 で導入された関数で、外部コマンドを実行するための推奨インターフェースだ。コマンドはリスト形式で渡すのが基本となる。

import subprocess

result = subprocess.run(["echo", "Hello, World!"])
print(result.returncode)  # 0

run()CompletedProcess オブジェクトを返す。returncode が 0 なら正常終了、0 以外ならエラーを意味する。これは Unix のプロセス終了コードの慣習に従ったものだ。

コマンドをリスト形式で渡す理由は、シェルインジェクションを防ぐためにある。各要素が個別の引数として OS に渡されるため、意図しないコマンドが実行されるリスクを減らせる。

import subprocess

# リスト形式(推奨)
subprocess.run(["ls", "-la", "/tmp"])

# 複数の引数も要素ごとに分ける
subprocess.run(["mkdir", "-p", "output/data"])

shell=True の使い方と注意点

パイプやリダイレクトなど、シェルの機能を使いたい場合は shell=True を指定し、コマンドを文字列で渡す。

import subprocess

# パイプを使う例
subprocess.run("ls -la | grep .py", shell=True)

# 環境変数の展開
subprocess.run("echo $HOME", shell=True)

ただし shell=True にはセキュリティ上のリスクが伴う。ユーザー入力をコマンド文字列に埋め込むと、任意のコマンドが実行される可能性がある。

リスト形式(shell=False)

各引数が個別にプロセスへ渡される。シェルインジェクションのリスクがなく安全

文字列形式(shell=True)

シェル経由で実行される。パイプやリダイレクトが使えるが、ユーザー入力を含む場合は危険

信頼できない入力を扱う場面では shell=True を避け、リスト形式を使うべきだ。パイプが必要な場合も、Python 側で複数の subprocess.run() をつなぐことで安全に実現できる。

check=True でエラーを検出する

デフォルトでは、外部コマンドが失敗しても run() は例外を投げない。returncode を自分で確認する必要がある。check=True を指定すると、コマンドが非ゼロの終了コードを返した場合に CalledProcessError が発生するようになる。

import subprocess

try:
    subprocess.run(["ls", "/nonexistent"], check=True)
except subprocess.CalledProcessError as e:
    print(f"コマンドが失敗: 終了コード {e.returncode}")
$ python app.py
ls: /nonexistent: No such file or directory
コマンドが失敗: 終了コード 2

check=True を付けておけば、コマンドの失敗を見落とすことがなくなる。スクリプトの途中で外部コマンドが失敗したのに後続処理が走ってしまう、という事故を防げるため、特に理由がなければ付けておくのが無難だ。

タイムアウトの設定

外部コマンドが応答しなくなった場合に備えて、timeout パラメータで制限時間を設けられる。指定した秒数を超えるとプロセスが強制終了され、TimeoutExpired 例外が発生する。

import subprocess

try:
    subprocess.run(["sleep", "10"], timeout=3)
except subprocess.TimeoutExpired:
    print("タイムアウト: コマンドが3秒以内に完了しなかった")

ネットワーク通信を伴うコマンドや、処理時間が予測できないコマンドを実行する際には、タイムアウトを設定しておくとプログラムがハングアップするのを防げる。

CompletedProcess の属性

subprocess.run() が返す CompletedProcess オブジェクトには、実行結果に関する情報がまとまっている。

returncodeプロセスの終了コード(0 が正常)
args実行されたコマンド(リストまたは文字列)
stdout標準出力の内容(capture_output 時)
stderr標準エラー出力の内容(capture_output 時)

stdoutstderr はデフォルトでは None になっており、出力をキャプチャするには追加のオプションが必要になる。出力の取得方法については次の記事で詳しく扱う。

古い API との関係

subprocess モジュールには call()check_call()check_output() といった古い関数も残っている。これらは Python 3.5 より前に使われていたもので、現在は run() に統一することが推奨されている。

os.system() も外部コマンドを実行できるが、これは常にシェル経由で実行され、出力のキャプチャもできないため、subprocess.run()への移行が推奨されている。

os.system() は終了コードしか返せず、stdout/stderr の制御ができない。

既存のコードで call()os.system() を見かけたら、subprocess.run() に書き換えることを検討するとよいだろう。run() のほうがパラメータが統一されており、エラーハンドリングも柔軟に行える。