はじめに
2017年に書いた記事の内容が2系ベースであり,かついい加減情報を更新したほうがいいなと思い,編集に着手した結果,subprocess.run()
をはじめとする大幅な追記が必要となりそうになったため,本記事を新規に作成した.
目標として,以前からのsubprocess関数の説明もしつつ(サポートは終了していない),subprocess.run()
やsubprocess.Popen()
による同義な記述を行う.さらには,これらを用いたより多様な記述を取り上げる.
お急ぎの方はcmd記述の共通ルールと,run()以降を読めば全く問題ない.
そもそも何をするモジュールなんですか,という話は公式か,この記事などを参照するとよい.
結論は何かと言われれば,**今後はできる範囲ではすべてsubprocess.run()
に任せよう.それより複雑な処理が求められる場合は,subprocess.run()
の土台でもあるsubprocess.Popen()
を使用しよう.**調べた限りは本記事で他に紹介する関数は一切不要(すべて代替可能)だよ,ということ.
import io
import os
import time
import subprocess
実験用ファイル
print('Hello world!')
古いシェルコマンド実行手法
もはや使う機会のないものばかりだが,古いコード発掘時の助けになるやもなので一応まとめる.
os.system()
os.system(command)
(公式)
サブシェル内でコマンド (文字列) を実行します。この関数は標準 C 関数 system() を使って実装されており、system() と同じ制限があります。sys.stdin などに対する変更を行っても、実行されるコマンドの環境には反映されません。
(中略)
subprocess モジュールは、新しいプロセスを実行して結果を取得するためのより強力な機能を提供しています。この関数の代わりに subprocess モジュールを利用することが推奨されています。
# ターミナル上に結果が出力される
print(os.system('ls')) # 0
# ターミナル上→ sh: 1: tekito: not found
print(os.system('tekito')) # 32512(←環境依存? 0以外の数字が返る)
os.spawn
関数群(os.spawnl(mode, path, ...)
,os.spawnle(mode, path, ..., env)
,os.spawnlp(mode, file, ...)
,os.spawnlpe(mode, file, ..., env)
,os.spawnv(mode, path, args)
,os.spawnve(mode, path, args, env)
,os.spawnvp(mode, file, args)
,os.spawnvpe(mode, file, args, env)
)はよくわからない詳しくやると面倒そうなので割愛(spawnという動詞自体は,「(プロセスを)生成する」といったニュアンスらしい).
os.popen()
os.popen(cmd, mode='r', buffering=-1)
(公式)
コマンド cmd への、または cmd からのパイプ入出力を開きます。戻り値はパイプに接続されている開かれたファイルオブジェクトで、 mode が 'r' (デフォルト) または 'w' かによって読み出しまたは書き込みを行うことができます。
(中略)
これは、subprocess.Popen を使用して実装されています。サブプロセスを管理し、サブプロセスと通信を行うためのより強力な方法については、クラスのドキュメンテーションを参照してください。
# 出力結果をreadする
print(os.popen('ls h*.py','r').readlines())
'''
['hello.py\n', 'hello2.py\n']
'''
subprocessにおけるcmd記述の共通ルール
文字列で与える→shell=True
・ クオート混入による誤作動リスク
文字列のリストで与える→shell=False (default)
・ ワイルドカード等が使えず,低自由度
(公式)
shell が True なら、指定されたコマンドはシェルによって実行されます。あなたが Python を主として (ほとんどのシステムシェル以上の) 強化された制御フローのために使用していて、さらにシェルパイプ、ファイル名ワイルドカード、環境変数展開、~ のユーザーホームディレクトリへの展開のような他のシェル機能への簡単なアクセスを望むなら、これは有用かもしれません。
(公式)
shell=True
でシェルを明示的に呼びだした場合、シェルインジェクション の脆弱性に対処するための、すべての空白やメタ文字の適切なクオートの保証はアプリケーションの責任になります。
# 文字列リストで与える方法(以降の5つはこの記法では不可)
print(subprocess.call(['ls','-l'], shell=False)) # 0
# シェルパイプライン
print(subprocess.call('echo -e "a\nb\nc" | wc -l', shell=True)) # 0
# セミコロン
print(subprocess.call('echo Failed.; exit 1', shell=True)) # 1
# ワイルドカード
print(subprocess.call('ls -l *.py', shell=True)) # 0
# 環境変数
print(subprocess.call('echo $HOME', shell=True)) # 0
# HOMEを表すチルダ記号
print(subprocess.call('ls -l ~', shell=True)) # 0
subprocess.run()による統合前の関数たち
下記に示す3種類がある.それらは下表の3要素で大別される.いずれもsubprocess.run()
においてON/OFF切り替えが可能となったため,今後は一切不要となった.
関数名 | 終了ステータス | 出力結果 | CalledProcessError |
---|---|---|---|
(.run()での引数・attribute) | .returncode | .stdout | check |
subprocess.call() | 〇 | ||
subprocess.check_call() | 〇 | 〇 | |
subprocess.check_output() | 〇 | 〇 |
subprocess.call()
subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None)
コマンドの実行.返り値は終了ステータス(0なら正常終了).
主要なオプション①(標準入出力,タイムアウト)
(他のsubprocess関数でも共通)
標準入力・出力にファイルを指定する際は,open()
に入れた状態で.
余談に近いが,Python3系では,str型(mode='r'/'w'
)よりもbytes型(mode='rb'/'wb'
)を選んでおけばだいたい問題ない(調べればわかる話なのだが,どっちもサポートしているか,bytes型のみサポートしている場合が多い).
print(subprocess.call(['cat', 'hello.py'])) # 0
# 標準入力・標準出力を指定する(2つ目はhello2.pyが作成される)
print(subprocess.call(['cat'], stdin=open('hello.py','rb'))) # 0
print(subprocess.call(['cat', 'hello.py'], stdout=open('hello2.py','wb'))) # 0
# timeoutを指定する
print(subprocess.call(['sleep', '3'], timeout=1)) # TimeoutExpired
# (補足)実行ディレクトリ指定のcwdオプションでも~はサポートされていない.
print(subprocess.call(['ls','-l'], shell=False, cwd = "~")) # FileNotFoundError
subprocess.check_call()
subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None)
コマンドの実行.返り値は終了ステータス(0なら正常終了).異常終了時に,CalledProcessError
を返す.
# 正常終了
print(subprocess.call(['cat', 'hello.py'])) # 0
print(subprocess.check_call(['cat', 'hello.py'])) # 0
# 異常終了:エラーステータスではなく,例外エラーを返す
print(subprocess.call(['cat', 'undefined.py'])) # 1
print(subprocess.check_call(['cat', 'undefined.py'])) # CalledProcessError
subprocess.check_output()
subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None, text=None)
コマンドの実行.返り値が標準出力.
(公式)
デフォルトで、この関数はデータをエンコードされたバイトとして返します。
ということなので,文字列などであればデコードしてから処理を行う.
o = subprocess.check_output('ls h*.py', shell=True)
print(o) # b'hello.py\nhello2.py\n'
print(o.decode().strip().split('\n')) # ['hello.py', 'hello2.py']
主要なオプション②(標準エラー出力,特殊値)
(他のsubprocess関数でも共通)
stderr
に与えるものによって出力先を変更できる.
- subprocess.DEVNULL:標準入出力先をos.devnull(ビットバケツ、ブラックホール)に指定
- subprocess.PIPE:標準入出力先へのパイプ指定
- subprocess.STDOUT:標準エラー出力が標準出力と同じハンドルに出力されるよう指定(2>1&.stderrに対してのみ)
# 出力はすべて捨てる
o = subprocess.call(['cat', 'undefined.py'], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
# 標準出力を取り出す
try:
o = subprocess.check_output(['cat', 'hello.py'])
print(o) # b"print('Hello world!')"
except subprocess.CalledProcessError as e:
print('ERROR:', e.output)
# 標準エラー出力を取り出す
try:
o = subprocess.check_output(['cat', 'undefined.py'], stderr=subprocess.PIPE)
print(o)
except subprocess.CalledProcessError as e:
print('ERROR:', e.stderr) # ERROR: b'cat: undefined.py: No such file or directory\n'
# 標準エラーを標準出力に統合して取得
try:
o = subprocess.check_output(['cat', 'undefined.py'], stderr=subprocess.STDOUT)
print(o)
except subprocess.CalledProcessError as e:
# e.stderrでなくe.stdoutなのに注意.なお,e.outputでも可.
print('ERROR:', e.stdout) # ERROR: b'cat: undefined.py: No such file or directory\n'
subprocess.run()による置換
(公式)
サブプロセスを起動するために推奨される方法は、すべての用法を扱える run() 関数を使用することです。より高度な用法では下層の Popen インターフェースを直接使用することもできます。
ということで,先述のコードたちのrun()
による置換.指定オプションなどは上表も参照のこと.
subprocess.run()
subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None)
subprocess.call()の置換
.returncode
をつける.
subprocess.call()
の紹介時に用いたstdin
以外にも,input
というオプションがある.これは,後述するsubprocess.Popen().communicate()
を用いた標準入力の指定であり,文字列を指定する.
print(subprocess.run('ls -l h*.py', shell=True).returncode) # 0
# 標準入力・標準出力を指定する(hello2.pyが作成される)
print(subprocess.run(['cat'], stdin=open('hello.py','rb'), stdout=open('hello2.py','wb')).returncode) # 0
# (input使用.上とほぼ同義)
print(subprocess.run(['cat'], input=open('hello.py','rb').read(), stdout=open('hello2.py','wb')).returncode) # 0
# timeoutを指定する
print(subprocess.run(['sleep', '3'], timeout=1).returncode) # TimeoutExpired
subprocess.check_call()の置換
check=True
を追加する.また,.check_returncode()
という後出し用メソッドもある.
# 成功ステータス
print(subprocess.run(['cat', 'hello.py']).returncode) # 0
print(subprocess.run(['cat', 'hello.py'], check=True).returncode) # 0
# エラーステータスではなく,例外エラーを返す
print(subprocess.run(['cat', 'undefined.py']).returncode) # 1
print(subprocess.run(['cat', 'undefined.py'], check=True).returncode) # CalledProcessError
# (補足)check_returncode()を用いた後出しエラー出力
p = subprocess.run(['cat', 'undefined.py'], check=False)
print(p.returncode) # 1
print(p.check_returncode()) # CalledProcessError
subprocess.check_output()の置換
stdout
オプションと.stdout
を使用.
o = subprocess.run('ls h*.py', shell=True, stdout=subprocess.PIPE, check=True).stdout
print(o) # b'hello.py\nhello2.py\n'
print(o.decode().strip().split('\n')) # ['hello.py', 'hello2.py']
標準エラー出力関連操作も,新規のオプション機能により,run()
を用いるとcheck_output()
のときよりも自由度が広がる.
# check=False (default)で,エラーで止まることなくstrerrを覗ける
o = subprocess.run(['cat', 'undefined.py'], check=False, capture_output=True)
print((o.stdout, o.stderr)) # (b'', b'cat: undefined.py: No such file or directory\n')
# 標準エラー出力を標準出力に統合.
o = subprocess.run(['cat', 'undefined.py'], check=False, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
print(o.stdout) # b'cat: undefined.py: No such file or directory\n'
# capture_output=Trueで標準出力,標準エラー出力ともにPIPEが指定される.
try:
o = subprocess.run(['cat', 'undefined.py'], check=True, capture_output=True)
print(o.stdout)
except subprocess.CalledProcessError as e:
print('ERROR:',e.stderr) # ERROR: b'cat: undefined.py: No such file or directory\n'
subprocess.Popen()による自由度の高い処理
公式
このモジュールの中で、根底のプロセス生成と管理は Popen クラスによって扱われます。簡易関数によってカバーされないあまり一般的でないケースを開発者が扱えるように、Popen クラスは多くの柔軟性を提供しています。
というわけで,柔軟性の紹介.特に,run()
では再現できないものを中心に扱っていく.
大きな特徴として,subprocess.Popen()
はプロセスを生成するだけで,終了を待たない.終了したかの確認には.poll()
を使用し,終了を確認してから次に進むためには..wait()
を実行する.
置換例(call()
)
Popen()
の多くの引数やアトリビュートはrun()
と共通である.
subprocess.call()
の再現のためには,subprocess.run()
でも使用した.returncode
を用いる.他のコードの再現時も,run()
の時と同様にすればよい.
# 標準入力・標準出力を指定する(2つ目はhello2.pyが作成される)
p = subprocess.Popen('ls -l h*.py', shell=True)
p.wait()
print(p.returncode) # 0
# 標準入力・標準出力を指定する(hello2.pyが作成される)
p = subprocess.Popen(['cat'], stdin=open('hello.py','rb'), stdout=open('hello2.py','wb'))
p.wait()
print(p.returncode) # 0
# timeoutを指定する
p = subprocess.Popen(['sleep', '3'])
p.wait(timeout=1) # TimeoutExpired
デッドロックとPopen.communicate()
Popen.communicate()
は,(必要であれば文字列入力を入力を与えて),(標準出力, 標準エラー出力)
の形のタプルを返すメソッド.
(公式)
プロセスと交信する:データを標準入力に送信します。 ファイルの終わりに達するまで、stdoutおよびstderrからデータを読み取ります。 プロセスが終了するのを待ちます。 (中略)communicate()はタプル (stdout_data, stderr_data) を返します。
(公式)
警告 .stdin.write, .stdout.read, .stderr.read を利用すると、別のパイプの OS パイプバッファーがいっぱいになってデッドロックが発生する恐れがあります。これを避けるためには communicate() を利用してください。
この記事でも詳細に解説がなされている.
バッファに入出力データがたまりすぎる状況が発生するリスクがあるなら,一度に管理してくれるcommunicate()
を使いなさい,といった感じ.
実際にcommunicate()
を用いてデッドロックに対応した方の(記事)も存在する.
デメリットとして,一つのプロセス内で複数回にわたる入力・出力のやり取りができなくなることがある.下記に紹介するリアルタイム出力管理やインタラクティブ処理の際はcommunicate()
との併用ができない.
また,タイムアウト処理に必要なオプションtimeout
が,Popen().strout
などにはなくてcommunicate()
には存在するといった違いもある.これについては,この記事で紹介されているような解決策がある.
シェルパイプラインの再現
公式でも紹介されている.shell=True
として文字列でを与えてもよいが,下記のようにそれを避けた文法でも再現が可能.
途中のSIGPIPEについての詳細は,こことかこことかで説明がされている.
p1 = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE) # 出力先にパイプ
p2 = subprocess.Popen(['grep', 'python'], stdin=p1.stdout, stdout=subprocess.PIPE) # 入力にp1の受取
p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits.
print(p2.communicate()[0].decode().strip().split('\n')) # stdout取得
.poll()
による状態確認とリアルタイム出力管理
Popen()
は終了を待たないという話をしたが,.poll()
により,終了状態か否かを確認できる.終了していなかったらNone,終了していたらそのステータスを返す.
.stdout.readline()
で1行ずつ出力を取得できる.これは,出力がリアルタイムで改行されるたびに取得が可能となる.対して.stdout.read()
などの一度にすべての出力を取得するメソッドを用いると,自動で終了待機状態になる.
cmd = 'echo Start;sleep 0.5;echo 1;sleep 0.5;echo 2;sleep 0.5;echo 3;sleep 0.5;echo Finished'
p = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
# リアルタイムに取得
while p.poll() is None:
print('status:',p.poll(), p.stdout.readline().decode().strip())
print('status:',p.poll())
print()
p = subprocess.Popen(cmd, shell=True, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
# 2秒たたないと出力されない
print('status:',p.poll(), p.stdout.read().decode().strip())
'''
status: None Start
status: None 1
status: None 2
status: None 3
status: None Finished
status: 0
status: None Start
1
2
3
Finished
'''
もっとちゃんとしたリアルタイム出力管理コードのひな型がこの記事で紹介されている.
インタラクティブ入出力
何回かに分けて入力を送り,その都度出力を得る,といったやり取りは,下記のようにして実現される.参考にしたのはこの記事.
一度にデータの入出力を終えるcommunicate()
では再現できないため,大きなデータのやり取りが必要になると,デッドロックが発生するリスクがある.
まず,準備として,複数の入力を必要とし,その都度出力を生成するpythonコードを作成しておく.
a = int(input())
print('n =', a)
b = int(input())
print(' + {} ='.format(b), a+b)
c = int(input())
print(' - {} ='.format(c), a-c)
d = int(input())
print(' * {} ='.format(d), a*d)
e = int(input())
print(' / {} ='.format(e), a/e)
これを,下記のように実行.flushしないとバッファにたまった状態で入力されていない状態のままになってしまっているので注意.
numbers = range(1, 6)
# 非インタラクティブ(communicate()を用いて一度にすべて送信)
p = subprocess.Popen(['python', 'calc.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# 改行コードで連結
str_nums = '\n'.join(map(str, numbers))
o, e = p.communicate(input=str_nums.encode())
print(o.decode())
'''
n = 1
+ 2 = 3
- 3 = -2
* 4 = 4
/ 5 = 0.2
'''
# インタラクティブ(.stdin.write()を駆使して一行ずつ送信)
p = subprocess.Popen(['python', 'calc.py'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for n in numbers:
# 数字を送る
p.stdin.write(str(n).encode())
# 改行を送る(input()に対しての入力なので必須)
p.stdin.write('\n'.encode())
# バッファの解放(大事)
p.stdin.flush()
# 結果を一行読む(複数行にわたる場合はwhile文を使う)
print(p.stdout.readline().decode().strip())
'''
n = 1
+ 2 = 3
- 3 = -2
* 4 = 4
/ 5 = 0.2
'''
並列実行(情報不足)
Popen()
は終了を待たないため,複数プロセスを同時に走らせることが可能.非同期処理といえばasyncio
モジュールで管理がされるが,その中にsubprocess
に対応させたものも存在する(公式).その他にも,この記事やこの記事など,非同期処理については様々な記事が存在する.様々な手法のなかでの取捨選択時には「何が最も低遅延で,かつリスクが少ないか」を把握しなければならないと思うが,現状そこまでの知識がないため,ここではこれ以上の議論は割愛する.
下記はとりあえずの簡易版.
# 順列
print('start')
s = time.time()
running_procs = [
subprocess.run(['sleep', '3']),
subprocess.run(['sleep', '2']),
subprocess.run(['sleep', '1']),
]
# run()だと順に実行を待って計6秒近くかかる
print('finish [run()]', time.time() - s)
# ----------------------------
# 並列
print('start')
s = time.time()
procs = [
subprocess.Popen(['sleep', '3']),
subprocess.Popen(['sleep', '2']),
subprocess.Popen(['sleep', '1']),
]
# Popen()だと,開始だけならすぐに終わる
print('finish [run Popen()]', time.time() - s)
# 各プロセスの終了待ち
[p.wait() for p in procs]
# 一番長い時間のかかるプロセスに等しい約3秒で処理が完了する
print('finish [wait Popen()]', time.time() - s)
終わりに
run()
を用いれば大抵の多種多様な処理がすぐに実行でき,Popen()
を駆使すれば,非同期かつインタラクティブなはるかに自由度の広い応用が可能となることが分かる.