2026年8月4日

2026年8月4日

Pythonのsubprocessでコマンドを実行する方法|run・エラー処理・タイムアウト・安全な書き方

Python から外部コマンドを実行するには、標準ライブラリ subprocess の subprocess.run() を使います。基本形は「コマンドをリストで渡す」「capture_output=True, text=True で出力を受け取る」「check=True と timeout で失敗と固まりを検知する」の3点です。この記事では、この基本形から、出力の文字化け、終わらないコマンドの止め方、ログを流しながら実行する方法、パイプのつなぎ方、そして shell=True を避けるべき理由までを、WP-CLI を呼び出す実用例を交えて説明します。

まずは subprocess.run の基本形

import subprocess

result = subprocess.run(
    ["ls", "-l", "/var/log"],
    capture_output=True,  # 標準出力と標準エラーを受け取る
    text=True,            # バイト列ではなく文字列で受け取る
    check=True,           # 終了コードが 0 以外なら例外にする
    timeout=30,           # 30秒で終わらなければ例外にする
)
print(result.returncode)  # 0
print(result.stdout)      # コマンドの出力

戻り値の CompletedProcess には、args(実行したコマンド)、returncode(終了コード)、stdout、stderr が入ります。

コマンドは文字列ではなく、引数ごとに分けたリストで渡します。"ls -l /var/log" のような1つの文字列を渡すと、Linux ではそれ全体が1つのコマンド名とみなされ、FileNotFoundError になります。ファイル名に空白が含まれていても、リストで渡せば1つの引数として正しく扱われます。

古い記事でよく見る os.system() や subprocess.call()、check_output() も動きますが、新しく書くなら run() にまとめるのが読みやすく、出力・終了コード・タイムアウトを一か所で扱えます。

失敗を検知する:check と例外

外部コマンドは、失敗しても Python 側では何も起きません。check=True を付けないと、エラーで終わったコマンドの後ろでもスクリプトがそのまま進みます。自動化では、失敗したら止まるか、失敗を記録して扱いを決めるかを、どちらかに明示しておくことが大切です。

run() で起きる主な例外は次の3つです。

例外 起きる場面 確認できる情報
FileNotFoundError コマンドが見つからない(PATH に無い・名前の誤り) コマンド名
subprocess.CalledProcessError check=True で終了コードが 0 以外 returncode、stdout、stderr
subprocess.TimeoutExpired timeout の時間内に終わらない timeout、それまでの出力
import logging
import subprocess


def run_command(args: list[str], timeout: int = 60) -> str | None:
    try:
        result = subprocess.run(
            args, capture_output=True, text=True, check=True, timeout=timeout
        )
    except FileNotFoundError:
        logging.error("コマンドが見つかりません: %s", args[0])
        return None
    except subprocess.CalledProcessError as e:
        logging.error("失敗 (終了コード %d): %s\n%s", e.returncode, args, e.stderr)
        return None
    except subprocess.TimeoutExpired:
        logging.error("%d 秒で終わりませんでした: %s", timeout, args)
        return None
    return result.stdout

list[str] | None のような型ヒントの書き方は Python 3.10 以降のものです。古い Python で動かす場合は型ヒントを外してください。

エラー時のログに stderr を含めるのがポイントです。終了コードだけでは原因がわからないことが多く、コマンド自身が出したエラーメッセージが一番の手がかりになります。

grep のように「見つからなかった」ことを終了コード 1 で表すコマンドもあります。こうしたコマンドでは check=False にして、returncode を自分で判定します。

実用例:WP-CLI でプラグインの一覧を取得する

WordPress のサーバー運用では、WP-CLI を Python から呼び出して、結果を加工する使い方がよくあります。--format=json を指定すると、出力をそのまま JSON として読めます。

import json
import shutil
import subprocess

WP = shutil.which("wp")
if WP is None:
    raise SystemExit("wp コマンドが見つかりません。WP-CLI をインストールしてください")

result = subprocess.run(
    [WP, "plugin", "list", "--format=json", "--path=/var/www/html"],
    capture_output=True, text=True, check=True, timeout=120,
)
plugins = json.loads(result.stdout)
for p in plugins:
    if p.get("update") == "available":
        print(f"更新あり: {p['name']} {p['version']}")

shutil.which() で先にコマンドの場所を確かめておくと、「見つからない」原因を早く切り分けられます。cron や systemd から実行すると PATH が対話シェルと違うため、手動では動くのに定期実行だと見つからない、ということがよく起きます。フルパスを使えばこの問題を避けられます。

WP-CLI の出力に含まれる項目名は、WP-CLI のバージョンによって変わることがあります(2026年10月現在)。項目は p.get() で読み、存在しない場合にも落ちないようにしておくと安心です。

文字化けを防ぐ:encoding を指定する

text=True だけを指定すると、出力は OS の既定の文字コードで文字列に変換されます。Linux ではたいてい UTF-8 ですが、Windows では UTF-8 以外になることがあり、日本語の出力が化けたり、UnicodeDecodeError で止まったりします。

result = subprocess.run(
    ["git", "log", "-5", "--oneline"],
    capture_output=True, encoding="utf-8", errors="replace", check=True,
)

