Bash 入門

Bash getopts コマンド

コマンドラインオプション(Command line options)は、ユーザーがスクリプトのソースコードを修正することなく、スクリプトの挙動をカスタマイズできる非常に柔軟な手法を提供します。

これまでの章では、$1$2 などの位置パラメータ$@ などの特殊変数を使用してユーザー入力を取得してきました。しかし、スクリプトが複雑になると、より高度なオプション解析メカニズムが必要になります。特に、詳細情報を出力する -v のようなフラグ(Flags)や、-f filename のようにファイル名を指定する引数付きのオプションを扱う場合、単純な位置変数だけでは対応が難しくなります。

Bash はこの問題を解決するために、getopts というビルトインコマンドを提供しています。これにより、スクリプトは標準的な方法で、短精度オプション(シングルキャラクターオプション)とその関連引数を一つずつ処理できるようになります。

1. getopts の基礎を深く理解する

getopts コマンドは、コマンドラインオプションと引数をパース(解析)するために使用されます。渡されたオプションを順次処理するため、ループ文と組み合わせてスクリプトに提供されたすべてのオプションを走査するのに最適です。

getopts の基本構文は以下の通りです。

getopts option_string name [arguments]
  • option_string(オプション文字列): 各文字が有効な短精度オプションを表す文字列です。
    • 文字の後にコロン (:) が続く場合、そのオプションは引数を伴う必要があることを示します。
    • 文字列がコロン (:) で始まる場合、getopts は「サイレントエラー報告(silent error reporting)」モードで動作します。このモードでは、無効なオプションに遭遇してもエラーメッセージを自動出力せず、変数 name? に設定し、無効なオプション文字を OPTARG 変数に格納します。また、引数が不足している場合は name: に設定されます。
  • name: シェル変数の名前です。getopts はオプションを見つけるたびに、その文字(例: a, b, c)をこの変数に代入します。
  • [arguments]: オプションの引数リストです。省略した場合、getopts はデフォルトで現在のシェルが受け取った位置パラメータ($@)をパースします。

getopts の実行時には、以下の極めて重要な内部変数が自動的にセットされます。

  • OPTIND: スクリプト起動時に 1 で初期化されます。次に処理すべき引数のインデックスを記録します。すべてのオプション処理が終わると、OPTIND は最初の非オプション引数を指します。これは「オプション」と「通常の引数」を切り分けるために不可欠です。
  • OPTARG: オプションが引数を必要とする場合(オプション文字列内でコロンが続く場合)、その引数の値が OPTARG に格納されます。

2. 基本的な例:コマンドラインオプションのパース

詳細フラグ (-v) と出力ファイル指定 (-o filename) を受け取るスクリプトを想定してみましょう。

#!/bin/bash

# OPTIND を 1 にリセット。スクリプトが source で読み込まれた際の混乱を防ぐため。
OPTIND=1

# 変数の初期化とデフォルト値の設定
VERBOSE=0
OUTPUT_FILE=""

# オプションのパース
# 'v' はシンプルなフラグ、'o:' は -o の後に引数が必須であることを示す
while getopts "vo:" opt; do
  case "$opt" in
    v)
      VERBOSE=1
      echo "詳細出力モード (Verbose mode) が有効になりました。"
      ;;
    o)
      OUTPUT_FILE="$OPTARG"
      echo "出力ファイルがセットされました: $OUTPUT_FILE"
      ;;
    \?) # 無効なオプションにマッチ
      echo "エラー: 無効なオプションです -$OPTARG" >&2
      exit 1
      ;;
    :) # 引数が不足しているオプションにマッチ
      echo "エラー: オプション -$OPTARG には引数が必要です。" >&2
      exit 1
      ;;
  esac
done

# 位置パラメータを左にシフトし、パース済みのオプションをスキップする
# 実行後、$1, $2 などは最初の「非オプション引数」を指す
shift $((OPTIND-1))

# 残りの位置パラメータを処理
if [ "$#" -gt 0 ]; then
  echo "残りの位置パラメータ: $@"
else
  echo "残りの位置パラメータはありません。"
fi

