次: , 前: Limitations of Builtins, 上: Portable Shell


10.10 通常のツールの制限

あらゆるマシンで見つかることが期待できる小さなツールセットには,知ってお くべき制限がいくつか含まれているはずです.

awk
ユーザ関数呼び出しで,カッコの前に空白を残さないでください. GNU awkはそれを拒絶します.
          $ gawk 'function die () { print "Aaaaarg!"  }
                  BEGIN { die () }'
          gawk: cmd. line:2:         BEGIN { die () }
          gawk: cmd. line:2:                      ^ parse error
          $ gawk 'function die () { print "Aaaaarg!"  }
                  BEGIN { die() }'
          Aaaaarg!
     

プログラムを決定的にしたい場合,配列上のforに依存しないでください.

          $ cat for.awk
          END {
            arr["foo"] = 1
            arr["bar"] = 1
            for (i in arr)
              print i
          }
          $ gawk -f for.awk </dev/null
          foo
          bar
          $ nawk -f for.awk </dev/null
          bar
          foo
     

HP-UX 11.0のネイティブのAWKのように,内部アンカーに調子が悪い正規表現の エンジンがあるものもあります.

          $ echo xfoo | $AWK '/foo|^bar/ { print }'
          $ echo bar | $AWK '/foo|^bar/ { print }'
          bar
          $ echo xfoo | $AWK '/^bar|foo/ { print }'
          xfoo
          $ echo bar | $AWK '/^bar|foo/ { print }'
          bar
     

