ログオフ
開発2026-07-07

zshで『bashなら動くのに』となる単語分割の罠——forループとffmpegのオプション変数

macOSのデフォルトシェルはzsh。bashのつもりで書いたスクリプトが、エラーも出さずに黙って別の動きをすることがある。for x in $VAR が1回しか回らない問題と、ffmpegのオプションを変数にまとめたら Trailing garbage で落ちる問題。根っこは同じ。

macOS のデフォルトシェルは zsh になった。bash のつもりで書いたスクリプトが、エラーも出さずに黙って別の動きをすることがある。原因はほぼ「クオートで囲っていない変数の扱い」の 1 点に集約する。

先に共通の原因を書く。bash は、クオート(")で囲っていない変数を空白で区切って複数の引数にする(これを単語分割と呼ぶ)。zsh はこれをしない。 だから VAR="a b c"; cmd $VAR と書くと、bash は a b c の 3 引数、zsh は a b c の 1 引数になる。この違いが、次の 2 つの症状を生む。

落とし穴1:for x in $VAR がzshだと1回しか回らない

事象

VAR="a b c"
for x in $VAR; do echo "$x"; done
  • bash:a / b / c と 3 回まわる
  • zsh:a b c が丸ごと入って 1 回で終わる

ループが空振りするのに、エラーは一切出ない。だから気づきにくい。

原因

zsh は $VAR を分割せず 1 語として for のリストに渡す。リストの要素が 1 個なので、1 回しかまわらない。

解決策

# ① リストは変数に詰めず直接列挙する(一番素直)
for x in a b c; do echo "$x"; done

# ② どうしても変数経由なら ${=VAR} で分割を明示する
for x in ${=VAR}; do echo "$x"; done

# ③ 配列にする(おすすめ)
arr=(a b c)
for x in "${arr[@]}"; do echo "$x"; done

落とし穴2:ffmpegの共通オプションを変数にまとめたら Trailing garbage で落ちる

事象

複数の ffmpeg 呼び出しでエンコード設定を使い回そうと、オプションを変数にまとめた。

ENC="-c:v libx264 -pix_fmt yuv420p -movflags +faststart"
ffmpeg -i in.mp4 $ENC out.mp4

zsh でこう落ちる。

Trailing garbage at the end of a stream specifier

原因

zsh は $ENC を分割しないので、-c:v libx264 -pix_fmt yuv420p -movflags +faststart という文字列全体が 1 個の引数として ffmpeg に渡る。ffmpeg は -c:v の値としてこの塊を受け取り、c:v の後ろをストリーム指定子(処理対象の映像/音声トラックを指定する記法)として解釈しようとして壊れる。bash なら単語分割されて意図どおり複数オプションになるので、bash で書いたスクリプトを zsh でまわすと踏む。

解決策

for のときと同じで、分割を明示するか配列にする。

# ① 分割を強制する
ffmpeg -i in.mp4 ${=ENC} out.mp4

# ② 配列(空白や特殊文字を含む引数にも強い・こちらが安全)
enc=(-c:v libx264 -pix_fmt yuv420p -movflags +faststart)
ffmpeg -i in.mp4 "${enc[@]}" out.mp4

補足

  • 根っこは「zsh は未クオート変数を単語分割しない」の一点だ。for でも ffmpeg でも curl でも、変数に複数の引数を詰めて $VAR で展開する書き方は zsh で崩れる
  • 迷ったら 配列 + "${arr[@]}" が最も安全。空白やクオートを含む引数もそのまま保てる
  • setopt shwordsplit で zsh を bash 寄りの分割挙動にもできるが、スクリプト全体の挙動が変わる。局所的に ${=VAR} か配列で対処するほうが事故が少ない

※ 本記事にはアフィリエイトリンクが含まれます。

開発 一覧へ