Pythonとシェルスクリプト(sh)の連携:実践的テクニックとライブラリ活用術

システム開発やデータ処理、日々の自動化タスクにおいて、Pythonとシェルスクリプト(sh/Bash)は非常に強力なツールです。Pythonはその豊富なライブラリと高い可読性で複雑なロジックやデータ処理を得意とし、一方シェルスクリプトはファイル操作やコマンド実行、パイプライン処理など、OSレベルのタスクを簡潔に記述できます。

これら二つの言語は、それぞれ得意分野が異なりますが、組み合わせることで互いの短所を補い、より効率的でパワフルなソリューションを構築できます。例えば、シェルスクリプトでファイルの一括処理を行い、その結果をPythonで詳細に分析する、あるいはPythonで生成したデータをシェルコマンドで他のシステムに連携するなど、様々な応用が考えられます。🔧

このブログでは、Pythonとシェルスクリプトを連携させるための具体的な方法、特にPythonの標準ライブラリであるsubprocessモジュールの使い方や、連携をさらに容易にする便利なPythonライブラリshについて詳しく解説します。また、シェルスクリプト側からPythonスクリプトを実行する方法や、連携をスムーズに行うための実践的なテクニックや注意点にも触れていきます。

Pythonスクリプト内から外部のシェルコマンドを実行する必要がある場面は多々あります。ここでは、そのための主要な方法をいくつか紹介します。

1. os.system (非推奨)

最も簡単な方法の一つですが、現在では非推奨とされています。

import os

# 例: カレントディレクトリの内容を表示
return_code = os.system("ls -l")
print(f"終了コード: {return_code}")
非推奨の理由:
  • セキュリティリスク: ユーザーからの入力をコマンド文字列に含める場合、シェルインジェクション攻撃に対して脆弱です。
  • 柔軟性の欠如: コマンドの標準出力や標準エラー出力を簡単に取得・制御することができません。戻り値として得られるのは終了コードのみです。
  • エラーハンドリング: コマンド実行時のエラーを詳細にハンドリングするのが難しいです。
Pythonの公式ドキュメントでも、より強力で安全なsubprocessモジュールの使用が推奨されています。

2. subprocessモジュール (推奨) ✨

Python 3.5以降で推奨される、外部プロセスを起動・管理するための標準モジュールです。os.systemよりもはるかに高機能で安全です。

2.1. subprocess.run()

シンプルにコマンドを実行し、その完了を待つ場合に最もよく使われる関数です。Python 3.5で導入されました。

import subprocess

# コマンドをリスト形式で渡すのが安全
try:
    # capture_output=True で標準出力と標準エラーを取得
    # text=True で出力を文字列としてデコード (Python 3.7+)
    # check=True で終了コードが非ゼロの場合に CalledProcessError 例外を発生させる
    result = subprocess.run(["ls", "-l"], capture_output=True, text=True, check=True)

    print("コマンド成功!")
    print("標準出力:")
    print(result.stdout)
    print("標準エラー:")
    print(result.stderr) # 成功時は通常空
    print(f"終了コード: {result.returncode}")

except FileNotFoundError:
    print("エラー: コマンドが見つかりません。")
except subprocess.CalledProcessError as e:
    print(f"エラー: コマンド実行に失敗しました (終了コード: {e.returncode})")
    print("標準出力:")
    print(e.stdout)
    print("標準エラー:")
    print(e.stderr)
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

# 存在しないファイルを表示しようとしてエラーを起こす例
try:
    subprocess.run(["ls", "存在しないファイル"], capture_output=True, text=True, check=True)
except subprocess.CalledProcessError as e:
    print("\n--- エラー発生時の例 ---")
    print(f"エラー: コマンド実行に失敗しました (終了コード: {e.returncode})")
    print("標準エラー:")
    print(e.stderr)

