UNIXの発展で大きく寄与をしたのは、シェルスクリプトである。シェルスクリプトによって既存のコマンドとユーザプログラムを組み合わせ、新たなプログラムを作成する過程は、ソフトウェアの「のり」と呼ぶにふさわしい。
シェルスクリプトでベースになった技術は、デバイスとファイルを区別しないファイルシステム、入出力のリダイレクト、forkとpipeによるストリームのパイプライン処理である。
ここでは、シェルスクリプトの「のり」としての役割に焦点を当てて解説する。
Last modified: Wed Mar 30 22:21:21 JST 2005
UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMulticsと呼ばれる大型のオペレーティングシステムを開発していた頃、「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのが、現在のUNIXである。
彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11(システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト)という現在のPDA以下のハードウェアを購入し、その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのである。
「インタラクティブで便利なコンピュータサービス」は、情報の流れである「ストリーミング」を複数のコマンド間でつなぎ合わせた「パイプライン」を形成することによって実現された。
再利用率の高いコマンドとシェルスクリプトによるカスタマイズコマンドを作成することによってユーザのニーズに応じた処理を短時間で提供できることがUNIXの最大の強みとなった。
「パイプライン処理」の例を実演を交えて紹介する。シェルとのやりとりは、キーボードからのコマンドライン入力によって行う。
デモで使用するコマンドを以下に示す。
take% xev | Event2Plot | Graph
コマンドライン入力の各パートの説明を表[コマンドライン解説]に示す。プロンプトの後に入力されたコマンド文字列によって、xev, Event2Plot, Graphの3つのプロセス(1)が起動され、同時に処理が行われる。
これによってxevが出力したイベント情報をEvent2PlotがX Yの座標に変換しGraphに渡し、別のウィンドウに同時にプロットすることができる。これがUNIXのパイプライン処理のすばらしいところである。
パート文字列 | 内容 |
---|---|
take% | %とそれに先行する文字列はプロンプトと呼ばれる入力促進文字列。 |
xev | Window内で発生したX-EVENTを標準出力に出力するXウィンドウコマンド |
| | コマンド間のストリーミングを結ぶパイプを示す。単に「パイプ」と呼ばれる。 |
Event2Plot | X-EVENTの内マウスの動きを示すMotionNotifyイベントを選択し、それからマウスの座標を抽出するシェルスクリプト |
Graph | GNUのプロットデータ表示コマンドgraphのコマンドのオプションを一つにまとめたシェルスクリプト |
デモの画面を図[パイプラインデモ画面]に示す。
例に使用したシェルスクリプトEvent2Plotの内容を以下に示す。
#! /bin/sh awk -F, ' /MotionNotify/ { getline; gsub(/\(/, "", $4); gsub(/\)/, "", $5); printf("%d %d\n", $4, 100 - $5); }'
同様にGraphは
#! /bin/sh /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5
いずれも数行と非常に短いプログラムであるが、これだけのプログラムでデモのような処理を行えるがUNIXなのである。
UNIXでパイプラインを実現するための鍵になる部分をforkシステムコール(2)とpipeシステムコールの実装に見ることができる。
forkシステムコールは、UNIXでプロセスを生成する唯一の関数である。forkによってカーネルは呼び出し元のコピーを「子プロセス」(3)が生成される。forkによって生成された親子間では、ファイルテーブルやi-nodeテーブルとプログラムの命令コード(4)を共有する。これによって2つのプロセスが同じファイルにアクセスすることができるようになる。forkシステムコールの仕組みを図[forkの仕組み]に示す。
シェルによる「パイプライン処理」を実現するためには、プロセスによるファイルの動的切り替えが必要になるため、このような親子関係のプロセス生成メカニズムを導入したものと思われれる。
pipeシステムコールは、本当に不思議な関数である。自分が出力したものを自分へ送るストリームの輪(パイプ)がpipeシステムによって生成される。pipeシステムコールの仕組みを図[pipeの仕組み]に示す。
この不思議なパイプをforkシステムコールと組み合わせることよって「パイプライン」を形成することができる。図[forkとpipeの組み合わせ]にforkとpipeの組み合わせでパイプラインを形成するメカニズムを示す。
ここで、UNIXのファイル記述の割り当て規則である「最後にcloseされた最も小さな記述子をdupで割り当てる」を利用している。これは、createやopen等のファイルを割り当てるシステムコールでも適応されている。共通ルールである。
パイプラインを形成するためには、もう一つの難関が存在していた。それが端末やハードディスク、テープ装置、プリンタ等のハードウェアデバイスである。UNIXのファイルシステムの特徴を挙げると
特にi-nodeとファイルテーブルを使って、ファイルとハードウェアデバイスを同様に扱うことができるようにしたのが大きな特徴である。
UNIXのファイルシステムの構成を図[UNIXのツリー階層ファイルシステム]に示す。
UNIXの構造を図[UNIX内部構造]に示す。
カーネルがハードウェアとコマンドの間に位置し、コマンドはシステムコールによってカーネルに対してハードウェアへのアクセス要求を送る。
シェルが一番外側に位置し、ユーザとコマンドの間に位置し、ユーザからの要求に応じてコマンドを実行し、その結果をユーザに返す役割をする。
どのようにしてユーザがシェルと会話するかを次節で説明する。
デモで説明したようにシェルとの会話は、プロンプトが表示されたコマンドラインに実行したいコマンドと引数を渡すことによって行う。
コマンドは、プロンプトの直後に指定し、ブランクまたはタブで区切って、引数を指定する。通常マイナス"-"が先行した引数はオプションと呼ばれ、実行するコマンドのモードやデフォルトの設定値の変更等を行う。オプションに続いて引数(arg1, arg2)を指定する。
コマンドの引数の終わりは、セミコロン";"または改行キーである。一行のコマンドラインに複数のコマンドの実行を指定する場合には;を使用する。コマンドの実行は、改行キーを押すことによって行われる。
% command -option arg1 arg2 ...
forkシステムコールで説明した親プロセスがオープンしていたファイルを子プロセスが読み込むことができる機能を使って動的にコマンドの入出力ファイルを切り替える機能が「リダイレクト」である。
UNIXのCプログラムでは、main関数がプログラムの開始部分(エントリポイント)であり、プログラムmainを呼び出す前に標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)にそれぞれ、0, 1, 2のファイル記述子がオープンされた状態でmainが呼び出される。
従って標準入力から読み込み、標準出力に書き込むプログラムを書いておけば、実行時にリダイレクト機能を使って入力ファイルや出力先を動的に切り替えることが可能になる。
入力リダイレクトには、"<"が使われ、出力のリダイレクトには">"と">>"が使用される。">>"が指定された場合には、ファイルの終わりに追加する。
リダイレクトの例をリファレンスから引用する。
$ echo test >/tmp/test.out; # /tmp/test.outに"test"の文字列を出力 $ echo more test >>/tmp/test.out # /tmp/test.outに"more test"を追加 $ wc < /tmp/test.out # /tmp/test.outの文字数をカウントする
コマンドの引数にファイル名を指定する場合、メタ記号によって複数のファイルを一括して指定することができる。メタ記号の一覧を以下に示す。
メタ記号 | 説明 |
---|---|
? | 任意の一文字にマッチする |
[abef] | []で囲まれた文字のいずれかの一文字にマッチする |
[a-z] | a-zのように-の間に含まれる一文字にマッチする |
[^abc] | [の直後に^が指定されるとそれ以降に指定した文字列以外の文字にマッチする |
* | 任意の文字列にマッチする |
例)ファイルが.bakで終わるすべてのファイルを削除する場合、
% rm *.bak
と指定する。(6)
コマンドを逐次処理するのではなく、並行して処理する場合には、バックグラウンド指定(&)を使用する。
$ rm -r . & ; # カレントディレクトリ以下のすべてのファイルをバックグランドで削除する
バックグラウンドで起動しているプロセスをフォアグラウンドに変更するには(7)、fgコマンドを使用する。
シェルでよく使われるコマンドを表[基本コマンド]に示す。
各コマンドの使い方は、manコマンドを使って知ることができる。
例)cp(コピーコマンド)の使い方を調べると次のように出力される。
NAMEの次にcpコマンドの機能が短く記述され、SYNOPSISの後にコマンドの入力形式が記述され、DESCRIPTION以下にオプションおよび機能の詳細な説明記述されている。
% man cp CP(1) FSF CP(1) NAME cp - copy files and directories SYNOPSIS cp [OPTION]... SOURCE DEST cp [OPTION]... SOURCE... DIRECTORY cp [OPTION]... --target-directory=DIRECTORY SOURCE... DESCRIPTION Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY. -a, --archive same as -dpR --backup[=CONTROL] make a backup of each existing destination file -b like --backup but does not accept an argument -d, --no-dereference never follow symbolic links <以下省略>
コマンド名が分からない場合でも、コマンドの一覧を表したり、キーワードでコマンドを検索することができる。(8)
% man -k compare File::Compare (3) - Compare files or filehandles I18N::Collate (3) - compare 8-bit scalar data according to the current locale bcmp (3) - compare two memory areas comm (1) - compare two sorted files line by line infocmp (1m) - compare or print out terminfo descriptions memcmp (3) - compare two memory areas strcasecmp (3) - case insensitive character string compare strcmp (3) - character string compare strcoll (3) - locale specific character string compare strncasecmp (3) - case insensitive character string compare strncmp (3) - character string compare test (1) - check file types and compare values tiffcmp (1) - compare two zcmp, zdiff (1) - compare compressed files
コマンド名 | 機能 |
---|---|
echo | 引数を出力する |
pwd | カレントディレクトリを出力する |
ls | 指定されたファイルリストを出力する |
cp | ファイルをコピーする |
cat | 引数で指定された複数のファイルを結合して出力する |
rm | 指定されたファイルを削除する |
grep | 第1引数で指定されたパターンマッチする行を第2引数以降で指定されたファイルから検索する |
cd | 指定されたディレクトリに移動する |
wc | ファイルの行数、単語数、文字数をカウントする |
test | 引数で指定された条件を検証する |
シェルスクリプトとは何かを、デモで使用したシェルスクリプトを例に説明する。
これは、Graphの内容である。
#! /bin/sh /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5
最初の行の
#! /bin/sh
が起動するシェルを指定している。ここでは/bin/sh
を指定している。
次の行が実際に実行するコマンドgraphとそのオプションを指定している。
/usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5
ただし、これだけではシェルスクリプトは実行する事ができない。最後に作成したGraphを実行可能にするために次のコマンドを実行する。
% chmod +x Graph
これでシェルスクリプトGraphが完成し、単にGraphと入力するだけでスクリプトに記述した内容が実行される。(9)
このようにシェルスクリプトでは、コマンドラインから入力するコマンドとその引数をファイルに記述し、一つのコマンドとして登録することができる。
シェルは、コマンドを組み合わせるだけではなく、その使い方を拡張するためにも利用できる。
例)標準入力から第一引数に指定されたパターンを検索するプログラムtgrep(10)を複数ファイルの検索に拡張する場合の拡張方法を示す。
新しいシェルスクリプトをの名称をTgrep.shとし、その仕様は、引数が1個の場合には、標準入力から検索し、引数が2個以上の場合には、各ファイルに対してパターンに一致する行を検索するものとする。
引数のチェックで使われるシェルの文法がcase文である。シェルのケース文には正規表現が指定できるので、Cなどのプログラミング言語のcase文よりも使いやすい。case文の文法は節[case文]を参照。
まず、引数の数を調べるところは、特殊シェル変数の$#で引数の個数を取り出し、case文で処理を振り分ける。各条件に適応する処理は、2個のセミコロン(;;)で区切られる。
#! /bin/sh case $# in 1) echo pattern: $1 ;; [2-9]) echo pattern: $1; shift; echo files: $* ;; esac
まずは、このようにシェルスクリプトが正しい動きをしているかechoを使って引数をチェックしてみる。
% Tgrep.sh 1 pattern: 1 % Tgrep.sh 1 2 3 4 pattern: 1 files: 2 3 4
これでは、10個以上の場合にうまくいかない。そこで、引数のパターンを0個、1個、それ以外に変える。
#! /bin/sh case $# in 0) echo invalid argument.; exit 1;; 1) echo pattern: $1 ;; *) echo pattern: $1; shift; echo files: $* ;; esac
各ケース文が正しく動作することを確認する。
% Tgrep.sh 1 2 3 4 5 6 7 8 9 10 11 pattern: 1 files: 2 3 4 5 6 7 8 9 10 11 % Tgrep.sh 1 pattern: 1 % Tgrep.sh invalid argument.
引数のチェックが完了したところで、各ファイルに対するtgrepの適応に進む。同じパターンを何回か繰り返す処理には、for文を使用する。for文の文法は節[for文]を参照。
テストで使っていたechoをtgrepに替え、2個以上の処理にfor文を入れる。
#! /bin/sh case $# in 0) echo invalid argument.; exit 1;; 1) tgrep $1 ;; *) PATTERN=$1; shift; for i in $* do tgrep $PATTERN $i; done ;; esac
できたスクリプトを試してみる。
UNIX$ ./Tgrep.sh graph UNIX1st.* <td>GNUのプロットデータ表示コマンドgraphのコマンドのオプションを一つにまとめたシ ェルスクリプト</td> /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 <p>次の行が実際に実行するコマンドgraphとそのオプションを指定している。</p> /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 <td>GNUのプロットデータ表示コマンドgraphのコマンドのオプションを一つにまとめた シェルスクリプト</td> /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 次の行が実際に実行するコマンドgraphとそのオプションを指定している。 /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 <bibliography> </bibliography> <td>GNUのプロットデータ表示コマンドgraphのコマンドのオプションを一つにまとめた シェルスクリプト</td> /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 次の行が実際に実行するコマンドgraphとそのオプションを指定している。 /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 <bibliography> </bibliography>
これでは、どのファイルにパターンがマッチしたのか分からない。
そこで、tgrepの出力行の先頭にファイル名を追加することにする。各出力行の処理には、while文とread文を使用する。節[while文]各出力行をread文でLINE変数にセットして、それをファイル名と一緒にechoコマンドで出力すればよい。
#! /bin/sh case $# in 0) echo invalid argument.; exit 1;; 1) tgrep $1 ;; *) PATTERN=$1; shift; for i in $* do tgrep $PATTERN $i | while read LINE do echo $i: $LINE done done ;; esac
完成したTgrep.shの出力は、希望したとおりのものになっている。
UNIX$ ./Tgrep.sh graph UNIX1st.* UNIX1st.html: UNIX1st.html: /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 UNIX1st.html: /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 UNIX1st.html: UNIX1st.html: /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 ... 一部省略 UNIX1st.sdoc: UNIX1st.sdoc: /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 UNIX1st.sdoc: /usr/local/bin/graph -T X -x 0 100 -y 0 100 -m 0 -S 5 UNIX1st.sdoc: 次の行が実際に実行するコマンドgraphとそのオプションを指定している 。 ... 以下省略
このようにわずか13行のシェルスクリプトでtgrepが実際に活用できるコマンドとなる。
シェルスクリプトを作成する上で役に立つTIPをいくつか紹介する。
ファイルに指定されたファイルに対して、特定のコマンドを実行したいとき、while文とread文を使って、1ファイルずつ処理する方法もあるが、コマンドが複数ファイル指定に対応している場合には、ファイルに記述されているファイルをコマンドラインに渡した方が便利である。そのようなときに便利なのが、バックアクセント`である。
バックアクセント指定をするとそこで指定されたコマンドが別のシェルで処理され、その結果がコマンドラインの引数に置換されます。(11)
例)/tmp/1.lst(12)には、rmコマンドで削除したいファイルa.dat, b.dat, c.datが指定されている。ここで、`cat /tmp/1.lst`とすると行単位(縦)に指定されていたファイル名がa.dat b.dat c.datのコマンドラインに横に展開され、置き換えられる。
この結果rm `cat /tmp/1.lst`は、rm a.dat b.dat c.datとして処理される。これは、正規表現などのパターンでファイルを指定しにくい時などに有効な方法である。
% cat /tmp/1.lst a.dat b.dat c.dat % rm `cat /tmp/1.lst`
シェルスクリプト中で一時ファイルを作成する場合、一時ファイル名が固定ファイルの場合、同じコマンドが同時に使用された場合、一時ファイルが上書きされ、壊れてしまうことがある。このような場合、マシン上で一意な一時ファイル名を作成できるとこのような心配はなくなる。シェルスクリプトではプロセスIDは、マシン上で一意な値として使われる。プロセスIDは、特殊シェル変数$$によって得ることができる。一時ファイルの作成で使用されるもう一つのテクニックは、"here document"と呼ばれる方法である。here documentを使用するとコマンドへの標準入力をシェルスクリプト上に記述し、必要ならシェル変数の置換も行うことができる。
TMP_FILE=/tmp/temp.$$ NAME='Hiroshi TAKEMOTO' cat << EOF > $TMP_FILE My name is $NAME. This is a simple shell script example. EOF echo `cat $TMP_FILE` rm $TMP_FILE
上記プログラムを1.shとファイルに記述し、次のように処理すると、
% vi 1.sh % sh 1.sh My name is Hiroshi TAKEMOTO. This is a simple shell script example.
となる。この例のようにシェルスクリプトを実行する方法として、起動シェルの引数としてシェルスクリプトとその引数を指定するやり方がある。特に一時的なスクリプトの場合にこの方式が使われる。
シェル変数にクォーティング文字としてダブルクォート、シングルクォート、バックスラッシュがある。これまで何の説明もなくダブルクォートやシングルクォートを使ってきたが、その使い分けを説明する。
空白やタブを含む複数の単語を1個の引数としてコマンドに渡す場合で、かつシェル変数の展開を行う場合
コマンドのラインでのダブルクォートは、シェルで取り除かれ、コマンドにはシェル変数を展開して、ダブルクォートを除いた文字列渡される。
シェル変数の展開を行わない場合と、ダブルクォートも含めてコマンドに渡す必要がある場合
ダブルクォートと同様にシングルクォートを除いた文字列がコマンドに渡される。
シェルでは、空白(ブランク、タブ)で区切られた最初の引数がコマンド名として扱われ、その後の引数がコマンドへの引数として渡されます。コマンドの区切りは、セミコロン(;)または改行とパイプ(|)、括弧()、バックグランド指定(&)で区切られる。
複数のコマンドを一行に記述したいとき、if thenを1行に記述したいとき、明示的にコマンドの終わりを示したいときにセミコロンを付ける。
通常、ハイフン(-)で始まる引数はコマンドのオプションとして扱われ、コマンド オプション コマンド引数の順で指定するのが慣例となっている。
$ echo hello world # 改行をコマンドラインの終わりに使用している hello world $ wc -l test.c; # コマンドの終わりを明示的に示すために、セミコロンを付けている test.c 120
コマンドを逐次処理するのではなく、並行して処理する場合には、バックグラウンド指定(&)を使用する。
$ rm -r . & ; # カレントディレクトリ以下のすべてのファイルをバックグランドで削除する
バックグラウンドで起動しているプロセスをフォアグラウンドに変更するには(13)、fgコマンドを使用する。ログインシェルがshの場合、logoutするとバックグラウンドプロセスが、終了してしまう。このため、nohupコマンドを使用することによってlogout後もバックグラウンドプロセスが継続して稼働する。
UNIXでは、リダイレクト機能によって入出力ファイルを実行時に切り替えることができる。入力リダイレクトには、"<"が使われ、出力のリダイレクトには">"と">>"が使用される。">>"が指定された場合には、ファイルの終わりに追加する。
$ echo test >/tmp/test.out; # /tmp/test.outに"test"の文字列を出力 $ echo more test >>/tmp/test.out # /tmp/test.outに"more test"を追加 $ wc < /tmp/test.out # /tmp/test.outの文字数をカウントする
コマンドの出力ストリームを次のコマンドの入力ストリームにすることをUNIXではパイプ処理と呼ぶ。パイプ処理は、基本的なUNIXコマンドを組み合わせて必要な情報を取り出す基本となる。
$ env | grep DIO; # 環境変数の内、DIOを含む変数名の一覧を表示する $ cat test.sh | sed -e '/^$/d' -e '/^#/d' | wc -l # test.shのコメント、空行を除く行数を計算する $ tar -cf - . | (cd ~/tmp; tar -xf -) # カレントファイル以下を~/tmpにコピーする
一連の処理の入出力をまとめ、別シェルプロセスで実行したい場合、括弧()で括ることによってグループ処理を指定することができる。
$ (export LANG=ja_JP.SJIS; ovstart) # 別プロセスで環境変数LANGを変更し、ovstartコマンドを実行する $ (sleep 10; echo hello) & # バックグランドで5秒待ってから"hello"と表示する。(プロンプトはすぐにでる)
一連のシェルコマンドをファイルに列記し、一度に実行することができる。このようにコマンドの処理を記述したファイルをシェルスクリプトと呼ぶ。
helloと出力し、5秒待って、再度hello againと出力するスクリプトをリスト[test.sh]に示す。シェルスクリプトでは#から行末までをコメントとして扱う。ただし、最初の行の#!と記述したコメントは特殊な意味を持ち、起動シェル(コマンドでも可)を指定する行である。
# #以降はコメントとして扱われる。 echo hello sleep 5 echo hello again
このプログラムを実行するためには、
$ chmod +x test.sh
とtest.shに実行権を設定すればよい。
UNIXのファイル名を指定する場合に、ファイルの正規表現を使用することができる。シェルで使用できる正規表現は、*, ?, []がある。
ファイル展開の例を示す。
$ ls a*.doc # aで始まり、拡張子がdocのファイルとマッチする $ ls a?b.txt # aで始まり、3文字目がbで拡張子が.txtのファイルにマッチする $ rm [0-9]* # 私は、数字で始まるファイルを一時ファイル名に使用する。これは一時ファイルの削除を意味する $ ls [^0-9]* # 一時ファイルを除くファイル名を表示する
shでは、シェル変数と環境変数の2つの変数が使用される。シェル変数はカレントシェル内のみで有効な変数であり、環境変数はシェルから起動されるすべての子プロセスに引き継がれる変数である。シェルスクリプトでは、これらの変数を使ってコマンド間での情報交換を行っている。
シェル変数に値をセットするには、変数名と値の間に空白を入れずに=を入れる。代入する値の中に空白が含まれる場合シングルクォート(')またはダブルクォート(")で括る。シェル変数名にはアルファベット、数字、アンダースコア(_)からが使用できる。
簡単なシェル変数の例を示す。
$ HOST_NAME=IES00 # シェル変数HOST_NAMEに"IES00"をセットする $ IN_FILE="" # シェル変数IN_FILEに空文字をセットする $ PS="ps ef" # シェル変数PSに"ps ef"をセットする
シェル変数を環境変数として宣言するには、exportコマンドを使用する。
$ export TERM
のようにexportの後に環境変数とする既存の変数名を指定する方法と、
$ export NEW_TERM=vt100
のようにNEW_TERMに値を設定して、新規環境変数NEW_TERMを宣言する方法の2通りがある。
シェル変数は$変数名で参照することができる。ただし変数名の区切りが認識しづらい場合、ダブルクォート(")(14)や、${}で括って参照する。
$ VAR=hello $ echo $VAR hello $ echo "${VAR}_again" hello_again
シェル変数には配列指定も可能であり、変数名[添え字]の形式で指定する。すべての配列要素を参照する場合には、$配列名[@]とする。
$ LINE[1]=foo $ LINE[2]=bar $ echo $LINE[1] $LINE[2] foo bar
shには、位置変数指定、特殊変数参照、特殊置換指定がある。
コマンドの実行結果をシェル変数に代入するためにバッククォート(`)指定がある。
$ DIR=`pwd`
とするとシェル変数DIRにpwdの実行結果(カレントディレクトリの値)がセットされる。
例えば、変数iの値を1プラスする指定は、
$ i=`expr $i + 1`
とすることで算術計算をシェルスクリプト内で実現することができる。
シェルスクリプト内で、処理の流れを変える制御コマンドとして、if, case, for, while文がある。処理の終わりは、fi, esac, doneで表し、ネスト構造を取ることも可能である。
if文は、
if expr_list then block_stmt [else block_stmt] fi
の形式を取る。expr_listの最後にセミコロンを付けない場合には、
if expr_list then block_stmt else block_stmt fi
のように改行してthenを記述する(私はこちらの形式を使用している)。expr_listの終了コードが0以外の場合に条件が真となる。
if分がネストする場合には、elif文を使用する。
if expr_list then block_stmt elif expr_list then block_stmt else block_stmt fi
if文の例を示す。
if [ $? -ne 0 ] # 直前のコマンドが正常に終了しなかった場合 then if [ -e $TMP_FILE ] # 一時ファイルが存在すれば、 then rm -f $TMP_FILE; # 一時ファイルを削除し、処理を終了する exit 1; fi fi
通常if文の条件判定にはtestコマンドを使用する。shでは、慣例として"["(testのシンボリックリンク)を利用する。この場合には終わりを"]"で囲む。よく使われるtestコマンドオプションを以下に示す。(15)
for文は、次の形式で使用する。
for name in word_list do block_stmt; done
word_listは空白で区切った単語の列を指定し、指定された順で変数nameにセットされ、doループの間のblock_stmtが実行される。in word_listが省略された場合には、すべての引数($*と同等)が展開される。
# 10回ループを回す場合の簡単な例 for i in 1 2 3 4 5 6 7 8 9 10 do echo $i; # ループカウンタの値を出力する done
case文は、条件に合致したパターンの実行文を処理する。
case word in pattern1) block_stmt ;; # pattern1に合致した場合、 .... pattern2) block_stmt ;; # pattern2に合致した場合、 *) block_stmt ;; # 上記のいずれでもない場合、 esac
のように定義する。patternにはshの正規表現が使用できる。通常case文は引数のチェックに用いられ、その場合には位置引数をシフトするshiftコマンドと一緒に使用する。
例)プログラムのオプションが、[-t INTERVAL] [-c CONF_FILE] INPUT_FILEの場合、以下の例のようになる
set -- `getopt t:c: $*`; # オプションのチェックと展開して、位置引数に再セットする for i # 各位置引数に対して、 do case $i in -t) INTERVAL=$2; shift; shift ;; # INTERVALに-tの次の引数をセット -c) CONF_FILE=$2; shift; shift ;; # CONF_FILEに-cの次の引数をセット --) shfit; break ;; # オプションの最後に達した場合 -?) echo 不適当なオプション; exit 1;; esac done
while文は、条件が真の間、ループを回す。
while expr_list do block_stmt; done
のように定義する。よくあるパターンは、ファイルからの入力処理であり、read文と組み合わせて使用する。
i=0; # 添え字iを初期化する cat /tmp/test.dat | while read line # /tmp/test.datの各行をlineに読み込む do IN_LINE[i]=$line; # 入力ファイルの各行を配列IN_LINEにセットする i=`expr $i + 1`; done
よく使う処理を関数として定義することによって、コマンドやシェルスクリプトと同様に扱うことができる。
name() { block_stmt; }
関数名nameの後に()を付けて、処理を中括弧{}で括ることで関数が定義できる。関数への引数は、位置変数$nで取得することができる。環境変数およびシェルスクリプト中のシェル変数は、関数内でも参照することができる。ただし、関数内部で定義されたシェル変数は、その関数内部でのみ参照することができる。
heredocumentを日本語で何と言えばよいか分からないが、スクリプト中にコマンドへの標準入力を記述できる機能と言えば、理解できるだろうか。heredocument中のシェル変数は、置換可能であるので、テンプレートファイルとして使用したり、awk等のプログラムを一時的に作成する場合に使用する。
heredocumentは、<<の後に終端文字列を指定し、その終端文字列で始まる行までをheredocumentの入力とする。
heredocumentの例を以下に示す。
DATE=`date` cat <<EOF Today is $DATE. EOF
とすると、
Today is Wed Mar 14 14:29:32 JST 2001.
のように表示される。
shでは、cshとは異なり、標準エラー出力や標準出力の指定を2, 1のように記述番号を使って指定することができる。この機能を使えば、エラーメッセージだけを別のファイルに集めることができる。
$ test.sh 2>/tmp/err.out # コマンドtest.shのエラーメッセージを/tmp/err.outに出力する
cshには、標準出力と標準エラー出力をまとめる機能があるが、これをshで実現するときには、
$ test.sh 2>&1 > /tmp/std+err.out # /tmp/std-err.outにstdout, stderrが出力される
のように"2>&1"を指定することで出力を結合することができる。
出力内容を捨てたい時やファイルを空にしたい時に特殊ファイル/dev/nullを使用する。標準出力をリダイレクトで/dev/nullに送れば、そのメッセージが捨てられる。大量のメッセージを出力するプログラムでそのメッセージが必要ない場合にこの方式を使用することによって処理スピードが格段に速くなる。
ファイルを空にするときには、
$ cp /dev/null file_name
のようにするとfile_nameで指定されたファイルのサイズが0になる。
[1] | Bourne, Stephen. The Unix System. Addison-Wesley Publishing, |
[2] | MAURICE J. BACH. THE DESIGN OF THE UNIX OPERATING SYSTEM. , |
[3] | John Lions. Lions's Commentary on UNIX. アスキー出版, |