5. シェルプログラミング(2) |
4. では主に関数と変数の処理を取り扱った. ここでは,より本格的なプログラミングを実現するために フロー制御構造について述べる. これは if, for, while 等といったプログラムの実行の流れを制御する機構である. | ||
<-- 「4. シェルプログラミング(1)」へ戻る | ↑ 目次へ戻る | 「6. コマンドラインオプションと定義済み変数」へ進む --> |
5.1 if / else5.2 条件の評価
5.3 for
5.4 case
5.5 select
5.6 while と until
5.1 if / elseフロー制御構造の中でもっとも単純な 条件文 を実現するのが,この if と else である.
5.2 条件の評価
この構文はまたは
if 条件 then 文 fiとなる. また,else-if という具合に if 文を複数連結することもでき, その場合は elif (else if の省略形)というキーワードを用いて,
if 条件 then 文 else 文 fiとする. なお,elif 句は複数個続けて書いてよい.
if 条件 then 文 elif 条件 then 文 else 文 fi
- 終了ステータスと return
上の if 文での条件にはどういったものを書けばよいだろうか. C や Pascal に慣れた人ならばそれは論理式 (真あるいは偽の値をとる式)となることを期待するであろう. bash ではこれが若干異なる. 論理式ではなく, コマンドの終了ステータス が利用される.
すべての UNIX コマンドは終了する際に呼び出し元のプロセス (親プロセス)に整数コードを返す. これを終了ステータスという. 通常は 0 が正常終了 であり,それ以外(1 から255)は異常終了を表す.
つまり,終了ステータスが 0 のとき C や Pascal でいう真になり, それ以外のとき偽となる.
(C では 0 が偽,それ以外が真であった.)例として次の関数 cd を考える.
これを ~/.bashrc に書いておけば, cd でディレクトリを移動するたびに,自動的に ls を実行してくれる. まず,if 文の評価のため,
function cd { if builtin cd $1 then ls else echo "移動できません" fi }が実行される. これはコマンドラインの第 1 引数を cd コマンドに渡して実行させる. builtin というキーワードはこの際, cd として(関数ではなく)シェルの組込みコマンドの cd を使うことを指定するものである. もしこれが無いと,再び関数として処理され (コマンドよりも関数の方が優先度が高い)無限ループに陥ってしまう.
builtin cd $1
これが正常に終了すれば,ls が実行され, 異常終了すれば echo "移動できません" とメッセージを表示する.次に, この関数/スクリプトが別の関数/スクリプトに利用されることを考えると, やはり終了ステータスを正しく出力する必要があるだろう. 上の例のように特別に指定しない場合は,] 最後に実行されたコマンドの終了ステータスがそのまま返される. つまり,上の場合,ls コマンドか echo コマンドのいずれかが最後に実行されるので, それらの終了ステータスが返される. するとほとんどの場合,終了ステータスは 0 (正常終了)になるだろう. おそらく, if 文で実行された cd コマンドの終了ステータスを保存しておいて, それをこの関数の終了ステータスとするのが妥当だろう. そのためにシェル変数 $? が用意されている. この変数は 直前に起動されたコマンドの終了ステータス が収められている.
したがって,以下のようにするとよい:
function cd { if builtin cd $1 then es=$? ls else es=$? echo "移動できません" fi return $es }
- 終了ステータスの組合せ
終了ステータスを組み合わせることで, 一度に複数の評価を実行することもできる. その構文は C プログラムでの AND , OR の構文と同じで
のように記述する. これらではまず,statement1 が実行され, その終了ステータスが評価される. そこで正常終了(終了ステータス 0) した場合,&& ならば引き続いて statement2 を実行し, || ならばそのまま終了する. 逆に statement1 が異常終了 (終了ステータス 0 以外)した場合, && ならばそのまま終了し,|| ならば引き続いて statement2 を実行する.
statement1 && statement2 statement1 || statement2&& は AND(論理積)を意味するので, 両方のステートメントが真(正常終了)であるときに限り, 真(正常終了)となる. したがって,1 つ目を評価した段階で既に偽(異常終了)である場合は, 2 つ目を評価するまでもなく全体の評価は偽(異常終了)であるので, 1 つ目の段階で直ちに終了する.
同様に,|| は OR(論理和)を意味し, 少なくとも一方のステートメントが真(正常終了) であるときに真(正常終了)となる. したがって,1 つ目を評価した段階で既に真(正常終了)である場合は, 2 つ目を評価するまでもなく全体の評価は真(正常終了)であるので, 1 つ目の段階で直ちに終了する.
if 文の条件部を [ ] で括っているスクリプトをよく目にする. なにげに括弧で括っているだけのようにも見えるが, これには test コマンドと同じ働きをもつ という重要な意味がある. (test コマンドはファイル属性の検査等に利用される組込みコマンドである.)
- 文字列の比較
まずは文字列を比較する演算子を示す:
文字列比較演算子 演算子 次の場合に真 str1 = str2 str1 と str2 が一致する str1 != str2 str1 と str2 が一致しない -n str1 str1 が NULL ではない(長さが 0 より大きい) -z str1 str1 が NULL である(長さが 0) 例えば,
とすると第1引数が NULL であるときにエラーメッセージを出力して, 終了ステータス 1 で終了する(異常終了).
if [ -z "$1" ] then echo "引数を指定してください" exit 1 fi
このとき,[ と ] の左右には 1 個以上の空白が必要である ことに注意しよう.
- ファイル属性の確認
次にファイルの属性を調べる演算子のうち主に使われるものを以下に示す:
ファイル属性演算子 演算子 真の場合 -d file file が存在し,かつディレクトリである -e file file が存在する -f file file が存在し,かつ通常ファイルである(ディレクトリや特殊ファイルではない) -r file file に read パーミッションがある -s file file が存在し,空ではない -w file file に write パーミッションがある -x file file に execute パーミッションがある, ディレクトリの場合は search パーミッションがある -O file file の所有者である. -G file file のグループ ID が自分のものと一致する(複数のグループに属している場合はそのうちのどれかと一致する) file1 -nt file2 file1 が file2 よりも新しい file1 -ot file2 file1 が file2 よりも古い これらは [ と ] で囲んで利用される.] もし,複数の条件を組み合わせたければ,上述の && や | | を使って
などとする. (また,セミコロンを使って
if [条件] && [条件] thenと一行で書くこともできる.)
if [条件] && [条件]; thenこれとは別に 2 つの論理演算子 -a (AND) と -o (OR) を利用して,全ての条件を一つの [ ] 内に収めることも可能である. その場合,条件式を明確にするため ( ) を使って記述することができる. 例えば,
とすると,$1 が NULL でなく,ディレクトリ名であって, search パーミッションが与えられている場合に限り真となる. このとき ( ) はバックスラッシュ\ を使ってその機能をエスケープしておかなければならない.
if [ \( -n "$1" \) -a \( -d "$1" \) -a \( -x "$1" \) ]; then
- 整数による条件式
条件式に < や > を使ってもそれは数値の大小を意味せず, 辞書式順序での比較になってしまう. 例えば,
は真となる. (これが数値ならば偽であることは明らか.)
"6" > "57"整数を比較する演算子を以下に紹介する:
数値評価演算子 演算子 比較 -lt より小さい -le 以下 -eq 等しい -ge 以上 -gt より大きい -ne 等しくない 前述した if 構造は条件分岐を実現する機能であった. これに対し,繰り返しを実現する構造の一つがこの for 構造である.5.4 caseC 言語の for 文に慣れている人は,ある文を 10 回繰り返すといった構造を思い浮かべるかも知れないが, bash での for 文はこれとは違う. 指定されたリストに沿って処理を繰り返すものである.
for 構造の構文は
in list は省略可能で, 省略した場合は "$@" がデフォルトの値として設定される. これは前述した 位置パラメータ の一種で,コマンドライン引数のリストになる.
for name in list do $name を利用する文 done(例) aaa, bbb, ccc という文字列を順に表示する
(例) コマンドライン引数を順に表示する
for name in "aaa" "bbb" "ccc" do echo $name done
for args do echo $args donePascal の case 文や C の switch 文を連想してもらえば分かりやすいと思うが, 場合分けの構造を実現するためのものである. その構文はという構造になっており, 条件式に対して パターン1 が適合した場合は 処理1を実行, パターン2 が適合した場合は 処理2を実行,.... といった具合になる. なお,C や Pascal とは異なり, これらのパターンには 文字列やワイルドカードを含むパターンが利用 できる.
case 条件式 in パターン1) 処理1 ;; パターン2) 処理2 ;; ......... esac(例)コマンドライン第1引数が .c で終るときは .c を除く部分をオブジェクトの名前として gcc でコンパイルし, .java で終るときは javac でコンパイルし, いずれの場合でもない場合は "unknown type" と表示する.
(C 言語での default 文のように) いずれの場合にも当てはまらなかった時のデフォルト処理を指定するための特別な命令は用意されていないが,上のようにパターンに * を用いればよい.
case $1 in *.c ) gcc -o ${1%.c} $1 ;; *.java ) javac $1 ;; * ) echo "unknown type" ;; esacこれは簡単なメニューをすばやく生成することのできる優れた機能である.5.6 while と until
指定されたリストを番号付きで表示し,ユーザに番号を入力させる. そこで,指定された番号の内容が変数に格納され, それぞれの処理へと移行するというものである.
つまり,シェルスクリプトでありながらユーザとの簡単な対話が可能なのである.select の構文は for のそれに酷似しており
となっている. (for が select に代わっただけである.) これも in listは省略可能で, 省略した場合は "$@" がデフォルトの値として利用される.
select name in list do $name を利用する文 done(例)which host ? と表示して, ユーザにホスト番号を指定させる. そして,指定されたホストへの rlogin を試みる.
(ユーザとの対話に利用するプロンプトは PS3 に指定する.)
PS3='which host ? ' select loginhost in edu1 edu2 edu3 edu4 edu5 abc xyz do case $loginhost in edu[1-5] ) rlogin -l ユーザ名 "$loginhost".edu.foo.bar.ac.jp ;; abc | xyz ) rlogin -l ユーザ名 "$loginhost".hoge.foo.bar.ac.jp ;; * ) echo "invalid number" ;; esac break done
(この select 文を終了させるために break 文を使っている. select 文は処理を無限に繰り返す設計になっているため,これが必要となる.)if 文では条件が真か偽かで場合分けを行い, それぞれ用意された処理を実行するが,これを繰り返すようにしたのが while や until である. while の構文はで 条件 が真である限り 処理 を繰り返す. 逆に,条件が偽である限り繰り返すのが until で
while 条件 do 処理 doneという構文になっている. これは 「コマンドが正しく実行されるまで処理を実行し続ける」 といったシチュエーションに有効である. (条件 にそのコマンドを書けば異常終了したときに 処理 が実行され,再び条件に戻る.)
until 条件 do 処理 done(例)コピースクリプト
これでは $1 を $2 にコピーすることを試み, 失敗した場合は 5 秒待って再度試みる.
until cp $1 $2 do echo "waiting..." sleep 5 done