NUL 安全の処理系|find -print0 | xargs -0

設計パターン&テンプレ
スポンサーリンク

この記事の狙い

ファイル名に空白・タブ・改行・ワイルドカードが含まれても壊れない“NUL セーフ”な処理系を作る方法をまとめます。
中核は find -print0xargs -0、シェル側は read -r -d '' / mapfile -d '' です。

前提と対象

  • Bash 4+/set -Eeuo pipefail 推奨
  • POSIX ではファイル名に NUL(\0)だけが禁止。つまり改行は合法 → 改行区切りは危険
  • GNU/BSD どちらの find/xargs-print0/-0 をサポート(古環境は要確認)

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

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

# 1) 生成: NUL 区切りで列挙
find . -type f -print0

# 2) 消費: xargs -0 で安全に引き渡す
find . -type f -print0 | xargs -0 -I{} printf '<%s>\n' "{}"

# 3) Bash で読む: read -r -d '' / mapfile -d ''
while IFS= read -r -d '' path; do
  printf 'FILE: %q\n' "$path"
done < <(find . -type f -print0)

なぜ NUL 区切りか

  • 改行区切りだと、"foo\nbar" や先頭 -* を含むファイル名で崩壊
  • \0 はファイル名に現れない唯一のバイト → 区切りとして絶対衝突しない
  • find -print0 は各パス末尾に \0xargs -0\0 を区切りに引数化

主要レシピ

1) find → xargs の王道

# 拡張子 .log を gzip 圧縮(並列、上書き安全)
find logs -type f -name '*.log' -print0 \
| xargs -0 -P4 -n1 -I{} sh -c '
  set -Eeuo pipefail
  gzip -n -9 -- "{}"
'
  • -P4 で並列、-n1 で 1 ファイルずつ
  • -I{}引数は必ずクォートして受ける("{}"
  • 可能なら -exec ... {} + も検討(プロセス数削減)。ただし安全なログや分岐を入れたい時は xargs が書きやすい

2) while で逐次処理(Bash)

while IFS= read -r -d '' p; do
  # p を安全にログ
  printf '-> %s\n' "${p@Q}" >&2      # Bash 4.4+ は @Q
  # 処理本体
  cp -- "$p" /backup/
done < <(find /data -type f -print0)

3) 配列 → NUL → 外部コマンド

files=("a b" "c"$'\n'"d" "--weird*name")
printf '%s\0' "${files[@]}" | xargs -0 -I{} sha256sum -- "{}"

4) NUL に対応した GNU ツール群

  • sort -z / uniq -z / grep -z / sed -z / tr -d '\0'
  • 例:重複ファイル名(理論上)を NUL 単位でユニーク化
find . -type f -print0 | sort -z | uniq -z | xargs -0 -I{} echo "{}"

BSD 系では -z がないコマンドもあるため、Bash 側で読むread -d '')アプローチを優先

5) 先頭 - 対策とワイルドカード保護

常に -- とクォートを併用:

xargs -0 -I{} rm -- "{}"
xargs -0 -I{} grep -H -- 'pattern' "{}"

6) rsync / git などの “from0” 連携

NUL 区切りのリストを直接渡せるツールは中間壊れが起きない

# rsync: --files-from と組み合わせ
printf '%s\0' ./a ./b$'\n'c | rsync -a --from0 --files-from=- ./ dest/

# git: -z フラグ
git ls-files -z | xargs -0 -I{} git blame -- "{}"

設計指針(原則)

  1. 「境界は NUL」の前提で統一:生成も消費も \0
  2. ログは stderr、パラメータはクォート済み表示%q / @Q
  3. -- でオプション終端"{}" で引数安全化
  4. NUL 非対応のコマンドにどうしても渡す時は、Bash ループで 1 件ずつ渡す
  5. 並列化は xargs -P&+wait出力先の競合回避を先に設計

ありがちな落とし穴 → 回避

症状原因解決
スペース・改行入りで壊れる改行区切り-print0 / -0 / -d '' を使う
-rf と解釈されて削除先頭 - のファイル名-- を必ず挟む
グロブが展開される未クォート"{}" / "$var" を徹底
BSD で grep -z が無い実装差Bash で読むread -d '')or 代替ツール
文字化けロケール差LC_ALL=C で比較/ソートを局所化

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

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

# 問題児ファイルを作成
touch "a b" "--opt" "c"$'\n'"d" "*star?"

# 1) find -print0 | xargs -0 で全件拾えるか(カウント)
n1="$(find . -maxdepth 1 -type f -print0 | xargs -0 -I{} printf 'X\n' | wc -l)"
[[ "$n1" -eq 4 ]] || { echo "xargs-0 fail: $n1"; exit 1; }

# 2) Bash の read -d '' で全件読めるか
cnt=0
while IFS= read -r -d '' p; do ((cnt++)); done < <(find . -maxdepth 1 -type f -print0)
[[ "$cnt" -eq 4 ]] || { echo "read-d0 fail: $cnt"; exit 1; }

# 3) 危険名でも rm できる(-- とクォートで)
find . -maxdepth 1 -type f -print0 | xargs -0 -I{} rm -- "{}"
[[ "$(find . -maxdepth 1 -type f | wc -l)" -eq 0 ]] || { echo "rm fail"; exit 1; }

echo "PASS"

実務のTips

  • 長いパイプラインは可観測性をset -o pipefaillogger 併用、tee >(…) で分岐ログ
  • 一時ファイルは避けたい → まずは NUL パイプでつなぐ。どうしても必要な時だけ mktemp
  • 大量 I/O の最適化xargs -P$(nproc)、コマンドが複数引数対応なら -n を増やして起動回数削減
  • NFS やリモート FSfind はローカルでリスト化 → 転送ツールへ --from0 で渡す

互換性と移植性

  • macOS/BSD でも find -print0 / xargs -0 は利用可
  • GNU の sort -z など -z 系は GNU 依存が多い → Bash ループで代替可能
  • POSIX 互換シェルでは read -d '' は不可(Bash 必須)。ポータビリティ優先なら xargs -0 に寄せる

セキュリティと安全設計

  • 外部入力のパスは再評価しないeval 禁止)
  • ログへパスを出す際は マスクやクォート表示%q / @Q
  • 破壊的操作(rm 等)は**--** と set -o noclobber 等で二重安全化。重要処理はドライランを先に

参考リンク

スポンサーリンク
Bash玄

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

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

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

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

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

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

Bash玄をフォローする