いろいろな *[!c]sh 実装のキライなところ - Shell Script Advent Calendar 2016 - ダメ出し Blog

2016-12-25(Sun) [sh][shell] [更新履歴]

Shell Script Advent Calendar 2016 の 25日目の記事です。最終日ですね。クリスマスですね。私の誕生日ですね。 今年は子供達が一緒だからさみしくないもん。

Advent Calendar 最終日のネタとして相応しくないような気もしますが、 いろいろな *[!c]sh を使って経験した「えっ? ナニソレ? おかしくね?」 と感じた嫌いな仕様を紹介してみたいと思います。登場するシェルは bash, dash, ksh (AT&T ksh), mksh, zsh です。

移植性のよいシェルスクリプトを書くのにお役立てください。

bash の嫌いなところ

set -u 時に空の配列変数展開すると強制終了 (〜 bash 4.3)

シェルオプション set -u は未定義の変数を展開しようとすると エラーで強制終了してくれる便利なオプションです。 変数名の typo バグを発見してくれます。

少し古い bash の場合、配列要素が空っぽの配列変数を展開しようとした場合も未定義扱いとなります。

$ bash -c 'set -u; typeset -a names; echo "${names[@]}"'
bash: names[@]: 未割り当ての変数です
$ bash -c 'set -u; names=(); echo "${names[@]}"'
bash: names[@]: 未割り当ての変数です

非常に面倒くさいですが、条件付き変数展開を利用して対策しましょう。

$ bash -c 'set -u; typeset -a names; echo ${names[0]+"${names[@]}"}'

bash 4.4 では大丈夫でした、わーい…と思ったら別のがバグってます。

$ bash -c 'set -u; typeset -a names; echo "${#names[@]}"'
bash: names: 未割り当ての変数です

こう書けば回避できます。

$ bash -c 'set -u; typeset -a names=(); echo "${#names[@]}"'
0

LANG=en_US.UTF-8 時に [A-Z] のパス名展開が意外な文字にマッチする

メッセージ等は英語にしたい、ただし文字列は UTF-8 として扱いたいとき、ロケールを en_US.UTF-8 にするとよいです。

しかし、bash は en_US-UTF-8 のときのパス名展開が特殊で、 例えば [A-Z] をパス名展開に用いると、A から Z の英字だけでなく なんと c から z の英字にもマッチします。

$ mkdir tmp
$ cd tmp
$ touch a b c x y z A B C X Y Z
$ LC_ALL=C /bin/bash --noprofile --norc -c 'echo [A-Z]'
A B C X Y Z
$ LC_ALL=ja_JP.UTF-8 /bin/bash --noprofile --norc -c 'echo [A-Z]'
A B C X Y Z
$ LC_ALL=en_US.UTF-8 /bin/bash --noprofile --norc -c 'echo [A-Z]'
A b B c C x X y Y z Z

これは仕様なんだそうです。

bash だけでなく en_US は罠がありそう。ロケールわからん。

近年の Linux ディストリビューションであれば C.UTF-8 というロケールが利用できるので、 en_US.UTF-8 の代わりにこれを利用しましょう。 en_US.UTF-8 が使えないほかの環境では bash 以外を利用しましょう。

遅い

bash は遅い。

bash, dash, mksh の嫌いなところ

チルダ展開の評価順位

bash, dash, mksh はチルダ展開が変数展開の前になります。

$ echo $LOGNAME
fumiyas
$ echo ~fumiyas
/home/fumiyas
$ echo ~$LOGNAME
~fumiyas

同様に、ユーザー名部分がダブルクォートで括られていると評価されません。

$ echo ~"$LOGNAME"
~fumiyas
$ echo ~"fumiyas"
~fumiyas

ksh, zsh はいずれもチルダ展開されます。これは便利。

$ echo ~$LOGNAME
/home/fumiyas
$ echo ~"$LOGNAME"
/home/fumiyas
$ echo ~"fumiyas"
/home/fumiyas

dash の嫌いなところ

位置パラメーターが空のときに shift すると強制終了

https://twitter.com/koie/status/707128070864392192

From: 鯉江 @koie

くっそ、またdashに罠が。 shift || true でエラー回避しようとしてもエラーで終了してしまう。 bash ashはok.

dash は位置パラメーターが空だったり位置パラメーター数を越える数だけ shift すると、エラーになるだけでなく、終了してしまいます。

$ dash -c 'echo $#; shift; echo end'
0
dash: 1: shift: can't shift that many

回避策1。最初の位置パラメーターが未定義なら shift を実行しないようにします。

$ dash -c 'echo $#; ${1+shift}; echo end'
0
end

回避策2。位置パラメーターの数が 0 なら shift を実行しないようにします。

$ dash -c 'echo $#; [ $# -gt 0 ] && shift; echo end'
0
end

read に変数未指定だとエラー

組込みコマンド read は、入力中の不要の行を読み捨てたり、 ユーザーとの対話を前提としたシェルスクリプトで Enter キーの入力待ちに利用することがあります。 そのような場合、入力を変数に保存しても無駄なので read の引数に変数名は指定したくないところですが、 dash は変数を一つも指定しないとエラーになります。

$ dash -c 'echo -n "Hit Enter key to continue..."; read'
Hit Enter key to continue...dash: 1: read: arg count

これはオリジナルの sh 由来のようです。

https://twitter.com/n_soda/status/707395548353994752

From: SODA Noriyuki @n_soda

@satoh_fumiyasu オリジナルBourne shellも $ sh -c read sh: read: missing arguments ですよ。(Solaris 9で確認)

export と同時に変数に値を設定する際にチルダ展開されない

https://twitter.com/hirose31/status/713254558605029376

From: ひろせ31 @hirose31

dashでexportつきだどチルダが展開されないのって仕様なんすかね? /bin/dash -c ‘set -x; V=~hirose31; export E=~hirose31;’ + V=/home/hirose31 + export E=~hirose31

これはバグなような気がします。

$ dash -c 'V=~fumiyas; export V; echo "$V"'
/home/fumiyas
$ dash -c 'export V=~fumiyas; echo "$V"'
~fumiyas

ksh (AT&T ksh) の嫌いなところ

組込みコマンド echo に移植性がない

ksh の組込みコマンド echo の仕様は OS の /bin/echo の仕様になっています。つまり、#!/bin/ksh でスクリプトを書いたとしても、 echo を使ってしまうと OS のポータビリティなくなります。

参考:

シグナルで終了したコマンドの $? の値が 128 + シグナル番号でない

ksh だけ 256 + シグナル番号になります。

$ dash -c 'sh -c "kill -9 \$\$"; echo $?'
Killed
137
$ bash -c 'sh -c "kill -9 \$\$"; echo $?'
bash: 1 行: 23370 強制終了            sh -c "kill -9 \$\$"
137
$ ksh -c 'sh -c "kill -9 \$\$"; echo $?'
ksh: 23354: Killed
265
$ mksh -c 'sh -c "kill -9 \$\$"; echo $?'
Killed
137
$ zsh -c 'sh -c "kill -9 \$\$"; echo $?'
137

ksh (AT&T ksh), mksh の嫌いなところ

function の関数内での $0 の値が関数名になってしまう

ksh, mksh では function で関数を定義すると、その関数内の $0 はスクリプト名 (起動時のコマンド名) ではなく、関数名が展開されます。

$ ksh -c 'funcname() { echo "$0"; }; funcname' progname argv1 argv2 argv3
progname
$ ksh -c 'function funcname { echo "$0"; }; funcname' progname argv1 argv2 argv3
funcname

zsh でもデフォルトは同様ですが、zsh オプション FUNCTION_ARGZERO で変更できます。

$ zsh -c 'set -o no_FUNCTION_ARGZERO; function funcname { echo "$0"; }; funcname' progname argv1 argv2 argv3
progname

set -xfunction 関数には無効

シェルオプション set -x を設定すると、実行するコマンドラインの内容が $PS4 の値を先頭に付けて標準エラー出力に出力されます。 シェルスクリプトの実行トレースに便利です。

しかし ksh, mksh では、set -xfunction の関数内は対象外となります。

$ ksh -c 'func() { echo $1 in-function; }; set -x; func 1; func 2'
+ func 1
+ echo 1 in-function
1 in-function
+ func 2
+ echo 2 in-function
2 in-function
$ ksh -c 'function func { echo $1 in-function; }; set -x; func 1; func 2'
+ func 1
2 in-function
+ func 2
2 in-function

ksh, mksh で function 関数に set -x 相当を有効化するには、 別途 typeset -ft 関数名 を実行してやる必要があります。

typeset +f で関数名をすべて得られるので、次のようにすれば一括して適用できます。 ただし、これ以降に新たに定義した関数には適用されないので注意。

typeset -ft $(typeset +f)

zsh の嫌いなところ

$status が予約されている

スクリプトで $status という変数を利用したいことがありますが、 zsh では予約されていて使えません。

$ zsh -c 'status=0'
zsh:1: read-only variable: status

echo がデフォルトでエスケープシーケンスを解釈する

zsh の echo はデフォルトでエスケープシーケンスを解釈します。 ほかの実装とは異なるので注意。

参考(再掲):

対話シェル時に [!abc] でヒストリー展開しようとする

パス名展開の文字クラスの否定の記述は [!...] という形式になりますが、 zsh は対話シェルのとき (コマンドヒストリーが有効のとき) に !...] の部分をヒストリー展開しようとします。

$ echo [!abc]
zsh: event not found: abc]

コマンドヒストリーが無効なシェルスクリプトでは大丈夫なので、 あまり大きな問題ではありせん。 対話シェル時は POSIX sh とは非互換ですが [^...] 形式を使いましょう。


ところで、12月25日はクリスマスな上に、 OSS 界隈で地味に活躍されているふみやすさんの誕生日ですね!
http://www.amazon.co.jp/registry/wishlist/27M7TV8CEEF6G?sort=priority

逆に、あなたの書いた OSS や Blog や Advent Calendar が気に入ったら何か送りたく なってしまうかもしれないので、プロフィールや Web サイトに あなたの Amazon 欲しいものリストの URL を貼っておいてくださいね!


私が勤める OSSTech っていう某弊社で社員募集しているようです。 人材紹介会社を介さなければ、入社後に 20万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


よろしければ、これまで参加した/参加予定のほかの Advent Calendar もどうぞ。