Pythonでシェルコマンドを実行する方法:subprocess.runの使い方

机の上のモニター python

Python のプログラムから ls や git のようなコマンドを動かすときは、標準ライブラリの subprocess を使います。subprocess.run にコマンドと引数をリストで渡すと、コマンドが終わるまで待ち、終了コードや出力を返します。

実行結果は Linux の Python 3.12 で動かしたものです。Windows で違う点は最後にまとめました。

基本の書き方

import subprocess

result = subprocess.run(["echo", "こんにちは"], capture_output=True, text=True)
print(result.returncode)
print(result.stdout)
0
こんにちは

コマンドは、空白で区切った単語ごとにリストの要素にします。ls -l /tmp なら ["ls", "-l", "/tmp"] です。

capture_output=True を付けると、コマンドが画面に出す内容を result.stdout(標準出力)と result.stderr(標準エラー)で受け取れます。付けないと、出力は画面にそのまま出て、変数には入りません。text=True は、出力を bytes ではなく文字列で受け取る指定です。result.returncode は終了コードで、0 なら成功です。

失敗したら止める

subprocess.run は、コマンドが失敗しても例外を出しません。returncode を見ないと、失敗に気づかないまま次の処理に進みます。check=True を付けると、終了コードが 0 以外のときに subprocess.CalledProcessError が出ます。

try:
    subprocess.run(["ls", "/no-such-dir"], capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e:
    print("失敗:", e.returncode)
    print(e.stderr)
失敗: 2
ls: cannot access '/no-such-dir': No such file or directory

コマンドそのものが見つからないときは、check=True を付けていなくても FileNotFoundError になります。

try:
    subprocess.run(["no-such-command"])
except FileNotFoundError as e:
    print("FileNotFoundError:", e)
FileNotFoundError: [Errno 2] No such file or directory: 'no-such-command'

パイプを使う shell=True と、その危険

|(パイプ)や * などシェルの書き方を使いたいときは、shell=True を付けて、コマンドを1つの文字列で渡します。

r = subprocess.run("ls / | wc -l", shell=True, capture_output=True, text=True)
print(r.stdout)
20

shell=True で外から来た文字をつなげるのは危険です。ファイル名に ; を入れると、別のコマンドまで動きます。

filename = "memo.txt; echo 別のコマンドも動いた"

# shell=True で文字列をつなげた場合
r = subprocess.run(f"cat {filename}", shell=True, capture_output=True, text=True)
print(r.stdout, r.stderr)

# リストで渡した場合
r = subprocess.run(["cat", filename], capture_output=True, text=True)
print(r.stdout, r.stderr)
別のコマンドも動いた
 cat: memo.txt: No such file or directory

 cat: 'memo.txt; echo 別のコマンドも動いた': No such file or directory

上は echo まで実行されました。ここに rm が入っていたら、ファイルが消えます。下のようにリストで渡すと、全体が1つのファイル名として扱われ、echo は動きません。利用者の入力やファイル名を使うときは、shell=True を付けずにリストで渡します。パイプが必要なら、出力を Python で受け取ってから処理するほうが安全です。

時間がかかりすぎたら止める

timeout に秒数を渡すと、時間内に終わらないコマンドを止めて subprocess.TimeoutExpired を出します。止まったままになりうるコマンド(ネットワークにつなぐものなど)には付けておきます。

try:
    subprocess.run(["sleep", "5"], timeout=1)
except subprocess.TimeoutExpired:
    print("時間切れ")
時間切れ

os.system との違い

古いコードでは os.system("ls -l") もよく見かけます。os.system は終了コードしか返さず、出力を変数で受け取れません。いつも shell=True と同じ動きをするので、上の危険もそのままです。

code = os.system("echo os.system から実行")
print("戻り値:", code)
os.system から実行
戻り値: 0

新しく書くなら subprocess.run を使います。

実際に動かすと、os.system のコマンドの出力が、プログラムのほかの print より先に画面に出ることがありました。Python の print の出力はいったんためられ、コマンドの出力は直接出るためです。順番が大事なときは、print(..., flush=True) にしておきます。

Windows で動かすとき

Windows では、dir や echo はコマンドプロンプトの中にある命令で、単独のプログラムではありません。subprocess.run(["dir"]) は FileNotFoundError になるので、shell=True を付けて subprocess.run("dir", shell=True) と書きます。git や python のようにプログラムとしてあるコマンドは、Linux と同じくリストで渡せます。

日本語の出力が文字化けするときは、text=True の代わりに encoding="utf-8" や encoding="cp932" を指定して、コマンドが出す文字コードに合わせます。

なお、ターミナルで python と打つと出る >>> の画面(Python シェル)は、ここで扱った OS のシェルとは別のものです。そちらの使い方は「Pythonシェル(対話モード)の使い方:起動・終了とOSのシェルとの違い」にまとめています。

参考リンク

アイキャッチ画像: Photo by Fernando Hernandez on Unsplash

タイトルとURLをコピーしました