subprocess回りで少しハマったのでメモ。
デプロイ用のヘルパースクリプトの作成中に、cp -pr相当の処理が必要になった。Pythonで書く(不完全な)cp実装は以下の通り。
def _copy(src, dst):
if os.path.isfile(src):
shutil.copy2(src, dst)
return
if os.path.isdir(src) and os.path.isdir(dst):
target = os.path.join(dst, os.path.basename(src))
# TODO Fix behavior like cp -pr (targetディレクトリが存在していた場合、その下にあるファイル/ディレクトリは保持されるべき)
if os.path.exists(target):
shutil.rmtree(target)
shutil.copytree(src, target)
return
if os.path.isdir(src) and os.path.isfile(dst):
raise IOError(
"(Src:directory/Dest:file) This combination isn't allowed")
制約(コメント参照)はあるものの、今回の用途ではそのケースは発生しえなかったため、まあOK。
しかし「うわっ、Subprocessでcp動かせばいいのに、それしないエンジニア(笑)の人って…」という幻聴が、いつもの如く横から聞こえてきたため、subprocessを使う実装も書いてみることとなった。
def _copy(src, dst):
subprocess.Popen(['cp', '-pr', src, dst]).communicate()
ただ、この実装だとsrcにワイルドカードが使えない。ワイルドカードを解釈するためには、Popenにshell=Trueを渡し、コマンドをシェルを介して実行する必要がある。
http://docs.python.jp/2.7/library/subprocess.html#subprocess.Popen
def _copy(src, dst):
subprocess.Popen(['cp', '-pr', src, dst], shell=True).communicate()
で、「これでFinish?」と思いきや、そんなことはなく。上の実装だとcpコマンドが全く動かなくなる。
shell=Trueとした場合、前述の通りシェルを介してコマンドが実行されるわけだが、それは以下のコマンドと等価になる。
/bin/sh -c 'cp' '-pr' (src) (dst)
一見すると良さそうだが、これだと-prはcpのオプションとしてみなされず、src, dstもcpの引数と見なされない。つまり、cpを何の引数もなく実行したことになり、エラーになる。
正しくは、以下のように一つの文字列で指定してやらなくてはならない
/bin/sh -c 'cp -pr (src) (dst)'
で、これに対応するsubprocessを実行するには、Popenに与えるコマンドを、配列ではなく文字列で指定すればよい。
def _copy(src, dst):
subprocess.Popen('cp -pr {0} {1}'.format(src, dst), shell=True).communicate()
結論
必要なければshell=Trueにするべきでなしw
まあ実際、シェルインジェクションの原因にもなるので、「外部入力を使うときには絶対Trueにすんなよ!」とライブラリリファレンスにも書いてあるし…