まずはコマンド行引数の扱い方に関する簡単なヒントを示しましょう。それから、bash の基本的なプログラミング構成体の説明に移ってゆくことにしたいと思います。
初回の記事にあるサンプル・プログラムでは、"$1" という環境変数を使いました。この変数は、最初のコマンド行引数を参照しています。 同様に、"$2" や "$3" なども、スクリプトの 2 番目や 3 番目の引数を参照するために使用できます。以下はその例です。
#!/usr/bin/env bash
echo name of script is $0
echo first argument is $1
echo second argument is $2
echo seventeenth argument is $17
echo number of arguments is $#
|
上記の例については、見るだけですぐに理解できるかもしれません。ただし、2 つの細かな点については説明が必要でしょう。"$0" はコマンド行から呼び出されるスクリプトの名前に展開され、"$#" はスクリプトに渡される引数の数に展開されます。上記のスクリプトをいじりながら、いろいろなコマンド行引数を渡して、操作のコツをつかんでください。
場合によっては、すべての コマンド行引数を 1 度に参照するほうが便利かもしれません。その目的にために、bash では、"$@" 変数が用意されています。この変数は、スペースで区切ったすべてのコマンド行パラメーターに展開されます。 この変数の使い方については、本稿の後半で、"for" ループについて説明するときに取り上げることにしましょう。
C、Pascal、Python、Perl のような手続き型言語でのプログラミング経験がある人であれば、"if" ステートメントや "for" ループのような標準的なプログラミング構成体についてすでにご存じでしょう。 bash は、そうした標準的な構成体のほとんどについて、独自バージョンを持っています。ここからは、bash のいくつかの構成体を紹介しながら、他のプログラミング言語の構成体 (読者の皆さんがよくご存じの構成体) との違いを説明してゆくことにしましょう。プログラミング経験があまりない人も、特に心配はいりません。補足情報や例を多用しているので、それらを参考にすれば本文の説明に付いてゆけるはずです。
C でファイル関連のコードをプログラミングした経験があるなら、たとえばあるファイルが別のファイルよりも新しいかどうかといったことを判別するだけでも、相当厄介な作業になることをご存じでしょう。なぜかと言えば、C には、そのような比較処理を実行するための組み込み構文がないからです。結局のところ、2 つの stat() 呼び出しと 2 つの stat 構造を使って、手作業で比較処理を実行しなければなりません。 それとは対照的に、bash には、標準のファイル比較演算子が組み込まれており、たとえば "/tmp/myfile が読み取り可能か" ということを判別するのは、"$myvar が 4 よりも大きいか" ということを判別するのと同じぐらいに簡単です。
以下の表は、ごく一般的に使用される bash の比較演算子をまとめたものです。各オプションの正しい使い方についても、具体的な例を挙げておきました。それぞれの例は、"if" の直後に入れるような形になっています。 たとえば、次のようになります。
if [ -z "$myvar" ]
then
echo "myvar is not defined"
fi
|
| 演算子 | 説明 | 例 |
|---|---|---|
| ファイル比較演算子 | ||
| -efilename | filename が存在する場合に真 | [ -e /var/log/syslog ] |
| -dfilename | filename がディレクトリーである場合に真 | [ -d /tmp/mydir ] |
| -ffilename | filename が普通のファイルである場合に真 | [ -f /usr/bin/grep ] |
| -Lfilename | filename がシンボリック・リンクである場合に真 | [ -L /usr/bin/grep ] |
| -rfilename | filename が読み取り可能である場合に真 | [ -r /var/log/syslog ] |
| -wfilename | filename が書き込み可能である場合に真 | [ -w /var/mytmp.txt ] |
| -xfilename | filename が実行可能プログラムである場合に真 | [ -L /usr/bin/grep ] |
| filename1 -ntfilename2 | filename1 がfilename2 よりも新しい場合に真 | [ /tmp/install/etc/services -nt /etc/services ] |
| filename1 -otfilename2 | filename1 がfilename2 よりも古い場合に真 | [ /boot/bzImage -ot arch/i386/boot/bzImage ] |
| 文字列比較演算子(引用符に注目: 空白文字によってコードが混乱するのを防ぐよい方法) | ||
| -zstring | string の長さがゼロの場合に真 | [ -z "$myvar" ] |
| -nstring | string の長さがゼロ以外の場合に真 | [ -n "$myvar" ] |
| string1 =string2 | string1 とstring2 が等しい場合に真 | [ "$myvar" = "one two three" ] |
| string1 !=string2 | string1 とstring2 が等しくない場合に真 | [ "$myvar" != "one two three" ] |
| 算術比較演算子 | ||
| num1 -eqnum2 | 等しい | [ 3 -eq $mynum ] |
| num1 -nenum2 | 等しくない | [ 3 -ne $mynum ] |
| num1 -ltnum2 | より小 | [ 3 -lt $mynum ] |
| num1 -lenum2 | 以下 | [ 3 -le $mynum ] |
| num1 -gtnum2 | より大 | [ 3 -gt $mynum ] |
| num1 -genum2 | 以上 | [ 3 -ge $mynum ] |
場合によっては、同じ比較処理を複数の方法で実行できます。たとえば、次の 2 つのコード断片は同じ働きをします。
if [ "$myvar" -eq 3 ]
then echo "myvar equals 3"
fi
if [ "$myvar" = "3" ]
then
echo "myvar equals 3"
fi
|
上記の 2 つの比較処理はまったく同じことを実行しますが、最初のコードでは算術比較演算子を使っているのに対し、2 番目のコードでは文字列比較演算子を使っています。
大抵の場合、文字列や文字列変数を囲む二重引用符は省略しても構わないのですが、必ずしもほめられた方法とは言えません。なぜでしょうか。基本的に、コードの動作に問題はありませんが、環境変数にスペースやタブが入っている場合は、 bash が混乱してしまいます。以下は、混乱した比較処理の例です。
if [ $myvar = "foo bar oni" ]
then
echo "yes"
fi
|
上記の例の場合、myvar が "foo" と等しければ、コードは本来の働きをして、何も表示しません。しかし、myvar が "foo bar oni" と等しい場合は、コードが失敗して次のエラーが表示されます。
[: too many arguments
|
この場合は、"$myvar" の中にスペースがあるとき (つまり、"foo bar oni" と等しいとき) に、bash が混乱してしまったわけです。bash が "$myvar" を展開した後の比較処理は次のようになります。
[ foo bar oni = "foo bar oni" ]
|
ここでは、環境変数が二重引用符の中に入れられていないため、bash は、大括弧の間に引数がありすぎるとみなしたわけです。この問題は、文字列引数を二重引用符で囲んでおくだけで簡単に回避できます。 とにかく、文字列引数と環境変数をすべて二重引用符で囲む習慣を付けておけば、ほかにも多くの同じようなプログラミング・エラーを防ぐことができるのです。以下は、"foo bar oni" の比較処理の本来の コードです。
if [ "$myvar" = "foo bar oni" ]
then
echo "yes"
fi
|
上記のコードは本来の働きをして、びっくりするような結果にはなりません。
条件付きの構文についてはこれぐらいにして、bash のループ構成体の話に移りましょう。まずは標準の "for" ループです。初歩的な例を以下に示します。
#!/usr/bin/env bash
for x in one two three four
do
echo number $x
done
output:
number one
number two
number three
number four
|
実際にどんな処理が行われたのでしょうか。"for" ループの "for x" という部分では、"$x" という新しい環境変数 (ループ制御変数ともいう) を定義しています。この変数は、"one"、"two"、"three"、"four" という値に連続的に設定されています。それぞれの割り当ての後で、ループの本体 (つまり、"do" と "done" の間のコード) が 1 度実行されました。この本体では、標準的な展開構文を使って、 その他の環境変数と同じようにループ制御変数 "$x" を参照しています。さらに確認しておきたいのは、"for" ループが常に、"in" ステートメントの後の一種の単語リストを受け入れているという点です。 この場合は、4 つの英単語を指定しましたが、単語リストでは、ディスク上のファイルやファイル用のワイルドカードを参照しても構いません。以下の例で、標準的なシェル・ワイルドカードの使い方を示しましょう。
#!/usr/bin/env bash
for myfile in /etc/r*
do
if [ -d "$myfile" ] then
echo "$myfile (dir)"
else
echo "$myfile"
fi
done
output:
/etc/rc.d (dir)
/etc/resolv.conf
/etc/resolv.conf~
/etc/rpc
|
上記のコードは、/etc ディレクトリー内の "r" で始まるすべてのファイルでループしました。このループを実行するために、bash はまずワイルドカード /etc/r* を取り込んで展開し、 /etc/rc.d /etc/resolv.conf /etc/resolv.conf~ /etc/rpc という文字列に置き換えました。ループの内部では、"-d" 条件演算子を 1 度使用して、myfile がディレクトリーかどうかに基づいて、2 つの別々のアクションを実行しました。ディレクトリーの場合は、出力行に " (dir)" が付けられるわけです。
単語リストの中では、複数のワイルドカードや環境変数を使用することも可能です。
for x in /etc/r??? /var/lo* /home/drobbins/mystuff/* /tmp/${MYPATH}/*
do
cp $x /mnt/mydir
done
|
bash は、必要な場所でワイルドカードと変数の展開を実行して、単語リストを作成します。もちろん、この単語リストは非常に長くなる可能性があります。
上記のワイルドカード展開の例ではすべて絶対 パスを使っていますが、以下のように相対パスを指定することも可能です。
for x in ../* mystuff/*
do
echo $x is a silly file
done
|
上記の例では、現行作業ディレクトリーからの相対パスに基づいて、bash がワイルドカード展開を実行します。これはちょうど、コマンド行で相対パスを使用する場合と同じでしょう。ワイルドカード展開をいろいろ試してみてください。 ワイルドカードで絶対パスを使った場合は、ワイルドカードが絶対パスのリストに展開されます。そうでない場合は、生成される単語リストでも相対パスが使われます。現行作業ディレクトリー内のファイルを参照する場合 (たとえば、"for x in *" と入力する場合) は、結果リスト内のファイル名の先頭にパス情報が入ることはありません。先頭のパス情報は、"basename" 実行可能プログラムを使って切り捨てることもできます。 その場合は、次のようにします。
for x in /var/log/*
do
echo `basename $x` is a file living in /var/log
done
|
もちろん、スクリプトのコマンド行引数に対してループを実行するのも、たいへん便利な方法です。以下は、本稿の冒頭で紹介した "$@" 変数の使い方を示した例です。
#!/usr/bin/env bash
for thing in "$@"
do
echo you typed ${thing}.
done
output:
$ allargs hello there you silly
you typed hello.
you typed there.
you typed you.
you typed silly.
|
2 番目のタイプのループ構成体について調べる前に、シェルの算術を実行する方法を習得しておくほうがよいでしょう。簡単な整数計算であれば、シェルの構成体で実行できるのです。算術式を "$((" と "))" の間で囲むだけで、式の計算が実行されます。いくつかの例を示しましょう:
$ echo $(( 100 / 3 ))
33
$ myvar="56"
$ echo $(( $myvar + 12 ))
68
$ echo $(( $myvar = $myvar ))
0
$ myvar=$(( $myvar + 1 ))
$ echo $myvar
57
|
算術処理についてはこれぐらいにして、さらに 2 つの bash ループ構成体、つまり "while" と "until" を取り上げることにしましょう。
"while" ステートメントは、条件が満たされている限り処理をいつまでも続けます。形式は次のとおりです。
while [ condition ]
do
statements
done |
"while" ステートメントは、一定の回数だけループするように指定するのが普通です。たとえば、次の例では、10 回だけループするようになります。
myvar=0
while [ $myvar -ne 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done |
お分かりのとおり、ここでは算術展開を使用することによって、最終的には条件が満たされなくなるように指定してあります。もちろん、条件が満たされなくなった時点でループは終了します。
"until" ステートメントの機能は、"while" ステートメントの機能と逆です。つまり、一定の条件が満たされない 限りいつまでも処理が続けられます。以下は、前の "while" ループとまったく同じ動作をする "until" ループです。
myvar=0
until [ $myvar -eq 10 ]
do
echo $myvar
myvar=$(( $myvar + 1 ))
done
|
CASE ステートメントも、たいへん便利な条件付き構成体です。以下にコード断片のサンプルを示します。
case "${x##*.}" in
gz)
gzunpack ${SROOT}/${x}
;;
bz2)
bz2unpack ${SROOT}/${x}
;;
*)
echo "Archive format not recognized."
exit
;;
esac
|
上記のコードでは、まず "${x##*.}" が展開されます。"$x" はファイルの名前であり、"${x##.*}" は、そのファイル名の最後のピリオド以下の文字列以外をすべて切り捨てる働きをします。 そのようにして得られた文字列が、")" の左に列挙されている値と比較されます。この場合は、"${x##.*}" がまず "gz" と、次に "bz2" と、そして最後に "*" と比較されることになります。 もし "${x##.*}" がいずれかの文字列やパターンと一致するなら、")" の直後の行が ";;" の地点まで実行され、そこからさらに、終端の "esac" の後の行が実行されてゆきます。どのパターンや文字列とも一致しない場合はどの行も実行されませんが、 上記のコード断片の場合は、少なくとも 1 つのコード・ブロックが実行されます。というのは、"*" パターンが、"gz" や "bz2" と一致しなかったものをすべて検出することになるからです。
bash では、Pascal や C といった他の手続き型言語の場合と同じように、関数を定義することさえできます。しかも、bash の場合は、スクリプトでコマンド行引数を使うのとほとんど同じ仕組みによって、 関数で引数を使うことさえ可能です。では、ここでサンプルの関数定義を見てから、さらに話を進めることにしましょう。
tarview() {
echo -n "Displaying contents of $1 "
if [ ${1##*.} = tar ]
then
echo "(uncompressed tar)"
tar tvf $1
elif [ ${1##*.} = gz ]
then
echo "(gzip-compressed tar)"
tar tzvf $1
elif [ ${1##*.} = bz2 ]
then
echo "(bzip2-compressed tar)"
cat $1 | bzip2 -d | tar tvf -
fi
}
|
上記のコードでは、"tarview" という関数を定義しています。その関数は、一種の tarball を引数として受け入れます。この関数を実行すると、引数である tarball のタイプ (非圧縮、 gzip 圧縮、bzip2 圧縮のいずれか) が判別され、1 行の情報メッセージとして出力されてから、tarball の中身が表示されます。以下に示すのは、上記の関数を呼び出す方法です (コードを入力するか、 貼り付けるか、参照するかした後、スクリプトかコマンド行のいずれかから呼び出すことになるでしょう)。
$ tarview shorten.tar.gz
Displaying contents of shorten.tar.gz (gzip-compressed tar)
drwxr-xr-x ajr/abbot0 1999-02-27 16:17 shorten-2.3a/
-rw-r--r-- ajr/abbot1143 1997-09-04 04:06 shorten-2.3a/Makefile
-rw-r--r-- ajr/abbot1199 1996-02-04 12:24 shorten-2.3a/INSTALL
-rw-r--r-- ajr/abbot 839 1996-05-29 00:19 shorten-2.3a/LICENSE
....
|
お分かりのとおり、関数定義の内部では、コマンド行引数を参照する場合と同じ仕組みで、引数を参照することができます。さらに、"$#" マクロは引数の数の値に展開されます。本来の働きをしない可能性があるのは、"$0" という変数だけでしょう。この変数は、"bash" という文字列 (シェルから対話式で関数を実行した場合) か、関数の呼び出し元のスクリプトの名前に展開されます。
関数の内部で環境変数を作成しなければならない場合も少なくないでしょう。もちろん、それは可能ですが、いくつかの技術的なポイントを押さえておく必要もあります。ほとんどのコンパイル言語 (C など) では、関数の内部に変数を作成すると、 その変数が別個のローカル名前空間に置かれます。したがって、C 呼び出しの myfunction に関数を定義して、その関数の中に "x" という変数を定義するならば、"x" というグローバル変数 (関数の外部にある変数) は影響を受けないので、いわゆる副作用を防ぐことができます。
これは、C に関して言えることですが、bash の場合は違います。bash では、関数の内部に環境変数を作成すると、その変数がグローバル 名前空間に追加されます。つまり、関数の外部にあるグローバル変数が上書きされ、 しかも、その関数自体が終了した後でも、その変数は存在し続けることになるわけです。
#!/usr/bin/env bash
myvar="hello"
myfunc() {
myvar="one two three"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
|
このスクリプトを実行すると、"one two three three" という出力が生成されます。これはつまり、この関数内に定義されている "$myvar" がグローバル変数 "$myvar" を上書きしたこと、そしてその関数自体が終了した後もループ制御変数 "$x" が存在し続けていること (しかも、グローバル変数 "$x" が定義されていたとすれば、その変数も上書きされたであろうこと) を示しています。
この簡単なサンプルでは、バグを見つけるのも容易であり、代わりの変数名を使って埋め合わせをするのも難しくありません。しかし、それは正しいやり方とは言えないでしょう。この問題を解決する最善の方法は、 最初からグローバル変数を上書きする可能性をなくしてしまうということです。そのためには、"local" コマンドを使います。"local" コマンドによって関数の内部に変数を作成する場合は、変数がローカル 名前空間に置かれることになるので、グローバル変数を上書きすることはありません。以下は、上記のコードをインプリメントして、グローバル変数の上書きを避ける方法を示したものです。
#!/usr/bin/env bash
myvar="hello"
myfunc() {
local x
local myvar="one two three"
for x in $myvar
do
echo $x
done
}
myfunc
echo $myvar $x
|
この関数からは、"hello" という出力が生成されます。グローバル変数である "$myvar" は上書きされませんし、myfunc の外部で "$x" が存在し続けることもありません。この関数の最初の行では、 後から使用するローカル変数 x を作成しているのに対し、2 番目の行 (local myvar="one two three") では、ローカルの myvar を作成し、しかも その変数に値を割り当てています。 最初の行は、ループ制御変数をローカルにする点で価値があります。というのは、"for local x in $myvar" という指定が認められていないからです。そういうわけで、この関数はグローバル変数を上書きしません。 関数を設計する場合は、いつでもこの方法を使うようにお勧めします。"local" を使用するべきでない と言えるのは、グローバル変数を明示的に変更する場合だけでしょう。
今回は、bash の重要な機能を取り上げたので、そろそろ bash ベースのアプリケーションを開発する方法に話を進めてもよいでしょう。次回の記事では、まさにその点を説明する予定です。それでは、次回をお楽しみに !
- developerWorks にある bash の最初の記事、bash 例解、第 1 回もお読みください。
-
GNU の bash ホーム・ページをご覧ください。
アメリカはニューメキシコ州アルバカーキ (Albuquerque) 在住の Daniel Robbins 氏は、Gentoo Project の責任者、Gentoo Technologies, Inc. の CEO、Linux Advanced Multimedia Project (LAMP) の良き指導者、「Caldera OpenLinux Unleashed」、「SuSE Linux Unleashed」、「Samba Unleashed」などの Macmillan 書籍の共同執筆者という多彩な肩書きを有しています。コンピューターとのかかわりは小学校 2 年生のころからであり、Logo プログラミング言語と、ちょっと怖いパックマンとの出会いがきっかけでした。そういう生い立ちもあってか、SONY Electronic Publishing/Psygnosis のリード・グラフィック・アーティストとしても活躍中です。この春に出産を予定している愛妻 Mary さんとの時間を大切にする良き夫でもあります。氏の連絡先はdrobbins@gentoo.org です。