ホームページ制作,維持・保守
サーバーシステム構築・管理
ネットワーク構築・管理
IT環境サポート
インフラ構築サポート
ITアウトソーシング
インフラ基礎講座

どの環境でも使えるシェルスクリプトを書くためのメモ

森之宮 0 154 0

シェルスクリプトは環境依存が激しいから……

などとよく言われ、敬遠される。それなら共通しているものだけ使えばいいのだが、それについてまとめているところがなかなかないので作ってみることにした。

「どの環境でも使える=POSIXで定義されている」と定義

「どの環境でも使える」とは、なかなか定義が難しい。あまりこだわりすぎると「古いものも含め、既存のUNIX全てで使えるものでなければダメ」ということになってしまう。しかし、私個人としては 今も現役(=メンテナンスされている)のUNIX系OSで使いまわせること にこだわりたい。

とはいっても全てのOSやディストリビューションについて調べられるわけではないので、この記事では基本的に最新のPOSIXで定義されていることをもって、どの環境でも使えると判断するようにした。(飽くまで「基本的に」ということで)

従って、互換性確保のため、シェルの中で使ってよい機能は Bourneシェルの範囲 ということにする。(bash,ksh,zsh,あるいはcsh等の拡張機能は使わないようにする)

随時バージョンアップ予定

新しいことを発見したり、教わったりしたら、随時この記事をバージョンアップしていこうと思うので、ツッコミ歓迎。

各論

最終行の改行を省略したシェルスクリプトファイルにすべきではない

シェルスクリプトの最後の行だからといって、行末のLF(0x0A)を省略するのは止めるべきだ。それは環境によって異なる動作を引き起こす原因になり得る。

例えば次のようにして、ヒアドキュメントセクションの終了宣言行で終わるシェルスクリプトを作ってみる。

$ printf '#! /bin/sh\n'    >> test.sh
$ printf 'cat <<HEREDOC\n' >> test.sh
$ printf '  hoge\n'        >> test.sh
$ printf 'HEREDOC'         >> test.sh
$ chmod +x test.sh
$ 

コードを見ればわかるように最後の行にだけ行末にLF(0x0A)を付けていないわけだが、一部の環境でこれを実行すると次のようになってしまう。

$ ./test.sh
  hoge
HEREDOC$ 

ヒアドキュメントセクションの終了文字列と解釈されずに表示されてしまうのだ。他にも予期せぬ動作を招く恐れがあるので、最終行でもちゃんと行末には改行を付けよう。

シェルパターン

シェルパターンとは、DOSで言うならワイルドカードといえば話が早いかもしれない。しかしUNIXのそれはもっと多機能で、ファイル名指定時のみならずcase文の条件指定時にも使えるし、何よりimage[1-9][0-9][0-9].jpgなどと指定すればそのディレクトリーの中に存在する、"image100"から"image999"までのファイルを一括指定できるなど、正規表現ほどではないにしろ表現力が高いことが特長だ。

しかしこのブラケットに要注意。ブラケット記号の中に列挙した文字「以外」を表す文字は、正規表現で馴染み深い^ではなく、!である。

^は一応POSIXでも言及しているが、全ての実装で使えるとは限らない。この事実が厄介の元になっており、逆に言えば^を「以外」の意味として解釈する環境もあれば、通常文字としてそのまま解釈する環境もあるということだ。

従って、シェルパターンにおけるブラケットの中で^自身を文字として指定したければブラケット内の2文字目以降に記述すべきである。

シェル変数

まず、配列は使えない。従ってPIPESTATUSも使えない。

変数の中身を部分的に取り出す記述に関して使っても大丈夫なものに関しては、POSIXのページ(2.6.2)を見るとまとまっている。

PIPESTATUS的な変数が必要な場合

例えばPIPESTATUSに依存したシェルスクリプトが既にあって、それをどの環境でも使えるように書き直したいと思った場合、実は可能だ。詳しいやり方については、別記事「PIPESTATUSさようなら」を参照してもらいたい。

スコープ

→local修飾子を参照

