
Bash スクリプトは便利で強力です。しかし、その力ゆえに不用意なコードや設計ミスが重大な問題を引き起こすことがあります。ここでは日常的に現れる落とし穴を避けるための実践的なルールと、運用時に役立つ追加の方法論をまとめました。
定義: Shebang — スクリプトの先頭に置く特別なコメントで、スクリプトを実行するインタプリタを指定します。
目次
- Shebang を正しく使う
- 変数は必ず引用する
- エラー時にスクリプトを止める
- 失敗を伝え、前倒しで処理を中断する
- 各コマンドをデバッグで可視化する
- 他コマンドを呼び出すときは長いパラメータ名を使う
- コマンド置換は近代的な表記を使う
- デフォルト値を宣言する
- ダブルダッシュでオプションと引数を区切る
- 関数内はローカル変数を使う
- 実践チェックリスト & テストケース
- セキュリティ強化と運用ルール
- インシデント対応ランブック
- 付録: チートシート、用語集
Shebang を正しく使う
スクリプトの最初の行は必ず shebang(#! で始まる行)で始め、どのインタプリタで実行するかを明示しましょう。shebang がないと、スクリプトは呼び出したシェルによって実行され、期待と異なる挙動をする可能性があります。
伝統的な書き方の例:
#!/bin/bash
echo "Hello, world"または移植性を高めるために env を使う方法:
#!/usr/bin/env bash
echo "Hello, world"- /bin/bash を直接指定する利点: 明確に特定の実行ファイルを使うため、システム上の別の bash によるハイジャックを防げる可能性があります(セキュリティ的に有利なことがある)。
- /usr/bin/env を使う利点: PATH にある最適な実行ファイルを選ぶため、様々な環境で可搬性が高い。開発者のローカル環境やコンテナ環境で便利です。
選択は運用ポリシーと脅威モデル次第です。配布用パッケージやセキュアなサーバ環境では /bin/bash を使い、開発ツールやユーザー向けスクリプトでは env を使う、といったルールを決めておくとよいでしょう。
重要: Shebang はスクリプトを「スタンドアロンの実行ファイル」にします。単に sh スクリプトとして読み込まれるのではなく、明示的にどのシェルで実行すべきかを示します。
変数は必ず引用する
ファイル名や引数に空白や特殊文字が含まれていると、Bash の単語分割で意図せぬトークン分割が起きます。変数展開時は常にダブルクォートで囲みましょう。
悪い例:
#!/bin/bash
FILENAME="docs/Letter to bank.doc"
ls $FILENAMEこの場合、ls は 3 つの引数を受け取ったかのように扱います。
良い例:
ls "$FILENAME"変数名を中括弧で囲むのも良い習慣です。中括弧は文字列が連続する状況で誤解を防ぎます。
echo "_${FILENAME}_ is one of my favourite files"ヒント: 単一引用符(’)は変数展開を無効化します。展開したいときは必ずダブルクォート(”)を使ってください。
エラー時にスクリプトを止める
未検出の失敗は危険です。短いスクリプトでも、途中で失敗したコマンドを無視して続行するとデータ破壊や未定義状態を招きます。
基本的な安全策として、スクリプトの先頭で次を宣言します。
set -eset -e の説明(要約): 単一コマンドでもパイプラインでも、非ゼロ終了コードが返ったら即座に終了します。ただし、サブシェルや条件式内の振る舞いは複雑なので注意が必要です。
パイプラインの早期失敗を検出するには次も使います:
set -o pipefailこれにより、パイプラインの最初の失敗を見逃さずにスクリプト全体を非ゼロで終わらせます。
注意点: set -e は万能ではありません。if や &&, || の中にあるコマンドや、サブシェルの扱いなど例外があります。重要なコマンドは明示的にチェックする習慣を併用してください。
失敗を伝え、前倒しで処理を中断する
set -e は補助線に過ぎません。特定のコマンドについては明示的に終了コードをチェックして、適切な後処理やログ出力を行いましょう。
標準的なチェック例:
cd "$DIR"
if [ $? -ne 0 ]; then
echo "ディレクトリに移動できませんでした: $DIR" >&2
exit 1
fiより簡潔な書き方:
cd "$DIR" || { echo "ディレクトリに移動できませんでした: $DIR" >&2; exit 1; }注意: 中括弧 { } を使う際は、前後にスペースが必要です。
各コマンドをデバッグで可視化する
デバッグ時には xtrace を使うと便利です。
set -o xtrace
# または短縮形
set -x実行すると、各コマンドが展開された形で標準エラーに出力されます。これは実行時に実際に何が渡されているかを確認するのに非常に有用です。

運用上のヒント:
- スクリプト全体に set -x を付けるのではなく、問題のある関数やセクションだけで有効化/無効化するとログが読みやすくなります。
- 機密情報(パスワードやトークン)が出力されないようにログ出力前にマスク処理を行ってください。
他コマンドを呼び出すときは長いパラメータ名を使う
短い単一文字のオプションは覚えやすい反面、可読性に欠けます。スクリプトでは「自己文書化」のために長いオプション名(–recursive など)を使いましょう。
例:
# 悪い例
rm -rf filename
# 良い例
rm --recursive --force filename長いオプションは他の開発者がスクリプトを読んだときに直感的に意図を理解できます。
コマンド置換は近代的な表記を使う
コマンドの出力を変数に代入する際、推奨される書き方は $(…) です。古いバッククォート記法は入れ子にしづらく、可読性が低いため避けてください。
# 推奨
VAR=$(ls)
# 避けるべき(非推奨)
VAR2=`ls`デフォルト値を宣言する
環境変数や引数にフォールバックを与えると、呼び出し側が値を渡し忘れた場合でも安全に動作するようになります。
CMD=${PAGER:-more}ネストも可能です。コマンドライン引数 → 環境変数 → ハードコードされたデフォルト、の優先度でフォールバックできます。
DIR=${1:-${HOME:-/home/default}}ダブルダッシュでオプションと引数を区切る
ファイル名がハイフン(-)で始まると、多くのコマンドがそれをオプションと解釈してしまいます。危険を避けるため、明示的に “–“ を使ってオプションの終わりを示しましょう。
# 危険な例: カレントディレクトリに -rf というファイルがあると...危険
rm *
# 安全な例
rm -- *.mdこれはスクリプトが予期せぬ引数を受け取った時の防御に有効です。
関数内はローカル変数を使う
Bash の変数はデフォルトでグローバルです。関数内で意図せず値を上書きしてしまうと、スクリプト全体が壊れる原因になります。関数内では必ず local を使いましょう。
function run {
local DIR=$(pwd)
echo "doing something..."
}
DIR="/usr/local/bin"
run
echo $DIR # 上書きされない実践チェックリスト(開発→ステージング→本番)
開発段階から本番運用までに満たすべきチェックリストを示します。自動 CI に組み込むことを前提にした実務向け項目です。
- shebang が適切に指定されている(/bin/bash か /usr/bin/env bash)。
- スクリプトの先頭に set -e と set -o pipefail を記載している。例外がある場合はコメントで理由を残す。
- すべての変数展開はダブルクォートで囲まれている。
- 関数内で副作用を持つ変数は local 宣言している。
- コマンド置換は $(…) を使用している。
- 引数は検証(存在チェック、型チェック、許可リスト)している。
- 危険なコマンド(rm, mv, cp など)には – を付けるか、フルパスのファイル名を検証している。
- ログは標準出力と標準エラーを分け、機密情報はマスクする。
- 主要な操作に対して unit テストや統合テストがある。
- 実行に必要な最小権限だけを使う(root を安易に使わない)。
テストケースと受け入れ基準
簡易的なテストケース例を示します。CI パイプラインに追加して自動検証できるようにします。
- 変数に空白が含まれるファイル名での検証
- 入力: FILENAME=”my file with spaces.txt”
- 実行: スクリプト内で ls “$FILENAME” を呼ぶ
- 期待: ファイルが正しくリスト、エラーなし
- 存在しないディレクトリでの cd の扱い
- 入力: DIR=/path/does/not/exist
- 実行: cd “$DIR” || exit 1
- 期待: スクリプトが非ゼロで終了し、適切なエラーメッセージを出力する
- パイプラインの早期失敗検出
- 入力: 前半のコマンドが失敗するパイプライン
- 実行: set -o pipefail が有効
- 期待: スクリプトは非ゼロで終了する
受け入れ基準(Критерии приёмки):
- 主要機能が正しく動作し、エラー時に適切な終了コードを返すこと。
- ログは期待される形式で出力され、機微情報は含まないこと。
- CI が定義したすべてのテストをパスすること。
セキュリティ強化のチェックポイント
- 最小権限: スクリプトが root 権限を必要とする場合、具体的な理由と範囲を文書化する。
- 入力検証: 外部入力(引数、環境変数、stdin)はホワイトリスト方式で検証する。
- 外部コマンド依存: 実行する外部コマンドのフルパスを指定するか、PATH の制御を行う。
- シェルインジェクション対策:
evalの使用は避ける。どうしても使う場合はユーザー入力を厳密に検証・エスケープする。 - 機密情報: トークンやパスワードは環境変数もしくは専用の秘密管理ツールで管理し、ログに出力しない。
サンプル: PATH を固定して実行するテンプレート
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# 信頼できる PATH のみを設定
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
# 以下スクリプト本体ここでは set -u も追加しています。set -u は未定義変数を参照した場合にエラーにするため、バグの早期発見に役立ちます。
重要: set -u は一部のユースケース(存在しないオプションのチェックなど)で誤検知を招くことがあるため、扱いに注意してください。
インシデント対応ランブック(簡易版)
障害発生時に迅速に復旧するための手順を簡易ランブックとして残しておきます。
- 影響範囲の特定
- 実行されたスクリプト、引数、環境を特定する。
- 実行ログを収集する(set -x を使っていた場合は詳細なコマンドログを取得)。
- 直ちに止めるべきか判断
- 状況に応じて、同一サーバ上の定期ジョブを一時停止する。
- ネットワークや外部サービスへの不要なアクセスを遮断する。
- ロールバック/修復
- 可能な限り安全な方法でデータを復旧する。バックアップからの復元やリードオンリー化を検討。
- 破壊系コマンド(rm -rf 等)が誤実行された場合は、ファイルシステムのスナップショットやバックアップからの復元を実施。
- 根本原因分析
- スクリプトのどの部分で失敗または誤操作が起きたかを特定する。
- 失敗を再現できる最小限のテストケースを作る。
- 再発防止策
- チェックリストの追加、CI テストの拡充、インプット検証の強化、パーミッションの見直しなどを行う。
- ドキュメント化と連絡
- 対応ログ、原因、対応内容、再発防止策を関係者に共有する。
よくある誤解と失敗例
- set -e があれば安心
- 誤解: set -e だけで全ての失敗を検出できると思い込む。
- 現実: 条件分岐やサブシェル内のコマンドは想定外に無視されることがある。重要なコマンドは明示的にチェックする。
- Bash は単一の標準に従う
- 誤解: /bin/bash の挙動はどこでも同じだと思う。
- 現実: バージョンや配布(Debian 系、RedHat 系、macOS など)で微妙に違いがある。可搬性のために POSIX 準拠を意識する場合は sh に合わせた書き方を検討する。
- eval は便利だから多用すべき
- 誤解: 複雑な文字列操作は eval で簡単にできる。
- 現実: eval はシェルインジェクションのリスクが高く、外部入力が混ざると危険。
代替アプローチ
- POSIX sh 互換で書く: スクリプトをより広い環境で動作させたい場合は、Bash 固有機能を避けて sh 互換で書く。
- 高レベル言語に置き換える: 複雑なロジックや文字列処理、堅牢なエラー処理が必要な場合は Python、Go、Rust などに移行することを検討する。
- 小さなヘルパーツール: 複雑なファイル操作やパース処理は、小さなバイナリツール(Go など)に切り出すと安全性とテスト容易性が向上する。
メンタルモデルとヒューリスティクス
- 最小特権の原則: スクリプトは必要最低限の権限で実行する。
- 単一責任: 1 つのスクリプトは 1 つの責務に集中させる。複数の責務がある場合は小さなモジュールに分ける。
- 防御的プログラミング: 外部入力を信用しない。検証→正規化→使用の流れで扱う。
- 可観測性: 失敗時に原因を特定できるように、十分なログを残す。
チートシート: よく使う雛形
安全なスクリプトの雛形を示します。
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# PATH を固定
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
# ロギング関数
log() {
echo "[INFO] $(date --iso-8601=seconds) $*"
}
err() {
echo "[ERROR] $(date --iso-8601=seconds) $*" >&2
}
# 引数チェック
if [ ${#} -lt 1 ]; then
err "Usage: $0 "
exit 2
fi
TARGET="$1"
# 安全にファイルへアクセスする例
if [ ! -e "$TARGET" ]; then
err "ファイルが存在しません: $TARGET"
exit 3
fi
log "処理を開始します: $TARGET"
# 実際の処理 ロール別チェックリスト
- 開発者
- スクリプトは小さく、責務が明確か
- 単体テスト、引数ケーステストを追加しているか
- 運用(Ops)
- 実行環境(PATH、ユーザー権限)をドキュメント化しているか
- ログと監視の出力先が明確か
- セキュリティ担当
- 機密情報が漏洩していないか
- インジェクション可能な箇所はないか
1行用語集
- shebang: スクリプトの先頭で実行するインタプリタを指定する行。
- set -e: コマンドが非ゼロ終了したら即座にスクリプトを終了する設定。
- pipefail: パイプライン内の任意のコマンドが失敗した場合に失敗とみなす設定。
- xtrace (set -x): コマンド実行前にコマンドを標準エラーに出力する。
互換性と移行の注意
- POSIX 準拠: スクリプトを多様な Unix 系で動かす予定がある場合、Bash 固有構文を避け POSIX 準拠の書き方を検討してください。
- macOS: 古いバージョンの macOS には古い bash がバンドルされている場合があるため、/usr/bin/env を使って Homebrew の bash を指定するケースがある。
- コンテナ: コンテナイメージ上では PATH やシステムユーティリティの位置が違うことが多いので、CI で実行する環境を合わせてテストしてください。
まとめ
Bash スクリプトの安全性と可搬性を高めるための要点:
- shebang を明示しておく(/bin/bash か /usr/bin/env bash)。
- 変数は常に引用する。中括弧で名前を囲む習慣をつける。
- set -e と set -o pipefail を使い、重要なコマンドは明示的にチェックする。
- デバッグ時は set -x を活用する。機密情報のログ漏洩に注意。
- 危険な引数解釈を避けるために – を使う。
- 関数内の変数は local で宣言する。
- 必要なら高レベル言語に置き換え、テストを自動化する。
最後に: ルールを一貫して適用し、CI と実行時の検査(静的解析/テスト)を組み合わせれば、Bash スクリプトの事故を大幅に減らせます。
まとめ(短い箇条書き):
- 安全なスクリプト雛形を作り、プロジェクトで共有する。
- CI にテストを組み込み、リリース前に自動検査を実施する。
- インシデント時のランブックを整備し、ログと監視を充実させる。