Linux 講座 / Bash エラー処理と安全なファイル操作
安全なファイルスクリプトは、入力と目的地をまず確認し、作業失敗を明示的に処理し、失敗した中間結果を正常な結果のように残してはいけません。set -e ひとつに依存するのではなく、それぞれのコマンドの失敗が何を意味するかを判断し、一時ファイルの整理や上書き条件を設計してください。
この記事は 最初の Bash スクリプト を基に、失敗分岐とファイル操作の限界について扱います。&&・||・パイプラインの基本実行ルールは、まず シェル実行フロー で確認してください。
実習で保証する範囲を決める
次の例は、自分だけが使うホームディレクトリ下で、普通のファイルひとつを新しい名前でコピーし、内容が同じか確認します。目的地ディレクトリは既に存在しており、他のユーザーが変更できない信頼できる場所である必要があります。攻撃者が同時にパスを変更する環境に対するセキュリティツールやデータベースバックアップツールではありません。管理者権限で実行しません。
検査・一時コピー・公開に分けた全体例
lab=$(mktemp -d "$HOME/linux-safe-copy.XXXXXX")
cd "$lab"
mkdir output
printf "important sample\n" > "source file.txt"
cat > checked-copy.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
if (( $# != 2 )); then
printf "Usage: %s SOURCE DESTINATION\n" "$0" >&2
exit 2
fi
source_file=$1
destination=$2
if [[ ! -f "$source_file" || -L "$source_file" ]]; then
printf "Source must be a regular non-symlink file\n" >&2
exit 1
fi
if [[ -e "$destination" || -L "$destination" ]]; then
printf "Destination already exists\n" >&2
exit 1
fi
parent=$(dirname -- "$destination")
if [[ ! -d "$parent" ]]; then
printf "Destination directory is missing\n" >&2
exit 1
fi
work=$(mktemp -d "$parent/.copy-work.XXXXXX") || exit 1
cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ! cp -- "$source_file" "$work/payload"; then
printf "Copy failed\n" >&2
exit 1
fi
if ! cmp -s -- "$source_file" "$work/payload"; then
printf "Verification failed\n" >&2
exit 1
fi
if ! ln -- "$work/payload" "$destination"; then
printf "Destination creation failed\n" >&2
exit 1
fi
printf "Created: %s\n" "$destination"
EOF
bash -n checked-copy.sh
bash checked-copy.sh "source file.txt" "output/copy file.txt"
cmp "source file.txt" "output/copy file.txt"
通常のファイルを一時ディレクトリにコピーして比較した後、lnで新しい目的地名を作成します。一時ファイルを目的地と同じファイルシステム上に作成したので、ハードリンクを使用できます。すでに名前が存在する場合、lnは失敗し、上書きしません。EXITでは一時名を削除しますが、目的地リンクは残ります。ハードリンクをサポートしていないファイルシステムでは最後のステップが失敗するため、強制的に他の方法で回避せず、要件を再確認してください。
この例では内容のコピーを検証しますが、所有権・ACL・拡張属性の保持や書き込み完了の永続化(電源断への耐久性)までは保証しません。コピー中に元のファイルが継続的に更新される作業にも適しません。rm -rfはmktempが成功して作成した作業パス1つのみを対象とし、入力された元・目的地パスには使用しません。
失敗条件を実際に変更して試す
bash checked-copy.sh "source file.txt" "output/copy file.txt"
printf "exit=%s\n" "$?"
bash checked-copy.sh missing.txt output/new.txt
printf "exit=%s\n" "$?"
find output -maxdepth 1 -name ".copy-work.*" -print
最初の再実行は目的地がすでに存在するため、ステータス1で終了し、既存の内容を保持する必要があります。存在しない元ファイルも失敗しなければなりません。正常終了・一般的な失敗の後には、最後の find で一時作業パスが表示されてはいけません。SIGKILL や電源喪失の場合には trap が実行されないため、残ったファイルの別途整理ポリシーが必要です。失敗テスト後に、元ファイルと最初のコピーが同じかどうかを cmp で再度確認します。
set -eとpipefailを過信しないこと
bash -c 'false | cat; printf "status=%s\n" "$?"'
bash -o pipefail -c 'false | cat; printf "status=%s\n" "$?"'
最初のコマンドはパイプの最後の cat が成功するので通常 status=0、2番目は pipefail のため status=1 を出力します。pipefail は失敗をステータスに反映するオプションであり、自動復旧機能ではありません。set -e は if チェック、条件リスト、パイプなど文脈によって適用例外があるため、予想した位置で停止するかどうか個別にテストする必要があります。
if ! command; thenは失敗分岐を書くのに便利ですが、その中の$?は否定演算後の状態なので、元の終了コードをそのまま保持しません。元のコードを残す必要がある場合は、if command; then ...; else rc=$?; ...; fiのように失敗直後に保存してください。スクリプトが返すコードとエラーメッセージも、呼び出し側が区別できるように設計します。
運用自動化に適用する前に確認すること
- 変数展開は引用符で保護し、ファイル名引数の前の -- のサポート有無を確認します。
- プレビュー モードと実際の変更モードを分離し、プレビューでデータが変わらないようにします。
- パスの所有権・権限、シンボリックリンク、同時実行、元の変更可能性を検討します。
- 成功・失敗・シグナル終了を別々にテストし、復元可能なバックアップを残します。