正規表現

これはAWKgrepsed等、コマンドによっても使えるメタキャラは違うし、grepなら-Eオプションを付けるかどうかでも違うし、さらにGNU版でしか使えないものもあるので注意が必要。(*BSD上でもGNU版が採用されている場合もある。→grep参照)

しかし、 正規表現メモ というスバラシいまとめページがあるのでここを見れば、使っても互換性が維持できるメタキャラがすぐわかる。

え、シェル変数の正規表現?それは一部シェルの独自拡張なので論外。

→ロケールも参照

ただし、文字クラスは使わない方が無難

[[:alnum:]]のように記述して使う「文字クラス」というものがある。これは正式名称をPOSIX文字クラスといい、その名のとおりPOSIX準拠であるのだが、一部の実装ではうまく動いてくれない。(Raspberry PiのAWKなど)

まぁ、それはPOSIXに準拠してないそっちの実装が悪いといってしまえばそれまでなのだが、そもそも設定されているロケールによって全角を受け付けたり受け付けなかったりして環境の影響を受けやすいので使わない方がよいだろう。

乱数

乱数を求めたい時、シェル変数のRANDOMを使うのは論外。それなら、とAWKのrand関数とsrand関数を使えばいいやと思うかもしれないがちょっと待った!

論より証拠。FreeBSDで次の記述を何度も実行してみれば、非実用的であることがすぐわかる。

FreeBSD上でAWKを使って乱数を発生させると
$ for n in 1 2 3 4 5; do awk 'BEGIN{srand();print rand();}'; sleep 1; done
0.0205896
0.0205974
0.0206052
0.020613
0.0206209

つまり動作環境によっては乱数としての質が非常に悪いのだ。AWKが内部で利用しているOS提供のrand(3)とsrand(3)を、FreeBSDは低品質だったオリジナルのまま残し、新たにrandom(3)という別の高品質乱数源関数を提供することで対応しているのが理由なのだが。(Linuxではrand(3)とsrand(3)を内部的にrandom(3)にしている)

/dev/urandomを使うのが現実的

ではどうすればいいか。POSIXで定義されているものではないが、/dev/urandomを乱数源に使うのが現実的だと思う。例えば次のようにしてod、sedコマンドを組み合わせれば0~4294967295の範囲の乱数が得られる。

/dev/urandomからの乱数取得
$ od -A n -t u4 -N 4 /dev/urandom | sed 's/[^0-9]//g'

最後のsedは、なぜtr -Cd '0-9'にしないのか。理由は、→trコマンド参照

/dev/urandomをどうしても使いたくない場合

乱数の品質は/dev/urandomほど高くないものの、代替手段はある。psコマンドの結果は実行するたびに必ず変化するのでこれを種として取り入れる。

具体的には、プロセスID、実行時間、CPU使用率、メモリ使用量の各一覧あたりが刻々と変化するのでこれらを取得するとよいだろう。更に、現在日時も加え、これらに基づいて2^32未満の範囲でAWKのsrand()に渡す乱数の種を生成しているのが下記のコードだ。

psコマンドを乱数の種として取り入れる
LF=$(printf '\\\n_');LF=${LF%_}     # sedで改行を扱うための定義
(ps -Ao pid,etime,pcpu,vsz; date) | # 乱数源(プロセス情報一覧+日時) 
od -t d4 -A n -v                  | # 数値化する
sed 's/[^0-9]\{1,\}/'"$LF"'/g'    |
grep '[0-9]'                      |
tail -n 42                        | # 100000000未満の数字を
sed 's/.*\(.\{8\}\)$/\1/g'        | # 42個まで用意(2^32未満にするため)
awk 'BEGIN{a=-2147483648;}        # # 上の値を足してsigned long値を作る
     {a+=$1;}                     #
     END{srand(a);print rand();}'

ロケール

ロケール環境変数によって動作が変わる可能性がある

コマンドによっては、ロケール環境変数(LANGLC_*)の内容によって動作が変わるものがある(環境によっては変わらないものもある)。具体

0 コメント