Python: subprocessでOSコマンドを実行する

ちょっとしたツールのためにPythonからGitやDockerなどのコマンドを実行してゴニョゴニョする、ということはよくあるかと思います。

OSコマンドを手軽に実行するPython標準ライブラリ subprocess で頻用する機能について使い方を整理します。

docs.python.org

環境

$ python --version
Python 3.6.8

subprocess

Python上から他のプログラム (コマンド) を別のプロセスで実行することができる標準ライブラリです。

使い方はシンプルで subprocess.run(["実行したいコマンド", "オプションなど", ...]) でOKです。

  • 結果はデフォルトでは標準出力に表示されます (= printを実行したときと同じ)
import subprocess

subprocess.run(["touch", "hoge.txt"])
subprocess.run(["ls"])
# hoge.txt

CompletedProcess

runメソッドの返り値はCompletedProcessで、ステータスコード (returncode) 、標準出力 (stdout) 、エラー出力 (stderr)などをプロパティで持ってます。

  • Pythonインタプリタの標準出力に流れてますので、stdoutはNoneとなります
result = subprocess.run("ls")
print(result.returncode)  # 0
print(result.stdout)  # None

結果を文字列で取得する

実行結果を文字列で取得したい場合、stdout=subprocess.PIPE でrunする必要があります。これによって標準出力がCompletedProcessのstdoutへパイプされます。

  • runにencoding=utf-8を渡すとデコードも文字列で取得できます
  • 全く出力が不要なら stdout=subprocess.DEVNULL で捨てることもできます
result = subprocess.run(["ls"], stdout=subprocess.PIPE)
print(result.stdout.decode("utf-8"))  # print(result.stdout)の出力はバイナリなのでデコードしてます
# fuga.txt
# hoge.txt
# 

出力だけほしいならsubprocess.getoutputメソッドを使うこともできます。こっちのほうがシンプルです。

output = subprocess.getoutput("ls")
print(output)
# fuga.txt
# hoge.txt

2020/1/27 追記

Python 3.7以降であれば、text=Trueでも文字列でゲットできます。1

result = subprocess.run(["ls"], stdout=subprocess.PIPE, text=True)
print(result.stdout)
# fuga.txt
# hoge.txt
# 
result = subprocess.run(["ls"], stdout=subprocess.PIPE, )

エラーハンドリング

実行に失敗した場合も、同様にCompletedProcessが返されます。returncodeやstderrなどのプロパティでその後の処理をハンドリングします。

result = subprocess.run(["ls", "-"], stderr=subprocess.PIPE)
print(result.returncode)  # 1
print(result.stderr.decode("utf-8"))
# ls: -: No such file or directory
#

check=Trueとしておくと、0以外のリターンコードの場合に例外 subprocess.CallProcessError がスローされます。エラー時には強制終了させたいなどのケースにはうってつけです。

result = subprocess.run(["ls", "-"], check=True)
# ls: -: No such file or directory
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "/Users/ohke/.pyenv/versions/3.6.8/lib/python3.6/subprocess.py", line 438, in run
#     output=stdout, stderr=stderr)
# subprocess.CalledProcessError: Command '['ls', '-']' returned non-zero exit status 1.

タイムアウト

timeout引数に秒数を渡すことでタイムアウトも制御できます。タイムアウトした場合、subprocess.TimeoutExpiredがスローされます。

result = subprocess.run(["sleep", "10"], timeout=2)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "/Users/ohke/.pyenv/versions/3.6.8/lib/python3.6/subprocess.py", line 430, in run
#     stderr=stderr)
# subprocess.TimeoutExpired: Command '['sleep', '10']' timed out after 2 seconds

シェル

shell=Trueとするとシェルで実行されます。このとき、第1引数はリストではなく文字列を渡すことができます。実用上はリストを作るのがめんどくさいからシェルに渡す、というケースが多いかと思います。

  • コマンドインジェクションなどのリスクは伴います
subprocess.run("mkdir -p dir1", shell=True)

カレントディレクトリを移動する

cwd引数にパスを渡すと、カレントディレクトリを変更してコマンドを実行できます。

subprocess.run(["touch", "./file1.txt"], cwd="./dir1")

subprocess.run(["ls", "./dir1/file1.txt"])
# ./dir1/file1.txt

  1. podhmoさん、コメントありがとうございました。