python でコマンドを実行するには
subprocess モジュールを使う
以前にも書いたんだけど、気になったので、再度調べ直した。
os.popen があるじゃないと思われるかもしれないが、os.popen は内部的に subprocess (shell=True)を使ってるので同じことである。
後述するが、シェルインジェクションも考慮すると、shell=Falseにするべきなので、考えなしのos.popen利用は避けよう。
suprocessでコマンドを実行する
単純にコマンドを実行するには、subprocess.call を使うのが楽ですね バッククォート ` やos.system のかわりに subprocess .call() を使うようです。
import subprocess cmd = "sleep 30" proc = subprocess.call( cmd , shell=True)
実行した結果はこちら。(シェル経由)
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND takuya 16804 0.0 0.0 20960 4076 pts/4 Ss 03:49 0:00 /bin/bash takuya 16864 1.0 0.0 23572 4864 pts/4 S+ 03:49 0:00 \_ python proc.py takuya 16865 0.0 0.0 4180 580 pts/4 S+ 03:49 0:00 \_ /bin/sh -c sleep 30 takuya 16866 0.0 0.0 5152 584 pts/4 S+ 03:49 0:00 \_ sleep 30
shell=True を渡したので、シェルsh -c 'sleep 30' が実行されている。
この場合は、終了待ちをしています。call の部分でブロックされます。
PythonをCtrl+Cで止めたら、sh も一緒に止まりました。
シェル経由をしない場合
今度は、シェル経由をせずに、直接プロセスを起動する場合。
import subprocess cmd = "sleep 30" proc = subprocess.call( cmd .strip().split(" ") )
実行中のプロセスのツリーはこちら。
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND takuya 16804 0.0 0.0 21112 4164 pts/4 Ss 03:49 0:00 /bin/bash takuya 16963 0.0 0.0 23572 4864 pts/4 S+ 03:53 0:00 \_ python proc.py takuya 16964 0.0 0.0 5152 584 pts/4 S+ 03:53 0:00 \_ sleep 30
シェル経由ではないので、コマンドとコマンド引数を配列で渡しています。
終了待ちをしています。callでブロックされます。Ctrl+Cで終了したら、sleep も殺してくれます。
Popen を用いる場合。
os.spawn の代わりに最近では Popenを用いるべきらしいです。
popen には使い方がいくつかあります。
単純に呼び出した場合。
import subprocess from subprocess import Popen from time import sleep cmd = "sleep 30" proc = Popen( cmd .strip().split(" ") )
単純に呼び出した場合のプロセスツリーです。
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND takuya 16804 0.0 0.0 21112 4172 pts/4 Ss 03:49 0:00 /bin/bash takuya 17038 0.0 0.0 5152 584 pts/4 S 03:58 0:00 sleep 30
単純なPopenは終了待ちをせずにサブプロセスを起動するだけです。
親プロセスのPythonはsleep の終了待ちをせずに、sleepより先に終了してしまい、孤児プロセスとなったsleep はroot プロセス(init)に引き取られています。
import subprocess from subprocess import Popen from time import sleep cmd = "sleep 30" proc = Popen( cmd .strip().split(" ") ) sleep(5)
time.sleep を使って、起動直後の状態を確認してみます。
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND takuya 16804 0.0 0.0 21112 4172 pts/4 Ss 03:49 0:00 /bin/bash takuya 17044 0.1 0.0 23572 4864 pts/4 S+ 03:58 0:00 \_ python proc.py takuya 17045 0.0 0.0 5152 584 pts/4 S+ 03:58 0:00 \_ sleep 30
起動直後では、親プロセスがPythonで子プロセスにsleep がちゃんと入っています。
起動後、親プロセスのPythonが先に終了するので 、子プロセスのsleep が孤児になるわけですね。
Popen+シェル経由で、サブプロセスを起動する。
今度は、シェル経由でサブプロセスを起動して、Pythonが終了待ちをしないPopenを見てみます。
import subprocess from subprocess import Popen from time import sleep cmd = "sleep 30" proc = Popen( cmd,shell=True )
Pythonが先に終了するので、sh -c が孤児プロセスとして引き取られていました。
takuya@atom:~$ ps uxf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND takuya 16718 0.0 0.0 94388 1876 ? S 03:49 0:00 sshd: takuya@pts/0 takuya 16719 0.0 0.0 22816 6012 pts/0 Ss 03:49 0:00 \_ -bash takuya 16802 0.0 0.0 22656 1176 pts/0 S+ 03:49 0:00 \_ screen takuya 17138 0.0 0.0 4180 580 pts/4 S 04:04 0:00 /bin/sh -c sleep 30 takuya 17139 0.0 0.0 5152 584 pts/4 S 04:04 0:00 \_ sleep 30
シェル経由なので sh -c が出てきてます。
こちらも、起動後、親プロセスのPythonが先に終了するので 、子プロセスのsleep が孤児になるわけですね。
そのままでは終了を待ち合わせしません。
終了待ちをする Popen#wait を使う。
popen = Popen( cmd,shell=True )
popen.wait()
これで、Waitできる。でもPopenってなに?
Popen オブジェクトについて
Popenによってコマンドを実行し、サブプロセスを起動した場合はPopenオブジェクトが返却される。
Popenオブジェクトのインスタンスは、process ID や STDIN/STDOUT などを変数に持つプロセスを抽象化したオブジェクトのようです。
import subprocess from subprocess import Popen from time import sleep cmd = "sleep 30" proc = Popen( cmd,shell=True ) print( "process id = %s" % proc.pid )
実行結果は次のようになっていて
takuya@atom:~$ python proc.py process id = 17216
実行後のプロセスは次のようになっていました。
takuya@atom:~$ ps uxf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND takuya 16718 0.0 0.0 94388 1876 ? S 03:49 0:00 sshd: takuya@pts/0 takuya 16719 0.0 0.0 22816 6012 pts/0 Ss 03:49 0:00 \_ -bash takuya 16802 0.0 0.0 22656 1176 pts/0 S+ 03:49 0:00 \_ screen takuya 17216 0.0 0.0 4180 580 pts/4 S 04:09 0:00 /bin/sh -c sleep 30 takuya 17217 0.0 0.0 5152 580 pts/4 S 04:09 0:00 \_ sleep 30
これから、間違いなくサブプロセスのプロセスIDを取得できていることが分かりました。
Popenオブジェクト便利ですね。
Popen#waitをしながら、Ctrl+Cを押した。
subprocess.call と同じように終了待ちをPopenで実現するには、 Popen#waitを使う。
Ctrl+Cでpythonを止めると、起動した子プロセスも一緒に死んでくれて便利。
孤児プロセスが出ないのは嬉しい。
import subprocess from subprocess import Popen from time import sleep cmd = "sleep 30" proc = Popen( cmd,shell=True ) print( "process id = %s" % proc.pid ) proc.wait()
ただし、Pythonが面倒見てくれるのはCtrl+Cを押した時だけ。
別のターミナルから、SIGINTを送信したら。
takuya@atom:~$ kill -INT 17343 takuya@atom:~$ ps uxf USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND takuya 17354 0.0 0.0 16388 1252 pts/5 R+ 04:17 0:00 \_ ps uxf takuya 16804 0.0 0.0 21268 4252 pts/4 Ss+ 03:49 0:00 /bin/bash takuya 16718 0.0 0.0 94388 1876 ? S 03:49 0:00 sshd: takuya@pts/0 takuya 16719 0.0 0.0 22816 6012 pts/0 Ss 03:49 0:00 \_ -bash takuya 16802 0.0 0.0 22656 1176 pts/0 S+ 03:49 0:00 \_ screen takuya 17344 0.0 0.0 4180 580 pts/4 S 04:16 0:00 /bin/sh -c sleep 30 ## initに引き取られた孤児プロセス takuya 17345 0.0 0.0 5152 584 pts/4 S 04:16 0:00 \_ sleep 30
TERM も INT も HUP も孤児が出来た。キーボードを押したら孫まで消してくれるのに。。。Ctrl+C押した場合とシグナル送信では若干動きが違う?
この辺は理由がわからないけど、ruby もそうだったので、Linuxのシグナルとプロセス管理をやっぱり勉強しなおしだね。
サププロセスを終了する Popen#terminate
teminate() を使うとSIGTERMシグナルが送信進されて、プロセスが終了される。
これもきちんと終了してくれる。
一定時間経過したらプロセスを終了するには
sleep と組み合わせて戦う。
import subprocess from subprocess import Popen from time import sleep cmd = "sleep 30" proc = Popen( cmd,shell=True ) print( "process id = %s" % proc.pid ) sleep(5) proc.terminate()
stack overflowには、Decoratorをと例外を使ってタイムアウトを実現する方法が紹介されていた。
Popenと wait と terminate を使う時の注意点
プロセスを扱うときに、PIPE.stdin/stdoutがデッドロックになる可能性があるので、注意しろとドキュメントに書いてあった。
terminate は SIGTERMを送信するが、WindowsはWin32APIの ProcessTerminate()を送信する。実行環境で違いがある。
実装系に依って動作が異なるようだ。
また、OSXで動かした時は shell=Trueをつけても、シェル経由にならなかった。
shell=True (シェル実行)とは
シェル実行とは、/usr/bin/bash のような、シェルプロンプトで実行するということである。
シェルで実行すると、python -> bash -> スクリプト の呼び出し関係になる。
例えば次のプログラム
## shell=Trueの例 os.popen('echo 1 ; echo 2 ; echo $SHELL')
上記は、次のシェルスクリプトと同じである。
#!/usr/bin/env bash echo 1 ; echo 2 ; echo $SHELL
pythonがbash などのシェルコマンドに文字列を渡して実行することになる。そのため、複数行コマンドや複数コマンドの実行が可能になる。 とても便利で手軽であるが、文字列のエスケープ処理では対応しきれない「限界」があるためユーザーが入力する文字列を実行コマンドに含めないように配慮が必要である。注意してもユーザーが入力する文字列をエスケープで排除するのは技術的に不可能である。そのためShell=Falseを使う。
インジェクションを考慮すると、shell=Falseが一番安全です。暗黙的にshell=Trueになってる関数で変数を埋め込むのは注意が必要なので、そもそも使わないという選択肢が良いと思います。
OSX で shell=True 付きの動作
takuya@~/Desktop$ pstree -s python
-+= 00001 root /sbin/launchd
\-+= 02974 takuya /Applications/iTerm.app/Contents/MacOS/iTerm2
\-+= 42127 takuya /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp takuya
\-+- 42128 root login -fp takuya
\-+= 42129 takuya -bash
\-+= 42234 takuya /usr/local/Cellar/python/2.7.11/Frameworks/Python.framework/Versions/2.7/Resources/Python.ap
\--- 42235 takuya sleep 30
理由は良く分からないけど、shell=Trueをつけて試したがシェル経由にはらなかった。疑問が残った。
Pythonのsubprocess は実行環境次第なところがあるらしく、OSを変えたらチェックが必要かもしれない。
複数プロセスをパイプするとき
subprocess.PIPを使って、プロセスをパイプする。
cmd1 = "ls -lt "
cmd2 = "head -n 5 "
p1 = subprocess.Popen(cmd1.strip().split(" "), stdout=subprocess.PIPE)
p2 = subprocess.Popen(cmd2.strip().split(" "), stdin=p1.stdout)
p1.stdout.close()
output = p2.communicate()[0]
comunicate を呼び出すと、waitになるし、stdout.close()して次に渡されるようですね。
p2が起動後に、p1のSTDOUTをclose()したらp1 がp2からのSIGPIPEを受け取れるのでパイプを使えるとのこと。
check_output でも出来るようです
output=check_output("ls -alt / | head -n 2", shell=True)
戻り値が byte 何だけど、str でほしいよ。
byte で出てくるので、decode が必要になったりして、めんどくさい。
ret = subprocess.check_output("ls -l ", shell=True,universal_newlines=True)
universal_newlines=True をつけると unicode な str でもらえるよ。
プログラムにSTDINを渡したい。
ruby や phpや bashなどSTDINでソース・コードを渡せるスクリプトをpythonから実行するには
p1 = Popen(['php'],shell=False,stdin=subprocess.PIPE,stdout=subprocess.PIPE) p1.stdin.write(b'<?php echo "hello";') p1.stdin.close() p1.wait() ret = p1.stdout.read() print(str(ret, 'UTF-8'))
このように、手順を踏めば、pythonから外部スクリプトを実行して、その結果を変数に受け取ることができる。jsonなどで外部コードとやり取りすれば、プロセス間通信のようなことも可能になる。
getoutput と check_output
python の subprocess (プロセス実行) には getoutput と check_outputがある。
外部コマンドの実行をするときに、getoutput 利用が増えているが、これを使うときは注意が必要
Popenのような、配列引数を渡すことができない。
import subprocess import shlex cmds = shlex.split('which ls') ret = subprocess.getoutput(cmds) # =>''
次のように、getoutputを実行しシェル経由を確認する。
ret = [
subprocess.getoutput('which ls; which bash'),
subprocess.getoutput('echo $SHELL'),
]
結果は次のようになる。
[
'/usr/bin/ls\n/usr/bin/bash',
'/bin/bash'
]
結果から、getoutputはシェルスクリプトとして実行されているとわかります。
実はgetoutputは、内部でcheck_output(cmd, shell=True) を呼び出している。だから、プロセスを実行しているわけではなく、シェルが実行されている。
実際にソース・コードを見てみましょう。
getoutput
def getoutput(cmd): return getstatusoutput(cmd)[1]
getstatusoutput
def getstatusoutput(cmd): try: data = check_output(cmd, shell=True, text=True, stderr=STDOUT) # ...
subprocess.getoutput のソース・コードを見れば、実体はsubprocess.check_outputです。getoutput とは、check_output(cmd, shell=True..)だとわかります。つまり、シェル・スクリプトが実行できることになる。意図しない実行ができてしまうことになる。
これらのことから、subprocess.getoutput はバグ誘発関数と言える。
特に、文字列をシェルで実行するため、変数埋込を使うと思わぬ挙動に遭遇するだろう。
次のようにしているとき、変数で意図しない動作を引き起こすことになる。
dir='name' ret = subprocess.getoutput(f'rm -rf ~/.config/{dir}) # =>''
もし、dir='../../../' だったらどうでしょう。 また、dir=';;;; rm /etc/passwd だったらどうでしょう
シェルコマンドインジェクションが成立してしまう。また、シェルの脆弱性に影響を受ける。過去にbashの脆弱性がありました。shell=Trueのコードは影響を被ります。
コマンドを実行するときは、シェル文字列を渡さずに、popenやcheck_outputが無難である。popenはデフォルト値がshell=Falseです。
getoutput は名前がわかりやすく、イメージしやすいのでよく紹介される。getoutput記事も増えている。しかし、シェル経由を避けてプロセス起動が無難だろう。個人的には、check_output に配列を渡せば良いと思う。
# getoutput より、check_output
cmds = shlex.split('which ls;')
ret= subprocess.check_output(cmds)
シェル・スクリプトなら、次のようにエラーになるので安全
subprocess.CalledProcessError: Command '['which', 'ls;']' returned non-zero exit status 1.
変に「エスケープ処理」をするよりマシです。そもそも実行できないので安全です。
世間のセキュリティ対策に、コマンドインジェクション対策は「エスケープしろ」とか言われますが。そもそも間違い。ソース・コードのセキュリティ監査項目やプログラム・ガイドラインに「エスケープしてないと認めない」とかテンプレ項目があるらしい。個人的にかなり違和感を覚える。
SQLインジェクション対策なら、「プリペアしろ、エスケープすんな。」である。なぜならエスケープには限界があるためである。シェル・インジェクションの対策も同じで、「スクリプトを実行できないようにしろ、プロセスをちゃんと起動しろ。」と言える。これに尽きると思うんですよね。エスケープ済みか監査するという対策は、セキュリティ監査の項目自体が誤りで技術的に間違ってると思うんですよね。
## 安全なコマンド実行結果の取得
cmd = 'ls'
cmds = shlex.split(f'which {cmd}')
ret= subprocess.check_output(cmds)
ret = ret.decode().strip()
以上のことから、変数が含まれる場合は、getoutput より、check_outputが望ましいと考えます。
まとめ
コマンドの実行
- 単純な呼び出し(終了待ちする系)
call ( "ls -l " , shell=True)check_call ( "ls -l " , shell=True)終了STATUSが0以外なら例外check_output ( "ls -l ", shell=True )stdoutを取得
- Popenを使ったプロセス起動 (終了待ちなし)
popen = Popen( "ls -l" ,shell=True)popen . wait ()popen.terminate()popen.communicate()[0]stdout取得popen.reterncodeexit status 終了STATSUを取得popen .pidプロセスIDを取得popen.poll()終了しているか取得
- シェル経由かそうでないか
シェル経由shell=True をつけて文字列で渡す非シェル経由コマンドを配列で渡す。os.popenやgetoutputはシェル経由でインジェクションが起きる。
| 関数 | popenを使って同等のこと |
|---|---|
| call | Popen( cmd_list ).wait() |
| check_output | Popen( cmd_list ).communicate[0] |
追記
Popenの読み方がよくわからん。 Popen = p-open なのか、 Pop-en なのか、どっちなんだろう。
2016-04-25追記
split だと正規表現が使えないので
cmd .strip().split(" ")
正規表現を使う場合は
import re
cmd = re.split('\s+', cmd.strip() )
のほうが良さそうです。
2024-08-08
getoutput について「大幅に追記」
2024-08-08
os.popen について言及。Shell=Falseについて言及。シェルコマンド・インジェクションについて言及
2019-09-03
str / byte の取り方について補足
参考資料
https://docs.python.org/2/library/subprocess.html#replacing-functions-from-the-popen2-module
http://stackoverflow.com/questions/2281850/timeout-function-if-it-takes-too-long-to-finish