Bash 入門

Bash 関数の戻り値

ファンクション(関数)は、モジュール化され、再利用可能でメンテナンスしやすい Bash スクリプトを作成するための鍵となります。これまでの学習で、関数を定義する方法やパラメータ(引数)を渡して特定のタスクを実行させる方法を学びました。

しかし、関数の用途は単に「タスクを実行する」ことだけに留まりません。実行結果やステータスをスクリプトの呼び出し元にフィードバックする必要があります。この関数がアウトプットを提供するプロセスを「戻り値(リターン値)」と呼びます。他のプログラミング言語のように return 文で直接データ値を渡すのとは異なり、Bash の関数は終了ステータスコード(exit status codes)と標準出力(standard output)を利用する独自のメカニズムを持っています。

1. 終了ステータスコードによる戻り値

Bash において、関数内の return コマンドは、伝統的なプログラミング言語のように文字列や整数などの「データ値」を返すわけではありません。代わりに、関数の終了ステータス(終了コード、exit code とも呼ばれる)を設定します。

終了ステータスは 0 から 255 までの整数値です。慣例として、0 は実行成功を意味し、0 以外の値(non-zero)は何らかのエラーや失敗が発生したことを示します。この規約は、ほとんどの Unix ライクなオペレーティングシステムのコマンドやプロセスで共通しています。

1.1 return コマンドと $? の理解

関数が return コマンドに到達すると、即座に関数を抜けます。return に渡された整数引数が、その関数の終了ステータスとなります。引数なしで return を使用した場合、関数内で最後に実行されたコマンドの終了ステータスが、そのまま関数の終了ステータスとなります。関数が明示的な return 文なしで終了した場合も同様に、最後に実行されたコマンドのステータスが返されます。

直前に実行されたコマンドや関数の終了ステータスを取得するために、Bash は特殊変数 $? を提供しています。関数呼び出しの直後に $? をチェックすることで、その関数が返した終了ステータスを確認できます。

コード例 1:成功または失敗の状態を返す関数

#!/bin/bash

check_file_exists() {
  local filename="$1" # filename をローカル変数として宣言
  if [[ -f "$filename" ]]; then
    echo "ファイル '$filename' は存在します。"
    return 0 # 成功を示す
  else
    echo "ファイル '$filename' は存在しません。"
    return 1 # 失敗を示す
  fi
}

# 関数を呼び出し、その終了ステータスをチェック
check_file_exists "non_existent_file.txt"
if [[ $? -eq 0 ]]; then
  echo "関数レポート:成功。"
else
  echo "関数レポート:失敗。"
fi

echo "---"

# 次のチェック用のテストファイルを作成
touch "existing_file.txt"
check_file_exists "existing_file.txt"
if [[ $? -eq 0 ]]; then
  echo "関数レポート:成功。"
else
  echo "関数レポート:失敗。"
fi

rm "existing_file.txt" # テストファイルを削除

この例では、check_file_existsreturn 0 で成功を、return 1 で失敗を表しています。呼び出し側のスクリプト$? をチェックすることで、関数の実行結果に基づき条件分岐を行っています。

1.2 エラータイプごとに固有の終了ステータスコードを使用する

成功を 0、一般的な失敗を 1 とするのが最も一般的ですが、異なる 0 以外の終了ステータスコードを使用して、特定のエラータイプを示すこともできます。これにより、関数がなぜ失敗したのかについてより詳細な情報を提供でき、複雑なエラーハンドリングメカニズムを構築するのに役立ちます。

コード例 2:特定のコードを返す関数

#!/bin/bash

validate_number_input() {
  local input="$1" # input をローカル変数として宣言
  if [[ -z "$input" ]]; then
    echo "エラー:入力が空です。" >&2 # エラー情報を標準エラー (stderr) に出力
    return 1 # 終了コード 1:入力が空
  elif ! [[ "$input" =~ ^[0-9]+$ ]]; then
    echo "エラー:入力は正の整数である必要があります。" >&2 # stderr に出力
    return 2 # 終了コード 2:数値以外
  else
    echo "入力 '$input' は有効です。"
    return 0 # 終了コード 0:成功
  fi
}

# テストケース
validate_number_input ""
status_empty=$?
echo "空入力の戻りステータス:$status_empty"

