Mobile Factory Tech Blog

技術好きな方へ!モバイルファクトリーのエンジニアたちが楽しい技術話をお届けします!

Pythonでシェルコマンドを実行する

この記事はモバイルファクトリー 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 を利用する以外にも、 shutilospathlibglob などのモジュールを組み合わせることで、よりbetter shell scriptらしい可読性の高いコードが書けるようになりそうです。

最後までお読みいただきありがとうございました。
それでは良いお年を!


  1. もちろんシェルスクリプトに限った話ではありませんが、その傾向が強い言語だと思っています。