主な引数:

  • args: 実行するコマンドと引数をリストまたは文字列で指定します。リスト形式が推奨されます(シェルインジェクション対策)。
  • capture_output (bool): Trueにすると、標準出力(stdout)と標準エラー出力(stderr)をキャプチャします。デフォルトはFalse
  • text (bool): Trueにすると、stdoutstderrをシステムのデフォルトエンコーディングでデコードし、文字列として扱います (Python 3.7+)。encoding引数でエンコーディングを指定することも可能です。デフォルトはFalse(バイト列)。
  • check (bool): Trueにすると、コマンドの終了コードが0以外(エラー終了)の場合にCalledProcessError例外を送出します。デフォルトはFalse
  • shell (bool): Trueにすると、シェル経由でコマンドを実行します。コマンドを文字列で渡すことが必須になります。セキュリティリスクが高まるため、信頼できない入力を扱う場合はFalse(デフォルト)のままにし、引数をリストで渡すことが強く推奨されます。
  • input (bytes or str): サブプロセスの標準入力に渡すデータを指定します。text=Trueの場合は文字列、そうでなければバイト列で渡します。
  • timeout (int or float): コマンドの実行タイムアウト時間を秒数で指定します。タイムアウトした場合、TimeoutExpired例外が発生します。
  • cwd (str): コマンドを実行する際のワーキングディレクトリを指定します。
  • env (dict): サブプロセスの環境変数を指定します。デフォルトでは親プロセスの環境変数を引き継ぎます。

2.2. subprocess.Popen()

より低レベルなインターフェースで、プロセスの生成と管理をより柔軟に行いたい場合に使用します。非同期実行、パイプラインの構築、プロセスとの対話的な通信などが可能です。

import subprocess

