高度なBashスクリプティング: 自動化のためのシェル機能の習得

配列、プロセス置換、厳格モード、ShellCheck、パラメータ展開を使って、より安全な自動化を実現する高度なBashスクリプティングを学びます。

高度なBashスクリプティング:自動化のためのシェル機能を極める

スクリプトが数行のコマンドから本格的な自動化へと成長するにつれて、Bashスクリプティングは難しくなります。変数の安全な扱い、入出力のクリーンな処理、一時ファイルの削減が必要になります。

このガイドでは、デプロイスクリプト、ログチェック、メンテナンスジョブで使える高度なBashスクリプティング機能を紹介します。目的は巧妙なシェルコードではなく、再実行可能でデバッグしやすく、他のエンジニアに引き継げるコードを書くことです。

1. 実際のリストにはBash配列を使う

配列を使うと、スペースで文字列を分割せずに複数の値を格納できます。これは、ファイル名やサービス名、ユーザー入力にスペースが含まれる場合に重要です。

インデックス配列

インデックス配列は最も一般的なタイプで、0から始まる数値インデックスで要素にアクセスします。

例:

# インデックス配列を初期化
COLORS=("red" "green" "blue" "yellow")

# 要素へのアクセス
echo "2番目の色は: ${COLORS[1]}"

# 要素の追加
COLORS+=( "purple" )

# 全要素の表示
echo "すべての色: ${COLORS[@]}"

一般的な配列操作:

操作 構文 説明
要素数の取得 ${#ARRAY[@]} 要素の総数を返します。
特定要素の長さ取得 ${#ARRAY[index]} 指定インデックスの文字列の長さを返します。
繰り返し処理 for item in "${ARRAY[@]}" 全要素を処理する標準的なループ構造。

配列展開は常に "${ARRAY[@]}" のようにクォートしてください。これにより、"/var/log/my app.log" が2つの引数ではなく1つの引数として扱われます。

連想配列

連想配列は小さなキーと値のマップのように機能します。Bash 4以降が必要なので、古いmacOSホストをサポートする場合は対象システムを確認してください。

連想配列は -A で宣言する必要があります:

# 連想配列として宣言
declare -A CONFIG_MAP

# キーと値のペアを代入
CONFIG_MAP["port"]=8080
CONFIG_MAP["hostname"]="localhost"
CONFIG_MAP["timeout"]=30

# 値へのアクセス
echo "ポート設定: ${CONFIG_MAP["port"]}"

# キーでの繰り返し
for key in "${!CONFIG_MAP[@]}"; do
    echo "キー: $key, 値: ${CONFIG_MAP[$key]}"
done

2. 一時ファイルの代わりにプロセス置換を使う

<(command) または >(command) と記述するプロセス置換を使うと、コマンドの出力をファイルのように扱えます。これは、ツールがファイルパスを期待するが、データがコマンドから生成される場合に便利です。

役立つ場面

例えば、2つの生成されたサービスリストを比較する必要があるとします。diff はファイルパスを期待しますが、それらのリストを /tmp に書き込む必要はありません。

プロセス置換なしの場合:

# 非効率的で乱雑
output1=$(command_a)
echo "$output1" > /tmp/temp1.txt
output2=$(command_b)
echo "$output2" > /tmp/temp2.txt
diff /tmp/temp1.txt /tmp/temp2.txt
rm /tmp/temp1.txt /tmp/temp2.txt

クリーンな直接比較

プロセス置換を使うと、diff に一時的なファイル記述子を渡し、スクリプトをよりシンプルに保てます。

プロセス置換ありの場合:

# クリーンな1行比較
diff <(command_a) <(command_b)

このパターンは commdiff、および複数のファイル入力を必要とするツールでうまく機能します。

構文のバリエーション:

  • <(command) はコマンドの出力を読み取り側に渡します。
  • >(command) は書き込まれた出力を別のコマンドに送ります。

3. 厳格モードとShellCheckを追加する

高度なBashスクリプティングでは、予期しないことが発生した場合に大きなエラーを出すべきです。厳格モードは、変数の欠落やパイプラインの破損を、静かに被害を及ぼす前に検出するのに役立ちます。

必須の厳格モードオプション

ほとんどの自動化スクリプトは次のように始めるべきです:

set -euo pipefail
  1. -e: コマンドが失敗したら終了します。
  2. -u: 未設定の変数をエラーとして扱います。
  3. -o pipefail: パイプライン内のいずれかのコマンドが失敗したら、パイプライン全体を失敗とします。

例:

# pipefailがない場合、wcが正常終了するため成功に見えることがある
cat file.log | grep successful_pattern | wc -l

# set -o pipefail を設定すると、grepの失敗でパイプライン全体が失敗する

ShellCheckを使う

ShellCheckは、クォートの誤り、安全でない展開、到達不能コード、一般的な移植性の問題を検出します。

スクリプトをコミットする前に実行してください:

shellcheck your_script.sh

ShellCheckが変数をクォートするよう促したり、"${array[@]}" を使うよう指示した場合、無視する明確な理由がない限り、それを実際のバグとして扱ってください。

4. 出力を慎重にキャプチャする

$() を使ったコマンド置換は便利ですが、軽率に使うと失敗を隠したり、出力ストリームを混同したりする可能性があります。

STDOUTとSTDERRの両方をキャプチャする

コマンドからのすべてをログに記録したい場合は、標準出力と標準エラーの両方をキャプチャします:

# 標準出力と標準エラーの両方をVARIABLEにキャプチャ
VARIABLE=$(command_that_might_fail 2>&1)

# 終了コードだけが必要な場合、標準出力と標準エラーの両方を破棄
command_that_might_fail &> /dev/null

インラインクリーニングのためのパラメータ展開

パラメータ展開を使うと、単純なケースでは sedawk を起動せずに文字列をクリーニングできます。

  • ${variable%pattern} は最短一致のサフィックスを削除します。
  • ${variable%%pattern} は最長一致のサフィックスを削除します。
  • ${variable#pattern} は最短一致のプレフィックスを削除します。
  • ${variable##pattern} は最長一致のプレフィックスを削除します。

例:

FILE="report.log.bak"
# .bakに一致する最短のサフィックスを削除
CLEAN_NAME=${FILE%.bak}
echo $CLEAN_NAME  # 出力: report.log

# *.bakに一致するすべてのサフィックスを削除(ここでは.bakのみ削除)
CLEAN_NAME_LONG=${FILE%%.*}
echo $CLEAN_NAME_LONG # 出力: report

助けを求めるタイミング

スクリプトがファイルを削除する、本番サービスを変更する、機密情報を扱う、CIから実行される場合は、経験豊富なシェルユーザーにレビューを依頼してください。Bashは強力ですが、小さなクォートの誤りがパターンに一致するすべてのファイルに影響を与える可能性があります。

まとめ

実際のリストには配列、ファイルのようなコマンド出力にはプロセス置換、安全な失敗処理には set -euo pipefail、迅速なフィードバックにはShellCheckを使用しましょう。これらの習慣により、高度なBashスクリプティングは保守が容易になり、自動化実行中の驚きが大幅に減少します。