echo "---"

validate_number_input "abc"
status_non_numeric=$?
echo "非数値入力の戻りステータス:$status_non_numeric"

echo "---"

validate_number_input "123"
status_valid=$?
echo "有効入力の戻りステータス:$status_valid"

echo "---"

# 条件ブロック内で特定のステータスコードを処理
handle_input_validation() {
  local value="$1"
  validate_number_input "$value"
  local validation_status=$?
    
  case $validation_status in
    0) echo "バリデーション成功、値:$value" ;;
    1) echo "バリデーション失敗:入力が空です。" ;;
    2) echo "バリデーション失敗:入力が正の整数ではありません。" ;;
    *) echo "バリデーション失敗:未知のエラー ($validation_status)。" ;;
  esac
}

handle_input_validation "456"
echo "---"
handle_input_validation ""
echo "---"
handle_input_validation "test"

この例では、validate_number_input が固有のエラーコード(1 は空、2 は非数値)を提供しています。handle_input_validation 関数は case 文を使用してこれらのステータスを解析し、精密なエラーメッセージやリカバリ操作を実現しています。エラーメッセージを標準エラー(>&2)にリダイレクトすることで、メインの出力をクリーンに保つことができます。これは戻り値やデータを主目的とする関数において非常に良い習慣です。

2. 標準出力によるデータの返却

Bash の関数において、文字列や数値、アイテムのリストといった実際のデータを「返す」最も一般的で柔軟な方法は、それを標準出力(stdout)にプリントすることです。

関数内で echoprintf を使用してプリントされたテキストは、関数内部で他へリダイレクトされない限り、標準出力に送られます。呼び出し側のスクリプトは、コマンド置換(command substitution)を使用してこれらの出力をバリアブルにキャプチャできます。

2.1 コマンド置換の理解

コマンド置換は、コマンド(またはコマンドのように振る舞う関数)の出力をバリアブルの値として、あるいは別のコマンドのパラメータとして利用できるようにします。コマンド置換の構文は $(command_or_function_call) です。

コード例 3:フォーマットされた文字列を返す関数

#!/bin/bash

format_log_entry() {
  local level="$1"
  local message="$2"
  local timestamp=$(date +"%Y-%m-%d %H:%M:%S") # 現在のタイムスタンプを取得
  echo "[$timestamp] [$level] $message" # フォーマットされた文字列を標準出力に出力
}

# 関数を呼び出し、その出力をキャプチャ
log_message=$(format_log_entry "INFO" "システム初期化が完了しました。")
echo "キャプチャされたログ:$log_message"

echo "---"

critical_event=$(format_log_entry "CRITICAL" "ディスク容量警告:95% 使用中。")
echo "アラート:$critical_event"

ここでは、format_log_entry がフォーマットされた文字列を構築し、echo で出力しています。log_message バリアブル$(...) を通じてこの文字列全体をキャプチャしています。特定のデータ断片を生成することを目的とした関数において、これは Bash スクリプトの中で極めて強力かつ頻繁に使用されるパターンです。

2.2 数値データの返却

同様に、関数は計算を実行し、標準出力を通じて数値結果を返すことができます。

コード例 4:数値を計算して返す関数

#!/bin/bash

calculate_percentage() {
  local part="$1"
  local total="$2"
    
  # 数値入力の基本的なバリデーション
  if ! [[ "$part" =~ ^[0-9]+$ ]] || ! [[ "$total" =~ ^[0-9]+$ ]] || [[ "$total" -eq 0 ]]; then
    echo "エラー:無効な数値入力または合計がゼロです。" >&2
    return 1 # ステータスでエラーを示すが、echo してもよい(しなくてもよい)
  fi
    
  # Bash の算術式展開による計算
  # 先に 100 を掛けてから合計で割る(整数演算)
  # 浮動小数点が必要な場合は 'bc' を使用する
  local percentage_raw=$(( (part * 100) / total ))
  echo "$percentage_raw" # 計算されたパーセンテージを標準出力に出力
  return 0
}

# ディスク使用率を取得する想定(シミュレーション)
disk_total_blocks=1000000
disk_used_blocks=250000

usage_percent=$(calculate_percentage "$disk_used_blocks" "$disk_total_blocks")

