Next: , Previous: Special Shell Variables, Up: Portable Shell


10.9 シェル組み込みの制限

だめだよ全く,我々は本気なのに.制限のあるシェルもあるんです! :)

全ての組み込みコマンドやコマンドは,オプションをサポートし,そのため,ダッ シュで始まる引数を用いると,全く異なる動作をすることを覚えておくべきです. 例えば罪の無い‘echo "$word"’でも,wordがダッシュで始まるとき は予期しない結果となるはずです.この問題は,パイプでは‘x’を後で評価 するように,‘echo "x$word"’を使用することで避けることが可能です.

.
通常のファイル(‘test -f’を使用してください)を用いるときだけ .コマンドを使用してください.例えば,Bash 2.03は, ‘. /dev/null’で固まります.また,引数にスラッシュを含まない場合は .PATHを使用するので,現在のディレクトリのファイル foo.を使用したい場合,‘. ./foo’を使用する必要が あることを覚えておいてください.
!
!を使用することは不可能です.コードを書き換える必要があります.
break
break 2’の使用などは安全です.
cd
POSIX 1003.1-2001では,cd-L (“論理的”) と-P (“物理的”)オプションをサポートし,-Lがデフォル トであることを要求しています.しかし,伝統的なシェルはこれらのオプション をサポートしておらず,cdコマンドは-Pのように動作しま す.

移植性の高いスクリプトは,どちらのオプションもサポートしていると仮定すべ きではなく,どちらの動作もデフォルトと仮定すべきではありません.これは ちょっとトリッキーで,例えば,POSIXのデフォルトの動作では,現 在の論理的なディレクトリがシンボリックリンクの場合,‘ls ..’と ‘cd ..’では異なるディレクトリを参照している可能性があります. dir..の要素が無い場合,cd dirを使用しても 安全です.また,Autoconfが生成するスクリプトは,ac_top_srcdir の ような変数を計算するとき,この問題を調査するので(see Configuration Actions),これらの変数でcdしても安全です.

pwdコマンドの議論も参照してください.

case
引数を引用符で囲む必要はありません.分離は実行されません.

最後の‘;;’は不要ですが,使用した方が良いでしょう.

fnmatchのバグのため,bashはバックススラッシュを文字クラ スとして正しく処理することに失敗します.

          bash-2.02$ case /tmp in [/\\]*) echo OK;; esac
          bash-2.02$

このコードをunixms-dosの絶対パスとして使用したいとき,非常に 残念なことになります.このバグを回避するために,常にバックスラッシュを最 初に書いてください.

          bash-2.02$ case '\TMP' in [\\/]*) echo OK;; esac
          OK
          bash-2.02$ case /tmp in [\\/]*) echo OK;; esac
          OK

Ash 0.3.8のように,シェルによっては空のcase/esacで混乱する ものもあります.

          ash-0.3.8 $ case foo in esac;
          error-->Syntax error: ";" unexpected (expecting ")")

