位置パラメータの習得:Bashスクリプト引数ガイド

位置パラメータを習得して動的なBashスクリプトの力を解き放ちましょう。この包括的なガイドでは、`$1`、`$2`、`$#`(引数の数)や重要な`"$@"`(すべての引数)などの特別な変数を使用してコマンドライン引数にアクセスする方法を説明します。入力検証のベストプラクティスを学び、`$*`と`$@`の違いを理解し、ユーザー入力に完璧に適応する堅牢でエラーチェックされたスクリプトを作成する実践的な例をご覧ください。

位置パラメータの習得:Bashスクリプト引数ガイド

Bashスクリプトは、ファイル内の変数を編集する代わりに引数を受け入れるようになると、はるかに便利になります。バックアップスクリプトはソースディレクトリを受け入れるべきです。デプロイスクリプトは環境名を受け入れるべきです。クリーンアップスクリプトは1つ以上のパスを受け入れるべきです。これらの値は位置パラメータとして到着します:$1$2$3など。

難しいのは$1を読み取ることではありません。難しいのは、欠落した引数、スペースを含む引数、オプションフラグ、そしてスクリプトが「自分用」から「誰かが午前2時に実行するもの」に成長した瞬間を処理することです。


位置パラメータの構造

位置パラメータは、シェルによって定義された特別な変数で、スクリプト名に続くコマンドラインで提供される単語に対応します。これらは1から順に番号が付けられます。

パラメータ 説明 例の値(./script.sh file1 dir/を実行した場合)
$0 スクリプト自体(または関数)の名前。 ./script.sh
$1 スクリプトに渡された最初の引数。 file1
$2 スクリプトに渡された2番目の引数。 dir/
$N N番目の引数(N > 0)。
${10} 9を超える引数は中括弧で囲む必要があります。

$9を超える引数へのアクセス

引数1から9は$1から$9として直接アクセスできますが、10番目以降の引数にアクセスするには、環境変数や文字列操作との曖昧さを避けるために、番号を中括弧で囲む必要があります(例:$10ではなく${10})。


スクリプティングに不可欠な特別なパラメータ

数値パラメータに加えて、Bashは引数セット全体に関連するいくつかの重要な特別な変数を提供します。これらは検証と反復処理に不可欠です。

$#による引数のカウント

特別な変数$#は、スクリプトに渡されたコマンドライン引数の総数($0を除く)を保持します。これは入力検証を実装するための最も重要な変数です。

#!/bin/bash

if [ "$#" -eq 0 ]; then
    echo "エラー:引数が指定されていません。"
    echo "使用方法:$0 <入力ファイル>"
    exit 1
fi

echo "$# 個の引数が指定されました。"

すべての引数:$@$*

変数$@$*は両方とも引数の完全なリストを表しますが、特に引用符で囲まれた場合に動作が異なります。

$*(単一の文字列)

二重引用符で囲まれた場合("$*")、位置パラメータのリスト全体が単一の引数として扱われ、IFS(内部フィールド区切り文字)変数の最初の文字(通常はスペース)で区切られます。

  • 入力引数が:arg1 arg2 arg3 の場合
  • "$*" は次のように展開されます:"arg1 arg2 arg3"(1つの要素)

$@(個別の文字列 - 推奨)

二重引用符で囲まれた場合("$@")、各位置パラメータは個別の引用符で囲まれた引数として扱われます。これは引数を反復処理するための標準的で推奨される方法であり、スペースを含む引数を正しく保持します。

  • 入力引数が:arg1 "arg with space" arg3 の場合
  • "$@" は次のように展開されます:"arg1" "arg with space" "arg3"(3つの異なる要素)

引用符が重要な理由:デモンストレーション

引数 ./test.sh 'hello world' file.txt で実行されるスクリプトを考えます。

#!/bin/bash

# 引用符なしの$*はスペースで分割され、通常は間違っています。
echo "-- 引用符なしの$*を使用したループ --"
for item in $*; do
    echo "アイテム:$item"
done

# 引用符付きの"$@"は元の各引数を保持します。
echo "-- 引用符付きの$@を使用したループ --"
for item in "$@"; do
    echo "アイテム:$item"
done

./test.sh 'hello world' file.txt の場合、引用符なしのループは helloworld を別々のアイテムとして出力します。"$@" ループは hello world を1つの引数として保持します。この違いが、経験豊富なシェルユーザーがほぼ自動的に "$@" を使用する理由です。


引数処理の実践的なテクニック

1. 基本的な引数取得スクリプト

このシンプルなスクリプトは、特定のパラメータにアクセスし、$0を使用して役立つフィードバックを提供する方法を示します。

deploy_service.sh

#!/bin/bash
# 使用方法:deploy_service.sh <サービス名> <環境>

SERVICE_NAME="$1"
ENVIRONMENT="$2"

# 検証チェック(最低2つの引数)
if [ "$#" -lt 2 ]; then
    echo "使用方法:$0 <サービス名> <環境>"
    exit 1
