Bash 入門

Bash 関数進階:引数の渡し方と処理のベストプラクティス

Bashスクリプトにおいて、ファンクション(関数)はコマンドをグループ化し、コードのモジュール化を実現するために欠かせない要素です。これらの関数を柔軟かつ再利用可能にするためには、多くの場合「引数(Arguments)」という形で入力を受け取る必要があります。これは、前の章で学習したバリアブル(変数)を使ってデータを保存・操作する仕組みと非常に似ています。

関数に引数を渡すことで、スクリプトに動的な振る舞いを持たせることができます。つまり、同じ関数を呼び出すたびに異なるデータを処理させることが可能になり、関数内部のコードを修正する必要は全くなくなります。

1. 関数内部での引数へのアクセス

関数を呼び出す際に引数を指定すると、Bashは特殊な「位置パラメータ(Positional Parameters)」を使用して、それらの値を関数内部で利用できるようにします。これらはスクリプト全体で使用されるコマンドライン引数と似ていますが、そのスコープは関数内部に限定されます。

  • $1, $2, $3, ...: 関数に渡された個別の引数を表します。$1 は最初の引数、$2 は2番目、といった具合です。
  • $@: すべての引数を表し、各引数を独立したストリング(文字列)として扱います。展開されると "$1" "$2" "$3" ... と同等になります。スペースを含む引数を処理する際に極めて重要です。
  • $: すべての引数を表しますが、それらを1つの大きなストリングに連結します。展開されると "$1 $2 $3 ..." と同等になります。引数をループ*で回すようなシーンでは、通常 $@ ほど実用的ではありません。
  • $#: 関数に渡された引数の合計数を表します。
  • $0: 注意!関数内部であっても、$0 は関数名ではなくスクリプト自体の名前を指します。

名前を引数として受け取り、挨拶を表示するシンプルな関数 greet_user を見てみましょう。

#!/bin/bash

# greet_user という名前の関数を定義
greet_user() {
  echo "こんにちは, $1!" # $1 を使用して最初の引数にアクセス
}

# 引数を渡して関数を呼び出す
greet_user "Alice"

# 別の引数で再度呼び出す
greet_user "Bob"

この例では、greet_user "Alice" を実行すると、"Alice" が最初の引数として渡され、関数内部で $1 を通じて取得されます。出力結果は以下の通りです:

こんにちは, Alice!
こんにちは, Bob!

2. 複数の引数の処理

関数は複数の引数を受け取ることができます。その位置に応じて、$1$2 などを使用して個別にアクセスします。

ファイル名と内容を引数として受け取り、指定された内容でファイルを作成する create_file という関数を作成してみましょう。

#!/bin/bash

# ファイルを作成し内容を書き込む関数
create_file() {
  local filename="$1" # 第1引数(ファイル名)をローカル変数に格納
  local content="$2"  # 第2引数(内容)をローカル変数に格納
    
  # 両方の引数が提供されているかチェック
  if [ -z "$filename" ] || [ -z "$content" ]; then
    echo "使用法: create_file <ファイル名> <内容>"
    return 1 # エラーを示す非ゼロ値を返す
  fi
    
  echo "$content" > "$filename" # 内容をファイルに書き込む
  echo "ファイルを作成しました: '$filename'、内容は: '$content'"
}

# 関数を呼び出し、ファイル名と内容を渡す
create_file "report.txt" "これは本日の日次レポートデータです。"

# 別のファイル名と内容で再度呼び出し
create_file "summary.log" "本日のシステムアクティビティ概要。"

# 引数が不足している場合の呼び出しデモ
create_file "empty.txt"

上記のスクリプトの出力結果は以下の通りです:

ファイルを作成しました: 'report.txt'、内容は: 'これは本日の日次レポートデータです。'
ファイルを作成しました: 'summary.log'、内容は: '本日のシステムアクティビティ概要。'
使用法: create_file <ファイル名> <内容>

注意: ここでは local キーワードを使用して、filenamecontent を関数のローカル変数として宣言しています。これにより、スクリプトグローバルスコープに存在する可能性のある同名の変数との衝突を防ぐことができます。

3. すべての引数を走査する

関数が受け取る引数の数が不明な場合、$@ が非常に役立ちます。各引数を独立したストリングとして扱うため、for ループでの使用に最適です。

任意のアセット(アイテム)を受け取り、それらを1つずつ表示する process_items 関数を見てみましょう。

#!/bin/bash

