クォート戦略と文字列安全化|ダブルクォート前提の指針

データとパラメータ展開
スポンサーリンク

この記事の狙い

Bash の文字列安全化を“場当たり”ではなく一貫した戦略として運用できるようにします。
ダブルクォートを原則とし、配列・パラメータ展開・コマンド置換・ヒアドキュメント・read 入力・グロブ/IFS/ロケールとの相互作用を事故らない手順でまとめます。

前提と対象

  • Bash 4+(macOS 標準の古い Bash 3.x は一部機能が非対応)
  • set -Eeuo pipefail 前提の安全設計に馴染みがある方向け

TL;DR(最小実装・コピペ可)

#!/usr/bin/env bash
set -Eeuo pipefail

# 1) すべての展開は原則ダブルクォート
printf '%s\n' "${var}"
cmd -- "${arr[@]}"

# 2) コマンド置換は $(...) を使い、中で必ずクォート
out="$(some_cmd --opt "${arg}")"

# 3) 文字列を“安全表示”するとき
printf '%q\n' "$var"          # Bash 4.4+なら: echo "${var@Q}"

# 4) 入力は IFS と read -r を固定
IFS= read -r line || true     # 改行以外は壊さない

# 5) NUL セーフな受け渡し(ファイル名等)
find . -print0 | xargs -0 -I{} printf '<%s>\n' "{}"

設計の要点(原則)

  1. 常にダブルクォート"$var", "${arr[@]}", "$(cmd)"
  2. 配列は [@] を基本、結合目的以外で [*] を使わない
  3. stdout=データ/stderr=メッセージ:ログは必ず >&2
  4. 入力は read -r、必要なら IFS を局所化
  5. NUL セーフ:ファイル名・任意バイナリの受け渡しは \0 区切り
  6. ロケールは明示:比較やソートに影響する処理は LC_ALL=C を局所適用
  7. eval を使わない:どうしても必要なら代替(printf -v, declare -n)を選ぶ

レシピ集(安全な書き方)

1) 変数・配列の基本展開

# 変数
printf '%s\n' "${file}"

# 配列(要素単位)
for x in "${files[@]}"; do
  printf '[%s]\n' "$x"
done

# コマンド引数へ
some_tool -- "${files[@]}"    # -- は以降をオプション解釈から守る

2) 連結・フォーマット

# 連結は printf 一択(echo は不正確)
printf '%s/%s\n' "${dir}" "${base}"

# 事前クォート済み表示(ログ用)
printf 'moving %s -> %s\n' "${src@Q}" "${dst@Q}"   # Bash 4.4+
# 4.3以下:
printf -v s '%q' "$src"; printf -v d '%q' "$dst"; printf 'moving %s -> %s\n' "$s" "$d"

3) コマンド置換とパイプ

# 置換結果もクォート
sha="$(sha256sum -- "${file}" | awk '{print $1}')"

# パイプは“片側で”整形し、結果はクォートして扱う
count="$(printf '%s\n' "${items[@]}" | wc -l)"

4) 入力:IFS と read -r

# 1行ずつ安全に読む(バックスラッシュはそのまま)
while IFS= read -r line; do
  printf '<%s>\n' "$line"
done <"$path"

# 改行を含むまま配列化
mapfile -t lines <"$path"            # 行末の改行は除去、-d '' で NUL 区切り

5) NUL セーフ(ファイル名など)

# 生成: 配列 → NUL 区切り
printf '%s\0' "${files[@]}" | xargs -0 -I{} cp -v -- "{}" "$dst"

# 受信: find → NUL 区切り → 配列
mapfile -d '' -t files < <(find . -type f -print0)

6) ヒアドキュメント(引用の向き)

cat <<'USAGE'   # クォートあり: 変数展開しない(テンプレ・ヘルプ向け)
Usage:
  mycli [options]
USAGE

cat <<EOF       # クォートなし: ${var} を展開する(テンプレ化しない文章向け)
Hello, ${USER}
EOF

7) グロブ(ワイルドカード)と noglob

set -f           # 必要時のみ: グロブ無効(関数の中で局所化を推奨)
# …安全化したい短い区間…
set +f

通常はクォート徹底で十分です。set -f は例外的に。

8) 既定値の安全な適用(未定義/空判定)

: "${TMPDIR:=/tmp}"                 # 未定義/空なら代入
: "${API_TOKEN:?set API_TOKEN}"     # 未定義/空なら即エラー

9) ロケールの影響を局所化

LC_ALL=C sort  # バイト順で安定、言語依存の並び替えを避ける

アンチパターン → 安全書き換え

悪い例問題良い例
echo $var単語分割/グロブ暴発printf '%s\n' "${var}"
for x in ${arr[@]}; do空要素消失for x in "${arr[@]}"; do
out=$(cmd $arg)分裂・注入out="$(cmd -- "${arg}")"
echo <<EOF変数展開予期せずcat <<'EOF'
eval "$user_input"任意実行代替: printf -v, declare -n, 連想配列

“コピペ可”テストブロック(最小)

#!/usr/bin/env bash
set -Eeuo pipefail
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT

# 1) 配列の空要素・空白保持
arr=("a" "" "b c" "*")
out="$(for x in "${arr[@]}"; do printf "<%s>" "$x"; done)"
[[ "$out" == "<a><><b c><*>" ]] || { echo "array-quote"; exit 1; }

# 2) read -r と IFS
printf 'a b\c\nd\n' >"$TMP/in"
mapfile -t lines <"$TMP/in"
[[ "${#lines[@]}" -eq 2 ]] || { echo "mapfile"; exit 1; }
while IFS= read -r l; do last="$l"; done <"$TMP/in"
[[ "$last" == "d" ]] || { echo "read-r"; exit 1; }

# 3) NUL セーフ往復
printf 'x\0y z\0' >"$TMP/nul"
mapfile -d '' -t N <"$TMP/nul"
joined="$(printf '%s|' "${N[@]}")"
[[ "$joined" == "x|y z|" ]] || { echo "nul"; exit 1; }

echo "PASS"

運用設計(実務)

  • ライブラリ関数の入口と出口でクォートを徹底(引数受領時/出力直前)。
  • ログは stderr に集約し、ユーザー入力は %q/@Q で安全表示。
  • ファイル名リストは最初から最後まで配列で持ち、外部コマンド直前だけ \0 で橋渡し。
  • CI に ShellCheck を常時走らせ、SC2086/SC2046(未クォート)が出たら直す文化に。

互換性と移植性

  • ${var@Q}Bash 4.4+。それ以前は printf '%q' を使用。
  • mapfile/readarrayBash 4+。Bash 3.x では while IFS= read -r で代替。
  • BusyBox 等では一部挙動が異なるため、シェバンを Bash 固定する。

セキュリティと安全設計

  • コマンド注入対策:eval/未クォート展開/未検証の IFS 変更を避ける。
  • ログ漏洩対策:秘密値はマスクし、@Q などで“見た目の安全化”のみを過信しない。
  • 入力境界:ユーザー入力を変数名やパスの一部に直接組み込まない(検証・許可リスト)。

パフォーマンスの勘所(短く)

  • printf とパラメータ展開は軽量。外部コマンド(sed, awk)を減らすほど速くなる。
  • 大量の連結は printf を使い、配列コピーを避ける。

参考リンク

スポンサーリンク
Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする