Bash 入門

Bash 関数

Bashスクリプトが複雑になるにつれ、単なるコマンドの逐次実行だけではコードが肥大化し、メンテナンスが困難になります。他のプログラミング言語と同様に、Bashにおけるファンクション(関数)は、再利用可能なコードブロックをカプセル化する手法を提供し、スクリプトモジュール化、可読性、および保守性を劇的に向上させます。

スクリプト内の複数の箇所で同じコマンドセットを繰り返し記述する代わりに、それらをファンクションとして定義し、必要な時にいつでも呼び出すことができます。これにより、スクリプトがクリーンでデバッグしやすくなるだけでなく、巨大で複雑なタスクを、より小さく管理しやすいサブタスクへと分解することが可能になります。

ファンクションの定義と呼び出しをマスターすることは、よりロバスト(堅牢)でプロフェッショナルなBashスクリプトを書くための重要な一歩であり、高度なシステム運用の自動化(オートメーション)ソリューションを構築するための道を開きます。

1. Bash における関数の定義

Bashにおいて、ファンクションは本質的に、名前を付けてグループ化されたコマンドブロックです。その名前を「呼び出す(コールする)」と、ブロック内のすべてのコマンドが実行されます。

Bashにはファンクションを定義するための2つの構文があります。どちらも広く使われており、実行結果は全く同じです。

1.1 構文 1:function キーワードを使用する

これは function キーワードを直接使用するため、ファンクション定義であることを最も明確に示す直感的な方法です。

function my_function_name {
  # my_function_name が呼び出された時に実行されるコマンド
  echo "ここは my_function_name 関数の内部です。"
  ls -l
}
  • function キーワード: 直後に続くのがファンクション定義であることを明示的に宣言します。
  • my_function_name: ファンクションに指定する名前です。名前は記述的であるべきで、標準的なBashの命名規則(通常は小文字、単語間はアンダースコアで区切る。変数の命名と同様。例:check_status, backup_files)に従います。
  • { ... }: 波括弧(中括弧)はファンクションのボディ(本体)を包含します。呼び出し時に実行されるすべてのコマンドがここに記述されます。

1.2 構文 2:標準的な括弧 () を使用する

この構文は、他の多くのプログラミング言語(C, Java, JavaScriptなど)で非常に一般的であり、Bashにおいても有効かつ非常にポピュラーです。

another_function_name () {
  # another_function_name が呼び出された時に実行されるコマンド
  echo "ここは another_function_name 関数の内部です。"
  pwd
}
  • another_function_name: ファンクションの名前。
  • (): 関数名の直後に続く空の括弧は、これがファンクションであることを示します。他言語では括弧内にパラメータ(引数)を記述しますが、Bashのこの構文スタイルでは、これらは単に宣言の一部として機能します。(注意:Bashでは、引数は定義時の括弧内ではなく、呼び出し時に名前の後ろに続けて渡されます)。
  • { ... }: 同様に、波括弧がファンクションのボディを包含します。

1.3 関数定義に関する重要な注意事項

  • 定義の位置 (Placement): ファンクションは呼び出される前に定義されていなければなりません。Bashはスクリプトを上から下へ順番に実行するため、定義が解析される前に呼び出そうとすると、Bashは「command not found(コマンドが見つかりません)」というエラーを返します。優れたプログラミング習慣として、すべてのファンクション定義をスクリプトの冒頭に置くか、別のファイルにまとめて source コマンドで読み込むようにしましょう。
  • 命名規則 (Naming Conventions): 明確で記述的な名前を選びましょう。既存のBash組み込みコマンドやエイリアス(lscd など)と競合する名前を避け、予期せぬ挙動を防ぐことが重要です。
  • 可読性 (Readability): ファンクションのボディは単一のロジックタスクに集中させるべきです。1つの関数が長すぎたり、複数の無関係なタスクを実行している場合は、より小さく専門的な複数の関数に分割することを検討してください。

2. Bash における関数の呼び出し