多くのシェルでは,カッコで囲まれているケース文をサポートしておらず,それ は,対になっているカッコに依存しているツールを使用している我々のような人 間にとっては残念なことです.例えば,Solaris 8のBourneシェルがそうです.

          $ case foo in (foo) echo foo;; esac
          error-->syntax error: `(' unexpected

echo
単純なechoですが,移植性の問題の根源として最も驚くべきものかもし れません.移植性の高い‘echo’を使用することは,オプションとエスケー プシーケンスを削除しない限り不可能です.移植性を目標とする新しいアプリケー ションでは,‘echo’の代わりに‘printf’を使用すべきです.

オプションを期待しないでください.ECHO_Nなどの,-cをシミュ レーションする方法は,See Preset Output Variables.

引数へのバックスラッシュは,処理について同意がとれていないので使用しない でください.‘echo '\n' | wc -l’を用いれば,Digital Unix 4.0と MIPS RISC/OS 4.52のshでは答えは2になりますが, Solarisのsh,Bash,そしてZsh(のshエミュレーションモー ド)では答えは1になります.問題が本当にechoにあることに注意して ください.全てのシェルは,‘'\n'’をバックスラッシュと‘n’の組み 合わせであると解釈します.

これらの問題のため,不定の文字を含む文字列をechoに渡さないでく ださい.例えば,fooの値がバックスラッシュを含んでおらず,‘-’ で始まらないことを知っている場合,‘echo "$foo"’は安全ですが,それ以 外では以下のようなヒアドキュメントを使用すべきではありません.

          cat <<EOF
          $foo
          EOF

exit
exitのデフォルト値は$?を想定しています.残念ながらBash 2.04を移植したDJGPPのように,シェルによっては‘exit 0’を実行します.
          bash-2.04$ foo=`exit 1` || echo fail
          fail
          bash-2.04$ foo=`(exit 1)` || echo fail
          fail
          bash-2.04$ foo=`(exit 1); exit` || echo fail
          bash-2.04$

exit $?’を使用すると期待される動作に復帰します.

autoconfが生成するようなシェルスクリプトなどには,以前の終了状 態をクリーンアップする仕掛けを使用しているものもあります.シェルの最後の コマンドがゼロではないステータスで終了した場合も,呼び出し側がエラーの発 生を報告できるように,ゼロでないステータスで終了する仕掛けがあります.

残念ながら,Solaris 8 shのように,シェルによってはexit コマンドの引数を無視する仕掛けが存在するものもあります.これらのシェルで は,その仕掛けで呼び出しがプレーンのexitによるものなのか, exit 1によるものなのか決定できません.exitを直接呼び出す代 わりに,この問題を回避するためにAC_MSG_ERRORを呼び出してください.

export
組み込みのexportは,シェル変数を環境変数(environment variable)に複製します.変数がエクスポートされて更新される度に,環境変数 も更新されます.反対に,環境変数はがシェルから読み出される度に,開始時に エクスポートされたものとして印のついたシェル変数をインポートするべきです.

ああ,Solaris 2.5,irix 6.3,irix 5.2,AIX 4.1.5,そ してDigital unix 4.0のような多くのシェルは,受けとった環境変数を exportすることを忘れています.結果として,二つの変数は共存して います.環境変数とシェル変数の二つです.以下のコードは,この失敗を説明す るものです.

          #! /bin/sh
          echo $FOO
          FOO=bar
          echo $FOO
          exec /bin/sh $0

環境変数で‘FOO=foo’として実行した場合,これらのシェルはそれぞれ ‘foo’と‘bar’を交互に出力しますが,‘foo’を出力した後に,続 けて‘bar’を出力します.

このため,それぞれ更新した環境変数を再びexportすべきです.

false
falseがステータス1で終了することを期待してはいけません. Solaris 8のネイティブなBourneシェルは,ステータス255で終了します.
for
位置の引数までループするため,以下のように使用したとします.
          for arg
          do
            echo "$arg"
          done

シェルによっては,間違って解釈するので,forと同じ行にdo を 書いてはいけません

          for arg; do
            echo "$arg"
          done

明示的に位置に依存する引数を参照したい場合,‘$@’のバグがあるので, 以下のように使用してください.

          for arg in ${1+"$@"}; do
            echo "$arg"
          done

しかし,ZshはBourneシェルエミュレーションモードでも,‘${1+"$@"}’ で単語の分離を試みるのことを覚えておいてください.‘$@’の詳細は, Shell Substitutionsを参照してください.

if
!’の使用は移植性がありません.以下の例を考えます.
          if ! cmp -s file file.new; then
            mv file.new file
          fi

その代わりに以下を使用してください.

          if cmp -s file file.new; then :; else
            mv file.new file
          fi

ifの終了ステータスをリセットしないシェルもあります.

          $ if (exit 42); then true; fi; echo $?
          42

そこでは,適切なシェルなら‘0’を出力すべきです.これは,異常終了とな るので,Makefileでは特に問題です.これが,Automakeが生成するよう な適切に書かれているMakefileがごちゃごちゃした構成になっている理 由です.

          if test -f "$file"; then
            install "$file" "$dest"
          else
            :
          fi

printf
-’で始まる書式化文字列は問題になります.bash(例えば 2.05b)では,それをオプション文字と解釈しエラーとなるでしょう.オプション の終りの印となる‘--’は,書式化文字列をそのまま受けとるNetBSD Almquist shell (例えば0.4.6)ではよいとは言えません.‘-’を‘%c’ や‘%s’に書くのが,間違いを避ける最も簡単な方法でしょう.
          printf %s -foo

pwd
最近のシェルを用いると,pwdは“論理的な”ディレクトリ名を出力 し,その構成要素にはシンボリックリンクがある可能性があります.これらのディ レクトリ名は,構成要素はすべてディレクトリとなっている“物理的な” ディ レクトリ名とは異なります.

POSIX 1003.1-2001では,pwdは,-L (“論理的”) と-P (“物理的”)オプションをサポートし,-Lがデフォル トになっている必要があります.しかし,伝統的なシェルはこれらのオプション をサポートしておらず,pwdコマンドは-Pのように動作しま す.

移植性の高いスクリプトでは,どちらのオプションもサポートしていると仮定す べきではなく,どちらの動作もデフォルトと仮定すべきではありません.また, 多くのホストは‘/bin/pwd’が‘pwd -P’と同じですが, POSIX はこの動作を要求しておらず,移植性の高いシェルではそれに 依存すべきではありません.

通常,そのままpwdを使用するのが最善でしょう.最近のホストでは, これで論理的なディレクトリ名を出力し,以下の利点があります.

cdコマンドでの議論も参照してください.

set
この組み込みコマンドは,一般的なダッシュで始まる引数の問題に直面します. BashやZshのような現在のシェルでは,オプションの終りを指定する --(--以降の全ての引数は,例えば‘-x’であってもパラ メータです)を理解しますが,ほとんどのシェルは,オプションではない引数が 見つかるとすぐにオプションの処理を単純に停止します.このため,オプション の処理を終了するために‘dummy’や単純に‘x’を使用し,それを取り出 すためにshiftを使用してください.
          set x $my_list; shift

すべてのオプションを認識しないこととは"反対"の問題(例えば,‘set -e -x’で‘-x’をコマンドラインに割り当てるといった問題)があるシェルもあ ります.以下のように省略した方が良いでしょう.

          set -ex

shift
shiftするものが無いとき,shiftを使用することは悪い考 え方であるだけでなく,移植性が無くなってしまいことも追加されてしまいます. MIPS RISC/OS 4.52のシェルは,それを廃棄してしまいます.
source
POSIXが要求していないので,このコマンドは移植性がありません. 代わりに.を使用してください.
test
testプログラムは,多くのファイルと文字列のテストを実行する方法で す.それは別名の‘[’で呼び出されることも多いのですが,M4の引用符文字 という問題から,Autoconfのコードではその名前を使用することが要求されてい ます.

testを使用して複数の調査を行う必要がある場合,testの演算子 の‘-a’と‘-o’の代わりに,シェル演算子の‘&&’と‘||’ で 組み合わせてください.System Vでは,‘-a’と‘-o’の優先順位は,単 項演算子とは間違った関係になっています.従って,POSIXはそれら を指定しないので,それを使用すると移植性が無くなります.同じ文で ‘&&’と‘||’を組み合わせる場合,同じ優先順位があることを覚えてお いてください.

testで‘!’を使用してもかまいませんが,ifではでき ません.‘test ! -r foo || exit 1’.

test (files)
configureスクリプトでクロスコンパイルのサポートを可能にするた め,ホストシステムの代わりに,ビルドシステムの特徴のテストは,何もすべき ではありません.しかし,任意のファイルの存在を調査する必要があることが判 明するかもしれません.そうするために‘test -f’や‘test -r’を使用 してください.4.3BSDには‘test -x’が無いので使用しないでく ださい.また,Solaris 2.5には‘test -e’が無いので使用しないでくださ い.システム上にシンボリックリンクがあることをテストするためには, ‘test -L’ではなく‘test -h’を使用して下さい.いずれも POSIX 1003.1-2001に準拠した様式ですが,Solaris 8の /bin/shのような古いシェルでは,-hだけをサポートしていま す.
test (strings)
testは引数をオプションとして解釈するので(例えば, ‘string = "-n"’),特にstringがダッシュで始まる場合, ‘test "string"’を避けてください.

一般に信じられていることとは反対に,‘test -n string’と ‘test -z string’は,移植性があります.それにもかかわ らず,(Solaris 2.5,AIX 3.2,unicos 10.0.0.6,Digital unix 4等の)多くのシェルには信じられない優先順位があり,string がオペレータのように見える場合は混乱するかもしれません.

          $ test -n =
          test: argument expected

危険はありますが,代わりに‘test "xstring" = x’や‘test "xstring" != x’を使用してください.

以下のような慣用句はのバリエーションは普通に見つかります.

          test -n "`echo $ac_feature | sed 's/[-a-zA-Z0-9_]//g'`" &&
            action

与えられているパターンに一致するとき動作します.そのような構文は,常に使 用を避けるべきです.

          echo "$ac_feature" | grep '[^-a-zA-Z0-9_]' >/dev/null 2>&1 &&
            action

シェルの組み込みコマンドなのでより速くなっているため,可能な場所では caseを使用してください.

          case $ac_feature in
            *[!-a-zA-Z0-9_]*) action;;
          esac

ああ,POSIXの構文‘[!...]’をサポートしていないシェルは 知りませんが,文字クラスの否定は移植性が無いかもしれません(対話的モード では,zshは‘[!...]’の構文で混乱し,‘!’のため,ヒ ストリ内のイベントを探します).多くのシェルは,構文‘[^...]’ の 代替物をサポートしていません(Solaris,Digital Unix等).

以下は解決方法の一つです.

          expr "$ac_feature" : '.*[^-a-zA-Z0-9_]' >/dev/null &&
            action

以下の方が良いかもしれません.

          expr "x$ac_feature" : '.*[^-a-zA-Z0-9_]' >/dev/null &&
            action

foo’がバックスラッシュを含んでいるとき問題を回避するので, ‘expr "Xfoo" : "Xbar"’は,‘echo "Xfoo" | grep "^Xbar"’より堅牢です.

trap
少なくとも,シグナルの1,2,13,そして15をトラップすることは安全です.ま た,0のトラップも可能で,すなわち,スクリプトが終るとき(明示的な exitやスクリプトの終り)にtrapを実行するということで す.

POSIXでは,この点は絶対的に明確ではありませんが,‘$?’をト ラップするとき,トラップの前の最後に実行されたコマンドの終了ステータスが 設定されるべきだということは,広く認められています.曖昧な部分は以下のよ うに要約可能です.“トラップがexitで開始されるとき,実行された 最後のコマンドは何でしょう?exitの直前ですか?それとも exit自身ですか?”

Bashはexitを最後のコマンドと考えますが,ZshとSolaris 8 shは,トラップが実行されたときexitの処理 な ので,トラップを受信する前の終了ステータスだと考えられます.

          $ cat trap.sh
          trap 'echo $?' 0
          (exit 42); exit 0
          $ zsh trap.sh
          42
          $ bash trap.sh
          0

移植性の解決方法は簡単です.‘exit 42’にしたいとき,‘(exit 42); exit 42’を実行し,最初のexitはZshに対する42の終了ステータスを 設定するために使用され,二番目は,トラップを誘発し,Bashに対して終了ステー タスとしての42を渡すためです.

FreeBSD 4.0のシェルには,以下のバグがあります.コードが内部 trapの場合,空行で‘$?’が0にリセットされます.

          $ trap 'false
          
          echo $?' 0
          $ exit
          0

幸運にもこのバグはtrapのみに影響します.

true
心配しないでください.我々が知っている限りtrueには移植性があり ます.それにもかかわらず,常に組み込みコマンドというわけではなく(例えば Bash 1.x),移植性の高いシェルのコミュニティは,:の使用を好みが ちです.これには副作用があります.falsetrueより移 植性が高いかどうか尋ねてみたときのAlexandre Oliva の回答です.
それらが存在しない場合,シェルは,falseに対しては正しく, trueに対しては正しくない,異常終了のステータスを生成するので, ある意味ではそのとおりです.

unset
unsetのサポートを仮定することはできません.それにもかかわらず, PS1のような邪魔な変数を利用不可能にすることは非常に役立つので,存 在をテストし,提供されていればそれを使用し,unsetがサポー トされていないときは,無効にする値を与えてください.
          if (unset FOO) >/dev/null 2>&1; then
            unset=unset
          else
            unset=false
          fi
          $unset PS1 || PS1='$ '

無効にする値については,See Special Shell Variables. また,環境変数の caseについてはexportのドキュメントLimitations of Builtinsも参照してください.