if [[ $? -eq 0 ]]; then
  echo "ディスク使用率:$usage_percent%"
else
  echo "ディスク使用率の計算に失敗しました。"
fi

echo "---"

# 'bc' を使用した浮動小数点演算の例
calculate_float_percentage() {
  local part="$1"
  local total="$2"
    
  if ! [[ "$part" =~ ^[0-9]+(\.[0-9]+)?$ ]] || ! [[ "$total" =~ ^[0-9]+(\.[0-9]+)?$ ]] || (( $(echo "$total == 0" | bc -l) )); then
    echo "エラー:無効な数値入力または合計がゼロです。" >&2
    return 1
  fi
    
  # 'bc' を使用して浮動小数点演算を実行
  local float_percent=$(echo "scale=2; ($part / $total) * 100" | bc)
  echo "$float_percent"
  return 0
}

cpu_usage_part=15.7
cpu_total_part=100.0

cpu_percent=$(calculate_float_percentage "$cpu_usage_part" "$cpu_total_part")

if [[ $? -eq 0 ]]; then
  echo "CPU 使用率:$cpu_percent%"
else
  echo "CPU 使用率の計算に失敗しました。"
fi

calculate_percentage 内で算術演算の結果が echo され、呼び出し側のスクリプトがその数字を usage_percent にキャプチャしています。Bash 標準の算術式展開 $((...)) は整数しか扱えないため、精密な計算には bc を利用しています。

重要: 標準出力を通じてデータを返す際も、成功・失敗を示すために return コマンドを併用することは、優れたプログラミング習慣として推奨されます。

3. グローバル変数によるデータ返却(非推奨)

Bash では、バリアブルはデフォルトでグローバルなスコープを持ちます。つまり、関数内で local キーワードを使わずに宣言された変数はグローバル変数となり、関数の外部を含むスクリプト内のどこからでもアクセス・修正が可能です。これを利用して、関数内で値を代入するだけで「値を返す」効果を得ることができます。

コード例 5:グローバル変数を修正する関数

#!/bin/bash

SCRIPT_VERSION="1.0" # グローバル変数

update_version() {
  local new_major=$1
  local new_minor=$2
    
  # 直接グローバル変数 SCRIPT_VERSION に代入
  SCRIPT_VERSION="${new_major}.${new_minor}"
  echo "内部でバージョンを $SCRIPT_VERSION に更新しました"
}

echo "初期スクリプトバージョン:$SCRIPT_VERSION"
update_version "1" "1"
echo "関数呼び出し後の新スクリプトバージョン:$SCRIPT_VERSION"

この例では、update_version が直接グローバル変数 SCRIPT_VERSION を変更しています。関数呼び出し後、外部の変数にその変更が反映されています。

3.1 なぜグローバル変数の使用は推奨されないのか?

この方法は機能しますが、現代的なスクリプト作成(特に複雑な構成)においては、以下の理由から推奨されません。

  • 副作用 (Side Effects): 関数は可能な限り副作用を最小限に抑えるべきです。つまり、自身のスコープ外の変数を予期せず変更すべきではありません。グローバル変数の変更は、動作がグローバルな状態に依存するため、理解、テスト、デバッグを困難にします。
  • 再利用性の低下: グローバル変数に依存したり修正したりする関数は、特定の状態に密結合(タイトに結合)しているため、他のスクリプトで再利用するのが難しくなります。
  • 命名競合: 複数の関数が同じグローバル変数を修正したり、別の場所で使われている変数を知らずに上書きしたりすると、発見しにくいバグの原因になります。

そのため、成功・失敗のシグナルには終了ステータスコードを、実際のデータ取得には標準出力とコマンド置換を優先して使用することを推奨します。グローバル変数は設定情報や、本当にスクリプト全体で共有すべき値に限定し、関数内での修正は慎重に行うべきです。

4. 総合実戦:システム管理スクリプトの強化

これらの概念をシステム管理スクリプトに統合しましょう。効率的に情報を収集し、イベントを記録する戻り値付きの関数を作成します。

シナリオ:ディスク使用率の監視とイベント記録

スクリプトの要件:

  1. 特定のマウントポイントのディスク使用率(%)を取得する。
  2. イベントごとに標準化されたログメッセージを生成する。
  3. システムサービスの状態を確認する。