ファンクションが定義されたら、その呼び出しは非常に簡単です。通常のBashコマンドを使用するのと同様に、その名前を記述するだけです。

# 1. 関数の定義
function greet_user {
  echo "こんにちは!ここは greet_user 関数です!"
  echo "今日は $(date +%F) です"
}

# 2. 関数の呼び出し
greet_user

greet_user が呼び出されると、シェルはその定義場所にジャンプし、波括弧内のすべてのコマンドを実行します。実行が終わると、スクリプト内でその関数を呼び出した場所に戻り、以降のコードを続行します。

もう少し複雑な例で実演してみましょう:

#!/bin/bash

# --- 関数定義エリア ---

# 区切り線を表示する関数
function display_separator {
  echo "----------------------------------------"
}

# システムの稼働時間と負荷をチェックする関数
check_system_info () {
  echo "システム情報を収集しています..."
  uptime
}

# カレントディレクトリの内容を詳細形式でリスト表示する関数
list_current_directory () {
  echo "カレントディレクトリの内容を表示します:"
  ls -lh
}

# --- メインスクリプト実行エリア ---

echo "システム状態レポートの生成を開始します..."

display_separator
check_system_info
display_separator
list_current_directory
display_separator

echo "システム状態レポートの生成が完了しました。"

このスクリプトでは:

  • 2種類の構文を混ぜて、display_separatorcheck_system_infolist_current_directory の3つの関数を定義しました。
  • 「メインスクリプト実行エリア」で、これらの関数を順番に呼び出しています。これにより、スクリプトの本体ロジックが非常に明確になり、出力も整理されます。

3. 実践ケーススタディ

システム運用の実戦に即したシナリオにファンクションを適用してみましょう。

3.1 ケース 1:標準化されたログ記録関数の作成

システム管理スクリプトでは、さまざまなイベントを記録する必要があります。スクリプト内の至る所で echo "タイムスタンプ: ... メッセージ: ..." を繰り返す代わりに、1つの関数で統一処理します。

#!/bin/bash

# ログファイルのパスを定義
# 実際のシナリオでは、LOG_FILE はグローバル変数となることが多い
LOG_FILE="./my_admin_script.log"

# スクリプト開始情報を記録する関数
function log_script_start {
  # 現在のタイムスタンプを取得
  TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
  # メッセージをコンソールに表示し、ログファイルに追記
  echo "[$TIMESTAMP] INFO: スクリプトの初期化が完了しました。" | tee -a "$LOG_FILE"
}

# スクリプト終了情報を記録する関数
function log_script_end {
  TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")
  echo "[$TIMESTAMP] INFO: スクリプトの実行が終了しました。" | tee -a "$LOG_FILE"
}

# デモ用にログファイルが存在し書き込み可能であることを確認
touch "$LOG_FILE"
chmod 664 "$LOG_FILE"

# --- メインスクリプトのロジック ---
echo "管理者スクリプトが起動しました。"
log_script_start

# タスクの実行をシミュレート
echo "タスク A を実行中..."
sleep 1
echo "タスク A が完了しました。"

echo "タスク B を実行中..."
sleep 1
echo "タスク B が完了しました。"

log_script_end
echo "管理者スクリプトの実行が終了しました。"

# ログファイルの最新の記録を表示
echo -e "\n--- $LOG_FILE の最新ログ ---"
tail "$LOG_FILE"

原理の解析:

  • log_script_startlog_script_end の2つの関数を定義しました。
  • 各関数内部には、タイムスタンプ付きの特定のログエントリを生成するロジックが封じ込められています。
  • tee -a "$LOG_FILE" コマンドにより、出力を標準出力(画面)に送ると同時に、指定されたログファイルへ追記しています。
  • メインプログラムの最初と最後でこれらの関数を呼び出すことで、スクリプトの実行記録を統一されたフォーマットで保持できます。

3.2 ケース 2:サービス状態のチェック(プレースホルダのデモ)

