Python でシェル経由でコマンド実行するときのバッドノウハウ

どちらの記事でも Python の subprocess を使ってシェルを介在せずにコマンドを実行する方法が紹介されています。 シェルを介在すると、エスケープの問題考えるのが面倒だったり、 kill してみたらシェルだけ殺して肝心のコマンドがずっと残ってるというアホみたいな問題を避けられるのでお勧めです。 いい子はこれを使いましょう。

この記事ではどうしてもシェルの機能が使いたい場合や、 subprocess の PIPE の組み立てが面倒な場合のための、バッドノウハウを紹介していきます。

ちなみに、バッドノウハウと呼んでるのは、安全安心 one size fits all ではなく、メリット・デメリット・やり方をいちいち調べないといけなくて、しかもその調べる行為がほとんどコンピューターや計算機科学の勉強にならないからです。 なので、「使っては行けない禁断の術式」とかじゃ無いですし、かいつまんで紹介することで調べる時間を減らしてバッドさを軽減するのがこの記事の目的です。

os.system, os.popen

C 言語の system, popen 関数を呼びます。こいつらは渡された文字列を、 sh -c に渡すだけです。

Note that POSIX does not specify the meaning of the return value of the C system() function, so the return value of the Python function is system-dependent.

と書かれてますが、エラー時の値はともかく成功時の戻り値は 0 を期待できると思います。

が、本当にカジュアルに最低限のタイプ数でコマンドを実行するときに os.system に直接文字列突っ込むくらいで後は使わないで良いと思います。

pipes

os.system, os.popen をラップして、コマンドの組み立てをサポートしてくれるマイナーな標準ライブラリです。シェルのエスケープとかやってくれます。

Python 2 では undocumented API として pipes.quote というコマンド引数をクォートで囲んで中のクォートをエスケープしてくれる関数があり、 Python 3 では shlex モジュールで documented API になりました。この関数が使いたいときは

try:
    from shlex import quote
except ImportError:
    from pipes import quote

とかしましょう。バッドノウハウですね。マイナーなモジュールですが、 quote 関数があるということは覚えておいて損はないはず。

subprocess.* の shell=True オプション

subprocess モジュールが提供してる Popen クラスやユーティリティ関数は shell というキーワード引数でシェル経由にしてくれるオプションがあります。 check_call や check_output を使うとコマンドが失敗した時に例外にしてくれるから戻り値のチェックをサボれるなど、 os モジュールよりもちょっとだけ Pythonic です。

とはいえ、こいつには重大な罠があります。通常はコマンドを ["echo", "hello world"] のように実行するのですが、 shell=True のときは "echo hello world" と書かなければなりません。これは「シェルを使う」ことがどういうことかを考えれば当然のことなのですが、問題は、 shell=True の時に間違えてリストを渡すと例外を投げてくれず、単にリストの先頭の文字列を実行してしまうことです。

間違ったことをやろうとしたらすぐに例外で丁寧に教えてくれ無駄なことに時間を使わないで済むのが Python の良いところなのですが、この部分はそうなっておらず、思い通りに動かないなあれあれ?と数時間を無駄にできます。気をつけてください。

なお、 shell=True の場合にコマンド文字列を組み立てるための機能はありません。前述の quote などをつかって文字列処理で丁寧に作ります。

sh

こちらは非標準ライブラリです。 pip install sh でインストールできます。 名前に反して、シェルは起動しません。シェルのように気軽にパイプとかを使えるDSLです。 黒魔術を使って関数呼び出しがコマンド実行になるようにしています。

少しだけドキュメントからサンプルを抜き出してみます。

# PIPE
# sort this directory by biggest file
print(sort(du(glob("*"), "-sb"), "-rn"))

# サブコマンド
# resolves to "git branch -v"
print(git.branch("-v"))

subprocess は PIPE とか面倒。でもコマンドオプションとか動的に指定する必要あるので shell=True で文字列操作するのも面倒、という場合にお勧めです。

黒魔術度が高いので標準ライブラリに入ることは無いと思いますが、社内標準とかにしてやれば、より良いスクリプティングライフが送れると思います。

Shell Command

こちらは、 subprocess モジュールを shell=True で利用するのをサポートする非標準ライブラリです。 pip install shell_command でインストールできます。

文字列のフォーマットを拡張して安全なコマンドの生成をしてくれます。黒魔術度は皆無ですが、そもそもシェル回避できるならシェル回避した方がいいので、楽に PIPE をしたい程度なら sh の方を使い、こちらは本当にシェルを使いたいときに使いましょう。

Pythonic なので、うまく subprocess モジュールに統合する形で標準ライブラリに入って欲しいところです。

このブログに乗せているコードは引用を除き CC0 1.0 で提供します。