echo "スクリプトの実行が完了しました。"
echo "最終的な VERBOSE ステータス: $VERBOSE"
echo "最終的な OUTPUT_FILE ステータス: '$OUTPUT_FILE'"

スクリプトの実行方法:

  1. ./myscript.sh -v -o output.log input.txt (正常動作、残り引数あり)
  2. ./myscript.sh -o mydata.csv -v (正常動作、オプションの順序は不問)
  3. ./myscript.sh -x (エラー:無効なオプション)
  4. ./myscript.sh -o (エラー:引数不足)

2.1 コードの解説

  • OPTIND=1: getopts が引数リストの先頭から確実に解析を開始するようにします。
  • while getopts "vo:" opt; do: getopts がオプションを見つけ続ける限り、ループが回ります。
  • "vo:": オプション文字列。v は引数なし、o: は引数ありを意味します。
  • shift $((OPTIND-1)): ループ終了後、OPTIND は最初の非オプション引数を指しています。shift コマンドで既読のオプションをすべて「切り捨てる」ことで、$1 を正しく最初の通常引数に向けることができます。これが混合引数を扱う際の核心的なテクニックです。

3. ロングオプションと混合オプションの扱い

注意点として、Bash ビルトインの getopts は短精度オプション(1文字)専用に設計されています。--verbose--output-file といったロングオプションはネイティブではサポートしていません。

実務でロングオプションが必要な場合は、手動でパースするか、外部ツールの getopt (末尾に s がつかないもの)を組み合わせて使用します。しかし、ほとんどの標準的なスクリプトにおいては、getopts で十分対応可能です。

4. 実戦ケーススタディ:システムバックアップスクリプト

システム管理におけるバックアップスクリプトをブラッシュアップしましょう。プロフェッショナルなスクリプトには、ソースディレクトリ、ターゲットディレクトリ、そして「ドライラン (dry-run)」用のフラグが必要です。

#!/bin/bash
# system_backup.sh - コマンドラインオプション付きバックアップスクリプト

# OPTIND をリセット
OPTIND=1

# デフォルト値
SOURCE_DIR=""
DEST_DIR=""
DRY_RUN=0
VERBOSE=0
BACKUP_NAME="system_backup_$(date +%Y%m%d_%H%M%S)"

# ヘルプ表示関数
usage() {
  echo "Usage: $0 [-s ソースディレクトリ] [-d ターゲットディレクトリ] [-n バックアップ名] [-v] [-r]"
  echo "  -s <dir>   : バックアップ元ディレクトリ (必須)"
  echo "  -d <dir>   : バックアップ先ディレクトリ (必須)"
  echo "  -n <name>  : アーカイブ名のカスタマイズ (デフォルト: タイムスタンプ付き)"
  echo "  -v         : 詳細出力モードを有効にする"
  echo "  -r         : ドライラン (dry run) を実行。実際のバックアップは行わない"
  exit 1
}

# オプションのパース
while getopts "s:d:n:vr" opt; do
  case "$opt" in
    s) SOURCE_DIR="$OPTARG" ;;
    d) DEST_DIR="$OPTARG" ;;
    n) BACKUP_NAME="$OPTARG" ;;
    v) VERBOSE=1 ;;
    r) DRY_RUN=1 ;;
    \?)
      echo "エラー: 無効なオプション -$OPTARG." >&2
      usage
      ;;
    :)
      echo "エラー: オプション -$OPTARG には引数が必要です。" >&2
      usage
      ;;
  esac
done

# パース済みオプションを削除
shift $((OPTIND-1))

# 必須パラメータのバリデーション
if [ -z "$SOURCE_DIR" ] || [ -z "$DEST_DIR" ]; then
  echo "エラー: ソースディレクトリとターゲットディレクトリは必須です。" >&2
  usage
fi

if [ ! -d "$SOURCE_DIR" ]; then
  echo "エラー: ソースディレクトリ '$SOURCE_DIR' が存在しないかディレクトリではありません。" >&2
  exit 1
fi