システム管理では、Webサーバーやデータベースなどの各種サービスの状態を確認する必要があります。関数の構造的な役割を示すために、まずはメッセージを表示するだけの「プレースホルダ(placeholder)」関数を作成し、後で実際のチェックロジックを組み込めるようにします。

#!/bin/bash

# 架空の 'nginx' サービス状態をチェックする関数
function check_nginx_status {
  echo "'nginx' Web サーバーの状態を確認しています..."
  # ここは将来的に実際のコマンドに置き換えます。例: systemctl is-active nginx
  echo "Nginx 状態: (実行中 running とシミュレート)"
}

# 架空の 'mysql' サービス状態をチェックする関数
check_mysql_status () {
  echo "'mysql' データベースサーバーの状態を確認しています..."
  # ここは将来的に実際のコマンドに置き換えます。例: systemctl is-active mysql
  echo "MySQL 状態: (停止中 stopped とシミュレート)"
}

# --- メインスクリプトのロジック ---
echo "デイリーサービスのヘルスチェックを開始します..."
check_nginx_status
echo "" # 出力を読みやすくするために空行を挿入
check_mysql_status
echo ""
echo "デイリーサービスのヘルスチェックが完了しました。"

原理の解析:
たとえ check_nginx_statuscheck_mysql_status が現時点では echo によるシミュレーションであっても、関連するコマンドを意味のある名前の下にグループ化する方法を示しています。これにより、メインスクリプトのフローが一目瞭然になります:「ヘルスチェック開始 -> Nginx確認 -> MySQL確認 -> 終了」。

3.3 ケース 3:バックアップディレクトリの作成と検証

ファイルバックアップの前に、バックアップ先ディレクトリが存在することを確認するのは一般的なタスクです。この例では、ディレクトリ作成(mkdir)、変数、および if 条件制御文を組み合わせています。

#!/bin/bash

# バックアップディレクトリのパス変数を定義
BACKUP_ROOT="/tmp/my_daily_backups"

# バックアップディレクトリの存在を保証する関数
function prepare_backup_directory {
  echo "バックアップディレクトリを準備しています: $BACKUP_ROOT"
  
  if [ -d "$BACKUP_ROOT" ]; then
    echo "ディレクトリ '$BACKUP_ROOT' は既に存在します。"
  else
    echo "ディレクトリ '$BACKUP_ROOT' が見つかりません。作成しています..."
    mkdir -p "$BACKUP_ROOT" # -p オプション:親ディレクトリがない場合も一括作成
    
    # 直前の mkdir コマンドのエグジットステータスを確認
    if [ $? -eq 0 ]; then
       echo "ディレクトリ '$BACKUP_ROOT' の作成に成功しました。"
    else
      echo "エラー: ディレクトリ '$BACKUP_ROOT' を作成できませんでした。"
    fi
  fi
}

# --- メインスクリプトの実行 ---
echo "バックアッププロセスを開始します..."

prepare_backup_directory # 関数の呼び出し

# バックアップファイルの作成をシミュレート
echo "バックアップファイルを '$BACKUP_ROOT' にコピーしています..."
touch "$BACKUP_ROOT/file_A_$(date +%F).log"
touch "$BACKUP_ROOT/data_B_$(date +%F).sql"
echo "バックアップファイルの作成が完了しました。"

echo "バックアップディレクトリの内容を表示:"
ls -l "$BACKUP_ROOT"

echo "バックアッププロセスが終了しました。"

原理の解析:

  • prepare_backup_directory 関数は、ディレクトリの存在チェック(-d)および作成(mkdir -p)の完全なロジックをカプセル化しています。
  • if 文と終了ステータスコード $? を利用して、堅牢なエラーハンドリングを行っています。
  • この複雑なロジックを1つの関数呼び出しにまとめることで、メインスクリプトはマクロなフロー(ディレクトリ準備 -> ファイルコピー)に集中でき、煩雑なディレクトリチェックの詳細に煩わされることがありません。