シェルスクリプトでよく利用される機能について, 実験し理解しよう.
作業の前に,端末またはブラウザで bash マニュアル を開いておこう.
コマンドの入出力を制御するために,リダイレクトがある. 基本的なリダイレクトは次の通り:
出力・追加リダイレクトでは,後述のように, 標準エラー出力を取り扱うためのものも用意されている.
また,今回は詳しく紹介しないが,入出力制御には,パイプラインもある:
次の例では, ヒアドキュメント(here document)について自己記述的に説明している:
$ echo "短文の出力には echo を利用する." 短文の出力には echo を利用する. $ cat << EOF > 複数行にわたる長文の出力には, > 複数の echo コマンドではなく, > cat コマンドと ヒアドキュメントを利用する. > EOF 複数行にわたる長文の出力には, 複数の echo コマンドではなく, cat コマンドと ヒアドキュメントを利用する. $ cat << FIN > 終端文字列は EOF とは限らないぜ.好きなように決めるんだ. > もし,EOF 限定だと,文字列 "EOF" を出力できないだろぅ? > あと,変数だって使えるんだぜ.$BASH > FIN 終端文字列は EOF とは限らないぜ.好きなように決めるんだ. もし,EOF 限定だと,文字列 "EOF" を出力できないだろぅ? あと,変数だって使えるんだぜ./bin/bash
なお,ここで行の先頭の > はプロンプトだ. シェルスクリプト化する場合,> を記述する必要はない.
ヒアドキュメントでは空白文字もそのまま出力されてしまうことに注意. シェルスクリプト内でインデントしたい場合には, というか,インデントは必須 なので,このままでは困る. しかし,<< の代わりに <<- を使えば, 行頭の Tab だけを無視してくれる.
というわけで,シェルスクリプト内のヒアドキュメントについては, <<- を使い,Tab でインデントしよう. スペースじゃダメ.
出力リダイレクトの基本的な使い方は次の通り: (前期のプログラミング言語IIで,すでに使っている.)
$ touch boo.c foo.c woo.h # 準備:練習用の空ファイルを作成 $ ls boo.c foo.c woo.h # ファイルを表示 $ ls > ls.txt # 無言 $ cat ls.txt # ls の実行結果は,リダイレクト先のファイルに出力されている boo.c foo.c woo.h
コマンドの実行結果にエラーがある場合, 次のようにリダイレクトを使い分ける:
$ ls boo.c poo.c ls: poo.c: そのようなファイルやディレクトリはありません # ← 標準エラー出力 boo.c # ← 標準出力 # 標準出力だけをリダイレクト $ ls boo.c poo.c > ls.txt ls: poo.c: そのようなファイルやディレクトリはありません $ cat ls.txt boo.c # 標準エラー出力も一緒にリダイレクト $ ls boo.c poo.c &> ls.txt # エラー出なくなった? $ cat ls.txt ls: poo.c: そのようなファイルやディレクトリはありません # ファイルに出てたー boo.c # 標準出力と標準エラー出力とを分離してリダイレクト $ ( ls boo.c poo.c > ls.txt ) &> err.txt $ cat ls.txt boo.c $ cat err.txt ls: poo.c: そのようなファイルやディレクトリはありません
なお,最後の例の ( ) は, 複合コマンド(複数のコマンドや入出力制御の組み合わせ)を 単純コマンド(ひとつのコマンド)として取り扱うためのものだ.
シェルスクリプトでは, スクリプト内で実行されたコマンドからのエラーメッセージを 必要としない(表示したくない)場合も多い. (エラー出力だけでなく標準出力をも必要としない場合さえある.) この場合,次のような特殊ファイルを利用すればよい:
$ ls boo.c poo.c ls: poo.c: そのようなファイルやディレクトリはありません # 標準エラー出力 boo.c # 標準出力 # 標準出力だけを端末表示し,標準エラー出力を捨てる $ ( ls boo.c poo.c > /dev/tty ) &> /dev/null boo.c # 標準出力
コマンドが 成功(正常に終了)したか/失敗(エラーが発生)したか を判断するために, 特殊変数 $? がある. コマンドの戻り値が自動的に $? へ代入される.
とにかく,使ってみよう:
$ ls boo.c boo.c $ echo $? 0 # 直前の ls は成功だった $ ls poo.c ls: poo.c: そのようなファイルやディレクトリはありません # エラー $ echo $? 1 # 直前の ls は失敗だった $ echo $? 0 # 直前の echo は成功だった
前回作成したスクリプト backup.bash にエラー処理を追加しよう. このスクリプトでエラーが発生し得るのは,次の部分:
echo "$file -> $file.org" \cp $file $file.org
たとえば, コピー元ファイル $file が存在しない場合や コピー先ファイル $file.org が書き込み禁止の場合, エラーとなってしまう. 確認しよう:
$ ls -l boo.c* # ファイルの詳細情報を表示 -rw-rw-r-- ... boo.c -rw-rw-r-- ... boo.c.org $ chmod a-w boo.c.org # a:全ユーザ -:アクセス拒否 w:書き込み $ ls -l boo.c* -rw-rw-r-- ... boo.c -r--r--r-- ... boo.c.org $ ./backup.bash boo.c boo.c -> boo.c.org cp: ファイル ``boo.c.org'' を作ることができませんでした: 許可がありません # 書き込み禁止ファイルへの書き込みなのでエラー $ chmod u+w boo.c.org # u:所有者 +:アクセス許可 w:書き込み $ ls -l boo.c* -rw-rw-r-- ... boo.c -rw-r--r-- ... boo.c.org $ ./backup.bash boo.c boo.c -> boo.c.org # 書き込めるようになった
もしかすると,上書きしてよいかどうか,たずねられるかもしれない. cp が cp -i へエイリアスされている場合, \cp の \ を付け忘れていると, このような状況になる.
シェルスクリプト中では, このようなエイリアス置換を抑制するために, cp ではなく \cp と書けばよい. または,/bin/cp としても OK.
ところで,上の実験でエラーが出た場合について, backup.bash コマンドを実行したつもりなのに, cp コマンドからのエラーが報告されてしまっていた. こんなエラーに直面したとき, 普通のユーザは思考停止におちいってしまうだろう. こんな事態を回避すべきだ.
このエラーを表示せず検出だけするには, backup.bash スクリプトの一部を次のように書き換えよう:
... echo -n "$file -> $file.org ... " \cp $file $file.org &> /dev/null # cp のエラーを非表示に if [ $? -ne 0 ] # エラーを検出したら... then echo "失敗" # backup.bash のエラーを表示 else echo "成功" fi ...
既に紹介したように,bash では,関数も定義できる. 基本パターンは次の通り:
... function func # 関数 func の定義 { ... return ステータス値 } ... func 引数1 引数2 ... # 関数 func の実行 if [ $? ... ] ... # 実行結果に応じた処理 ...
Cとは異なり, 関数定義の行に仮引数が記述されていないが, 間違いではない. 関数の仮引数は,コマンドライン引数と同様, $1,$2,…,$9 によって参照できる.
また,関数の戻り値は,ステータス値だ. コマンドのステータスと同様, 関数終了直後にだけ $? で参照できる.
Unix コマンドの多くは,簡単なヘルプ機能を備えている. 大抵の場合,次のようにオプション引数を付けて実行すると, コマンドの使い方を表示してくれる:
$ コマンド -h または コマンド --help
これを真似して,backup.bash にもヘルプ機能を追加してみよう. 次のように書き換える:
#!/bin/bash function help { cat <<- EOF [Tab] 説明:引数に指定されたファイルのバックアップコピーを作る [Tab] 使い方:backup.bash [-h] ファイル名 ... [Tab] オプション: [Tab] -h このヘルプを表示する. [Tab] EOF exit 1 } if [ $# -lt 1 ]; then help; fi if [ $1 = "-h" ]; then help; fi ...
なお,ヘルプ表示における括弧 [ ] は, そのキーワードが省略可能であることを示している.
shift コマンドを使うと, コマンドライン引数を 1 個ずつ削除できる. 次のシェルスクリプトを試せば,理解できるだろう. shift.bash:
#!/bin/bash if [ $# -lt 2 ]; then exit 1; fi echo $1 # 1 番目の引数を表示 shift # $1 を削除($2 以降は前へ移動) echo $1 # 元々の $2 を表示 shift echo $1 # 元々の $3 を表示
$ ./shift.bash boo foo woo boo foo woo
どんな場合に使うのか? 大抵のスクリプトでは, あるコマンドライン引数について処理が完了したら, その引数を二度と使うことはない. なので,完了した引数を削除してしまえば, いつでも最初の引数だけを処理対象にすれば OK, ということになるので, スクリプトを簡潔に記述できるようになるかもしれない.
たとえば, すべてのコマンドライン引数についての処理を 次のように記述できる:
... while [ $# -gt 0 ] do echo $1 shift done
これと同じことを前回は, for-in と $* によって実現していた. 比較してみよう.
backup.bash に次の実行例のようなオプション機能を追加せよ:
$ ls * boo.c foo.c woo.h # 通常のバックアップ(デフォルト動作) $ ./backup.bash *.c poo.txt boo.c -> boo.c.org ... 成功 foo.c -> foo.c.org ... 成功 poo.txt -> poo.txt.org ... 失敗 # これも通常のバックアップ $ ./backup.bash -b *.? boo.c -> boo.c.org ... 成功 foo.c -> foo.c.org ... 成功 woo.h -> woo.h.org ... 成功 # バックアップから元のファイルを復元(recovery) $ rm woo.h $ ./backup.bash -r woo.h woo.h.org -> woo.h ... 成功 # タイムスタンプ(timestamp)を比較し,必要な場合だけバックアップ # (タイムスタンプを比較するには,条件演算子 -nt や -ot が使える. # Bash マニュアルの「条件式」の項を参照.) $ touch foo.c # foo.c だけ更新しとく $ ./backup.bash -t *.? boo.c -> boo.c.org ... 更新済 foo.c -> foo.c.org ... 成功 woo.h -> woo.h.org ... 更新済 # 実行結果を冗長(verbose)に出力 $ ./backup.bash -v *.c poo.txt boo.c -> boo.c.org ... 成功 foo.c -> foo.c.org ... 成功 poo.txt -> poo.txt.org ... 失敗 2 個のファイルをバックアップしました. 1 個のファイルのバックアップに失敗しました.
なるべく,複数のオプションを組み合わせられるようにしてみよう.