# ターゲットディレクトリの存在確認と作成
if [ ! -d "$DEST_DIR" ]; then
  echo "ターゲットディレクトリ '$DEST_DIR' が見つかりません。作成しています..."
  mkdir -p "$DEST_DIR" || { echo "エラー: 作成に失敗しました。"; exit 1; }
fi

# バックアップコマンドの構築
BACKUP_ARCHIVE="${DEST_DIR}/${BACKUP_NAME}.tar.gz"
BACKUP_CMD="tar -czf ${BACKUP_ARCHIVE} -C ${SOURCE_DIR} ."

if [ "$VERBOSE" -eq 1 ]; then
  echo "詳細出力モード有効。"
  echo "Source: $SOURCE_DIR"
  echo "Destination: $DEST_DIR"
  echo "Archive Name: $BACKUP_NAME"
  echo "Command: $BACKUP_CMD"
fi

if [ "$DRY_RUN" -eq 1 ]; then
  echo "ドライランモードです。実際の処理は行われません。"
  echo "模擬コマンド: $BACKUP_CMD"
else
  echo "'$SOURCE_DIR' を '$DEST_DIR' へバックアップ中..."
  if eval "$BACKUP_CMD"; then
    echo "バックアップが完了しました: $BACKUP_ARCHIVE"
  else
    echo "エラー: バックアップに失敗しました。" >&2
    exit 1
  fi
fi

# 未認識の余分な引数がある場合の警告
if [ "$#" -gt 0 ]; then
  echo "Warning: 未認識の引数を無視しました: $@"
fi

5. 応用シナリオ:アプリ構成デプロイスクリプト

アプリケーションをデプロイするスクリプト configure_app.sh を考えてみましょう。設定ファイルパス (-c)、環境 (-e dev/test/prod)、そして強制上書きフラグ (-f) を扱います。

#!/bin/bash
# configure_app.sh - 設定を指定してアプリケーションをデプロイする

OPTIND=1
CONFIG_FILE="default.conf"
ENVIRONMENT="development"
FORCE_DEPLOY=0

print_usage() {
  echo "Usage: $0 [-c 設定パス] [-e 環境] [-f]"
  echo "  -c <path> : 設定ファイルパス (default: default.conf)"
  echo "  -e <env>  : デプロイ環境 (dev, test, prod) (default: development)"
  echo "  -f        : 既存ファイルを上書きして強制デプロイ"
  exit 1
}

while getopts "c:e:f" opt; do
  case "$opt" in
    c) CONFIG_FILE="$OPTARG" ;;
    e)
      ENVIRONMENT="$OPTARG"
      # 引数値のバリデーション
      if [[ ! "$OPTARG" =~ ^(dev|test|prod)$ ]]; then
        echo "エラー: 無効な環境です '$OPTARG'。 (dev, test, prod のいずれか)" >&2
        print_usage
      fi
      ;;
    f) FORCE_DEPLOY=1 ;;
    \?) echo "エラー: 無効なオプション -$OPTARG." >&2; print_usage ;;
    :) echo "エラー: オプション -$OPTARG には引数が必要です." >&2; print_usage ;;
  esac
done

shift $((OPTIND-1))

echo "--- デプロイ構成概要 ---"
echo "Config File: $CONFIG_FILE"
echo "Environment: $ENVIRONMENT"
echo "Force Deploy: $( [ "$FORCE_DEPLOY" -eq 1 ] && echo "Yes" || echo "No" )"
echo "-----------------------"

if [ ! -f "$CONFIG_FILE" ]; then
  echo "エラー: 設定ファイル '$CONFIG_FILE' が見つかりません。" >&2
  exit 1
fi

echo "デプロイシミュレーションを開始: '$CONFIG_FILE' を '$ENVIRONMENT' 環境に適用します。"

if [ "$FORCE_DEPLOY" -eq 1 ]; then
  echo "Warning: 強制モードです。既存のファイルは上書きされます。"
fi

# ここで実際のデプロイ処理を実行...
echo "デプロイシミュレーションが完了しました。"

この例では、オプションのパースと同時に、引数の値(環境名が特定の文字列であるか等)に対する制約・チェックを実装する方法を示しています。これにより、スクリプトの実行安全性が格段に高まります。