一見読みにくい記法もシェルスクリプトの流儀:スマートな紳士のためのシェルスクリプト(4)(2/2 ページ)
前回に引き続き、今回もOS付属のシェルスクリプトを読んでいく。「本当にこれでいいのか?」と思うような読みにくい記述も見つかるが、よく読むとシェルスクリプトならではの流儀を学ぶことができる(編集部)
パターン指定で変数の中身の一部を削除
nextboot(8)を読んでいくと、次のようなコードも見られる。
var=$1 # strip literal quotes if passed in value=${2%\"*} value=${value#*\"}
先に、変数を指定して、その中身をパターン指定に使わせる変数展開という手法を紹介した。変数展開では、変数が格納している文字列の内容を切り取って表示するということもできる。指定方法は4種類。「${変数名%パターン}」「${変数名%%パターン}」「${変数名#パターン}」「${変数名##パターン}」だ。分かりにくい記述方法だが、シェルスクリプトではこの方法で文字列を編集することが少なくない。
上に挙げたサンプルの変数展開の部分を解説しよう。「${2%\"*}」の部分は、変数${2}に入っている文字列から「"*」に一致する部分を末尾から探し、そこから末尾まで削除するという意味になる。例えば、変数${2}に入っている文字列が「 "-s" 」なら、「${2%\"*}」とパターンを指定すると、結果は「 "-s」となる。
そして、「${value#*\"}」は、先頭から「*"」に一致する部分を探し、そこから先頭までを削除するという意味になる。直前の変数展開で、変数${2}に入っている文字列は「 "-s」になっている。ここで、「${value#*\"}」とパターンを指定すると、変数の中身は「-s」になる。
パターン指定では、「\"」のようにバックスラッシュを使っているが、これは文字としてのダブルクオートを指定するためだ。バックスラッシュを使わずにダブルクオートを指定してしまうと、文字列を囲む記号として扱われてしまう。
ここで説明した2つのパターン指定を見ると、「%」が末尾からの一致削除、「#」が先頭からの一致削除を意味していることが分かる。
そして、「%%」や「##」とすると、ワイルドカード検索が最長一致検索に変わる。例えば、変数の中身が「 "-o" "-s" 」であるとする。このときに、「${2%\"*}」と指定すると、変数の中身は「 "-o" "-s」となる。そして、「${2i%%\"*}」とすれば、末尾から「"*」に一致する最長の部分を指定して削除するので、変数の中身は「 」となる。
この手法は、パスから特定の部分を切り出す用途でよく使う。パスからファイル名だけを切り出したり、パスから親ディレクトリのみを切り出す、といった用途で使う。プログラミング言語としての機能をほとんど持っていないシェルスクリプトにおいて、シェル本来の機能だけで文字列を操作するなら、#、##、%、%%を利用した変数展開が便利で、よく使われている。
改行コードを使わずに改行を指定する
nextboot(8)を見ていくと、下の例のような表記も見られる。
if [ -n "${kenv}" ]; then kenv="${kenv} " fi
汎用的なプログラミング言語を使っている場合、このようなインデントを無視したような記述は、誤って改行してしまったものではないかと思われてしまうが、シェルスクリプトでは、このような記述にも意味がある。
上記の例は、変数kenvの値に改行コードを追加して、それを変数kenvの新しい値として設定するという意味になる。2行目と3行目が、変数kenvに新しい値を設定している部分だ。2行目で変数kenvの中身を指定しているが、末尾に来るはずのダブルクオートが、改行した先の3行目に来ている。つまり改行を指定するために、そのまま改行しているということだ。その結果、このようにインデントを無視したような書き方になる。シェルスクリプトでは、このような書き方もよく使う。
コマンドの扱い方を出力する関数を用意する
nextboot(8)を見ていくと、次のような関数が見つかる。記法に変わったところはないが、シェルスクリプトで関数を作るときの一般的なパターンとして押さえておきたい。
display_usage() { echo "Usage: nextboot [-e variable=value] [-f] [-k kernel] [-o options]" echo " nextboot -D" }
これは、コマンドの扱い方(いわゆるusage)を出力する関数だ。シェルスクリプトでは、このようにusageを出力する機能を関数として実装することが多い。特別変わったことをしているわけではないが、シェルスクリプトではこうなっていることが多いということを覚えておきたい。
引数のオプションを処理するgetopts
シェルスクリプトでコマンドを作成するとき、ユーザーが引数で指定するオプションを処理する必要がある。このようなときは、getopts組み込みコマンドを利用する。getoptsはほかのプログラミング言語で利用できる関数などと比べてもその動作を理解しにくい。nextboot(8)のソースコードにある実例を読みながら、getoptsの動きを説明していく。
while getopts "De:fk:o:" argument ; do case "${argument}" in D) delete="YES" ;; e) var=${OPTARG%%=*} value=${OPTARG#*=} if [ -z "$var" -o -z "$value" ]; then display_usage exit 1 fi add_kenv "$var" "$value" ;; f) force="YES" ;; k) kernel="${OPTARG}" add_kenv kernel "$kernel" ;; o) add_kenv kernel_options "${OPTARG}" ;; *) display_usage exit 1 ;; esac done
getopts組み込みコマンドは引数を2つ取る。1つ目がオプションの定義、2つ目がオプションを格納する変数だ。例えば上の例では「getopts "De:fk:o:" argument」となっており、"De:fk:o:"がオプションの定義であることが分かる。"De:fk:o:"は、"-D -e args -f -k args -o args"といった使い方ができることを意味している。argsは引数を意味する。先のオプションの定義を見ると、オプションの後に":"を入れると、オプションが引数を取るようになることが分かる。
getopts組み込みコマンドは、シェルスクリプトに渡された引数を、指定されたオプションの定義に従って処理する。1回目の実行で1つ目のオプションを処理する。オプションはgetoptsの第2引数で指定した変数に格納される。オプションがさらに値を取る場合は、環境変数OPTARGにその値が入る。同時に環境変数OPTINTに次のオプションのインデックス番号が入る。
getoptsの実行が2回目になると、処理対象が2つ目のオプションになる。同様に、3回目の実行では3つ目のオプションが処理対象になる。処理すべきオプションがなくなると、getoptsはコマンドの終了値として0ではなく1を返す。ユーザーが、指定にないオプションを使っていた場合は、getoptsの第2引数で指定した変数に「?」を格納するようになっている。
getoptsの動きを知るために、次のような短いサンプルコードを書いてみた。ここでは「getopts-test.sh」ファイルとして保存しておく。
#!/bin/sh while getopts "De:fk:o:" opt do echo "${opt}) ${OPTARG} ${OPTIND}" done
次のようにオプションを指定しながら実行すると、どのように動作するかが一目で分かる。オプションの指定がある限りループが回って、オプションを示すアルファベットとオプションが取る引数、そしてオプションの処理順序を示す番号を表示する。
% ./getopts-test.sh -f f) 2 % ./getopts-test.sh -f -D f) 2 D) 3 % ./getopts-test.sh -f -D -e args1 f) 2 D) 3 e) args1 5 % ./getopts-test.sh -f -D -e args1 -k args2 f) 2 D) 3 e) args1 5 k) args2 7 % ./getopts-test.sh -f -D -e args1 -k args2 -E f) 2 D) 3 e) args1 5 k) args2 7 Illegal option -E ?) 1 % ./getopts-test.sh -f -D -e args1 -k args2 args3 args4 f) 2 D) 3 e) args1 5 k) args2 7 %
getoptsの動きは一見分かりにくいが、記述方法はほとんどの場合で共通している。一度記述したらそれをコピー&ペーストして書き換えて使うことが多いように思う。
ヒアドキュメントでコマンドに長大な引数を渡す
最後に「ヒアドキュメント」と呼ぶ機能を紹介しよう。下のサンプルを見てほしい。
cat > ${nextboot_file} << EOF nextboot_enable="YES" $kenv EOF
「<<」の後に指定した区切り文字列のみの行が現れるまで、すべての行が指定したコマンドの標準入力となる。上の例では、「EOF」が目印となる区切り文字列となり、これがサンプルの末尾にある。つまり、その間にある2行がcatコマンドへの標準入力ということになる。この部分をヒアドキュメントと呼ぶ。
ヒアドキュメント内では変数展開、コマンド置換、数値演算が可能。ただし、チルダやクオートはそのまま文字として扱うので注意が必要だ。
なお、区切り文字列がダブルクオートまたはシングルクオートでクオートされているか、バックスラッシュでクオートされている場合は、ヒアドキュメント内の文字列は展開されることなくそのまま文字として、指定したコマンドの標準入力に渡される。
ヒアドキュメントではインデントができないのではないかと考え、代わりにechoを使うケースも見られる。しかしヒアドキュメント内でのインデントは不可能ではない。目印となる区切り文字列の指定に「<<-」を使うことでインデントが可能になる。nextboot(8)には次のようなヒアドキュメントも存在する。
cat 1>&2 <<-EOF WARNING: loader(8) has only R/O support for ZFS nextboot.conf will NOT be reset in case of kernel boot failure EOF
「<<-」で区切り文字列を指定すると、ヒアドキュメント各行の先頭に存在するタブは削除となる。タブが続いていれば、2つ目以降のタブは残る。この機能を利用することでヒアドキュメントのインデントを実現できるのだ。
次回も、FreeBSDに標準で付属するシェルスクリプトを選んで、その中から便利に使えるテクニックを紹介していく。
Copyright © ITmedia, Inc. All Rights Reserved.