シェルコマンドの実行

PythonBeginner
オンラインで実践に進む

はじめに

運用作業では、Python でスクリプトを書く場合でも、既存のシステムコマンドを呼び出す必要があることがよくあります。そのため、Python のプロセス実行ツールを理解することが重要です。

subprocess モジュールを使うと、Python から別のプログラムを起動し、その出力を取得して、成功または失敗を安全に確認できます。これは、Python コードとシェルコマンドを連携させる標準的な方法です。

この実験では、次の方法を学びます。

  1. subprocess モジュールをインポートする。
  2. Python を使って簡単な Linux コマンド(ls -l)を実行する。
  3. コマンドの出力を取得し、さらに処理できるようにする。
  4. 出力をバイト列から読みやすい文字列にデコードする。
  5. コマンドが失敗したときに、エラーを適切に処理する。

Linux 環境の ~/project ディレクトリで作業します。

Subprocess のインポート

このステップでは、Python スクリプトを作成し、システムシェルと連携するために必要なモジュールをインポートします。

subprocess モジュールは Python の標準ライブラリに含まれているため、追加でインストールする必要はありません。このモジュールを使うと、Python スクリプトから基盤となるオペレーティングシステムと通信できます。

これは、Python コードと既存の Unix ツールをつなぐ標準的な橋渡し役です。運用作業では、すべてのシェルコマンドを一度に置き換えることはほとんどありません。通常は、Python をコーディネーターとして使い、システムツールを呼び出し、その結果を取得し、さらに高度なロジックを追加します。

まず、正しいディレクトリにいることを確認します。

cd ~/project

次に、nano エディターまたは任意の方法で、list_files.py という新しい Python ファイルを作成します。

nano list_files.py

ファイルの先頭に次の行を追加して、モジュールをインポートします。

import subprocess

print("Module imported successfully")

ファイルを保存してエディターを終了します(nano では、Ctrl+X、Y、Enter の順に押します)。

次に、スクリプトを実行して、正しく動作することを確認します。

python3 list_files.py

Module imported successfully というメッセージが表示されます。

コマンドの実行

このステップでは、スクリプトを変更して Linux コマンドを実際に実行します。サブプロセスを呼び出す推奨方法である subprocess.run() 関数を使用します。この関数は Python 3.5 で導入されました。

Python が別のプログラムを起動すると、独立したプロセスが作成されます。その子プロセスには、シェルから直接実行するコマンドと同じように、独自の終了コード、標準出力、標準エラーがあります。このプロセスの境界を理解すると、後のエラー処理を考えやすくなります。

もう一度 list_files.py を開きます。

nano list_files.py

ファイルの内容を次のように変更します。ls -l コマンドを使って、現在のディレクトリにあるファイルを詳細情報付きで一覧表示します。

import subprocess

## Run the 'ls -l' command
## The command and its arguments are passed as a list of strings
subprocess.run(["ls", "-l"])

コマンドを ["ls", "-l"] のようなリストで渡している点に注目してください。単一の文字列で渡すよりも安全で明確です。シェルインジェクションやファイル名に含まれるスペースによる問題を避けられます。

ファイルを保存してエディターを終了します。

次に、スクリプトを実行します。

python3 list_files.py

ls -l コマンドの出力がターミナルに直接表示されます。これは、デフォルトでは subprocess.run がコマンドの出力を標準出力(画面)に送るためです。

出力の取得

前のステップでは、出力が画面に直接表示されました。しかし、自動化スクリプトでは通常、その出力を変数に取得し、Python コードで処理できるようにします(たとえば、ファイル名の解析やエラーの確認など)。

これは、単にコマンドを実行することと、コマンドをプログラムに組み込むことの違いです。出力をデータとして取得すれば、結果を人がターミナルで読むだけでなく、スクリプトがその内容に基づいて判断できるようになります。

そのためには、capture_output=True 引数を使用します。

もう一度 list_files.py を開きます。

nano list_files.py

次のようにコードを更新して、結果を取得します。

import subprocess

## Run the command and capture the output
result = subprocess.run(["ls", "-l"], capture_output=True)

## Print the captured result object
print(result)

ファイルを保存してエディターを終了します。