# パイプライン処理の例: ls -l | grep ".py"
try:
    # Popenで最初のプロセスを開始 (ls -l)
    # stdout=subprocess.PIPE で標準出力をパイプに接続
    p1 = subprocess.Popen(["ls", "-l"], stdout=subprocess.PIPE, text=True)

    # 2番目のプロセスを開始 (grep ".py")
    # stdin=p1.stdout で p1 の標準出力を p2 の標準入力に接続
    # stdout=subprocess.PIPE で標準出力をキャプチャ
    p2 = subprocess.Popen(["grep", ".py"], stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

    # p1 の標準出力パイプを閉じる (p2 が EOF を受け取れるように)
    p1.stdout.close()

    # p2 の完了を待ち、出力を取得
    stdout, stderr = p2.communicate()

    print("パイプライン処理成功!")
    print("最終出力 (stdout):")
    print(stdout)
    if stderr:
        print("最終出力 (stderr):")
        print(stderr)
    print(f"grepの終了コード: {p2.returncode}")

except FileNotFoundError as e:
    print(f"エラー: コマンドが見つかりません。 {e.filename}")
except Exception as e:
    print(f"予期せぬエラーが発生しました: {e}")

# 非同期実行の例
print("\n--- 非同期実行の例 ---")
try:
    # バックグラウンドで sleep 5 を実行
    process = subprocess.Popen(["sleep", "5"])
    print(f"プロセスを開始しました (PID: {process.pid})。完了を待たずに次の処理へ進みます。")
    # ... ここで他の処理を実行 ...
    print("他の処理を実行中...")
    # プロセスの完了を待つ (任意)
    return_code = process.wait() # または communicate()
    print(f"プロセス (PID: {process.pid}) が終了しました。終了コード: {return_code}")
except Exception as e:
    print(f"エラー: {e}")

Popenrun()よりも複雑ですが、以下のような高度な制御が可能です。

  • プロセスの実行中に標準入力へデータを送り込んだり、標準出力/エラー出力をリアルタイムで読み取ったりする。
  • 複数のプロセスをパイプ(|)で接続する。
  • プロセスをバックグラウンドで実行し、完了を待たずにPythonスクリプトの他の処理を続ける(非同期実行)。

セキュリティ上の注意点: シェルインジェクション 🚫

subprocess.run()subprocess.Popen()shell=True を指定すると、コマンドがシステムのシェル(例: /bin/sh)を介して解釈・実行されます。これは、;&&|| といったシェルのメタ文字を使った複雑なコマンドを単一の文字列として簡単に実行できる反面、重大なセキュリティリスクを伴います。

もし、ユーザー入力などの信頼できない外部からの値をコマンド文字列に直接埋め込んでいる場合、悪意のあるユーザーが予期しないコマンドを挿入して実行できてしまう可能性があります(シェルインジェクション)。

import subprocess

# 危険な例: ユーザー入力をそのままコマンド文字列に埋め込む
user_input = "legit_file.txt; rm -rf /" # 悪意のある入力例
# subprocess.run(f"ls {user_input}", shell=True) # 絶対にやってはいけない!

# 安全な方法: shell=False (デフォルト) を使い、引数をリストで渡す
# これにより、各引数はシェルによって解釈されず、そのままコマンドに渡される
try:
    # user_input がファイル名として扱われる (中にメタ文字があっても安全)
    subprocess.run(["ls", user_input], check=True)
except subprocess.CalledProcessError as e:
    print(f"ls コマンドがエラー終了: {e}") # おそらくファイルが見つからないエラー
except FileNotFoundError:
    print("ls コマンドが見つかりません。")

# どうしても shell=True が必要な場合 (非推奨) は、shlex.quote() でエスケープする
import shlex
safe_user_input = shlex.quote(user_input)
# print(f"エスケープされた入力: {safe_user_input}")
# subprocess.run(f"ls {safe_user_input}", shell=True, check=True) # これでもリスクは残る場合がある

ベストプラクティス: 可能な限り shell=False (デフォルト) を使用し、コマンドと引数はリスト形式で subprocess.run()Popen() に渡してください。これにより、引数がシェルによって解釈されるのを防ぎ、シェルインジェクションのリスクを大幅に低減できます。

逆に、シェルスクリプトからPythonスクリプトを呼び出し、その機能を利用することも一般的です。

1. 単純な実行

シェルからPythonインタプリタにスクリプトファイルを指定して実行します。

#!/bin/bash

echo "Pythonスクリプトを実行します..."
python3 my_script.py
# もしくは python my_script.py (環境による)

echo "Pythonスクリプトの実行が完了しました。"

my_script.py の内容は以下のようになります。

# my_script.py
print("こんにちは、Pythonスクリプトからです!")
# ... 何らかの処理 ...

2. 引数を渡す

シェルスクリプトからPythonスクリプトへ情報を渡したい場合、コマンドライン引数を使用します。

#!/bin/bash

name="シェル"
count=5

echo "Pythonスクリプトに引数を渡して実行します..."
python3 process_data.py "$name" "$count" # 変数はダブルクォートで囲むのが安全

echo "Pythonスクリプトの実行が完了しました。"

Python側ではsys.argvまたはargparseモジュールを使って引数を受け取ります。

# process_data.py
import sys
import argparse

print(f"受け取った引数のリスト (sys.argv): {sys.argv}")

if len(sys.argv) > 2:
    name_arg = sys.argv[1]
    count_arg = sys.argv[2]
    print(f"sys.argv から取得: 名前={name_arg}, 回数={count_arg}")
else:
    print("引数が不足しています (sys.argv)")

print("-" * 20)

# argparse を使う方がより堅牢で分かりやすい
parser = argparse.ArgumentParser(description='シェルからデータを受け取るPythonスクリプト')
parser.add_argument('name', type=str, help='処理対象の名前')
parser.add_argument('count', type=int, help='処理回数')
# オプショナルな引数も定義できる
parser.add_argument('--verbose', '-v', action='store_true', help='詳細なログを出力する')

try:
    args = parser.parse_args() # sys.argv[1:] を自動的に解析
    print(f"argparse から取得: 名前={args.name}, 回数={args.count}, Verbose={args.verbose}")

    print(f"\n'{args.name}' を {args.count} 回処理します...")
    for i in range(args.count):
        if args.verbose:
            print(f"  処理 {i+1}...")
        # ... 何らかの処理 ...
    print("処理完了!")

except SystemExit:
    # argparse は引数エラー時に自動でヘルプを表示し終了する
    print("引数の解析に失敗しました。")
except Exception as e:
    print(f"エラーが発生しました: {e}")

Tips: シェルスクリプトからPythonに引数を渡す際は、スペースを含む可能性のある変数はダブルクォート(")で囲む習慣をつけましょう。Python側ではargparseを使うと、引数の型チェックやヘルプメッセージの自動生成などができ、非常に便利です。👍

3. Pythonスクリプトの出力をシェル変数に格納

Pythonスクリプトが標準出力に書き出した結果を、シェルスクリプト側で変数として受け取りたい場合は、コマンド置換($(...))を使います。

#!/bin/bash

input_value=10

echo "Pythonスクリプトを実行し、結果を変数に格納します..."
# python_calculator.py が標準出力に計算結果を出力すると仮定
result=$(python3 python_calculator.py "$input_value")

# エラーチェック (Pythonスクリプトが失敗した場合など)
if [ $? -ne 0 ]; then
  echo "Pythonスクリプトの実行に失敗しました。" >&2
  exit 1
fi

echo "Pythonスクリプトからの結果: $result"

# 結果を使ってさらに処理
processed_result=$((result * 2))
echo "シェルでさらに処理した結果: $processed_result"

対応するPythonスクリプト(例: python_calculator.py):

# python_calculator.py
import sys

if len(sys.argv) > 1:
    try:
        input_num = int(sys.argv[1])
        # 計算結果を標準出力に書き出す
        print(input_num * input_num)
    except ValueError:
        print("エラー: 数値を入力してください。", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"エラー: {e}", file=sys.stderr)
        sys.exit(1)
else:
    print("エラー: 引数がありません。", file=sys.stderr)
    sys.exit(1)

Tips: Pythonスクリプト側では、シェルで受け取りたい結果のみを標準出力(print())に出力し、エラーメッセージやデバッグ情報は標準エラー出力(print(..., file=sys.stderr))に出力するように区別すると、シェル側でのハンドリングが容易になります。

Pythonとシェルの連携をさらにスムーズにするための便利なライブラリを紹介します。

1. sh ライブラリ

shは、外部コマンドをまるでPythonの関数のように呼び出せるようにするサードパーティライブラリです。subprocessよりも直感的で簡潔な記述が可能になる場合があります。Unix系OS(Linux, macOSなど)でのみ動作し、Windowsはサポートされていません。

インストール: pip install sh

import sh
import sys

try:
    # コマンドを関数のように呼び出す
    print("--- ls コマンド ---")
    # 引数は通常の関数呼び出しのように渡せる
    # キーワード引数はハイフン付きオプションに対応 (例: _long=True は -l)
    # sh.ls("-l", "/tmp", color="never") と同等
    ls_output = sh.ls("-l", "/tmp", color="never")
    print(ls_output) # stdout が文字列として返る

    print("\n--- git status ---")
    # git status を実行
    git_status = sh.git.status() # サブコマンドは属性アクセスで
    print(git_status)

    print("\n--- パイプ処理 (ls -1 | wc -l) ---")
    # パイプは _in キーワード引数で実現
    file_count = sh.wc("-l", _in=sh.ls("-1"))
    print(f"ファイル数: {file_count.strip()}") # strip() で末尾の改行を削除

    print("\n--- 出力リダイレクト ---")
    # ファイルへのリダイレクト
    sh.echo("shライブラリからファイルに書き込み", _out="sh_output.txt")
    print("sh_output.txt に書き込みました。")
    print(sh.cat("sh_output.txt"))
    sh.rm("sh_output.txt")

    print("\n--- エラーハンドリング ---")
    # 存在しないファイルを ls しようとすると ErrorReturnCode 例外が発生
    sh.ls("/存在しないパス")

except sh.ErrorReturnCode as e:
    print(f"\nエラー発生!")
    print(f"コマンド: {e.full_cmd}")
    print(f"終了コード: {e.exit_code}")
    print(f"標準出力:\n{e.stdout.decode()}") # バイト列なのでデコード
    print(f"標準エラー:\n{e.stderr.decode()}") # バイト列なのでデコード
except sh.CommandNotFound as e:
    print(f"\nエラー: コマンド '{e.cmd}' が見つかりません。")
except Exception as e:
    print(f"予期せぬエラー: {e}")

# バックグラウンド実行
print("\n--- バックグラウンド実行 ---")
try:
    # _bg=True でバックグラウンド実行
    process = sh.sleep(3, _bg=True)
    print(f"sleep 3 をバックグラウンドで開始 (PID: {process.pid})")
    # ... 他の処理 ...
    print("他の処理を実行中...")
    process.wait() # 完了を待つ
    print("sleep プロセスが完了しました。")
except Exception as e:
    print(f"エラー: {e}")

shライブラリのメリット:

  • シェルコマンドをPythonの関数のように直感的に記述できる。
  • 引数やオプションの渡し方がシンプル。
  • パイプ処理やリダイレクトも比較的簡単に書ける。
  • サブコマンド(例: git status)もメソッドチェーンのように記述可能。

shライブラリのデメリット/注意点:

  • Unix系OS専用であり、Windowsでは動作しない。
  • サードパーティライブラリのため、別途インストールが必要。
  • subprocessほど細かい制御(例: stdin/stdout/stderrのストリーミング処理)は得意ではない場合がある。
  • コマンド名がPythonの予約語や既存の変数名と衝突する可能性がある(その場合は sh.Command("command-name")(...) のように呼び出す)。

2. argparse ライブラリ

これはシェル連携に特化したライブラリではありませんが、「シェルスクリプトからPythonスクリプトを実行する方法」で触れたように、Pythonスクリプトがコマンドライン引数を受け取る際のデファクトスタンダードです。シェルから渡された引数をパースし、型チェックやヘルプメッセージ生成を行うのに非常に役立ちます。

# (再掲) process_data.py の argparse 部分
import argparse

parser = argparse.ArgumentParser(description='シェルからデータを受け取るPythonスクリプト')
parser.add_argument('name', type=str, help='処理対象の名前')
parser.add_argument('count', type=int, help='処理回数')
parser.add_argument('--verbose', '-v', action='store_true', help='詳細なログを出力する')
parser.add_argument('--output-file', '-o', type=str, default='output.txt', help='出力ファイル名 (デフォルト: output.txt)')

try:
    args = parser.parse_args()
    print(f"名前: {args.name}")
    print(f"回数: {args.count}")
    print(f"Verbose: {args.verbose}")
    print(f"出力ファイル: {args.output_file}")
    # ... args を使って処理 ...
except Exception as e:
    print(f"引数解析エラー: {e}")
    # parser.print_help() # ヘルプを表示するなど

シェルスクリプトから python3 process_data.py シェル 10 -v -o result.log のように呼び出すと、これらの引数が適切にパースされます。

3. pathlib ライブラリ

Python 3.4 で標準ライブラリに追加されたpathlibは、ファイルシステムパスをオブジェクト指向的に扱うためのモジュールです。従来のos.pathよりも直感的でコードが読みやすくなることが多く、シェルスクリプトで行いがちなファイルやディレクトリの操作(存在確認、作成、削除、移動、一覧取得など)をPython側でエレガントに記述できます。

from pathlib import Path
import os # 比較用

# カレントディレクトリ
current_dir = Path.cwd()
print(f"カレントディレクトリ: {current_dir}")

# ホームディレクトリ
home_dir = Path.home()
print(f"ホームディレクトリ: {home_dir}")

# パスの結合 ( / 演算子を使える)
config_path = home_dir / ".config" / "my_app" / "settings.ini"
print(f"設定ファイルのパス: {config_path}")

# ディレクトリの作成 (mkdir)
# exist_ok=True: 既に存在してもエラーにしない
# parents=True: 中間ディレクトリもまとめて作成する
config_path.parent.mkdir(parents=True, exist_ok=True)
print(f"{config_path.parent} ディレクトリを作成 (または存在を確認)")

# ファイルの存在確認 (exists)
if config_path.exists():
    print(f"{config_path} は存在します。")
else:
    print(f"{config_path} は存在しません。")
    # ファイルへの書き込み (touch + write_text)
    config_path.touch() # 空ファイル作成 (シェルコマンドの touch)
    config_path.write_text("[General]\nsetting1 = value1", encoding='utf-8')
    print(f"{config_path} に書き込みました。")

# ファイルの読み込み (read_text)
if config_path.is_file():
    content = config_path.read_text(encoding='utf-8')
    print(f"\n{config_path} の内容:")
    print(content)

# ディレクトリ内のファイル/ディレクトリ一覧 (iterdir, glob)
print(f"\n{home_dir} 直下の内容:")
for item in home_dir.iterdir():
    # is_dir(), is_file() で種類を判別
    item_type = "ディレクトリ" if item.is_dir() else "ファイル" if item.is_file() else "その他"
    # print(f" - {item.name} ({item_type})") # 全て表示すると多いのでコメントアウト

print(f"\n{current_dir} 内の Python ファイル (*.py):")
# glob でパターンマッチング (シェルの *)
for py_file in current_dir.glob("*.py"):
    print(f" - {py_file.name}")

# ファイルの削除 (unlink)
# config_path.unlink(missing_ok=True) # missing_ok=True: ファイルが存在しなくてもエラーにしない
# print(f"{config_path} を削除しました。")

# ディレクトリの削除 (rmdir - 空である必要あり)
# config_path.parent.rmdir() # settings.ini があるとエラーになる
# print(f"{config_path.parent} を削除しました。")

# shutil モジュールと組み合わせるとさらに強力 (コピー、移動、ツリー削除など)
# import shutil
# shutil.rmtree(config_path.parent) # ディレクトリツリーごと削除

pathlibを使うことで、ファイルパスに関連する多くの操作がPythonicに記述でき、シェルコマンドの呼び出し回数を減らせる可能性があります。

4. その他(Pandas, Requestsなど)

シェルスクリプトだけでは扱いにくい、あるいは効率が悪いタスクは、Pythonの得意分野です。

  • データ処理・分析: CSVやExcel、JSONなどの複雑なデータ構造を扱う場合、Pandasライブラリを使えば強力なデータ操作・分析が可能です。シェルスクリプト(awk, sed, grepなど)でも可能ですが、コードが複雑になりがちです。
  • Web API連携: Requestsライブラリを使えば、Web APIからのデータ取得や送信が非常に簡単になります。curlコマンドでも可能ですが、認証処理やレスポンスのパースなどをPythonで行う方が柔軟な場合が多いです。
  • 複雑なロジック: シェルスクリプトは手続き的な処理が主ですが、Pythonならオブジェクト指向プログラミングや豊富な制御構文、エラーハンドリング機構を活用して、より複雑でメンテナンス性の高いロジックを実装できます。

これらのライブラリを活用し、シェルスクリプトでは難しい処理をPythonに任せ、その結果をシェルスクリプトで利用する、といった分業が効果的です。

1. 環境変数の共有

シェルスクリプトとPythonスクリプト間で情報をやり取りする簡単な方法の一つが環境変数です。

  • シェル → Python: シェルでexport MY_VAR="value"のように環境変数を設定すると、Pythonスクリプト内ではos.environ.get('MY_VAR')でその値を取得できます。subprocessでPythonを呼び出す際、デフォルトで親(シェル)の環境変数が引き継がれます。
  • Python → シェル (サブプロセス): subprocess.run()Popen()env引数を使って、起動するサブプロセス(シェルコマンドや別のスクリプト)に特定の環境変数を渡すことができます。
#!/bin/bash

# シェルで環境変数を設定
export SHARED_SECRET="abc123xyz"
export PROCESS_MODE="production"

echo "シェルからPythonに環境変数を渡して実行..."
python3 env_reader.py

echo "Pythonからシェルに環境変数を渡して実行..."
python3 env_setter.py # この中で subprocess を使ってシェルコマンドを実行
# env_reader.py
import os

secret = os.environ.get('SHARED_SECRET')
mode = os.environ.get('PROCESS_MODE', 'development') # デフォルト値も設定可能

if secret:
    print(f"Python側で環境変数 SHARED_SECRET を受け取りました: {secret[:3]}*** (隠蔽)")
else:
    print("環境変数 SHARED_SECRET が設定されていません。")

print(f"処理モード: {mode}")
# env_setter.py
import subprocess
import os

print("Pythonからサブプロセス(シェルコマンド)に環境変数を渡します。")

# 現在の環境変数をコピーし、追加・変更する
child_env = os.environ.copy()
child_env["PYTHON_VAR"] = "Set by Python"
child_env["PROCESS_MODE"] = "testing" # シェルで設定したものを上書き

try:
    # env 引数でカスタム環境変数を渡す
    # シェルコマンド `env | grep -E 'PYTHON_VAR|PROCESS_MODE'` を実行
    result = subprocess.run(
        "env | grep -E 'PYTHON_VAR|PROCESS_MODE'",
        shell=True, # この例ではパイプを使うため shell=True を使用 (注意が必要)
        capture_output=True,
        text=True,
        check=True,
        env=child_env
    )
    print("サブプロセスに渡された関連環境変数:")
    print(result.stdout)
except Exception as e:
    print(f"サブプロセスの実行エラー: {e}")

2. エラーハンドリング

  • Python側 (subprocess): subprocess.run()check=Trueを使うか、Popenの結果のreturncodeを確認し、非ゼロの場合はエラーとして扱います。try...except subprocess.CalledProcessErrorでエラー時の処理を記述します。標準エラー出力(stderr)の内容も確認するとデバッグに役立ちます。
  • シェル側: コマンド実行直後に特殊変数$?で終了コードを確認します(0が成功、それ以外がエラー)。set -eオプションを使うと、コマンドがエラー終了した場合にスクリプト全体を即座に終了させることができます。set -o pipefailを使うと、パイプラインの途中でエラーが発生した場合も検知できます。
#!/bin/bash

# エラー発生時にスクリプトを終了 (-e)
# パイプラインの途中でのエラーも検知 (-o pipefail)
set -eo pipefail

echo "エラーハンドリングのテスト"

python3 non_existent_script.py # 存在しないスクリプトを実行しようとする

# set -e により、上の行でエラーが発生すると、以下の行は実行されずにスクリプトが終了する
echo "このメッセージは表示されません。"

適切なエラーハンドリングは、スクリプトの信頼性を高める上で非常に重要です。

3. 一時ファイルの扱い

シェルとPython間で大量のデータをやり取りする場合、標準入出力だけでは扱いにくいことがあります。その場合、一時ファイルを利用する方法があります。

  • Pythonのtempfileモジュールを使うと、安全な一時ファイルや一時ディレクトリを簡単に作成・管理できます。
  • シェルスクリプトからは、その一時ファイルのパスを引数などで受け取り、読み書きを行います。
  • 処理が終わったら、Python側で忘れずに一時ファイルを削除します(tempfileのコンテキストマネージャを使うと自動削除が容易)。
import tempfile
import subprocess
from pathlib import Path

# 大量のデータを生成 (例)
large_data = "\n".join([f"Data line {i}" for i in range(10000)])

# 一時ファイルを作成 (コンテキストマネージャを使用)
with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', delete=False) as temp_file:
    temp_filepath = Path(temp_file.name)
    print(f"一時ファイルを作成: {temp_filepath}")
    temp_file.write(large_data)
    # ファイルポインタを先頭に戻さないと、直後の読み込みで空になる場合がある
    temp_file.seek(0)

    # シェルスクリプトに一時ファイルのパスを渡して実行 (例: wc -l)
    try:
        print("シェルスクリプト (wc -l) を実行して行数をカウント...")
        result = subprocess.run(["wc", "-l", str(temp_filepath)], capture_output=True, text=True, check=True)
        print(f"シェルからの結果:\n{result.stdout.strip()}")
    except Exception as e:
        print(f"シェルコマンドの実行エラー: {e}")
    finally:
        # finally ブロックで確実に一時ファイルを削除
        # コンテキストマネージャの delete=True でも良いが、シェル連携時は False にして明示的に消す方が安全な場合も
        print(f"一時ファイルを削除: {temp_filepath}")
        temp_filepath.unlink()

4. 処理の分担

それぞれの得意分野を活かして処理を分担しましょう。

  • 前処理・後処理 (Python): 複雑なデータ整形、バリデーション、APIからのデータ取得、結果の集計・可視化などはPythonが得意。
  • コア処理・並列実行 (シェル): 既存のコマンドラインツールの利用、単純なファイル操作の繰り返し、パイプラインによる連携、xargsなどを使った簡単な並列処理はシェルスクリプトが簡潔に書ける場合がある。

例えば、Pythonで大量のURLリストを生成し、シェルスクリプト(wgetcurlxargs)で並列にダウンロード、その後Pythonでダウンロードしたファイルを処理する、といった流れが考えられます。

5. Python仮想環境とシェルスクリプト

Pythonプロジェクトで仮想環境(venv, condaなど)を使っている場合、シェルスクリプトからその環境内のPythonインタプリタやライブラリを使いたいことがあります。

  • シェルスクリプトの冒頭でsource /path/to/your/venv/bin/activateを実行して仮想環境を有効化する。
  • あるいは、仮想環境内のPythonインタプリタのフルパス(例: /path/to/your/venv/bin/python)を直接指定してPythonスクリプトを実行する。
#!/bin/bash

VENV_PATH="/path/to/your/project/venv" # 実際のパスに置き換える

# 方法1: 仮想環境を activate
# source "$VENV_PATH/bin/activate"
# python my_project_script.py
# deactivate # 必要であれば非アクティブ化

# 方法2: 仮想環境内の Python を直接指定
PYTHON_EXEC="$VENV_PATH/bin/python"

if [ -f "$PYTHON_EXEC" ]; then
  echo "仮想環境内のPythonを使ってスクリプトを実行します..."
  "$PYTHON_EXEC" my_project_script.py --some-option
else
  echo "エラー: 仮想環境のPythonが見つかりません: $PYTHON_EXEC" >&2
  exit 1
fi

6. シェルスクリプトのベストプラクティス

連携するシェルスクリプト自体の品質を高めることも重要です。

  • Shebang: スクリプトの1行目には #!/bin/bash#!/usr/bin/env bash を記述する。
  • エラー処理: set -e, set -u, set -o pipefail をスクリプト冒頭で設定することを検討する(-uは未定義変数アクセスでエラー)。
  • 変数展開: 変数はダブルクォートで囲む ("$variable") 習慣をつける(スペース等による意図しない分割を防ぐ)。
  • コマンド置換: バッククォート (`command`) ではなく $(command) を使う。ネストが可能。
  • 可読性: 関数を活用し、適切なインデントとコメントを心がける。
  • ツール活用: ShellCheck などの静的解析ツールを使って潜在的な問題を検出する。

Pythonとシェルスクリプトは、それぞれ異なる強みを持つ強力なツールです。subprocessモジュールやshライブラリ、pathlibなどを活用することで、これら二つの世界を効果的に連携させ、より複雑で高度なタスクを自動化し、効率化することが可能になります。 🚀

連携の際には、以下の点を意識することが重要です。

  • 各言語の得意な処理を分担させる。
  • subprocessモジュールを基本とし、必要に応じてshライブラリを検討する。
  • セキュリティ(特にシェルインジェクション)に常に注意を払い、安全なコーディングを心がける (shell=False, 引数リスト形式)。
  • エラーハンドリングを適切に行い、スクリプトの信頼性を高める。
  • 環境変数、コマンドライン引数、一時ファイルなど、状況に応じた情報連携方法を選択する。
  • Python側だけでなく、連携するシェルスクリプト自体の品質にも気を配る。

このブログで紹介したテクニックやライブラリが、皆さんの開発や自動化の一助となれば幸いです。Happy scripting! 😊


比較表: Pythonからシェルコマンド実行方法の概要

方法 推奨度 主な特徴 メリット デメリット/注意点
os.system() 非推奨 最も単純なコマンド実行 記述が非常に簡単 セキュリティリスク(シェルインジェクション)、出力取得不可、柔軟性低い
subprocess.run() 推奨 同期的なコマンド実行、完了待ち 安全(リスト引数)、出力/エラー取得可、終了コード確認、タイムアウト、柔軟性高い Popenよりは機能限定的
subprocess.Popen() 推奨 (高度) 非同期実行、パイプライン、プロセス間通信 最も高機能・柔軟、詳細な制御が可能 run()より記述が複雑になる場合がある
sh ライブラリ 状況による コマンドをPython関数のように呼び出し 直感的、記述が簡潔、パイプ/リダイレクト容易 Unix系専用、要インストール、subprocess程の低レベル制御は不得意な場合も