#!/bin/bash

# 関数:指定されたマウントポイントのディスク使用率を整数で取得
# パーセンテージ(整数)を標準出力に返す。
# 無効なパスやコマンド失敗時は 0 以外の終了ステータスを返す。
get_disk_usage_percent() {
  local mount_point="$1"
    
  if [[ -z "$mount_point" ]]; then
    echo "エラー:マウントポイントが指定されていません。" >&2
    return 1
  fi
    
  # 'df -h' で情報を取得し、awk と tail で抽出
  local usage_line=$(df -h "$mount_point" 2>/dev/null | tail -1)
    
  if [[ -z "$usage_line" ]]; then
    echo "エラー:'$mount_point' の使用状況が見つかりません。" >&2
    return 2
  fi
    
  # 使用率(例:"75%")から '%' を除去して抽出
  local percentage=$(echo "$usage_line" | awk '{print $5}' | sed 's/%//')
    
  # 数値チェック
  if ! [[ "$percentage" =~ ^[0-9]+$ ]]; then
    echo "エラー:'$mount_point' の使用率解析に失敗しました。" >&2
    return 3
  fi
    
  echo "$percentage" # 標準出力に数値を返す
  return 0
}

# 関数:フォーマットされたログメッセージを生成
log_message() {
  local level="$1" # 例:INFO, WARN, ERROR
  local message="$2"
  local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    
  echo "[$timestamp] [$level] $message" # 標準出力に文字列を返す
  return 0
}

# 関数:システムサービスの状態をチェック
# active なら 0, inactive/failed なら 1 を返す。
check_service_status() {
  local service_name="$1"
    
  if [[ -z "$service_name" ]]; then
    echo "エラー:サービス名が指定されていません。" >&2
    return 1
  fi
    
  # systemctl is-active はアクティブなら 0、それ以外なら非 0 を返す
  systemctl is-active --quiet "$service_name"
  local status_code=$?
    
  if [[ $status_code -eq 0 ]]; then
    echo "サービス '$service_name' は稼働中 (active) です。" >&2
  else
    echo "服务 '$service_name' は停止中または失敗しています。" >&2
  fi
    
  return "$status_code" # ステータスコードをそのまま返す
}

# --- メインロジック ---

# 1. ルートファイルシステムの使用率をチェック
MOUNT_POINT="/"
disk_percent=$(get_disk_usage_percent "$MOUNT_POINT")
disk_check_status=$?

if [[ $disk_check_status -eq 0 ]]; then
  echo "ルートファイルシステム使用率:$disk_percent%"
  if [[ "$disk_percent" -gt 80 ]]; then
    log_entry=$(log_message "WARNING" "$MOUNT_POINT の使用率が高すぎます:$disk_percent%。")
    echo "$log_entry"
  else
    log_entry=$(log_message "INFO" "$MOUNT_POINT の使用率は正常です:$disk_percent%。")
    echo "$log_entry"
  fi
else
  log_entry=$(log_message "ERROR" "$MOUNT_POINT の取得に失敗。コード:$disk_check_status")
  echo "$log_entry"
fi

echo "---"

# 2. サービス状態をチェック(例:apache2)
SERVICE_NAME="apache2" # 環境により nginx, httpd などに変更
check_service_status "$SERVICE_NAME"
service_status_code=$?

if [[ $service_status_code -eq 0 ]]; then
  log_entry=$(log_message "INFO" "サービス '$SERVICE_NAME' は正常稼働しています。")
  echo "$log_entry"
else
  log_entry=$(log_message "CRITICAL" "サービス '$SERVICE_NAME' が停止または失敗しています!")
  echo "$log_entry"
fi

この実戦例のポイント:

  • get_disk_usage_percent は標準出力(echo "$percentage")で数値を返しつつ、return でエラーを制御。
  • log_message は標準出力でフォーマット済み文字列を返し、変数 log_entry にキャプチャ。
  • check_service_statussystemctl is-active の終了ステータスを自身の戻り値として利用し、メインロジックでの条件判断に使用。

このように終了ステータス(エラー処理用)と標準出力(データ伝送用)を組み合わせることで、強力で柔軟、かつモジュール化されたスクリプトシステムを構築できます。