0
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?

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

0
Posted at

macOS で書いたスクリプトを、サーバーに置いたら結果が変わりました。エラーは出ていません。

原因は zsh と bash の差です。エラーになるものより、黙って違う値を返すもののほうが多いことが、調べてみて分かりました。

以下、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 を書いて、その通りに実行していれば起きません。macOS のターミナルで動いたから大丈夫、という確認の仕方をやめる、というだけの話でもあります。

確かめ方

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

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

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

わたしはこれを、移植で困ってから調べました。書いているときに調べていれば、もっと早く終わっていました。


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

0
0
1

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
0
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?