# 任意の数のアイテムを処理する関数
process_items() {
  echo "$# 個のアイテムを処理中:" # $# で引数の総数を取得
  for item in "$@"; do       # "$@" を使用してすべての引数を巡回
    echo "- アイテム: $item"
  done
  echo "--- 処理終了 ---"
}

# 複数の単語を引数として渡す
process_items "リンゴ" "バナナ" "チェリー"

# スペースを含む文字列を引数として渡す
process_items "赤い リンゴ" "緑の バナナ" "甘い チェリー パイ"

# 引数を渡さずに呼び出す
process_items

process_itemsスクリプトの出力結果:

3 個のアイテムを処理中:
- アイテム: リンゴ
- アイテム: バナナ
- アイテム: チェリー
--- 処理終了 ---
3 個のアイテムを処理中:
- アイテム: 赤い リンゴ
- アイテム: 緑の バナナ
- アイテム: 甘い チェリー パイ
--- 処理終了 ---
0 個のアイテムを処理中:
--- 処理終了 ---

注意すべきは、"$@" がスペースを含む引数をいかに完璧に処理しているかです。スペースを含む文字列が、1つの独立したアイテムとして保持されています。もしループで "$*"(つまり for item in "$*")を使用すると、引数リスト全体が1つの巨大な文字列として扱われ、個別処理ができません。また、引用符なしの $* を使用すると、"赤い リンゴ" は "赤い" と "リンゴ" の2つに誤って分割されてしまいます。

4. 総合実戦:ユーザーアカウント管理スクリプト

「引数を持つ関数」をシステム管理の実践的なケースに統合してみましょう。これまでのセクションでは、エラーチェックを含むタスクを作成しました。これらを関数化することで、特定のタスクを再利用可能にし、コードをよりクリーンにできます。

ユーザーの追加、削除、および存在確認を行ういくつかの関数を作成します。各関数は username を引数として受け取ります。

#!/bin/bash

# ログ記録関数
log_message() {
  local level="$1"    # 第1引数:ログレベル (INFO, WARNING など)
  local message="$2"  # 第2引数:ログ内容
  echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message"
}

# ユーザーの存在を確認する関数
# 引数 1: username
user_exists() {
  local username="$1"
  # 標準出力と標準エラーを /dev/null へリダイレクト(出力を破棄)
  if id "$username" &>/dev/null; then
    return 0 # ユーザーが存在する (0 は成功/真)
  else
    return 1 # ユーザーが存在しない (1 は失敗/偽)
  fi
}

# 新規ユーザーを追加する関数
# 引数 1: username
add_user() {
  local username="$1"
  if user_exists "$username"; then
    log_message "WARNING" "ユーザー '$username' は既に存在します。追加をスキップします。"
    return 1
  else
    # 実際のシーンでは 'sudo useradd "$username"' を使用します
    # ここではデモ用にシミュレーションのみ行います
    echo "実行シミュレーション: useradd '$username'"
    log_message "INFO" "ユーザー '$username' を追加しました (シミュレーション)。"
    return 0
  fi
}

# 既存ユーザーを削除する関数
# 引数 1: username
delete_user() {
  local username="$1"
  if user_exists "$username"; then
    # 実際のシーンでは 'sudo userdel "$username"' を使用します
    echo "実行シミュレーション: userdel '$username'"
    log_message "INFO" "ユーザー '$username' を削除しました (シミュレーション)。"
    return 0
  else
    log_message "WARNING" "ユーザー '$username' は存在しません。削除できません。"
    return 1
  fi
}

# --- メインスクリプトのロジック ---
echo "--- ユーザー管理操作を開始 ---"

# 新規ユーザーの追加を試行
add_user "jdoe"

# 同じユーザーを再度追加 (チェック機能のテスト)
add_user "jdoe"

# 存在するユーザーの削除を試行 (システムに存在しない場合、warning へ流れます)
delete_user "testuser123"

# 明らかに存在しないユーザーの削除を試行
delete_user "nonexistent_user"

# 別の追加操作を実行
add_user "alice"

ロジックの解析:

  1. log_messagelevelmessage の2つの引数を受け取ります。
  2. user_existsusername を受け取り、その存在を静かに確認します。
  3. add_userusername を受け取り、まず user_exists を呼び出して確認してから、追加(シミュレーション)を行うか決定します。
  4. delete_user も同様のロジックで、確認後に削除を行います。

各関数は $1 を使用してコア引数にアクセスしています。log_message 関数は、複数の引数を受け取って詳細かつ統一されたフォーマットの出力を生成する、優れたユーティリティ関数の例です。このような構造により、スクリプト全体の可読性保守性、そしてロバスト性が飛躍的に向上します。