この記事はモバイルファクトリー Advent Calendar 2018の24日目の記事です。
はじめに
メリークリスマス! エンジニアのid:Carimaticsです。
突然ですが、シェルスクリプトは便利な言語ですね。
Unix系OSであればほぼ標準で利用でき、シェルスクリプトにより移植性の高いコードを記述することができます。
また、シェルコマンド自体も強力なものが数多く用意されており、処理を簡潔に表現することも可能です。
一方で、シェルスクリプトは普段から慣れ親しんでいなければ読み書きが難しい言語でもあります。1
というのも、最近久しぶりにシェルスクリプトを書いたのですが、書き方に迷う場面が多々あったのです。
めったに使わない構文であれば仕方ないとも思えるのですが、if文やfor文を書こうとするたびに手が迷ってしまい、大変もどかしい思いをしました。
また、シェルスクリプトの規模が大きくなると、保守性も困難になりがちです。
ということで、Pythonをbetter shell scriptとして利用できると楽そうだと考えました。
Pythonであれば比較的読み書きしやすい構文で書くことができます。
他にもRubyやPerlなどがある中でどうしてPythonなのかと言うと、これは趣味です。
シェルコマンドを軽くラップできれば漸進的にスクリプトを改善していけると考えたため、本記事ではとりあえずシェルコマンドを簡単に叩けるようになるまでを目標にしました。
動作確認環境
- Python 3.7.1
方針
- Pythonの標準ライブラリに含まれるもののみを利用する
- ひとまず3.x系のみ対応する
Pythonでシェルコマンドを実行する
簡単な方法
シェルコマンドを実行すること自体は簡単で、 subprocess
モジュールを利用することでシェルコマンドを子プロセスとして実行できます。
subprocess
モジュールでは、サブプロセスの実行は run()
関数を利用するのが推奨されています。
import subprocess subprocess.run('ls')
注意点として、 run()
で引数に文字列を渡す場合、引数なしで実行されるプログラムでなければなりません。
例えば、以下のように、コマンド引数をしているとエラーになります。
import subprocess subprocess.run('ls tmp')
実行すると以下のエラーが出ます。
FileNotFoundError: [Errno 2] No such file or directory: 'ls tmp': 'ls tmp'
ls tmp
というプログラムを実行しようとして失敗しています。
シェルコマンドに引数を渡す場合には、 run()
にトークン区切りのリストを渡す必要があります。
import subprocess subprocess.run(['ls', 'tmp'])
これで ls tmp
を実行することができます。
このような場合、 shlex
モジュールを用いて、コマンド文字列をリスト化できます。
import shlex import subprocess cmd = 'ls tmp' tokens = shlex.split(cmd) # => ['ls', 'tmp'] subprocess.run(tokens)
トークン区切りにする以外には、 run()
に対して shell=True
を渡すことで、シェルコマンド文字列をそのまま実行することができます。
import subprocess subprocess.run('ls tmp', shell=True)
しかし、この場合はシェルを実際に呼び出すことになるため、シェルインジェクションが発生する可能性があるので注意が必要です。
例えば、以下のような攻撃が発生し得ます。
import subprocess def ls(dir): cmd = f"ls {dir}". subprocess.run(cmd, shell=True) ls('; rm -fr tmp')
これは、シェルで ls ; rm -fr tmp
を実行することになり、tmpファイルを削除されてしまいます。
shell=True
を利用する場合は、外部からの入力には十分に注意しましょう。
パイプを利用する
shell=True
する方法
shell=True
することで、パイプを利用することは可能です。
ただし、前述の通りシェルインジェクションが発生する可能性があるため、利用には十分ご注意ください。
import subprocess def ls(dir, filter): cmd = f"ls {dir} | grep {filter}" subprocess.run(cmd, shell=True) ls('tmp', '.md')
shell=True
しない方法
そこで、 shell=True
を利用せずにパイプを利用するには、 subporcess.Popen
を利用します。
上記と同等の結果を得るには、以下の用に書きます。
import subprocess import shlex def ls(dir, filter): cmd_ls = f"ls {dir}" cmd_grep = f"grep {filter}" p1 = subprocess.Popen(shlex.split(cmd_ls), stdout=subprocess.PIPE) subprocess.Popen(shlex.split(cmd_grep), stdin=p1.stdout) p1.stdout.close() ls('tmp', '.md')
かなり冗長になってしまいました。
Popen
を利用する場合は、使いやすくスクリプトを整えてあげるか、セキュリティリスクを考慮した上で shell=True
の利用を検討しても良いかも知れません。
まとめ
かなり駆け足になってしまいましたが、Pythonでシェルコマンドを利用する方法について簡単に紹介しました。
subprocess
を利用する以外にも、 shutil
、 os
、 pathlib
、 glob
などのモジュールを組み合わせることで、よりbetter shell scriptらしい可読性の高いコードが書けるようになりそうです。
最後までお読みいただきありがとうございました。
それでは良いお年を!
-
もちろんシェルスクリプトに限った話ではありませんが、その傾向が強い言語だと思っています。↩