そのようなパターンに依存したり(すなわち,`/^(.*foo|bar)/'を使用する), そのようなAWKを拒絶する単純なテストを使用したりしないでください.

cat
オプションに依存しないようにしてください.しかし,表示不可能な文字を表示 するオプション-vは,移植性がありそうです
cc
`cc foo.c -o foo'のようなコンパイルが失敗したとき,(Reliant unixcdsのように)foo.oを残すコンパイラもあります.

HP-UX ccは,プリプロセスとアセンブラを行なう.Sファイル を受け入れません.`cc -c foo.S'は成功したように見えますが,実際には 何もしません.

`cc foo.c'で生成されるデフォルトの実行形式は,以下のようになるはず です.


cmp
cmpは,二つのファイルの生のデータの比較を実行しますが, diffは二つのテキストファイルを比較します.そのため, DOSのファイルを比較する場合,二つのファイルが異なっているかど うかを調査するだけの場合でも改行のエンコードの違いで見せかけの差が発生す ることを避けるため,diffを使用してください.
cp
伝統的に,ファイルのタイムスタンプの分解能は一秒になっていて,`cp -p'ではタイムスタンプをそのままコピーします.しかし,最近のファイルシス テムには分解能が1ナノ秒のタイムスタンプになっているものもたくさんありま す.残念ながら,`cp -p'の実装ではファイルのコピー時にタイムスタンプ を切り詰めるので,このためコピーされたファイルが結果としてソースよりも古 くなってしまいます.切り詰めの正確な量はcpが使用するシステムコー ルの分解能に依存します.伝統的に,これはutimeで,それは分解能が1 秒になっていますが,新しいcpの実装ではutimesを使用して いて,それは分解能が1マイクロ秒になっています.これらの新しい実装には, GNU coreutils 5.0.91やそれ以降,そしてSolaris 8 (sparc)のパッチ109933-02 やそれ以降が含まれます.残念ながら,2003年9月の段階では,完全なナノ秒の 分解能を持つタイムスタンプを設定するシステムコールはありません.

SunOS cp-fをサポートしていませんが,その mvはサポートしています.mvcp-fに関して異なっている理由については由来が推測できます. mvはデフォルトで,読み込み専用のファイルを上書きする前にプロン プトを表示します.cpはそうではありません.そのため, mvには-fオプションが必要ですが,cpには不要 です.mvcpは,読み込み専用のファイルに対して,動作 が異なり,その理由は,最も簡単なcpの形式では,読み込み専用のファ イルを上書きできませんが,最も簡単なmv形式では,それが可能だと いうことです.この理由は,cpはターゲットを書き込みアクセスで開 くのに対し,mvは単純にlink(または,新しいシステムでは rename)を呼び出すためです.

Bob Proulxは,`cp -p'は常に所有権のコピーを試みるとメモして います.しかし,実際に所有権をコピーするかどうかは,カーネルで実装されて いるシステムポリシーの決定に依存します.カーネルが許可している場合はそう なります.カーネルが許可していない場合は,そうなりません.cp自 身が制御しているものではありません.

SysVでは,ユーザはファイルを別のユーザにchown可能で,SysVにはstickyでは ない/tmpもあります.それは疑い無く,敵意のあるユーザのいないビジ ネス環境のSysVの遺産に由来しています.BSDは,rootだけがファイ ルをchown可能にし,stickyな/tmpを使用して,これをより安 全なモデルに変更しました.それは疑い無く,キャンパス環境のBSD の遺産に由来します.

LinuxはデフォルトでBSDに準拠していますが,chown可能 に設定することも可能です.別の例として,HP-UXはSysVに準拠していますが, 最近のセキュリティモデルを使用するよう設定し,chownできなくす ることが可能です.それは管理者が設定可能なパラメータなので,動作を示すた めにカーネル名を使用することは不可能です.

date
dateのバージョンによっては,特殊な%による指示語を理解しないも のもあり,残念ながら警告をする代わりに,それをそのまま通過させ,正しく終 了します.
          $ uname -a
          OSF1 medusa.sis.pasteur.fr V5.1 732 alpha
          $ date "+%s"
          %s
     

diff
-uには移植性がありません.

Tru64のように,実装によっては/dev/nullの比較で失敗するものもあり ます.その代わりに空のファイルを使用してください.

dirname
全てのホストに動作するdirnameがあるわけではなく,その代わりに AS_DIRNAMEを使用すべきです(see Programming in M4sh).例えば以 下のようにします.
          dir=`dirname "$file"`       # This is not portable.
          dir=`AS_DIRNAME(["$file"])` # This is more portable.
     

これは,POSIXで要求されている標準では,幾分微妙な扱いです.例 えばUN*Xでは`//1'は`/'になるのでしょうか?以下はPaul Eggertの 回答です.

古いUnixライクのものではそうはならず,前置される`//'は特殊なパス名 になります.それは“スーパールート”を参照し,他のマシンのファイルをアク セスするために使用されます.前置される`///',`////'などは, `/'と等価です.しかし,前置される`//'は特殊です.この伝統的は Apollo Domain/OSで始まったと考えていて,古いホストではまだそのOS を使用 しています.

POSIXでは可能ですが,`//'に対する特別扱いは要求されていま せん.そこでは,形式`//([^/]+/*)?'のパス名でのdirnameの動作は,実装 で定義されると告げています.これらの場合,GNU dirnameは`/'を返しますが,古いUnixライクのものでも動作す るように`//'を返した方が移植性が高いでしょう.


egrep
POSIX 1003.1-2001では,もはやegrepを要求していません が,より古いホストの多くはまだPOSIXgrep -Eでの置換を サポートしていません.この問題を回避するため,AC_PROG_EGREP を呼 び出し,$EGREPを使用してください.

空の代入は移植性が無く,代わりに`?'を使用してください.例えば, Digital Unix v5.0では以下のようになります.

          > printf "foo\n|foo\n" | $EGREP '^(|foo|bar)$'
          |foo
          > printf "bar\nbar|\n" | $EGREP '^(foo|bar|)$'
          bar|
          > printf "foo\nfoo|\n|bar\nbar\n" | $EGREP '^(foo||bar)$'
          foo
          |bar
     

$EGREPgrepの制限で苦しむことになります.

expr
`x'で始まるexprキーワードはないので,exprwordを間違って解釈しないように,`expr x"word" : 'xregex''を使用してください.

lengthsubstrmatch,そしてindexは使用し ないでください.

expr (`|')
`|'を使用することは可能です.POSIXでは,`expr ''' が 空の文字列を返すことを必須としていませんが,空の文字列を用いて空の文字列 (またはゼロ)とともに`|'を用いたときの結果は安全ではありません.例え ば以下を考えます.
          expr '' \| ''
     

GNU/LinuxとPOSIX.2-1992では,この場合は空の文字列を 返しますが,伝統的なunixでは`0'を返します(Solarisはそのような 例の一つです).最近のPOSIX.1-2001ドラフトでは,その指定は伝統 的なunixの動作に一致するよう変更されています(信じられないことですが, これを修正するには時すでに遅しです).同じ問題が,計算結果が空の文字列に なるときにも,以下の状態では発生します.

          expr bar : foo \| foo : bar
     

空の文字列を避けることで,この移植性の問題を避けてください.

expr (`:')
Solarisではサポートされていないので,パターン内に,`\?',`\+', そして`\|'を使用しないでください.

POSIX標準では,`expr 'a' : '\(b\)''が`0'を出力するか 空の文字列を出力するのかは明確ではありません.実際問題として,それはほと んどのプラットフォームで空の文字列を出力しますが,移植性の高いスクリプト では,これを仮定すべきではありません.例えば,QNX 4.25 ネイティ ブのexprは`0'を返します.

均一な動作を得る手段として,デフォルト値として空の文字列を使用することに なっていると信じているかもしれません.

          expr a : '\(b\)' \| ''
     

残念ながら,これは元の式として正確に動作します.詳細は, `expr (`:')'の項目を参照してください.

古いexprの実装(例えば,SunOS 4のexprとSolaris 8の /usr/ucb/expr)には,一致したサブ文字列が120バイトより長い場合, exprが異常終了するという,思慮の欠けた長さの制限があります.こ の状況では,exprが失敗した場合,`echo|sed'に頼りたいと思 うかもしれません.

残っているものはそれだけではありません!

QNX 4.25のexprには,空の文字列ではなく`0'となる ことに加えて,終了ステータスでおかしな動作があります.それはカッコが使用 されているときには,常に1になるということです!

          $ val=`expr 'a' : 'a'`; echo "$?: $val"
          0: 1
          $ val=`expr 'a' : 'b'`; echo "$?: $val"
          1: 0
          
          $ val=`expr 'a' : '\(a\)'`; echo "?: $val"
          1: a
          $ val=`expr 'a' : '\(b\)'`; echo "?: $val"
          1: 0
     

実際に,(sedのような)他の手法でexprプログラムで異常 終了を捕獲する準備がある場合,結果を二回得る可能性があるので,これは大き な問題となります.例えば以下を考えます.

          $ expr 'a' : '\(a\)' || echo 'a' | sed 's/^\(a\)$/\1/'
     

ほとんどのホストでは`a'を出力しますが,QNX 4.25では `aa'になります.単純な回避方法として,exprでのテストを構 成し,結果によってexprfalseで変数を設定する方法を 使用します.

fgrep
POSIX 1003.1-2001では,もはやfgrepを要求していません が,より古いホストの多くはまだPOSIXgrep -Fでの置換を サポートしていません.この問題を回避するため,AC_PROG_FGREP を呼 び出し,$FGREPを使用してください.
find
オプション-maxdepthGNU特有のようです.Tru64 v5.1, NetBSD 1.5,そしてSolaris 2.5のfindコマンドはそれを 理解しません.

`{}'の置換は,引数が正確に{}の場合のみ保証され,それが引 数の一部の場合は保証されません.例えば,DUとHP-UX 10.20とHP-UX 11では保 証されません.

          $ touch foo
          $ find . -name foo -exec echo "{}-{}" \;
          {}-{}
     

一方,GNU findは`./foo-./foo'を報告します.

grep
System Vの`grep -s'はエラーメッセージのみ抑制し,出力を抑制しないの で,出力を抑制するために`grep -s'を使用しないでください.その代わり に(ファイルが存在しない場合) grepの標準出力と標準エラー出力を /dev/nullへリダイレクトしてください.一致が見つかったかどうかを決 定するために,grepの終了ステータスを調査してください.

最後のパターンのみ尊重するgrep(例えば,AIX 6.5とSolaris 2.5.1)もあるので,-eで複数の正規表現を使用しないでください.ど ちらにしろ,Stardent Vistra SVR4のgrepには-e がありませ ん.... その代わりに拡張した正規表現と代入を使用してください.

Irix 6.5.16mのgrepは,それをサポートしていないので, -wに依存しないようにしてください.

ln
-fオプションがあるlnに依存しないようにしてください. 古いシステムではシンボリックリンクは利用不可能です.移植性のある代替物 `$(LN_S)'を使用してください.

2.04以前のバージョンのDJGPPに対して,lnは実行形式へのソフトリ ンクを,実際のプログラムを呼び出すスタブを生成することでエミュレートしま す.この機能は,Unix独自の実行形式以外のファイルでも動作します.そのため, `ln -s file link'はlink.exeを生成し,それは実行された場合に file.exeの呼び出しを試みます.しかしこの機能は実行形式に対しての み動作するので,このシステムでは`cp -p'が代わりに使用されます. DJGPPの2.04とそれ以降では完全なシンボリックリンクがサポートされています.

ls
移植性のあるオプションは-acdilrtuです.最近では,-l で 所有者とグループを出力しますが,伝統的なlsはグループを省略しま す.

最近では,すべての診断結果は標準エラー出力に出てきますが,伝統的な `ls foo'は,fooが存在しない場合,メッセージ`foo not found'を標準出力に出力します.伝統的なlsでは,`.c'ファイ ルが無い場合,`sources=`ls *.c 2>/dev/null`'は`sources="*.c not found"'と等価なので,そのようなシェルコマンドを書くときに注意してく ださい.

mkdir
mkdirのオプションには移植性はありません.`mkdir -p filename'の代わりにAS_MKDIR_P(filename)を使用すべきで す(see Programming in M4sh).
mv
移植性のあるオプションは,-f-iのみです.

ファイルシステム間で個別にファイルを移動することは(V6では)移植性がありま すが,常に強力でははありません.`mv new existing'をするとき, existingの古いものも新しいものも実際には存在していないという危険 な状態が存在します.

ファイルを/tmpから移動することで,これらのファイルを作成していた としても,好ましくない(が,まったく有効な)警告を発生することがあることを 覚えておいてください.システムによっては,/tmpにファイルを作成す ると,guidを自分が所属していないwheelに設定するものもあります.そ のためファイルがコピーされると,chgrpで失敗します.

          $ touch /tmp/foo
          $ mv /tmp/foo .
          error-->mv: ./foo: set owner/group (was: 3830/0): Operation not permitted
          $ echo $?
          0
          $ ls foo
          foo
     

この動作は,POSIXに準拠しています.

何らかの理由でファイル属性の複製に失敗する場合,mvは診断メッセー ジを標準エラー出力に書き出しますが,この異常終了で,mvは終了ス テータスを変更しません.

マウントポイントをまたがってディレクトリを移動することは移植性が無いので, cprmを使用してください.

開いているファイルの移動/削除は移植性がありません.以下の例はDOS/WIN32 では実行不可能です.

          exec > foo
          mv foo bar
     

以下も実行不可能です.

          exec > foo
          rm -f foo
     

sed
文字クラスの一部の場合でも,パターンに(エスケープされていない)セパレータ を含めるべきではありません.POSIX準拠では,Crayのsed は`s/[^/]*$//'を拒絶します.`s,[^/]*$,,'を使用してください.

sedのスクリプトは,八文字以上の分岐ラベルを使用すべきではなく,コメント を含めるべきでもありません.

NetBSD 1.4.2では,二番目のものをコマンドとして解釈しようと試み るので,sedによっては,余分な`;'を含めてはなりません.

          $ echo a | sed 's/x/x/;;s/x/x/'
          sed: 1: "s/x/x/;;s/x/x/": invalid command code ;
     

sedによっては,入力バッファに4000バイトの制限があるものもある ので,入力は妥当な長さの行にすべきです.

`\|'の交換は一般的ですが,POSIXはそのサポートを要求してい ないので,移植性の高いスクリプトでは避けるべきです.Solaris 8の sedは交換をサポートしていません.例えば,`sed '/a\|b/d'' は,リテラル文字列`a|b'を含んでいる行のみ検出します.

グループ内のアンカー(`^'と`$')は移植性がありません.

パターン内の入れ子状のカッコは,現在のホストでは完全に移植性あるものなの ですが,SVR3のように古いsedの実装ではサポートされていません.

もちろんオプション-eには移植性がありますが,それは不要です.ダッ シュで始まる有効なsedプログラムは無いので,明確にする役には立ちません. 唯一の有効性は,以下のように字下げを強制的に行なうときです.

          sed -e instruction-1 \
              -e instruction-2
     

これは以下の代わりのものです.

          sed instruction-1;instruction-2
     

もう一つの垢抜けた伝説として,“マッチしたもの”を意味するsコマン ドの一部を置換するとき,`&'を使用しても移植性はあるでしょう.すべて のベル研究所のV7 sedの子孫は(少なくとも,我々はそれより古い sedを経験したことはありません)サポートしています.

POSIXでは,`!'とそれ以降のコマンドの間に空白があってはい けません.アドレスと`!'の間の空白はOKです.例えば,Solaris 8では以 下のようになります.

          $ echo "foo" | sed -n '/bar/ ! p'
          error-->Unrecognized command: /bar/ ! p
          $ echo "foo" | sed -n '/bar/! p'
          error-->Unrecognized command: /bar/! p
          $ echo "foo" | sed -n '/bar/ !p'
          foo
     

sed (`t')
古いシステムには,新しいサイクルと開始するとき,その`t'フラグをリセッ トすることを“忘れる” sedがあるシステムもあります.例えば, MIPS RISC/OSirix 5.3で,以下のsedスクリプトを 実行した場合を考えます(行番号は,実際にはテキストの一部ではありません).
          s/keep me/kept/g  # a
          t end             # b
          s/.*/deleted/g    # c
          : end             # d
     

ファイルの内容は以下を考えます.

          delete me         # 1
          delete me         # 2
          keep me           # 3
          delete me         # 4
     

以下のようになります.

          deleted
          delete me
          kept
          deleted
     

以下のようにはなりません.

          deleted
          deleted
          kept
          deleted
     

なぜでしょう?一行目を処理しているとき,マッチするのでtフラグがセットさ れ,b行からd行まで移動し,出力が生成されます.二行目を処理しているとき, tフラグはセットされたままです(これはバグです).しかし,a行はマッチに失敗 しますが,置換が失敗するとき,sedはtフラグをクリアすることをサ ポートしていません.そのため,フラグがセットされているように見えるb行は, それをクリアし,dへ移動し,その結果,`deleted'の代わりに `delete me'になります.三行目を処理しているとき,マッチを示すt がク リアされるため,フラグがセットされ,その結果,b行はフラグをクリアし移動 します.最終的にフラグはクリアになっているので,四行目は正しく処理されま す.

sedの`t'について覚えておくべきことは二つあります.最初に, 成功した置換によっては,置換の直前だけでなく`t'ジャンプする ことを覚えておいてください.そのため,tフラグを実際にリセットするために, ごまかしの`t clear; : clear'を使用してください.

二番目は,それぞれの新しいサイクルでフラグをクリアするのをsed に依頼することはできません.

上記のスクリプトの移植性の高い実装の一つは,以下のようになります.

          t clear
          : clear
          s/keep me/kept/g
          t end
          s/.*/deleted/g
          : end
     

touch
要求されるタイムスタンプ(例えば-rオプション)を指定する場合, touchは通常,utimeutimesシステムコールを使用し, 結果として,`cp -p'に存在するタイムスタンプの切り詰めの問題と同様の 結果になるはずです.

古いBSDシステムには,空のファイルに対するtouchのよう なコマンドで,タイムスタンプを更新しない結果となるものもあるので,回避す るために,echoのようなコマンドを使用してください.

GNU touch 3.16r(とそれ以前の全て)は,空のファイルが NFSでマウントされている4.2のボリュームのとき,SunOS 4.1.3 での 動作で異常終了します.