encoding を指定すると文字列モードになるため、text=True は不要です。errors="replace" にしておくと、変換できないバイトが混ざっていても例外にならず、置き換え文字として残ります。コマンド側の出力言語や文字コードを固定したい場合は、env={**os.environ, "LANG": "C.UTF-8"} のように環境変数を渡す方法もあります(使えるロケール名は環境によって違います)。

終わらないコマンドを止める:timeout の注意点

run() に timeout を指定すると、時間切れのときに子プロセスを終了させてから TimeoutExpired を投げます。ただし止まるのは直接起動したプロセスだけです。シェルスクリプトのように、その中でさらに別のプロセスを起動している場合、孫プロセスが残ることがあります。

Linux でプロセスのグループごと止めたい場合は、Popen で新しいセッションとして起動し、時間切れのときにグループへシグナルを送ります。

import os
import signal
import subprocess

proc = subprocess.Popen(
    ["/opt/scripts/long_job.sh"],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
    start_new_session=True,  # 新しいプロセスグループで起動する(POSIX のみ)
)
try:
    out, err = proc.communicate(timeout=300)
except subprocess.TimeoutExpired:
    os.killpg(proc.pid, signal.SIGTERM)  # グループ全体に終了を依頼する
    out, err = proc.communicate()

SIGTERM で終わらないプロセスには、少し待ってから SIGKILL を送る処理を足します。いきなり SIGKILL にすると、子プロセスが後片付け(一時ファイルの削除など)をできません。

出力を流しながら実行する:Popen

バックアップのように時間がかかる処理では、終わるまで出力を待つのではなく、1行ずつ表示したりログに書いたりしたい場面があります。その場合は Popen を使い、出力を順に読みます。

import subprocess

with subprocess.Popen(
    ["rsync", "-a", "--info=progress2", "/var/www/", "/backup/www/"],
    stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    text=True, bufsize=1,
) as proc:
    for line in proc.stdout:
        print(line, end="")
returncode = proc.returncode
if returncode != 0:
    raise SystemExit(f"rsync が失敗しました (終了コード {returncode})")

stderr=subprocess.STDOUT で標準エラーを標準出力にまとめると、読む口が1つで済みます。標準出力と標準エラーを別々の PIPE にしたまま片方だけを読み続けると、もう片方のバッファが詰まって両方が止まる(デッドロック)ことがあります。別々に受け取りたいときは communicate() を使ってください。

出力がまとめて遅れて届く場合は、子プロセス側が出力をためています。Python のスクリプトを子として呼ぶなら python -u や環境変数 PYTHONUNBUFFERED=1 で改善します。

パイプをつなぐ:shell=True を使わずに書く

ps aux | grep nginx のようなパイプは、shell=True を使えば文字列のまま書けますが、Popen を2つつなげばシェルを介さずに書けます。

import subprocess

p1 = subprocess.Popen(["ps", "aux"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["grep", "nginx"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True)
p1.stdout.close()  # p2 が先に終わったとき、p1 が終了を受け取れるようにする
out, _ = p2.communicate()
print(out)

ただし、多くの場合は Python 側で処理するほうが簡単です。上の例なら ps aux の出力を受け取り、[line for line in out.splitlines() if "nginx" in line] で絞り込めば済みます。

shell=True を避ける理由

shell=True は、渡した文字列をシェルに解釈させます。リダイレクトやワイルドカードが使えて便利ですが、外から受け取った値を文字列に埋め込むと、その値によって実行されるコマンドが変わってしまいます(コマンドインジェクション)。

# 悪い例:filename に "a.txt; rm -rf ~" が入ると、削除コマンドまで実行される
subprocess.run(f"cat {filename}", shell=True)

# 良い例:リストで渡せば、filename は1つの引数としてしか扱われない
subprocess.run(["cat", "--", filename], check=True)

-- は「ここから後ろはオプションではない」という合図で、- で始まるファイル名がオプションとして解釈されるのを防ぎます(多くの GNU コマンドが対応しています)。どうしてもシェルの機能が必要な場合は、埋め込む値を shlex.quote() で囲みます。shlex.quote() は POSIX のシェル向けで、Windows のコマンドプロンプトには使えない点に注意してください。

なお Windows では、dir や copy のようなコマンドプロンプトの組み込みコマンドは単独の実行ファイルではないため、shell=True を付けないと実行できません。ファイル操作が目的なら、pathlib や shutil で書き換えるほうが OS をまたいで動きます。

まとめ

  • subprocess.run() に、コマンドをリストで渡すのが基本形
  • check=True と timeout を付け、FileNotFoundError・CalledProcessError・TimeoutExpired を扱う。ログには stderr を残す
  • 日本語の出力は encoding="utf-8", errors="replace" で文字化けと例外を防ぐ
  • 孫プロセスまで止めたいときは start_new_session=True と os.killpg を使う
  • 長い処理は Popen で1行ずつ読み、外から受け取る値は shell=True の文字列に埋め込まない

お気軽にご相談ください

お見積りへ お問い合わせへ
▲