fi

echo "サービス $SERVICE_NAME のデプロイを開始します"
echo "ターゲット環境:$ENVIRONMENT"

# 検証されたパラメータを使用してコマンドを実行
ssh admin@server-"$ENVIRONMENT" "/path/to/start $SERVICE_NAME"

2. 堅牢な入力検証

優れたスクリプトは、処理を進める前に入力を常に検証します。これには、カウント($#)のチェックと、多くの場合、引数の内容のチェック(引数が数値か有効なファイルパスかなど)が含まれます。

#!/bin/bash

# 1. 引数の数をチェック(正確に3つである必要があります)
if [ "$#" -ne 3 ]; then
    echo "エラー:このスクリプトには3つの引数(ソース、宛先、ユーザー)が必要です。"
    echo "使用方法:$0 <src_path> <dest_path> <user>"
    exit 1
fi

SRC_PATH="$1"
DEST_PATH="$2"
USER="$3"

# 2. 内容をチェック(例:ソースパスが存在することを確認)
if [ ! -f "$SRC_PATH" ]; then
    echo "エラー:ソースファイル '$SRC_PATH' が見つからないか、ファイルではありません。"
    exit 2
fi

# 検証が成功したら、続行
echo "$SRC_PATH を $DEST_PATH にユーザー $USER としてコピーしています..."

ベストプラクティスのヒント: 検証が失敗した場合は、常に明確で簡潔な 使用方法: ステートメントを提供してください。これにより、ユーザーはコマンドの呼び出しをすばやく修正できます。

3. shift を使用した引数の反復処理

shift コマンドは、引数を順次処理するための優れたツールであり、単純なフラグを処理する場合や、while ループ内で引数を1つずつ処理する場合によく使用されます。

shift は現在の $1 引数を破棄し、$2$1 に、$3$2 に移動し、$# を1つ減らします。これにより、最初の引数を処理し、引数がなくなるまでループできます。

#!/bin/bash

# 単純な -v フラグを処理し、残りのファイルを一覧表示します

VERBOSE=false

if [ "$1" = "-v" ]; then
    VERBOSE=true
    shift  # -v フラグを破棄し、引数をシフトアップします
fi

if $VERBOSE; then
    echo "詳細モードが有効になりました。"
fi

if [ "$#" -eq 0 ]; then
    echo "ファイルが指定されていません。"
    exit 0
fi

echo "残り $# 個のファイルを処理しています:"
for file in "$@"; do
    if $VERBOSE; then
        echo "ファイルをチェック中:$file"
    fi
    # ... ここに処理ロジック
done

注: shift は単純な解析に役立ちます。多くのフラグを持つ複雑なスクリプトの場合は、通常 getopts が短いオプションに適しています。長いオプションの処理はプラットフォームによって異なるため、外部の getopt を使用する場合は注意深くテストしてください。

より現実的なパーサー

多くの内部スクリプトは、1つのオプションフラグと1つの必須値から始まります。以下は、読みやすい状態を保つ小さなパターンです。

#!/usr/bin/env bash
set -u

dry_run=false
environment=""

usage() {
    echo "使用方法:$0 [--dry-run] --env <dev|staging|prod> <file>..." >&2
}

while [ "$#" -gt 0 ]; do
    case "$1" in
        --dry-run)
            dry_run=true
            shift
            ;;
        --env)
            if [ "$#" -lt 2 ]; then
                echo "エラー:--env には値が必要です。" >&2
                usage
                exit 2
            fi
            environment="$2"
            shift 2
            ;;
        --help|-h)
            usage
            exit 0
            ;;
        --)
            shift
            break
            ;;
        -*)
            echo "エラー:不明なオプション:$1" >&2
            usage
            exit 2
            ;;
        *)
            break
            ;;
    esac
done

if [ -z "$environment" ]; then
    echo "エラー:--env は必須です。" >&2
    usage
    exit 2
fi

if [ "$#" -eq 0 ]; then
    echo "エラー:少なくとも1つのファイルを指定してください。" >&2
    usage
    exit 2
fi

for file in "$@"; do
    if [ ! -f "$file" ]; then
        echo "エラー:ファイルが見つかりません:$file" >&2
        exit 3
    fi

    if $dry_run; then
        echo "$file を $environment にアップロードします"
    else
        echo "$file を $environment にアップロード中"
        # アップロードコマンドはここに記述
    fi
done

細かい点に注意してください。エラーメッセージは stderr に送られます。-- は「オプションの解析を停止する」ことを意味し、ダッシュで始まるファイル名を渡せるようにします。最後のファイルループは "$@" を使用しているため、release notes.txt は1つのファイル名として保持されます。

よくある間違い

最も一般的な間違いは引用符を忘れることです:

cp $1 $2

これは、いずれかのパスにスペースやシェルのグロブ文字が含まれていると壊れます。代わりに次を使用してください:

