1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

zshで書いたスクリプトをbashで動かすと、静かに違う結果を出す8箇所

1
Last updated at Posted at 2026-09-03

手元のスクリプトを別の環境に持っていけるか確かめようとして、zsh と bash に同じファイルを渡してみました。出力が8箇所で違いました。エラーは1つも出ていません。

エラーになるものより、黙って違う値を返すもののほうが多いというのが、並べてみて分かったことです。

以下、zsh 5.9 と bash 5.2.21 の両方で実行した結果です。同じスクリプトを両方に食わせて、出力を並べています。

黙って違う値になるもの

こちらが本題です。エラーが出ないので、実行しただけでは気づけません。

配列の添字が、1から始まるか0から始まるか

A=(alpha beta gamma)
echo "${A[1]}"
出力
zsh alpha
bash beta

zsh の配列は1始まり、bash は0始まりです。**どちらも「ある要素」を返すので、間違いに見えません。**ループの中で使っていると、1つずれた結果が最後まで通ります。

${#A} が、要素数か文字数か

A=(alpha beta gamma)
echo "${#A}"
出力
zsh 3
bash 5

zsh は要素数の3、bash は最初の要素 alpha の文字数の5を返します。どちらも整数なので、件数として使っていると気づきません。

$A が、全要素か最初の1つか

A=(alpha beta gamma)
echo "$A"
出力
zsh alpha beta gamma
bash alpha

bash で書いたつもりの人は ${A[@]} と書くので出会いませんが、zsh で書いたコードを持っていくと、要素が2つ消えます。

クォートしない変数が、単語に分かれるかどうか

V="x y z"
for w in $V; do echo "[$w]"; done
出力
zsh [x y z] (1回)
bash [x] [y] [z] (3回)

これがいちばん厄介だと思いました。zsh は変数展開で単語分割をしません。bash はします。ループの回数が変わります。

区切り文字で分けたい意図で書いていれば zsh で動かず、逆に分けたくないなら bash で壊れます。どちらの向きにも事故ります。

なお、コマンド置換 $(...) のほうは両方とも分割します。ここだけ挙動がそろっているので、余計に混乱しました。

${V:u} が、大文字化か素通りか

V=abcdef
echo "${V:u}"
出力
zsh ABCDEF
bash abcdef

zsh の大文字化の記法です。bash はエラーを出さず、**そのまま返します。**大文字にしたつもりの文字列が、小文字のまま後続に流れます。

bash で大文字化するなら ${V^^} です。記法そのものが違います。

エラーになるもの

こちらは、実行すれば分かります。

${=V}(強制的に単語分割する)

V="x y z"
for w in ${=V}; do echo "[$w]"; done

zsh では3回まわります。bash では止まります。

line 7: ${=V}: bad substitution

${(j:,:)A}(配列を区切り文字で結合する)

A=(alpha beta gamma)
echo "${(j:,:)A}"

zsh では alpha,beta,gamma です。bash では同じく bad substitution になります。

わたしはこれを実際に使っていました。移植するなら IFS を使う書き方に直します。

IFS=,
echo "${A[*]}"
unset IFS

これは両方で alpha,beta,gamma になります。

挙動が違うが、気づきやすいもの

グロブが一致しなかったとき

for f in /nonexistent/*.txt; do echo "[$f]"; done
結果
zsh no matches found でスクリプトが止まる
bash パターン文字列がそのまま1回まわる

方向が正反対です。zsh は止まるので気づけますが、bash は存在しないファイル名を処理しようとします。[ -e "$f" ] で確かめるか、bash 側で shopt -s nullglob を入れます。

再帰グロブ **

for f in g/**/*.txt; do echo "[$f]"; done

g/a/b/c/deep.txt があるとき、こうなりました。

出力
zsh g/a/b/c/deep.txt
bash g/**/*.txt (一致せず、パターンのまま)

zsh は既定で再帰します。bash は shopt -s globstar が要ります。しかも一致しなかった場合はパターンがそのまま流れるので、上の項目と組み合わさって二重に事故ります。

両方で同じだったもの

安心して使える書き方も確かめました。

A=(alpha beta gamma)
for w in "${A[@]}"; do echo "[$w]"; done   # 両方とも3回
echo "${#A[@]}"                            # 両方とも 3
V=abcdef; echo "${V:0:3}"                  # 両方とも abc
for w in $(echo "x y z"); do :; done       # 両方とも3回

set -u も、メッセージは違いますが両方で止まります。

zsh : MISSING: parameter not set
bash: MISSING: unbound variable

echo -e は、今回の環境では両方ともエスケープを解釈しました。ただしここはビルトインの実装と設定に左右されるので、確実にしたいなら printf を使います。

移植するときの最低限

自分用にまとめると、こうなりました。

配列は必ず "${A[@]}" と書く。個数は ${#A[@]}。添字は使わない(どうしても要るなら、どちらのシェルで動かすかを先頭に書く)。変数を単語に分けたいときは、分けたい形の配列を先に作る。zsh 固有の記法(${=V}、${(j:,:)A}、${V:u})は使わない。グロブは、一致しない可能性を必ず考える。

そして、**シバンを書く。**今回の差は全部、#!/bin/zsh か #!/bin/bash を書いて、その通りに起動していれば出ません。手元で動いたから大丈夫、という確認の仕方をやめる、というだけの話でもあります。

追記:指摘を受けて直したところ

公開後にコメントで2つ指摘をいただきました。どちらも当たっていたので、記事を直しています。

シバンを書いていなかった

そもそもシバンを書いていなかったのではないか、という指摘です。そのとおりです。書いていませんでした。

そのうえで、書いても残る問題が2つあります。

ひとつ、シバンが効くのは ./script.sh で実行したときだけです。

./script.sh        # シバンが読まれる
bash script.sh     # シバンは無視される。bash で動く
sh script.sh       # 同じく無視される

呼び方を自分で決められない場合があります。cron から呼ばれるとき、CI の設定に書かれているとき、他の人が実行するとき。

ふたつ、**置いた先に zsh が入っているとは限りません。**Ubuntu の最小構成には入っていませんでした。今回の検証でも apt-get install zsh から始めています。#!/bin/zsh と書いたスクリプトは、そこでは起動すらしません。

シバンは前提条件を宣言するものであって、環境を用意してくれるものではありませんでした。

OSの差ではなく、シェルの差だった

もう1つは、これは macOS か Linux かの話ではない、という指摘です。これも当たっています。Linux で zsh を動かせば zsh の結果になり、macOS で bash を動かせば bash の結果になります。

当初のタイトルは「macOSのzshで書いたスクリプトが、Linuxのbashで」でした。原因をOSに置いてしまっていたので、変えました。

同じ指摘の中で、配布や移植を考えるなら bash か POSIX sh で書くほうがよいという提案もいただきました。同意します。今回の8箇所は、結局その結論に行き着くための材料でした。zsh 固有の記法を使わなければ、この比較そのものが要りません。

ただし POSIX sh まで下げると、配列も "${A[@]}" も使えなくなります。手元の道具は bash、人に渡すものは sh、という切り方を試そうと思っています。

経緯については

なぜ想定と違うシェルで動いたのかを聞かれましたが、覚えていません。

書き出しを「サーバーに置いたら結果が変わりました」としていたのは、経緯として断定するには曖昧すぎました。直しています。確かなのは、両方のシェルに同じファイルを渡して比べた8箇所の出力だけです。そこは実測なので、値は変えていません。

指摘をくださったお二人に感謝します。

確かめ方

今回は、同じスクリプトを両方に渡して出力を比べました。

echo "=== zsh ==="; zsh test.sh
echo "=== bash ==="; bash test.sh

これだけです。移植の前に一度通せば、bad substitution は全部出ます。黙って違う値になるほうは出ないので、そこは出力を並べて目で見るしかありませんでした。

わたしはこれを、動かす先を変えようとしてから調べました。書いているときに調べていれば、もっと早く終わっていました。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

1
0
8

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?