スクリプトを実行します。

python3 list_files.py

次のような出力が表示されます。

CompletedProcess(args=['ls', '-l'], returncode=0, stdout=b'total 0\n-rw-rw-r-- 1 labex labex 0 ...', stderr=b'')

これは CompletedProcess オブジェクトです。stdout=b'...' の部分に注目してください。b は bytes(バイト列)を表します。つまり、Python は人間が読みやすいテキスト(文字列)ではなく、コンピューターが扱う生のデータをそのまま取得しています。次のステップで、このデータを人間が読めるテキストに変換します。

出力のデコード

出力はバイト列として取得されるため、読みやすく扱いやすい標準的な Python 文字列(UTF-8)にデコードする必要があります。

バイト列とテキストの違いは、Python の基本的な概念です。コンピューターは最終的に生のバイト列をやり取りしますが、人間は通常テキストを必要とします。自動化で発生する分かりにくいバグの多くは、現在扱っているデータがどちらの形式なのかを意識していないことが原因です。

もう一度 list_files.py を開きます。

nano list_files.py

stdout 属性にアクセスして、出力をデコードするようにコードを更新します。

import subprocess

## Run the command and capture the output
result = subprocess.run(["ls", "-l"], capture_output=True)

## Decode the bytes to a string
output_string = result.stdout.decode("utf-8")

## Print the clean string
print("--- Command Output ---")
print(output_string)

ファイルを保存してエディターを終了します。

スクリプトを実行します。

python3 list_files.py

今度は、シェルから ls -l コマンドを直接実行した場合と同じような、整形された出力が表示されます。ただし今回は、Python が出力しています。

注: 代わりに subprocess.run() に text=True(または universal_newlines=True)を渡すと、Python が自動的にデコードします。ただし、バイナリデータを扱ううえで .decode() メソッドを理解することは重要です。

エラー処理

コマンドが失敗すると、どうなるでしょうか。たとえば、存在しないディレクトリを一覧表示しようとした場合です。適切なスクリプトでは、このようなエラーを処理する必要があります。

Unix 系システムでは、コマンドの失敗は通常、ゼロ以外の終了ステータスで通知されます。subprocess.run(..., check=True) を使うと、このオペレーティングシステムの規約を Python の例外に変換できます。そのため、通常の Python の制御フローでスクリプトからエラーに対応できます。

これを示すために、handle_error.py という新しいスクリプトを作成します。

nano handle_error.py

次のコードを追加します。ここでは、存在しない /nonexistent ディレクトリを一覧表示します。また、check=True を指定しています。これにより、コマンドがゼロ以外の終了コードを返した場合(つまり失敗した場合)に、Python が CalledProcessError 例外を発生させます。

import subprocess

try:
    ## Try to list a directory that does not exist
    ## check=True raises an exception if the command fails
    subprocess.run(["ls", "/nonexistent"], check=True, capture_output=True)

except subprocess.CalledProcessError as e:
    print("An error occurred!")
    print(f"Return code: {e.returncode}")
    ## We can also print the error message generated by the command (stderr)
    print(f"Error message: {e.stderr.decode('utf-8')}")

ファイルを保存してエディターを終了します。

スクリプトを実行します。

python3 handle_error.py

エラーが捕捉されたことを示す、次のような出力が表示されます。

An error occurred!
Return code: 2
Error message: ls: cannot access '/nonexistent': No such file or directory

try-except ブロックを使わなければ、プログラムは詳細なトレースバックを表示して異常終了します。この方法なら、スクリプトで失敗を検出し、次に何をするかを決められます(たとえば、エラーをログに記録する、再試行する、または正常に終了するなど)。

まとめ

この実験では、Python の subprocess モジュールを使ってシェルコマンドを実行する基本を学びました。次の操作を練習しました。

  • subprocess モジュールをインポートする。
  • subprocess.run() を使ってコマンドを実行する。
  • capture_output=True を使って標準出力を取得する。
  • バイト列の出力を読みやすい文字列にデコードする。
  • check=True と try-except ブロックを使ってコマンドの失敗を処理する。

これらのスキルは、Python で自動化スクリプト、デプロイツール、システム管理ユーティリティを構築するうえで欠かせません。