cp -- "$1" "$2"

-- は多くのコマンドにオプション解析が終了したことを伝え、パスが - で始まる場合に役立ちます。

もう1つのよくある間違いは、検証が遅すぎることです。スクリプトが2つの引数を期待する場合、破壊的な操作を行う前にそれを確認してください:

if [ "$#" -ne 2 ]; then
    echo "使用方法:$0 <ソース> <宛先>" >&2
    exit 2
fi

呼び出し元に役立つ場合は、異なる終了コードを使用してください。使用方法のエラーは 2、ファイルが見つからない場合は 3、失敗した外部コマンドは独自のステータスを保持できます。巨大な終了コードの分類は必要ありませんが、不正な呼び出しの後に 0 を返すと、自動化の信頼性が低下します。

関数にも位置パラメータがあります

Bash関数内では、$1$2 は関数の引数を参照し、スクリプトの元の引数は参照しません。

log_copy() {
    local src="$1"
    local dest="$2"

    echo "$src を $dest にコピーしています"
    cp -- "$src" "$dest"
}

log_copy "$1" "$2"

これは便利ですが、関数内の $1 がスクリプトレベルの最初の引数を意味すると期待すると驚くかもしれません。値を明示的に渡してください。これにより、関数のテストと再利用が容易になります。

別のコマンドへの引数の転送

多くのラッパースクリプトは、別のコマンドを呼び出す前に少しセットアップを追加するためにのみ存在します。その場合、"$@" がラッパーを正直に保ちます。

#!/usr/bin/env bash
set -e

export APP_ENV=staging
exec /usr/local/bin/myapp "$@"

誰かが次のように実行した場合:

./run-staging.sh --config "config with spaces.yml" --verbose

ラップされたコマンドは同じ3つの引数を受け取ります。$* または引用符なしの $@ を使用した場合、設定パスが複数の単語に分割される可能性があります。

exec はオプションですが、ラッパーではシェルプロセスをターゲットプロセスに置き換えるため、しばしば便利です。これにより、systemd、Docker、またはプロセススーパーバイザーの下でシグナルがより予測可能に動作します。

驚きのないデフォルト値

引数がオプションの場合があります。Bashのパラメータ展開が役立ちます:

environment="${1:-dev}"

これは、「$1 が設定されていて空でない場合はそれを使用し、そうでない場合は dev を使用する」という意味です。これは親しみやすいローカルスクリプトには問題ありませんが、本番スクリプトでは注意してください。誰かが引数を忘れた場合、サイレントデフォルトが間違った環境にデプロイする可能性があります。

リスクの高いコマンドの場合は、明示的な入力を優先してください:

if [ "$#" -lt 1 ]; then
    echo "使用方法:$0 <環境>" >&2
    exit 2
fi

デフォルト値は、ログレベルや出力ディレクトリのデフォルト設定など、結果が小さい場合に最適です。引数がサーバーを選択したり、データを削除したり、デプロイメントターゲットを変更したりする場合は、リスクが高くなります。

位置パラメータと set -u

多くのBashスクリプトは set -u を使用して、設定されていない変数がエラーを引き起こすようにします。これによりタイプミスをキャッチできますが、欠落した位置パラメータの動作も変更されます。

#!/usr/bin/env bash
set -u

echo "最初の引数:$1"

引数なしでそのスクリプトを実行すると、Bashは「バインドされていない変数」エラーで終了します。このエラーは技術的には正しいですが、親しみやすくはありません。必須パラメータを読み取る前に $# を検証してください:

if [ "$#" -lt 1 ]; then
    echo "使用方法:$0 <入力ファイル>" >&2
    exit 2
fi

input_file="$1"

set -u の下でオプションパラメータの場合は、ガード付き展開を使用します:

mode="${2:-default}"

これにより、厳格モードを有用に保ちながら、欠落したオプション値がスクリプトをクラッシュさせるのを防ぎます。

位置パラメータが間違ったインターフェースである場合

位置パラメータは小さなコマンドに最適です:

backup.sh /var/www /backup/www.tar.gz

スクリプトが多くの値を受け取る場合、読みにくくなります:

deploy.sh prod us-east-1 api v2.4.1 true false 30

誰も5番目の引数が何を意味するかを覚えていたくありません。スクリプトがその時点に達したら、名前付きフラグまたは設定ファイルを使用してください:

deploy.sh --env prod --region us-east-1 --service api --version v2.4.1 --timeout 30

コードは少し長くなりますが、コマンドラインは自己文書化されます。これはチームで使用されるスクリプトにとって良いトレードオフです。

優れた位置パラメータ処理は、主に規律の問題です:早期に検証し、意図的に分割したい場合を除いてすべての展開を引用符で囲み、引数の転送には "$@" を使用し、使用方法のメッセージをそれをトリガーするチェックの近くに配置します。これらの習慣により、小さなスクリプトが実際のファイル名、実際のユーザー、実際の自動化に耐えられるようになります。