Skip to content

Latest commit

 

History

History
2366 lines (1760 loc) · 111 KB

sh-text.md

File metadata and controls

2366 lines (1760 loc) · 111 KB

シェルとは

シェル(shell)とはunixのコマンドインタプリタで、ユーザ端末から入力された 文字列を解釈し、その指示に従って仕事をするプログラムです。しかし、シェルは決し て特殊プログラムではありません。シェルも他のツールと同様にunix上の1つのコマン ドに過ぎません。シェルが他の多くのプログラムと違う点は自分自身がある特定の仕事 をするのではなく「他のコマンド類のまとめ役」として機能することです。

なぜコマンドインタプリタが「殻」を意味するシェル(shell)なのでしょうか。 ユーザから見るとオペレーティングシステムの核(kernel)を貝殻のように包んでいる ことに由来しているようです。 Rod Maris, Marc H. Meyer著「The UNIX Shell Programing Language」 には名前の由来について次のような記述があります。

It is called the shell, beacuse, like the shell of a nut or an egg, it is the part that we see from the outside. The inside part is called the kernel. The shell takes to us and to the kernel.

それは、胡桃や卵の殻のように、外から眺める部分なので、 シェル(殻:shell)と呼ばれている。内部は核(カーネル:kernel)と呼ばれる。 シェルはカーネルと我々との仲立ちをする。

kernelとはオペレーティングシステムの中枢で、プロセスやメモリ、ファイル等の管 理を行う部分です。unix上で何か仕事をする時には必ずお世話にならなければならな いものですが、ユーザ側からkernelに働きかける手段はシステムコールのみです。 プログラムを書くのでしたらシステムコールを操れますが、 対話形式ではシェルを通す以外触れる方法がありません。シェルを通して備え付 けのコマンドなり、自分の作ったプログラムを起動して初めてユーザはkernelへなん らかの指示を出すことができるわけです。プログラムを書かないユーザにとって はシェルこそがオペレーティングシステム、あるいはコンピュータそのものに見えま すから、シェルを使いこなすことがそのままコンピュータを使いこなすことになるわ けです。

unixの世界標準、POSIXのドラフトでは唯一のシェルとされる「Bシェル(Bourne Shell)」 をminix上で体験しながらunixの心髄に触れてみましょう。なお、文中でminixと表現し ている部分はそのままunixと読み換えても不都合は起こらないように配慮したつもり です。

シェルの機能

シェルのもっとも簡単に実感できる機能はユーザとの対話形式で入力された文字列 (コマンド)を解析し、指示された仕事を行うものでしょう。 「対話形式」だけを考えるとユーザから入力された文字列を修正する機能や、入力を 記憶しておき必要に応じて呼び出す履歴機能などに注目されます。しかし、対話形式 での操作はシェルの提供する機能の一部分に過ぎません。シェル本来の機能は「他の コマンド類のまとめ役」にあります。この機能を上手に組み合せるとわずかな手間でか なり複雑な仕事をこなすことができます。ひとつの仕事を能率良く処理できるよう、 独立した単純なコマンド類をまとめるため、シェルは次のような機能を持っています。

  • プログラムの実行
  • ファイル名の置換
  • 入出力の切り換え
  • パイプ機能
  • ユーザの環境設定
  • インタプリタ型のプログラミング言語

プログラムの実行

シェルはユーザが端末から入力したコマンドを実行する機能を持っています。 コマンドの入力が完了するとそれを解析し、何をすべきかを決定します。 シェルに対して入力された文字列はコマンドラインと呼ばれ、一般的に次の様な形をしてい ます。

$ コマンド名 [引数1 引数2 ... 引数n]

行頭の$はシェルがユーザからのコマンドを受け付ける状態にあることを示す プロンプトで、 ユーザがタイプするものではありません。引数を必要としないコマンドもたくさん ありますから省略することもできます。コマンド名や各引数間は一般にホワイ トスペース文字と呼ばれる「スペース」あるいは「水平タブ」で区切ります。 コマンドラインでホワイトスペース文字が重複した場合は区切りとして認識されるだけで、 重複した個数については無視されます。いま、

$ echo I drink coffee

と入力したとします。するとシェルはこのコマンドラインを走査し、実行すべきコマンド名 として行頭から最初の区切り文字までの文字列echoを取り出します。続いて次の 区切り文字までの文字列Iをechoへの最初の引数として取り出します。 同様な操作をコマンドラインの最後まで繰り返しdrinkとcoffeeを第2、 第3引数として取り出します。 コマンドラインの解析が終了するとシェルはここで取り出した3つの引数を echoコマンドに渡しその終了を待ちます。echoコマンドが終了するとプロンプトを表示 してユーザから次のコマンドラインの入力を待ちます。 それではもう一つ、

$ echo Do you        enjoy              minix?

を試してみてください。echoの出力は次のようになったはずです。

Do you enjoy minix?

これは先に説明したように区切り文字が重複していても 区切りとして認識するだけ で重複個数は無視されるために起こります。echoコマンドは受け取っ た引数を1個のスペースで分けてユーザの端末に表示するだけなのです。

この2つの例からコマンドラインを解釈するのはechoなどのシェルによって起動される コマンドではなく、シェル自身だということを理解してください。 シェルにより起動されたechoはシェルから渡された引数を見て仕事をして いるだけで、実際にコマンドラインとして入力された文字列がどのようなものか、あるいは どのような指示がされたかなどは知らないのです。

ちょっとせっかちな話しなのですが、後の例ではシェルはコマンドと引数の分離、実行 だけではなく、もう少し複雑な仕事をしています。それは次項で。

ファイル名の置換(展開)

シェルはコマンドラインを解析し、実行すべきコマンドや引数を決定する前に 特殊文字 * , ? , [ ] を見つけるとその特殊文字部分を ファイル名に置き換えます。 いま、ユーザのカレントディレクトリに次の様なファイルがあると仮定します。

$ ls
beer
carrot
ell
rabbit

ここでechoコマンドを使ってファイル名の置換を試してみましょう。 次のようにタイプしてください。

$ echo *
beer carrot ell rabbit

シェルは与えられたコマンドラインの解析を始めます。そして特殊文字 * を見つ けると、カレントディレクトリ内にあるすべてのファイル名で * 部分置き換えま す。そのあとにシェルは起動すべきコマンド名と渡すべき引数を決定します。

echoは特殊文字 * を解釈することも、存在していいることも分かりません。 この場合、echoは4個の引数がシェルから与えられたことを知っているだけなのです。 このようにシェルはgrepやedなどに比べると制限されているものの、次のような 正規表現を解釈することができます。これらの文字をメタ文字と呼びます。 なお、シェルによるメタ文字の展開はファイル名のみが対象となります。

特殊文字 意 味
* 0個以上の任意文字と一致
? 任意の1文字と一致
[ ] [ と ] で囲まれた文字のいずれかと一致

さて、ここで前項で例とした

$ echo Do you        enjoy              minix?
Do you enjoy minix?

を考えてみてください。このコマンドラインにはメタ文字?があるのに気が 付いたでしょうか。ところがechoの出力を見るとシェルにより置換が行なわれずにその まま表示されています。これはシェルが手抜きをしたわけではありません。 シェルはコマンドラインの解析で?を見つけています。そしてカレントディレク トリにあるファイル名との置換を試みています。しかし、置換条件を満たすファイル が見つからないので置換に失敗してし、入力された文字列を そのままを引数としてechoに渡してしまうので minix? がそのまま表示されたのです。

しかし、場合によってはシェルによる置換が思いもよらない弊害を引き起こすことが あります。このためにシェルによってコマンドラインにあるメタ文字の解釈を禁止す る方法が用意されています。

シェルがコマンドラインを解析する手順をまとめると概ね次のようになります。

  • コマンドラインから特殊文字 * , ? , [ , ] を探す
  • メタ文字が見つかればファイル名で置換を試みる
  • コマンドラインから起動すべきコマンドと引数を取り出しコマンドを起動する

入出力の切り換え(I/O redirection)

シェルは起動したコマンドの入出力先を切り換える機能(I/Oリダイレクト)を持っ ています。シェルはコマンドラインを解釈し、リダイレクトを表す 特殊な文字>、<、>>が見つかるとそれに従った処理 を行います。仮に

$ echo It will rain tomorrow >memo

それに続く語で指定されたファイルに(この場合はmemo)出力先を変更します。 この例ではカレントディレクトリにmemoというファイルを作り、そこにechoの出力 を書き出します。結果としてファイル ./memoに "It will rain tomorrow" が書き込 まれることになります。 もし、この時にファイル ./memoがなければ新たに作られますし、既にある時には 上書きされて古い内容は失われます。

ここで大切なことは、シェルはコマンドラインで指定されたコマンドの 実行を開始する前 にその標準出力を>に続いて指定されたファイルに切り換えているこ とです。 起動されたコマンドはシェルにより標準出力が切り換えられているのことを知らずに、 標準出力に結果を出力しているだけです。 I/Oリダイレクトを表す文字とその意味は次のようなものです。

特殊文字 意  味
[n]<file ディスクリプタn (省略時 0)でfileを読み込み用にオープン
[n]>file ディスクリプタn (省略時 1)でfileを書き込み用にオープン
[n]>>file ディスクリプタn (省略時 1)でfileを追加書き込み用オープン
<<'eof' 標準入力を次行からeofの直前行までとする(here document)
n>&m ディスクリプタnの出力をディスクリプタmに変更
n<&m ディスクリプタmの入力をディスクリプタnに変更
[n]<&- 入力ディスクリプタn (省略時 0)をクローズ
[n]>&- 出力ディスクリプタn (省略時 1)をクローズ

[ ] 内は省略可。ディスクリプタはオープンしたファイル番号で0〜9の数字で表す

ヒアドキュメント(here document)

<<はヒアドキュメントと呼ばれる興味深いメカニズムを提供して くれます。 通常の仕事でコマンドとそれが参照するデ−タが対になることがしばしば 起こります。例えば電話番号簿などでは検索プログラムとデータファイルの2つ が必要になります。ヒアドキュメント機能を利用してシェルスクリプトを書けば これらのファイルを1つにまとめることができ、保守性やディスクの使用効率が 良くなります。

grep $1 <<'END-OF-FILE'
JAL:(03)5489-1111
JAS:(03)3438-1155
ANA:(03)3272-1212
  ...
KLM:(03)3216-0771
END-OF-FILE

この例を理解するにはシェルスクリプトの基礎知識が必要になりますが、簡単 にメカニズムを説明します。grepは文字列検索プログラムで、$1はシェルに よりコマンドラインで指定されたキーワードと置き換えられます。 シェルは<<を見つけると、それに続く文字列END-OF-FILEを EOFマークとして覚えます。 そして、その直後の行から次のEOFマークの直前の行までをコマンドgrepへの入力に 結合します。ヒアドキュメントのEOFマークは1行単位で評価されますからEOFマーク には途中のデータ行に出現しない文字列を選ばなければなりません。

エラー出力を標準出力にマージする

標準出力の記録とエラー履歴を一緒に取るためにコマンドラインで次のような記述 をすると思います。

$ command >foo 2>&1

この動作を追いかけてみます。リダイレクト機能表でも分かるように >& の 働きは、左側に書かれたファイルディスクリプタの出力を右側に書かれたファイルディ スクリプタに変更することです。 シェルは標準入力・標準出力・エラー出力の3つのファイルをオープンしています。 そして、それらをファイルディスクリプタ0、1、2として管理しています。 ですから 2>&1 はファイルディスクリプタ2を1に、つまりエラー出力を標準 出力に変更することになります。さらに、>fooとして標準出力がfooにリダイレク トされていますから、fooには標準出力とエラー出力がマージされたものが書き出され ることになります。

リダイレクトのメカニズムを理解する上で大切なことは、コマンドラインで記述された ものが 右から左の順 に実行されるということです。エラー出力を標準出力にマージする この例では、まず最初に 2>&1 が実行されてから>fooが実行されます。 もし、コマンドラインで

$ command 2>&1 >foo

と書いたならば違った結果になってしまいます。どうなるかはご自分でお試しください。

パイプ機能

シェルはリダイレクト文字や正規表現をコマンドラインから解釈すると同様にパイプ 記号/|も識別します。シェルはパイプ記号/|をコマンドラインに 見つけるとその 前にあるコマンドの標準出力をその後ろにあるコマンドの標準入力に結合させます。 そしてシェルは両方のコマンドを同時に実行させます。次のコマンドラインを例にして 考えてみましょう。

$ who | wc -l

まず、シェルはコマンドラインを解析してwhoとwcの間にあるパイプ記号/|を 見つけます。 次にシェルは最初のコマンドwhoの標準出力をそれに続くコマンドwcの標準入力 と結合させて2つのコマンドの実行を開始します。コマンドwhoはログインしている ユーザのリストを標準出力に書き出します。whoの標準出力はwcの標準入力につながれ ていますから端末にwhoの出力は表示されません。whoと同時に起動されたwcはファイル 名の指定がないので標準入力からの行数を数えますので、whoの処理結果の行数を数え ることになります。 リダイレクトの項目でも触れたので繰り返しになりますが、シェルにより起動された 各々のコマンドは自分の標準入出力が何に割り当てられているかは知りません。

コマンドの区切り文字

シェルのコマンドラインではセミコロン;で区切ることにより複数のコマンド を書くことができます。

コマンドのグルーピング

シェルは幾つかのコマンドをコマンド群をまとめ、あたかも1つのコマンドのように 実行させることができ、これをコマンドのグルーピングと言います。 グルーピングを行なうにはまとめたいコマンド類を小括弧で囲んで ( ... )と するか、あるいは中括弧を使って { ... }のようにします。 この2つはいずれもコマンドのグルーピングを行なう点は同じですが、( ... )は サブシェルで実行され、{ ... }はカレントシェルで実行されます。 この違いを次の例で確認してください。

$ pwd
/usr/try
$ (cd /bin; ls -C)
$ pwd
/usr/try
$ { cd /bin; ls -C; }
$ pwd
/bin

( ... )はサブシェルで実行されるために終了後のカレントディレクトリは 変わりません。一方、{ ... }はカレントシェルで実行されますので終 了後のカレントディレクトリが変わってしまうのが確認できたと思います。

中括弧 { ... }を使ってグルーピングを1行に書くときに、 { の直後と、} の直前には1個のスペースが必要です。 さらに中括弧でグルーピングされたコマンドの末尾にはコマンドの区切りを 表すセミコロン;がなければいけません。これは中括弧がシェルの予約語 であるための制限です。

バックグランド処理

minixはマルチタスクOSですから複数のプログラムを同時に走らせることができます。 しかし、特に指定をしない限りシェルは入力されたコマンドの実行終了を待って次の コマンドを受け付けます。 プリンタへの出力やテープの巻き戻しなどは数分、あるいはもっと時間がかかる かもしれません。これを待っていては何のためのマルチタスクなのか分かりま せんので、シェルにコマンドの終了を待たずに次のコマンドの受付ができるよう にバックグランドで実行するように指示を出すことができます。

入力したコマンドをバックグランドで実行させるためにはコマンドの末尾にアンパーサ ンド/&を付けます。 こうするとシェルはプログラムをバックグランドで実行し、 そのプロセスIDを表示し、すぐにプロンプトを表示してユーザからの次の入力を 受け付け状態になります。ここで表示されるプロセスIDはバックグランドで実行され ているコマンドを識別する唯一のものです。

バックグランドで実行したプログラムはDELキーによって発生する端末割り込みで 中断させることができません。途中で停止させるにはここで表示されたプロセスID(psコ マンドで調べることもできます)を使ってSIGNAL9を送ります。

ユーザの環境設定

シェルはユーザの希望する環境を設定できるいくつかの環境変数と呼ばれるものを持っ ています。これらはユーザのホームディレクトリやコマンド入力を促すためのプロンプ ト文字列、ユーザが実行させたいコマンドを探すためのディレクトリ・リストなどを 記録しています。主な環境変数として次のようなものがあります。

HOME

ユーザがシステムにログインした時に自動的に決定されるユーザの家(HOME)とされる ディレクトリです。この環境変数HOMEはユーザのホームディレクトリを識別するため にプログラムから参照することができます。例えば、引数なしでcdコマンドを実行し た時にこのHOMEが参照され自分の家に迷わず帰ることができます。 もちろんこのHOMEはユーザが好きなものに書き換えることができます。 しかし、不用意に変更してしまうと HOME を参照するコマンドの動作に影響がある ばかりか、他のユーザに迷惑をかけることになりますから、注意してください。

HOMEはログインした時に/etc/passwd中のホームディレクトリ・フィールドに 従って定義されます。

Prompt String(PS1, PS2)

シェルがユーザにコマンドラインの受け付け状態にあることを示すために表示する文字列 は変数PS1に格納されます。ログイン後に表示されている/$がそうです。 コマンド行が複数にまたがり、コマンド入力状態継続していることを表す二次的なプロ ンプト文字列はPS2が示しており、通常は/>が格納されています。 この変数はユーザが自由に書き換えて構いません。

PATH

シェルはユーザからコマンドラインを解析して起動するプログラムを決定したならば そのコマンドを環境変数PATHが示すディレクトリ・リストから探します。 このPATHはログイン時に自動的に設定されます。 PATHを始めとする環境変数はechoコマンドで次のようにすれば、 その内容を見ることができます。

$ echo $PATH
/bin:/usr/bin::

環境変数PATHの前にドル記号/$を置いてやるとシェルはその部分を 環境変数PATHに格納されている内容で置換してからechoに引数として与えます。 この場合は "/bin:/usr/bin::" がコマンド探索用のディレクトリ・リストになります。

PATHの示すディレクトリ・リストはそれぞれのディレクトリをコロン:で区切っ て表されます。コマンドの実行が指示されたならばシェル は"/bin"->"/usr/bin"->""の順にディレクトリから指定 されたコマンドを探します。このディレクトリ・リストの最初の2つは 文字の通りの場所です。3番目の "" はカレントディレクトリ "./" を意味しています。 今まで頻繁に使ってきたechoコマンドは/usr/binにありますから、 コマンドが入力されるたびに シェルはこれらのディレクトリ・リストを参照して探していたわけです。

コマンドラインでコマンド名にパスを含めた指定をするとシェルはPATHの示す ディレクトリ・リストを無視して直接指定されたコマンドをだけを探します。例えば、

$ /bin/date

とすると、シェルはPATHの内容を無視して/bin/dateを実行させます。 このような絶対パスだけでなく、カレントディレクトリからの相対パスを指定しても 同様に環境変数PATHの内容は無視されます。

TERM

環境変数TERMはユーザの端末属性を記憶しています。どのような端末でも10進数 の65というコードを受け取ると"A"という文字を表示します。しかし、1というコード を受け取ると端末によっては強調モードになるかも知れませんし、 別の端末では反転モードになるかも知れず、動作の保証がありません。 スクリーンエディタなどの端末を制御 するプログラムは適切な制御コードを端末に送らないと正しい結果が得られません。 世間には無数の端末がありますからそれぞれに合わせたプログラムを一個一個作るのは 途方もない時間と労力が必要になり不経済です。これを避けるために、minixではたくさ んある端末の制御コードをデータベース化して/etc/termcapというファイルに持ってい ます。この中から自分の使っている端末のエントリを環境変数TERMに格納しておきます。

termcapについては本テキストで触れていません。興味を持たれた方は次章 以降でシェルプログラミング教材としているCabinetから参考文献を探すことが できますので読まれてはいかがでしょう。

.profile によるログイン環境の設定

ここで触れた環境変数を始めとする色々な設定は.profileというファイルに 記述してホームディレクトリに置けばログイン時に自動的に設定されます。 この.profileはログインと同時に一度だけ実行されるシェルスクリプトですから 環境設定以外にもログインした時に実行させたいものを書いておくことができます。

インタプリタ型のプログラミング言語

シェルはその中にインタプリタ型のプログラミング言語を持っています。このプログラ ミング言語は多くのプログラミング言語同様に条件判断やループ処理などの機能を持っ ており、複数のコマンドのまとめ役としてシェル本来の力を出してきます。 このプログラミング言語の理解を深め、minixの一番おいしい部分を楽しむことが このテキストの最大の目的です。

シェルプログラミングの基礎

今まで見てきたように、シェルには1つのコマンドを実行させたり、また複数 のコマンドを組み合わせて実行する機能ばかりか、あるコマンドの行った仕事 結果を見てさらに次のアクションを起こさせたり、他の処理に分岐させたりす ることができます。この機能を実現するために使われるのがシェル・プログラミ ング言語です。

端末に向って仕事をする時間が長いせいかもしれませんが、シェルというと 一般的にヒストリ機能等、対話型の操作性に目を奪われるかも知れません。 しかし、対話性を重視する環境はともすれば人間が機械の忠実な子守役を強いられる 可能性があります。1つのコマンドの結果をみてからそのつど人間が次の仕事を 機械に指示するのは本末転倒で何ともバカバカしいかぎりです。シェルの持つ プログラミング機能はこの人間が行うべき仕事、すなわち1つのコマンドの結果を 見て次に起こすアクションをあらかじめシェルに教えておき、実行時にそれらを 代行させることにあります。

シェル・プログラミング言語(シェル言語)そのものはいたって単純なものですが、 これを使いこなすにはminix上の各コマンドの使い方を理解しなければなり ません。シェルは各々のコマンドを起動し、その結果を文字列として引用しますが、 実際にデータを処理するのはminix上の小さなコマンドです。 データを処理しようと考えた時には数あるコマンドの中からどれを、どのような オプションで、どういう組み合わせで使えば目的を達することができるかを 考えなければなりません。

シェルスクリプトと実行方法

人間の仕事をシェルに代行させるための手順を書いたテキストファイルのことを シェルスクリプトといます。シエルスクリプトに記述できる内容はコマンドライン で受け付けられるものならば何でも書くことができます。 シェルにはパイプに代表されるような複数のコマンドを組み合せる機能や、 あるコマンドの実行結果を見てそれを次のコマンドに引用するためのロジック が組み込まれています。

シェルスクリプトは純粋なテキストファイルですからテキスト エディタで書き起こしたり、修正することができます。そして、そのスクリプトを 実行させるには端末からのシェルへの入力を代行させる意味で

$  sh<script

とシェルの標準入力に流し込む方法が使えます。 また、シェルは引数があるとそれを入力ファイルとして扱いますから

$  sh script

としても構いません。しかし、このようにスクリプトを起動する度にシェルの 引数とするのは面倒ですから、スクリプトに「実行権」を与えて使います。 「実行権」とはminixのファイルシステムに用意されている許可属性の1つで、 この権利が与えられているファイルはコマンドとして直接起動することができます。

テキストファイルとして書かれたスクリプトに実行権を与えるにはchmodコマンドを 使用して、

$ chmod  +x script

とします。このあとはスクリプトを修正してもファイルの実行権が失われることは ありません。

このようにしてコマンドラインからシェルスクリプトを起動すると、 カレントシェルは子プロセスとして もう一つシェルを走らせ、そこでシェルスクリプトを実行します。 この実際にスクリプトを実行するために走るシェルを サブシェル といいます。 子プロセスのサブシェルは親プロセス(カレントシェル)の環境を受け継ぐこと はできますが、その逆はできません。例えば、実行属性を与えたシェルスクリプト を使って環境変数を再設定しようとしても、再設定されるのはサブシェル側の環境変数 であってカレントシェルのものではありません。

現在の環境変数を再設定するにはスクリプトをカレントシェルで実行しなければなりません。 このためシェルにはドットコマンドと呼ばれるものが用意されおり、コマンドライン でドット.の後にスペースを置いて実行したいスクリプト名を書きます。

$  . script

ドット.を先頭に置くことによりカレントシェルはscriptを自分自身で 実行しますので、現在の環境変数を再設定することができます。 なお、ドット.に続くscriptには実行権の必要はありません。

シェル変数

すべてのプログラミング言語と同様にシェル言語でも変数に値を格納したり参照する ことができます。シェル変数は英文字あるいはアンダースコア/_で始まり、 その後ろに0個以上の英数字あるいはアンダースコアの ならび で表します。

シェル変数に値を格納するにはC言語などと同様にシェル変数と格納したい値を 等号=で連結し、

シェル変数=値

とします。この時に等号=の両側にスペース文字を入れてはいけません。 CやPASCALなどのソースを読みやすくするため 演算子の両側にスペースを置く習慣のある人は要注意です。

シェル変数の初期化と参照

シェル言語ではCやPASCAL等のプログラミング言語と異なりデータ型の概念 がなく、すべての変数は文字列として扱われます。使用する変数は前もって宣言す る必要がなく、シェルは新しい変数を見つけるとそれを登録し、ヌル値で初期化し ます。

シェル変数に格納されている値はシェル変数名の前にドル記号 $ を付けること で参照できます。シェルは$の後ろに続く文字列が正しいシェル 変数名ならばその変数に格納されている値と置き換えます。何も格納されていないシェル 変数をいきなり参照した場合には初期値のヌルで置き換えられます。 1つの例を見てみましょう。シェル変数valに "nora" という値(文字列)を格納し、 echoコマンドでこれを表示させるには次のようにします。

$ val=nora
$ echo $val
nora

最後のnoraはechoコマンドの出力です。このようにシェルは$valをその変数に格納さ れた内容と置き換えてからechoに引数として渡します。いま例に引いたvalを使い、 "nora_neko"と表示させようとする時には注意が必要です。

$ echo $val_neko

とするとシェルは "val" ではなく "val_neko" を変数名として処理します。これは、 シェル変数を表す$に英数文字が続いているならば最も長い語句を変数名として 切り出すために起こります。 ここでシェル変数 "val_neko" は未定義ですからヌル値で置き換えられ何も表示されませ ん。 この不都合を回避するには中括弧{,}でシェル変数部分 の範囲を明示してやります。

$ echo ${val}_neko
nora_neko

シェル変数は一般の文字列以外にもメタ文字と呼ばれるシェルにとっては特殊な意味を もつ文字を格納することもできます。次の例を見てください。

$ val=*
$ echo $val

普通の文字列が格納されている時にはシェル変数$valの値を展開するだけでしたが、 今度はもう少し手の込んだ仕事をしてきます。 シェルはコマンドラインに echo $val を受け取る とこれを走査して変数valを見つけ、その内容を変数valに格納されている * で置き換えます。この文字 * はシェルにとっては特別な意味を持つ文字 ですからカレントディレクトリ内の全ファイル名を引数としてechoを起動します。 この3つの例からシェルはどのように変数を評価しているかを理解してください。

ここまでの説明で気が付いたと思いますが、環境変数はシェル変数そのものです。 シェルスクリプト内で使われる一般のシェル変数とは違い、ユーザの 使用環境を整えるために使われるので環境変数と呼ばれています。

エクスポート変数

あるスクリプトから別のスクリプトにシェル変数値を渡したり、ログイン中に実行す るすべてのコマンドから参照させたいシェル変数は export 文で宣言することにより その内容を引き渡すことができます。 export 文の書式は以下の通りです。

export shell_variables

shell_variablesは他のスクリプトに引き渡したいシェル変数名のリストで、 スペースで区切ってならべます。

ユーザがログイン完了後に走る最初のシェルを ログインシェル と言い、すべての 親シェルとなります。ここでユーザが何かスクリプトを実行させるとログインシェル はこのために新しいシェルを走らせます。シェルスクリプトの実行方法を思い出して ください。

$ sh script

これはシェルのコマンドラインでscriptを引数としてもう一つのシェルを走ら せていることをにほかなりません。シェルスクリプトに実行属性を持たせて起動して も内部ではこれとまったく同じ手順で実行されています。 親シェルはこの時に自分のシェル変数の中でexportされているものだけを サブシェル側 でも参照できるようにコピーします。このシェル変数はエクスポート(export) 変数とも呼ばれ、その場所から実行されるサブシェルに対して伝えられていきます。 export宣言されなかったものはローカル変数として子孫には伝えられません。

もし、エクスポート変数がサブシェル側で書き換えられたならば、その影響はコピー されていく子孫側にだけ及び、親となったシェル側のシェル変数内容を変更することは 決してありません 親シェル側からすれば出て行くexportであって 入ってくるimportではありません。

readonly変数

間違って書き換えては困るシェル変数にはreadonly文で読み出し専用 属性を与え、保護することができます。 readonly文は引数として与えられたシェル変数を読み出し専用属性にします。 一般書式は次のようになります。

readonly shell_variables

読み出し専用属性を設定されたシェル変数を書き換えようとするとエラーメッセージ が表示されます。シェル変数をいったんread~onlyとしてしまうと再び元の状態 に戻す方法はありません。

なお、現在のシェルでエクスポート変数にread only属性を与えてもサブシェルに 渡されるのはその変数値だけであり、read only属性は渡されません。

引用符

シェルはコマンドラインにメタ記号などの特殊文字を見つけると展開や置換を 行います。 これはシェルの持つ便利な機能ですが、時としてユーザが期待しないことをやって しまいます。次の例をみてください。

$ echo * means all files in the directory.

echo コマンドで「* means all files in the directory.」とメッセージを出力 たいだけなのですが、シェルは自分の仕事を忠実に実行し * をカレント ディレクトリのファイル名で置換してしまいます。さらに * と「means」の間 にある2つのスペースも区切りとしては認識されますが、個数は無視されています。

期待通りの表示をさせるためにはコマンドラインをシェルの置換機能から保護しな ければなりません。そのために引用符を用います。シェルにはそれぞれ異なった 働きをする単一引用符 '、二重引用符 "、逆引用符 `の3つが 用意されており必ず対で使われます。

単一引用符 '

シェルは最初の単一引用符'を見つけるとそれを閉じる単一引用符'に 出会うまですべての特殊文字を無視します。先の例では

$ echo ' *  means all files in the directory.'

とすれば期待通りのメッセージが表示されます。

二重引用符 "

単一引用符と同様にほとんどの特殊文字を無視します。しかし二重引用符の中で あってもドル記号 $ 、逆引用符 ` 、 バックスラッシュ \ の3つ については認識されます。ドル記号 $ が解釈されますので二重引用符内では シェル変数の置換が行われます。次の2つの例は引用符で囲まれた文字列はまったく 同じものですが、囲んでいる引用符が一方は単一引用符で他方は二重引用符です。 実際に試して2つ違いを確かめてください。

$ echo ' $val means all files in the directory.'
$ echo " $val means all files in the directory."

バックスラッシュ / はその直後に続く一文字の特殊な意味を取り除く 時に使われます。二重引用符内ではシェル変数の置換が行われことを説明しましたが、

$ echo " \$val means all files in the directory."

とするとしてしまうと結果は大きく変わってしまいます。シェルは バックスラッシュに続く$を無視しますので $val はシェル変数ではなく 単なる文字列としてなってしまい、変数の置換は行われません。

逆引用符 `

単一引用符、二重引用符はコマンドラインの文字列をシェルから保護する働きが ありますが、逆引用符`はこれで囲まれたコマンドを実行し、その結果を 文字列として引用する機能を持っています。 言い替えるならば逆引用符に囲まれた文字列をみつけるとそれをコマンド として実行し、そのコマンドからの標準出力で逆引用符部分を置き換えます。

$ echo "The date & time is:~~`date` "

はその一例です。シェルはコマンドラインを走査しその中に date を見つけて dateコマンドを実行します。そして、その出力でコマンドラインの date 部分 を置き換えます。あとは今まで説明したようにしてechoコマンドを走らせます。

逆引用符内で実行できるコマンドは1つとは限りません。セミコロン;で 区切れば複数のコマンドを書くことができますし、( )を使ってのグルーピング やパイプを使って、

$ echo " `ls | wc -l` files in your directory."

などということもできます。

特殊なシェル変数

IFS

この変数にはシェルがコマンドラインを走査するときの区切り文字のリストが格納 されています。通常はホワイトスペース文字と呼ばれるスペース、タブ、改行文字 がこれにあたります。

このIFSの内容はユーザの任意の文字に変更することができますから、ホワイトスペー ス文字以外で区切られた1行から文字列を切り出す時に有効に働きます。このため、 IFSには1つのレコードを構成するそれぞれのフィールドを区切る 文字が格納されていると考えた方がより現実的です。 ちなみにIFSとは Internal Field Separatorの略です。

ここでちょっとIFSの内容を見てみましょう。IFSはシェル変数ですから

$ echo $IFS

とするだけで見れそうですが、次の1行が空くだけで何も表示されません。 これはIFSの内容が空白文字としてスペース、タブ、改行から成っているためです。 ちょっと工夫をして、次のようにすれば内容を見ることができます。

$ echo -n "$IFS" | od -b
0000000 040 011 012
0000003

これはechoの出力をodコマンドにパイプでつなぎIFSの内容の8進数ダンプしたもの です。先頭の数字は入力ファイルの先頭からのオフセット番地で、 それに続く 040 011 012 がIFSの内容となります。

$#

シェルスクリプトが実行されるとシェル変数 $# にはコマンドライン に与えられた引数の個数が格納されます。ユーザが入力した引数の数が正しいかを調 べたり、引数の数を見て処理を分岐させるときなどに利用します。

位置パラメータ($1〜$9, $0) と shift

シェルスクリプトも他のコマンド同様に引数を受け取るとができます。 シェルはコマンドラインを処理した後でこのために用意された特殊なシェル変数に 与えられた引数値を格納してからスクリプトを実行します。これらの特殊なシェル変数 は位置パラメータと呼ばれドル記号 $ に続く1文字の数字で表現します。 $1, $2 ... $9はそれぞれ第1、第2、... 第9引数に対応します。

位置パラメータはドル記号 $ に続く1個の数字で表されますので9個を超える 引数を直接参照することはできません。もし10個目、あるいはそれ以降の 位置パラメータを参照するにはshiftマンドを使います。 shiftコマンドを実行すると位置パラメータの内容が左に1つシフトし、 $1 に $2 の内容が、$2 には $3 の内容が、... と位置パラメータの内容が順次 左に送られ、$9 にいままで隠れていた第10引数の内容が入ります。 この時に引数の数を表すシェル変数 $# の内容も1つ減少します。

shiftコマンドで位置パラメータをシフトさせると古い $1 の内容は失われてしまい ます。もしその後の処理に古い $1 が必要ならばshiftを使う前に退避しておか なければなりません。

また、shiftコマンドに引数nを与えることにより一度にn回のシフト させることもできます。一般書式は次のようになります。

shift n

引数の数($#)がゼロになりこれ以上シフトできなくなった場合には エラーメッセージ "nothing to shift" が返されます。

$0 にはプログラム名(スクリプト名)が格納されています。これを使えば/usr/bin/compress の ように zcat にリンク張り、コマンド名によって処理内容を分けることもできます。

$*

シェル変数 $* はシェルスクリプトが受け取った $0 以外のすべての引数 に対応します。不特定数の引数を処理する時に利用します。

$@

$* と同じくスクリプトが受け取ったすべての引数に対応します。$* と の違いは二重引用符で囲んで "$@" としたときに位置パラメータの評価をせずに コマンドに渡すことです。もし、二重引用符で囲まなければ $* と同じ意味 になります。

$?

シェル変数 $? はシェルが最後に実行したコマンドの終了状態を保持してい います。 直前に実行したプログラムの終了状態を知りたいときに使います。

$$

シェル変数 $$ は現在のシェルのプロセス番号を保持しています。シェルス クリプト内で一時作業ファイルを作る時に利用します。 すべてのプロセスは重複しない固有の番号で管理 されていますから一時作業ファイル(テンポラリファイル)にプロセス番号を利用 すると他のプロセスが使ってかも知れない作業ファイルとの衝突を避けることがで きます。

$−

シェルにセットされているオプションを保持しています。

$!

バックグラウンドで実行された直前のプロセスのプロセス番号を保持しています。

set コマンド

setはシェルの内部コマンドです。シェルのオプションを設定/解除する機能だ けでなく、1つのレコードからシェル変数IFSで区切られたフィールド取り出し、 それを位置パラメータに代入する機能も持っています。

setによるシェルオプションの設定/解除機能は起動時にオプションをコマンド ラインで指定したのと等価です。 さらにスクリプト内の任意の位置でオプションの設定や解除ができますので スクリプトをデバッグするときに必要な部分だけの実行状態を監視することも できます。

位置パラメータへの代入機能はsetの引数として与えられた文字列(レコード)から IFSを区切としてそれぞれのフィールドを切り出し、$1 〜 $9 ... に 格納します。この機能はユーザが位置パラメータに値を代入できる唯一の方法で、 シェルスクリプト内で多用されます。

$  set `date` ; echo $1

を実行してみると メカニズムが良く分かると思います。 dateコマンドの出力がsetに渡されています。シェルのIFSはホワイト スペース文字ですからdateコマンドの出力をスペースで区切って位置パラメータに 代入します。 dateコマンドの出力の第1フィールドは曜日を表していますので、それが位置パラ メータ $1 に抜き出されているのがechoの出力で分かります。 また、この時に位置パラメータの数を表すシェル変数 $# にはsetコマンド で分解されたフィールド数が格納されています。これもechoで確認してみてください。 この代入が行われると古い位置パラメータの値は永久に失われてしまいます。 もし、あとで必要なものがあればユーザの責任で事前に退避しておかなければなりません。

setコマンドに引数を与えずに使うと、ユーザの環境内に存在するすべての変数 が表示されます。

シェルの構文

構文規則

シェルプログラム言語はminixの単純なコマンドを組み合せるための制御機構を備え ており、対話形式のコマンドラインで可能なことはすべて書くことができます。

1つのコマンドは改行文字(0x0a)、あるいはセミコロン;で終結します。 そして、if,while,do,done, ... など のシェルの予約語は必ず行の先頭になければなりません。 行の先頭とは改行文字の直後のことを言いますが、 セミコロンやパイプ文字の直後も行の先頭に含まれます。

注釈文

どんなプログラミング言語にも必ず用意されているのが注釈文(コメント文)です。 プログラムの保守を容易にし可読性を高めるために注意点やメモを挿入するのに 使われ、シェルスクリプトの実行には影響を及ぼしません。 シェル言語では # で始まる文字以降から行末までが注釈文とみなされます。 行頭から始まる場合はその行全体がコメント行として実行時に無視されます。

また、シェルスクリプト内には何も書かない空白行も許されます。コメント文と 組み合わせて適切に使用すると後日のデバッグや保守が容易になります。

入力文

read文

read文は標準入力から1行を読み込み、引数として与えられたシェル変数 リストに順次代入します。

read val-list

この時に、読み込む行の先頭にあるIFSで指定された 空白文字は無視されます。 標準入力が端末のキーボードならば、スクリプト内でユーザからの1行入力に使 うことができます。リダイレクトやパイプなどにより標準入力が切り換えられている ならばそこから1行を読み込むことになります。

readの引数がシェル変数リストならばIFSで区切られた語句がそれぞれに 代入されます。もし、読み込んだ1行から分解した語句の数よりもシェル変数名リスト val-list に列記された数が少なければあふれた語句は変数名リストの最後に書かれたものに まとめて代入されます。

read文の終了状態はEOFを検出しない限りゼロ(真)です。

条件判断と分岐

if文

シェル言語もほとんどのプログラム言語と同様に条件判断のための if 文を持って います。if文は1つ以上の条件をテストし、その結果に基づいてプログラムの流れを 分岐させます。if文の一般書式は次の通りです。

if cond
then
  commands
    ...
else
    commands
    ...
fi

あるいは

if cond
then
    commands
    ...
elif cond
then
    commands
    ...
else
    commands
    ...
fi

この文ではcondの位置に書かれたコマンドの終了状態(実行結果)を調べて 終了状態がゼロ(真)の場合にはthen節が、そうでない場合にはelse節が実行されます。 もし必要がなければelse節は省略することができますが、if文の終了を表す予約語 fiは省くことができません。なお、後者の書式ではelif節のネストができます。

終了状態がゼロ(真)の時にthen節が実行されることは他のプログラミング言語 からすれば逆の印象を受けるかもしれません。minixでは、あるコマンドが終了 するとその終了状態(exit status)を表す数値がシステムに返されます。 これはそのプログラムが正常に実行/終了できたかどうかを示すもので、 正常に終了した場合はゼロ(真)が返されます。もし、終了状態が非ゼロ(偽) ならば何らか の原因で、例えば引数の数が適切でなかったとか、プログラムがエラーを検出したと か ... 等々、異常が起きたと考えられます。ですからコマンドが正常に終了した ならばthen節に分岐、と考えれば受け入れやすいと思います。

testコマンド

初めてminixに接した時、使用目的にとまどうコマンドの1つです。 これはシェル内部に組み込まれたものではなく、 minixの一般コマンドの1つ ですが、シェルスクリプトで条件判断を行なう上で避けて通れませんから少し説明して おきます。

testコマンドは与えられた引数を条件式として評価し、その結果が真の時は 終了状態にゼロ(真)を、偽の場合はゼロ以外の値を返します。 testコマンドは下記のような文字列、整数、ファイル状態等について多くの 条件式を評価をすることができます。さらにそれらの論理演算を組み合わせてより 複雑な評価もできます。ただし、評価のショートカット は行われません。

文字列評価式: |str1 = str2|文字列str1とstr2は一致する| |str1!=str2|文字列str1とstr2は一致しない| |-n str|文字列strは空(null)でない| |-z str|文字列strは空(null)|

数値評価式: |int1 -eq int2|整数int1とint2は等しい   (int1 == int2)| |int1 -ge int2|整数int1はint2以上である  (int1 >= int2)| |int1 -gt int2|整数int1はint2よりも大きい (int1 > int2)| |int1 -le int2|整数int1はint2以下である  (int1 <= int2)| |int1 -lt int2|整数int1はint2よりも小さい (int1 < int2)| |int1 -ne int2|整数int1とint2は等しくない (int1 != int2)|

ファイル評価式: |-d file|fileはディレクトリである| |-f file|fileは通常ファイルである| |-r file|fileは読み出し可能である| |-s file|fileの長さは0バイトではない| |-w file|fileは書き込み可能である| |-x file|fileは実行可能である|

論理演算子: |!|直後に続く条件評価式の結果を否定する| |-a|2つの条件評価式の論理積(and)をとる| |-o|2つの条件評価式の論理和(or)をとる|

: コマンド

コロン:で表されるこのコマンドは引数を評価するだけで終了状態にゼロ(真)を 返します。何もしないコマンドは存在価値も無いと思われそうですが、シェル言語に とっては「何もしない」コマンドの必要性がしばしばあります。次の例にあるif文 やwhile文と組み合せた使い方はその典型です。

if cond ; then
    :
else
    commands
fi

while  :
do
    commands
done

また、:コマンドでは引数の評価も行なわれますのでシェル変数を検査させるため にも使われます。

&&と||

&& と || は左側に置かれたコマンドの実行結果を見て右側に置かれた コマンドを実行させるもので、&&はcommand1の終了状態が ゼロ(真)の時にcommand2を実行します。 一方、|| はcommand1の終了状態が非ゼロ(偽)の 時にcommand2を実行します。終了状態は最後に実行されたコマンドの ものになります。 それぞれの一般書式とif文で書いた等価式は次のようになります。

command1 && command2 -> if  command1; then command2; fi
command1 || command2 -> if ! command1; then command2; fi

case文

case文は1つのシェル変数値を評価し、同じパターンが見つかったならば1つあるい はそれ以上のコマンドを実行させます。一般書式は次のようになります。

case $val in
    pat1 ) commands
              ;;
    pat2 ) commands
              ;;
             ...
    patn ) commands
              ;;
 esac

ここでの処理はシェル変数valの値とpat1,pat2, ..., patnと連続して比較し、一致したものが見つかるとそこから2個の連続した セミコロン;;まで間に書かれたコマンド群を実行します。もし、一致するものが 見つからなかった場合は何もしません。最後のeascはcase文の終りを意味し 省略することはできません。

シェルでファイル名の置き換えに用いられるメタ文字もcase文の 比較対象(patn)として使うことができます。 また、シェルではパイプ記号として用いられる垂直バー | を使うと 複数パターンの論理和(or)を取ることができます。

ループ制御

for文

一組のコマンドを指定された回数だけ実行するのに使われ、 一般書式は次のようになっています。

for val in arg1 arg2 ... argn
do
    commands
    ...
done

doとdoneに囲まれたコマンド群がループの本体で、 これらのコマンド群はinの後ろに並べられた引数の数だけ繰返し実行されます。 このループが実行されると最初にarg1の内容がvalに、次にarg2 の内容がvalに...、と順次が参照されながらinに続くリストの中身が 空になるまでループ処理が継続します。つまり、inの後ろにn個の引数があった ならばn回ループが実行されることになります。 inに続く引数に*, ?, [ ]など のメタ文字が含まれる場合にはシェルにより展開されてからforループが実行されます。

シェル変数 $* はスクリプトに渡されたすべての引数に対応するためにfor文と 組み合わせて使うことができます。しかし、実際の使用に当っては少し注意が必要です。 次に示すものはコマンドラインに入力された引数を1行に1個づつ表示させるものです が、時として期待を裏切ります。

echo "Number of arguments: $#"
for i in $*
do
    echo $i
done

このスクリプトをfor.shとして実行してみましょう。まずは、

$ for.sh A B C
Number of argumets: 3
A
B
C

これは正常です。ではつぎにAとBを引用符で囲んでみましょう。

$ for.sh 'A B' C
Number of argumets: 2
A
B
C

さて、不思議なことが起こるものです。単一引用符は文字列をシェルから保 護しますから引数は2つ。事実、与えられた引数の数を示すシェル変数 $# は 間違いなく2と表示されています。なのになぜか3行に出力されてしまいました。

種明かしはこうです。'A B' はシェルから保護されて1つの引数として スクリプトに渡されます。しかし、保護されるのはコマンドラインでのことであり、 引数としてスクリプトに渡った時には単一引用符が外れています。ですから、 for文で $* が展開された時、in以降のリストにはAとBを1つにしている単一引 用符がありませんので、

for i in A B C

となってしまい、ループは3回実行されることになります。 これを回避するためにfor文にはin以降のリストを持たない特別な記述が許されてい ます。for.sh2行目のin以降を省略し、

echo "Number of arguments: $#"
for i
do
    echo $i
done

としたものです。これで試してみると

$ for.sh 'A B' C
Number of argumets: 2
A B
C

となるはずです。さらに、特殊なシェル変数 $@ を使うもの1つの方法です。 シェル変数 $* はコマンドラインでの引用符が外された状態の引数を $1, $2, $3 として持っています。この $* の代わりに "$@" を使う と "$1", "$2", "$3" として置き換えられるために $* で のような不都合は起こりません。 ただし、二重引用符で囲み "$@" としなければ $* とまったく同じよう に展開されてしまいます。

while文

ある条件が満たされている間ループを実行します。一般書式は次の通りです。

while cond
do
    commands
    ...
done

まず最初にcondコマンドが実行されその終了状態がテストされます。 もし終了状態がゼロ(真)ならばdoとdoneで囲まれたコマンド群を実行します。 そして、もう一度condが実行されて終了状態が検査されます。 もしゼロ(真)ならば再びdo〜doneのコマンド群が実行され、 非ゼロ(偽)ならばdoneの次のステップ に進みます。 for.shと同じ働きをwhile文で実現した例を示します。 ループ条件の判断にはtestコマンドを使って引数の数を検査しています。

echo "Number of arguments: $#"
while test $# -gt 0
do
    echo $1
    shift
done

until文

until文はwhile文とは反対に終了状態が非ゼロ(偽)である間ループが 実行されます。 期待する現象が起こるのを待って処理を行う場合などに使われ、一般書式は次 のようになります。

untilf cond
do
    commands
    ...
done

minixのようなマルチタスクOSで複数のプロセスが1つのファイルを共有する 時には排他制御をしなければなりません。 このためのロックファイルをスクリプトで作るときに次のような使い方をします。

until (ロックファイルを作る)
do
    sleep 30
done

untilの条件文で排他制御のためのロックファイルを作ろうとします。 既にロックファイルが存在していたならば、そのファイルは他のプロセスで使用中で すから作成に失敗して非ゼロ(偽)が返されます。ファイルへのアクセス権が得られない ならばdo〜doneが実行されます。この例では30秒間隔で再試行を 行ない、ロックファイルが作れたならば次の処理に進みます。

break文

ループ処理をしている時に、ある状態になったならばすぐにそのループから 脱出したい時があります。シェル言語ではこのためにbreak文が用意されています。 break文が実行されると制御はただちにそのループの外に移り、 doneの次のステップから実行されます。さらに、ループがネストしており、 複数のループを一気に脱出したい時にはbreakに引数として脱出したいループの数を 指定します。一般書式は次の通りです。

 break n

引数 nが省略された場合には1として、最も内側のループから脱出します。

continue文

continueはある条件が満たされているときなどにループ内のコマンド群をスキップ するために使われます。continue文はそれ以降のコマンド群をスキップするだけで ループは継続条件が満さたれている限り続けられます。break文と同様に引数を つけることによりn番目のループから実行させることができます。 一般書式は次の通りです。

continue n

ループ文のリダイレクト、パイプとバックグランドでの実行

for, while, untilのループ制御文はdo〜doneとで囲ま れるコマンド群を1つのコマンドのように実行しますのでループ全体の入出力をリダイ レクトしたり、パイプに接続することができます。バックグランドでの実行もループ全体 が対象となります。

次のリストはfor文の標準出力をリダイレクトする例です。

for i in beer carrot ell
do      echo $i
done >food

ループ全体が1つのコマンドとして扱われますからリダイレクト文字 > はループの終了を表すdoneの後書きます。この時のリダイレクト対象はループ 内で標準出力に書き出すものすべてが対象となります。 しかし、ループ内でリダイレクト先が明示されているものはループ全体の ものに優先してされます。例えば、上記のリストで

for i in beer carrot ell
do      echo "I like $i" >/dev/tty
echo $i
done >food

としたならば、3行目の出力は done >food に優先して /dev/tty に リダイレクトされます。(この場合には端末です)

同様に下記のようにパイプ記号 | を doneに続けて書くことにより ループの出力をパイプに流し込むこともできます。

for i in beer carrot ell
do      echo $i
done | food

ループ処理全体をバックグランドで実行させるにはループの終了を示す doneの後ろにバックグランドへ送る指示のアンパーサンド & を付けます。 例えば複数のソースからなるプログラムリストをバックグランドで連続紙に印刷 するには次のようにします。

for i in *.[hc]
do      pr -l66 -w132 $i | lpr
done &

forやwhile文などのリダイレクト処理はカレントシェルではなく、 サブシェルで実行される ことに注意してください。 このことを知らないとバグでもないのにおかしな現象に悩まされることになります。 次のスクリプトは自分自身を行番号付きで表示するもので、 仮に "myself" と名付けておきましょう。

n=0
while read line
do      n=`expr $n + 1`
echo "$n: $line"
done < $0
echo "total line= $n"@

これをmyselfとして実行してみてください。各行の先頭にふられた行番号は 正しく表示されていますが、最後に total line= 0 と表示され期待した結果が得ら れません。種明しをすると、ループ文の内側で行番号を表示するために 用いられているカウンタ n はサブシェル側の変数で、ループの外側にある変数 n はカレントシェルのものです。(このmyselfを起動したシェルから見ると 子、孫の関係になります)同じ名称の変数でもループの内と 外ではまったく別物ですから、 while ループの外側にある変数 n には スクリプトの最初で初期化されたままになっています。

ここで混同しないで頂きたいのですが、ループ文がサブシェルで実行されるのは スクリプト内 でリダイレクト処理を指定したときだけです。 リダイレクトを指定しないループ文はカレントシェルで実行されます。スクリプトmyself を次のように書き直してみてください。

n=0
while read line
do      n=`expr $n + 1`
echo "$n: $line"
done
echo "total line: $n"

そして、コマンドラインから "myself < myself " とすると行数を数える 変数nは期待する値を取ることで確認できます。さらに、入力をパイプから 読み込むよう、次のように書き直して変数nの値を調べてみてください。

n=0
cat $0 | while read line
do      n=`expr $n + 1`
echo "$n: $line"
done
echo "total line: $n"

算術演算

シェルプログラミングで算術演算を行うには次のような書式でexprコマンドに 算術式を引数として与えます。

expr val1 算術演算子 val2

この算術演算子には以下のようなものが使えます。

算術演算子 意   味
(arguments) 括弧で囲んで優先順位を明示する
str : regexp 文字列 str と正規表現 regexp を比較する
val1 * val2 val1とval2の積
val1 / val2 val1 ÷ val2の商
val1 % val2 val1 ÷ val2の余り
val1 + val2 val1とval2の和
val1 - val2 val1とval2の差
val1 op val2 opに <, <=, =, != >=, >を用いて比較演算を行ない条件成立ならば1を、不成立ならば0を返す
val1 & val2 val1, val2共に0でなければval1の値を、それ以外は0を返す
val1 val2

一致演算子str : regexpは左側の文字列strと右側の正規表現 regexpを比較して一致した文字数を返します。(もし一致なかったならばゼロが 返ります)この時の正規表現には ed と同じ記法が使えますし、edと 同じ記憶メカニズム

(正規表現)

を使ってstrの中から正規表現に一致した部分を抜き出すこともできます。

実行制御

exit文

シェルはスクリプトの最後(End OF File)に達すると自動的に終了しますが、 exitを使うことにより任意の位置で実行を終了させることができます。 一般書式は次の通りです。

exit n

引数nではシステムに返す終了状態を指定します。通常、コマンドが正常に終了 した時はゼロを返す慣わしになっています。もしnが省略された場合には exitの直前に実行されたコマンドの終了状態が返されます。

なお、ユーザ端末からコマンドラインでexitを実行させると現在のシェル を終了させることになります。もし、それがログインシェルならばログオフと同じ結果 となります。

exec文

シェルに代わって引数で指定されたコマンドを実行しますが、新しいプロセスは 作られません。書式は次の通りです。

exec argument

シェルはexecの実行にあたり現在のファイルをクローズし、新しいファイル をオープンしますのでそれ以降の入出力を引数で指定したものに切り換えることが できます。この機能を使ってexec文以降の標準入力をfileに切り換え たいならば

exec < file

とします。同様に標準出力やエラー出力をfileに切り換えたいのならば次の ようにします。

exec > file
exec 2> file

execの引数に普通のコマンドが交じっていたならば、それが実行される だけです。

eval文

引数をシェルの入力として解析してからコマンドとして実行します。引数を実行する 前にコマンドラインの解析が1度行なわれますから、結果としてコマンドラインを 2回走査させることができます。書式は次の通りです。

eval argument

wait文

ユーザが実行している子プロセスの終了を待ち、終了状態を保存します。 書式は次の通りです。

wait n

引数nには待ちたい子プロセスの識別番号(プロセスID)を指定します。 nを省略するとその時点で走っているすべての子プロセスの終了を待つこと になります。なお、このコマンド自体の終了状態は待っていたプロセスの終了状態 そのものです。

バックグランドで実行させた子プロセスのIDを知るには シェル変数 $! を参照します。

trap文

シェルスクリプトを書くときには実行中になんらかの原因で停止することも念頭に 置かなければなりません。停止する要因としてはユーザのDELキーよる割り込みとか、 異常終了、シグナル9が送られたとか、さまざまなものが考えられます。

この時にただちにシェルスクリプトを終了させてしまうとまずい場合があります。 例えば、一時作業ファイルを作って処理をしていて後始末をせずに終了して しまったのでは作業ファイルがディスクのゴミとして残ってしまいます。また、排他 制御のためのロックファイルを削除せずに終了したならば困ったことになります。 このような不都合を回避するためにtrap命令を使います。

trapはあるシグナルを受信した時になすべき仕事を指定するのに使い、書式 は次のようになっています。

trap command signal

commandはsignalに指定されたシグナルリストのいずれかを受信したときに 実行されるコマンドです。 signalを受信したときに実行するコマンドが2つ以上の 場合にはそれらを引用符で囲まなければなりません。signalは一連の番号で表され、 minixで使われる主なものは次のようなものです。

signal 意  味
0 シェルから脱出するときに必ず発生する
1 ハングアップ。通常、回線のキャリアが切断すると発生する
2 DELキーが押されたときに発生する端末割り込み
3 クイットシグナル。プロセスを停止させコアダンプを行なう
9 キルシグナル。すべてのプロセスで無視も受信もできない
15 kill コマンドにより発生するシグナル

次のリストはtrap文を使って一時作業ファイルを削除し、スクリプトを終了 させる例です。

prog=`basename $0`
tmpfile="/tmp/${prog}$$"
trap 'rm -f $tmpfile;exit 1'  2
any-command > $tmpfile
    ...

一時作業ファイル名には他のプロセスとの衝突を避けるためにプロセスIDを含めた ものが使われます。ここでもその慣例に従ってシェルスクリプト名とプロセスIDが 格納されているシェル変数 $$ で一時作業ファイルを作っています。 そしてtrap文です。ここではシグナル2を受け取ったときに 'rm $tmpfile;exit 1'が実行されるように設定します。なお、trapの 設定は一時作業ファイルが作られる前に行なわなければ意味がありません。

シグナルを無視したい時には実行すべきコマンドを書く部分を引用符で囲んで「ヌル」 にします。

trap '' signal

しかし、「コマンドを書かなければ無視される」と早合点しtrap文の第一引数 を省略して

trap signal

としてはいけません。このようにするとsignalを受け付けたときの処理をデフォ ルトに再設定してしまいます。例えばシグナル2ならばシェルスクリプトを停止させる 標準処理を行ないます。

なお、ユーザがあるシグナルを無視するように設定すると、そこから起動される サブシェルも(シェルスクリプト)そのシグナルを無視します。 しかし、あるシグナルを受信したならば特定の処理を行うように設定した場合には、 その処理内容はサブシェル側にはいっさい伝わらず、該当シグナルに対して既定の 処理を行うだけです。

シェルスクリプトのデバッグ

シェルはさまざまな実行環境を作り出すために幾つかのオプションを持っています。 スクリプトのデバッグ用としては -v、 -x の2つオプションを使うこと ができます。

-v はスクリプトからコマンドを1行読むごとに表示させるためのオプションで、 構文のチェックに利用できます。オプションを設定するにはシェルスクリプト内に

set -v

の1行を追加するか、あるいは次のようにしてシェルスクリプト走らせます。

sh -v script

-x オプションはコマンドを実行するたびにそのコマンド名と 引数を + に続いて表示するものです。 引数はシェルにより展開されたものが表示されますので 実行状態をトレースすることができます。 これらのオプションをまとめて、あるいは設定されて いるものだけを解除するには

set -

とします。もし、個別に解除したければ

set +v

などとします。なお、現時点でシェルに設定されているオプションを調べたい場合に はシェル変数 $- を参照します。

実際に使われることは少ないですが -n, -u なども役に立つかも知れません。 -nは読み込んだコマンドの実行を禁止させるためのオプションです。 予想もしないバグで大切なファイルを削除してしまうなどの被害を未然に防ぐことが できます。カレントシェルで set -n とするとEOF(End Of File)を入力するまでそ の端末からの操作ができなくなります。

-uオプションを設定すると値が格納されていないシェル変数を参照 しようとした時に unset variable のエラーメッセージが表示されスクリプトが 停止します。

それといまさらでしょうが、ソースデバッグもお忘れなく。結局、これに勝るデバッ グ手法はないようです。

シェルスクリプトの実例

短いシェルスクリプトを例に取りながら今までの復習をしてみましょう。

挨拶

きちんと挨拶されると気持ちがいいものです。そこでminixにもloginしたとき、 ご主人様にちゃんとご挨拶ができるように教えることにしましょう。

#!/bin/sh

set `date`
IFS=:
set $4

case $1 in
0[6-9] | 1[0-1] )
echo "Good morning. Sir"
;;
1[2-7] )
echo "Good afternoon. Sir"
;;
1[89] | 2[01] )
echo "Good evning. Sir"
;;
* )
echo "GO TO Bed!! :-)"
;;
esac

解説

1行目はminix上ではコメントとしての役割しかありませんが、unixの csh では #! に 続いて指定されたシェルでそのスクリプトを実行します。Bシェルは .*nix の標準シェル ですからそのまま他の環境に持ち込んでも走らせるオマジナイです。

ご挨拶スクリプトは最初にdateコマンドの出力をsetの引数として与え、スペースで 区切られたフィールドを切り出します。続いて IFS をコロンに設定して、いま切り出した 第4フィールドをさらに分割します。ここまでの操作でdateコマンドの出力から時間 を表す部分だけを取り出します。

そして、その時間を見て表示する挨拶メッセージを case 文で選択します。 「おはよう」「こんにちは」「こんばんわ」のどれも言えないような時間帯には あなたの健康を気づかって「寝た方がいいんじゃない?」となります。

このスクリプトをあなたの .profile に書いておくとloginした直後に挨拶メッセージ を見ることができます。

演習問題

さて、このスクリプトで挨拶メッセージを表示する時にあなたのログイン名を用いて "Good morning. taroh" などと表示させるにはどのように修正すればよいか考えて みてください。(メッセージ内にログイン名を直接埋め込むのは論外です)

配列(表引き)

Bシェルには配列操作のコマンドが組み込まれていませんが、シェル変数とコマンドを 上手に操ることにより配列に格納されている値の参照と等価な仕事を実現できます。

#!/bin/sh

@exec 3<&0 <month.name
i=0
while read k month
do
    i=`expr $i + 1`
    eval M_$k='"$month"'
done
exec 0<&3 3<&-
while test $i -gt 0
do
    eval echo $i -- $M_$i
    i=`expr $i - 1`
done

|ファイル month.name の内容| || |3 March| |1 January| |11 November| |2 Feburuary| |4 April| |12 December| |6 June| |8 August| |9 Sepember| |7 July| |10 October| |5 May|

解説

このスクリプトは大きく分けて配列として扱うシェル変数へ別ファイルから読み込んだ データをセットする部分(5〜9行目)と、それを表示する部分(11〜15行目)から成って います。ここでは12カ月の各月に対応する名称を書いた month.name というファイル用 意して使うことにします。

このスクリプトは最初に read 文を使いファイルからデータを読み込みますが、 この時に5行目から始まる while 文の入力を month.name にリダイレクトして

while read k month
do
    ......
done < month.name

としてはいけません。 理由はループのリダイレクトで 説明したように while 文はサブシェルで実行されるためです。 このためループ内のシェル変数はループの外側では参照できません。そこで3行目 の処理となるわけです。 exec とリダイレクトとを併せて考えてくださ い。3行目の exec 3<&0 <month.name はまずファイルディスクリプタ0の 入力をファイルディスクリプタ3に変更します。ファイルディスクリプタ0はシェルの標 準入力ですからこの処理で標準入力がファイルディスクリプタ3に保存されることにな ります。続いてファイルディスクリプタ0で month.name を読み込み用にオープンしま すので、それ以降の標準入力はファイル month.name となります。 これでカレントシェルで month.name の内容を read で読むことができるように なりました。

read で読み込んだものを配列状態でシェル変数に格納する部分が8行目です。 month.name から1行目の "3 March" を読み込んだとします。この内容は read 文 によりシェル変数 k に "3"、month に "March" 取り込まれ、8行目に来ます。 eval はシェルが引数を2度評価するのと等価な働きをします。1度目の評価で は $k の置換と右辺の単一引用符が外されますので

M_3="$month"

となります。続く2度目の評価で右辺の $month が置換されます。最終的に 8行目は次のようになり、シェル変数 M_3 への代入操作が実現できます。

M_3="March"

この処理は month.name がEOFになるまで繰り返されます。

while ループから抜けたならば10行目の exec 文でもう一度標準入力を 切り換えます。先ほどファイルディスクリプタ3に保存しておいた元の標準入力を復帰 させ、不要になったファイルディスクリプタ3をクローズします。これで3行目で標準 入力を切り換える前の状態に戻ったことになります。

最後はシェル変数を配列の添字を移動してアクセスする動作を真似て month.name から 読み込んだ内容を表示させます。シェル変数 $i は読み込んだ項目数を保持していますので、 これがゼロになるまで減算しながら表示を行ないます。13行目の eval での シェル変数の変遷はご自身でたどってみてください。

ハノイの塔

C言語を憶えたてのころに見たことがある方もたくさんいると思います。 再帰呼び出しのサンプルとして有名な 「ハノイの塔」をシェルスクリプトで書いたものです。速度は期待できません が再帰処理さえも簡単にこなしてしまうシェル言語には目を見張るものがあります。

再帰による「ハノイの塔」の解を求めるスクリプトそのものは簡単なものですが、 このことにこじつけて他のことも併せて説明しようと欲張ったので行数の 多いスクリプトになってしまいました。

#!/bin/sh
# Tower of HANOI

#: ${1?Parameter unset}
set -u

sub='./hanoisub'
export sub
trap 'rm -f $sub;exit' 0 1 2 3 15

cat > $sub << 'EOF'
eval X=`expr 6 - `expr $2 + $3``
if test $1 -gt 1; then
        $sub `expr $1 - 1` $2 $X
fi
echo "Move disk #$1 from  $2 to  $3."
if test $1 -gt 1; then
        $sub `expr $1 - 1` $X $3
fi
EOF

chmod +x $sub
$sub $1 1 2

解説

「ハノイの塔」スクリプトの本質は23行目の $sub $1 1 2 で、$1 には スクリプトの第1引数としてに与えられた円盤の枚数がセットされています。このあとは $sub が再帰呼び出しを重ねて解を表示してきます。

サブルーチン $sub の所在は11〜20行目のヒアドキュメント部分です。 サブルーチンをメインのシェルスクリプトに含めておき、実行するときにだけ サブルーチンをディスクに展開して使用し、 終了時には削除するようにしてあります。

では7行目から見て行きましょう。ここは前準備部分で、サブルーチンとする シェルスクリプト名を定義し、 それを export 宣言(8行目)しサブシェル側に伝わるようにしておきます。 続いて trap 文を使って終了する時には必ずサブルーチンとしてディスクに書き出 したスクリプトを削除するようにしておきます。これをスクリプトの最後に(24行目 あたりに)削除命令を入れておいたのではDELキーによる中断などの時にゴミを残して しまいます。

11行目の cat >$sub << 'EOF' が12〜19行目をサブルーチンとして ディスクに書き出します。

eval X=`expr 6 - `expr $2 + $3``
if test $1 -gt 1; then
        $sub `expr $1 - 1` $2 $X
fi
echo "Move disk #$1 from  $2 to  $3."
if test $1 -gt 1; then
        $sub `expr $1 - 1` $X $3
fi

$sub を export 宣言した意味がお分かりでしょうか。このサブルー チンにも同じシェル変数が3行目と7行目に使われています。もし、export されて いなければこの変数はサブシェル側では未定義となりますので シェルは expr $1 - 1 をコマンドと解釈して誤動作の原因になります。

サブルーチン $sub を切り出したならば実行属性を与えてスクリプトとして 実行できるようにします。そして、柱に通してある円盤の数を第1引数として $sub を起動します。解を求める作業が終了すれば $sub から戻ってきます。

ディスクに書き出したサブルーチンの後始末は9行目の trap が請け負っています。 実際には23行目の処理が済むスクリプトを終える時点でシグナル0が送られてきますか ら trap の第1引数で指定した rm -f $sub が実行されます。

4行目の set -u はスクリプトの デバッグで説明したスイッチで、 未定義の変数が参照された時にエラーメッセージを表示して スクリプトを停止させるものです。ここでは引数なしで hanoi が呼び出されたときの 処理に利用しています。

さて、5行目に見慣れないものが書かれています。これはシェル変数置換の一種で、 第一引数 ($1) が未定義ならばエラーメッセージ "Parameter unset" を表示してスクリプ トを停止させます。表示させたいメッセージを書かずに : ${1?} とすれば シェルが持っているエラーメッセージを表示します。この時にヌルコマンドとしての コロン : を行頭に置くことを忘れないでください。

クエスチョンマーク ? 以外 にも、マイナス記号 - や等号 = も使うことができ、 それぞれ次のような意味を持っています。

|: ${val ? msg}|シェル変数 val が未定義ならば msg を表示してスクリプトを停止| |: ${val - str}|シェル変数 val が未定義ならば str を代入する| |: ${val=str}|": ${val-str}" に同じ。但し、位置パラメータには適用不可|

本題からそれますが、このスクリプトを PS5523-S(386sx, 12MHz) で実行させたところ 円盤が8枚の時の255個の解を求めるのに7分20秒ほどかかりました。 この時に $sub(./hanoisub) が7つ重なり、さらにそれを呼び出した 親シェル hanoi、そしてログインシェルと9つのシェルが走ります。 1つのシェルは50kほどのメモリを必要としますから、小さいminixといえどもかなりの メモリが必要になります。(BSDあたりに比べると可愛いのですが) リアルモードのminixで円盤が8枚の解を求めるのは無理かも知れません。

演習問題

例として取り上げたハノイの塔のスクリプトは起動された本体からサブルーチン用の スクリプトを展開し、それを呼び出しています。解を求めるために2つのスクリ プトが必要になることに変わりありません。そこで、サブルーチンを用いずに1つ のスクリプトだけで解を求めるものを作ってみてください。

シェルスクリプトによる簡単なデータベース

シェル言語を使って特別なプログラムによらずにシェル言語と実装されているコマンド だけで少し実用的なスクリプト「超簡易文献データベース Cabinet」を 作ってみましょう。

概要

キャビネットから物を捜したり、出し入れしたりといったことを真似ていますので 名前もそのものズバリ Cabinet です。実際のキャビネットに収められている 物は文献やビデオなど多種多様です。違った種類のものを詰め込むような設計にもできます。 初心者にも分かりやすいようにできるだけ簡素な構成の方が良いでしょうから、 文献検索用に目的を限定します。

サンプルとた文献データにはこれから minix あるいは、本格的に unix を勉強する 際の手助けになる資料として使えるものを用意しました。

データ形式

扱うデータはすべて unix や minix の標準的なテキストファイルです。 テキストファイルは人間も読めますが、ユーザが扱いやすいデータ形式と、 minix のツール類が扱いやすい形式は違います。 人間は極めて融通がききますから(考えようによってはいいかげんな)、 1つのデータが数行になろうが、コーヒーをこぼしたシミがあろうが、 必要な情報だけを簡単に拾い出すことができます。 一方、minix のツールの多くは1行を処理単位とする時が最も効率良く動作します。 どれほど高度な処理ができる人間でも文字がベタ書きされていたのではうんざり します。1つの文献を「著者」「標題」「出版社」「出版年」「ISBN」から なるデータの集合で表すとします。さて、あなたは(a)と(b)のどちらが扱い易いで しょうか。

(a)
Andrew S.Tanenbaum
OPERATING SYSTEMS DESIGN AND IMPLEMENTATION
Prentice-Hall
1987
0-13-637331-3

Andrew S.Tanenbaum
COMPUTER NETWORK
Prentice-Hall
1981
0-13-165183-8


(b)
Alan Deikman:UNIX PROGRAMMING ON THE 80286 80386:M & T Publishing, ...
Alfred V.Aho, Brian W.Kernighan, Peter J.Weinberger:The AWK Progra ...
Allen I.Holub:COMPILER DESIGN IN C:Prentice-Hall:1990:0-13-155045-4 ...
Andrew S.Tanenbaum:COMPUTER NETWORKS:Prentice-Hall:1981:0-13-165183 ...
Andrew S.Tanenbaum:OPERATING SYSTEMS DESIGN AND IMPLEMENTATION:Pren ...

よほどのアマノジャクでない限り人間が扱い易いデータ形式は(a)の方だろうと 思います。一方、(b)は minix 上のコマンド群が扱いを得意としているものです。

そこで、1つの文献データの入力は(a)の形式で著者、表題、出版社、出版年、ISBN をそれぞれ1行に書き、5行で1つの文献として入力します。そして、それぞれの データは1行以上の空白行で区切ることとします。これを(b)のデータ形式に変換し て検索用のデータとして蓄えることにします。 このデータをスクリプトで検索し、目的のデータが見つかったならば再度(a)のよう な構成にして人間に提示すれば良いことになります。

Cabinetの部品

それではこのためにどのような部品を用意すればよいでしょうか。データの入力、 検索、出力と分けて考えてみます。

まず入力されるデータは通常のテキストファイルですからminix備え付けのエディタ ed, mined, elle, vi などから好きなものを使うことにし、(a)から(b)のデータ形式に 変換するスクリプトと組み合せることにします。 検索には正規表現が使えるgrepを使いましょう。出力は、データ形式(b)の フィールドがコロン「:」で区切られているので IFS を切り換えて 各々のフィールド取り出し echo で表示させます。 この方針で次のような5つのスクリプトを用意してみました。

  • add & - 文献データ追加用のスクリプト
  • upd & - 検索用データファイルの更新スクリプト
  • all & - 全文献データの表示のスクリプト
  • se & - 文献検索スクリプト
  • cab & - メニュー処理

Cabinetのスクリプト

add - 文献データ追加

引数として検索用データに新しい文献データをデータ形式(a)で書き込んだ ファイル名を与えることにより追加できます。 もし、引数がない場合はコンソールから文献データを1件だけ取り込みます。

#!/bin/sh
#        ADD data to the Cabinet File

: ${CABINET?}
ORG=$CABINET
REC="Auther Title Publisher Year ISBN"

if test -z "$CABINET"; then
        exit
fi

if test $# -ne 0; then
        for i ; do
                if test -f "$i"; then
                        continue
                else
                        echo "Error: '$i' dose not exist."
                        exit
                fi
        done
        if test -f "$ORG"; then
                cp $ORG ${ORG}.bak
        fi
        for i ; do
                echo >> $ORG
                cat $i >> $ORG
                echo "'$i' has been added to the $CABINET Cabinet"
        done
        upd
else
        echo "Type in NEW data"
        for i in $REC; do
                echo -n "$i --> "
                read line
                eval $i="$line"
        done
        echo '----------------------------'
        for i in $REC; do
                eval echo "$i: $$i"
        done
        echo '----------------------------'
        echo -n '                Ok? (y/n) '
        read line
        if test "$line" = y -o "$line" = Y; then
                echo >> $ORG
                for i in $REC; do
                        eval echo "$$i"
                done >> $ORG
                echo
                echo "This data has been added to the $CABINET cabinet."
                upd
        fi
fi

% ---------------------------------------------------------------------------

upd - 検索データの更新

人間による入力データは作業性を考えて複数行で1レコードを構成していますが、 minix のコマンドは1行で1つのレコードを構成するものを扱うように設計されて います。そのための変換を行ない、検索用のデータファイルの更新を行ないます。 このスクリプトは入力用のスクリプト add から呼び出されます。

#!/bin/sh
#         update cabinet

: ${CABINET?}
RACK=${CABINET}.items
TMP=/tmp/_$$
trap 'rm -f $TMP' 0 2

echo -n "Now, updating $CABINET cabinet. Just a minute, Please!"
lbuf=""
> $TMP
cat $CABINET | while read line; do
        if test "$line" = ""; then
                if test -n "$lbuf"; then
                        echo $lbuf >> $TMP
                        lbuf=""
                fi
        else
                if test "$lbuf" = ""; then
                        lbuf=$line
                else
                        lbuf="$lbuf:$line"
                fi
        fi
done
if test -n "$lbuf"; then
        echo $lbuf >> $TMP
fi
echo
sort -fut':' +0.0 -2.0 $TMP > $RACK

all - 全文献の表示

80桁のコンソールでも読み易いように3行で1件のデータを表示させています。 データの出力先は標準出力ですからパイプで more などのページャにつなげば ページ単位で停止させられますし、lpr を指定すればプリンタに出力できます。

#!/bin/sh
#        LIST all of the entries in the Cabinet File

: ${CABINET?}
RACK=${CABINET}.items

if test ! -s "$RACK"; then
        echo 'Empty Cabinet.'
        exit
fi

echo "  `wc -l < $RACK` item(s) in your Cabinet."
IFS=:
cat $RACK | while read A B C D E; do
        echo; echo $B
        echo $A
        echo "$C, $D(ISBN:$E)"
done
echo

se - 文献の検索

引数として与えられた文字列の条件を満たす文献すべてを探し出します。 検索キーワード文字列には正規表現が使えます。

#!/bin/sh
#        SEarch data in the Cabinet File 

: ${CABINET?}
RACK=${CABINET}.items
TMP=/tmp/_$$

trap 'rm $TMP; echo; exit' 0 2

if test $# -eq 0 -o -z "$CABINET" -o ! -f "$RACK"; then
        exit
fi

echo -n "looking for '$*' .."
grep "$*" $RACK > $TMP

if test -s "$TMP"; then
        echo "    `wc -l < $TMP` item(s)"
        IFS=:
        cat $TMP | while read A B C D E; do
                echo; echo $B
                echo $A
                echo "$C, $D(ISBN:$E)"
        done
        echo
else
        echo " Sorry, I can't find in the $CABINET Cabinet"
fi

cab - Cabinetメニュー

add, upd, all, se は部品として作られており、 単独で走らせることができます。 初心者にとってはメニューの方がなじみやすいでしょうから、 メニュー画面で数字で指定することにより5つの作業ができるようにしてみました。

#!/bin/sh
#

: ${CABINET=Book}
export CABINET
OUTPUT=more
OUT=Display
RACK=items

trap 'continue' 2

if test $# -ne 0; then
        if test -d $1; then
                CABINET=$1
        else
                exit
        fi
fi

while true ;do
        echo; echo; echo -n "
        $CABINET Cabinet  .. `wc -l < ${CABINET}.items` item(s)
      ==================================================
        Would you like to:

                1) Search data in the Cabinet
                2) Add data to the Cabinet
                3) List all of the Cabinet
                4) Change output (current: $OUT)
                5) Quit
      ==================================================
                                SELECT (1-5): "
        read command
        echo
        case "$command" in
                1 | s)  echo -n '[SEARCH] Enter Keyword: '
                        read line
                        if test ! -z "$line"; then
                            se "$line" | "$OUTPUT"
                            if test "$OUT" = Display; then
                                echo '>>> Hit ENTER to continue <<<'
                                read line
                            fi
                        fi;;

                2 | a)  echo -n '[ADD] Enter filename: '
                        read line
                        if test -z "$line"; then
                            add
                        else
                            add "$line"
                        fi;;

                3 | l)  echo '[ALL]'
                        all | $OUTPUT;;

                4 | c)  if test "$OUT" = Display; then
                            OUTPUT=lpr; OUT=Printer
                        else
                            OUTPUT=more; OUT=Display
                        fi;;

                5 | q)  exit;;
                *)      echo "??? '$command'";;
        esac
done

Cabinetの使い方

Cabinet は初心者の学習用として作った文献検索用のシェルスクリプトです。 使用方法についてはスクリプトを読んでいただければ一目瞭然ですし、それがまた シェルスクリプト理解への近道でしょう。さらに新しい機能を追加するなどして、 実際に手を加えてみてください。それが思い通りに走ったときには楽しさも倍増する こと請け合いです。とはいえ、「とにかく遊んでみたい」というせっかち屋さんのため に簡単な説明を用意します。

Cabinet 本体はデータを 追加・更新を行なう add、upd、検索を担当する se そして 全データをダンプする all の4つのスクリプトから構成されています。 これらはそれぞれがが独立して走るようになっており、コマンドラインから直接起動 させることができます。しかし、コマンドラインからは柔軟な使い方ができる反面、 シェル変数の設定などをユーザが直接で行わなければなりません。まったくの初心者 には取付きにくいかもしれません。

このために Cabinet はメニュー処理のためのスクリプト cab が 用意されています。シェルについてまったくの初心者はメニューで慣れてから 移った方が無難でしょう。

メニューの起動

メニュー処理用のスクリプト cab を走らせると Cabinet の操作メニューが表示されます。

$ cab

とタイプして起動した場合は親シェル(ほとんどの場合はログインシェルでしょう) からコピーされてきた変数 CABINET の内容で指定されたものを操作の対象と します。もし、この時に親シェルが CABINET という変数を export していな かったり、export していても中身が空の場合は Book を操作対象として立ち 上がります。このメニュー画面で操作できる対象は

  • 親シェルが export した変数 CABINET で指定されたもの
  • Book

の順となります。 メニュースクリプト cab が起動されると次のような画面が表れます。

Cabinet = Book  68 items
==================================================
Would you like to:

      1) Search data in the Cabinet
      2) Add data to the Cabinet
      3) List all of the Cabinet
      4) Change output (current: Display)
      5) Quit
==================================================
                                SELECT (1-5): 

一番上に表示されている "Book 68 items" とは現在開かれている (シェル変数 CABINET で指定されている)キャビネットの名称とその中にある データ件数です。この Book を対象に検索、追加、表示の操作を行います。

(1) データの検索

現在開かれているキャビネットから指定されたキーワードを含む項目を表示します。 "1" を選びリターンキーを押すと

[SEARCH] Enter Keyword: 

と探すためのキーワードを聞いてきます目的のものを指定してください。 指定するキーワードには一般の語句はもちろん、正規表現を用いることもできます。 例えば "IBM PC" というタイトルのついた文献を探したければ

[SEARCH] Enter Keyword: IBM PC

とタイプし(IBM PC)リターンキーを打ちます。 しばらくすると見つかった項目数とそれらの内容が表示されます。 もし、表示する内容が1画面に納まらない場合は more がポーズをかけますので、 スペースで1画面、リターンで1行先に進みます。

(2) データの追加

現在開かれているキャビネットに新しいデータを追加します。 追加は事前にテキストエディタなどで作っておいたファイルの内容を追加する方法と 1項目だけ手作業で入力する方法があります。どちらの場合も "2" を選択します。

[ADD] Enter filename: 

追加するデータを収めたファイル名を尋ねられますので該当するファイル名を指定して ください。ファイル名はパスを含むこともできますし、スペースで区切って複数の ファイルを指定することもできます。 もし、指定したファイルが存在しない場合はエラーとなりデータの追加は行われません。 この時に追加するデータの内容についてはまったく検査されませんので、間違った指定 をすると文献データにビデオやCDのデータ、最悪の場合にはプログラムソースなどを 追加してしまうこともあります。また、同じ文献のデータでもフィールドの列びが既存 のデータと一致していないとトンチンカンなものになってしまいます。 1つの物件のレコード構造についてはすべて使う人間の責任に任されています。

ここで使用している文献データのレコード構造はデータ形式 で説明したものです。例えば

[ADD] Enter filename: b00 ../b002

と指定するとカレントディレクトリにある b001 というファイルと ../b002 という ファイルの内容が文献データに追加されます。

もし、ここでファイル名を入力せずにリターンキーのみを押すと手作業で1件だけ データを追加できます。入力内容は既存の検索用ファイルのレコード構造と一致さ せてください。手作業の入力は概ねつぎのようになります。

[ADD] Enter filename:                       <- リターンのみ
Type in NEW data
Auther -> Andrew S.Tanenbaum
Title -> OPERATING SYSTEM DESIGN AND IMPLEMENTATION
Publisher -> Prentice-Hall
Year -> 1988
ISBN -> 0-13-637331-3

著者、タイトル、出版社、年、ISBN の4項目の入力が完了すると、 入力されたデータを表示し、確認を求めてきます。

----------------------------
Auther: Andrew S.Tanenbaum
Title: OPERATING SYSTEM DESIGN AND IMPLEMENTATION
Publisher: Prentice-Hall
Year: 1988
ISBN: 0-13-637331-3
----------------------------
                Ok? (y/n) 

ここで入力データに間違いがなければ "y" で答えてください。 "y" の入力でデータが追加され、それ以外のキーならば入力されたデータが捨てら れます。

(3) 全項目の表示

現在開かれているキャビネットに収められているすべてのデータを表示します。 ここでは "3" を選択するだけです。画面に表示するときは more が1画面ごとに ポーズを入れてきます。「4) Change output」で出力をプリンタ(lpr) に切り換え ているならばプリンタに連続出力されます。

(4) 出力の切り替え

検索されたデータと全項目を表示するときの出力先を切り換えます。 "4" を選択するたびにディスプレイとプリンタをスイッチし、現在の出力先は

4) Change output(current: Display)

として表示されています。

(5) 終了

メニューを終了し、シェルのコマンドラインに戻ります。

コマンドラインからの使用法

すでに説明したメニューからの操作は cab がそれぞれスクリプトを呼び出して 実現しています。それらをコマンドラインから呼び出して直接使うこともできます。 これらのスクリプトは実行されると操作対象とするキャビネットを決めるために 必ずシェル変数 CABINET の内容を参照します。 もし、この変数が export されていなかったり内容が空の場合は正常な処理は期待 できません。 文献データを処理したいのでしたら最初にログインシェルのプロンプトから シェル変数 CABINET に Book を設定します。

$ CABINET=Book; export CABINET

(1)検索

検索をするスクリプトは se(SEarch) です。これに検索させたいキーワードを 引数として与えます。例えば "IBM PC" というものをキーワードとして与えたい場合 には

$ se 'IBM PC'

とします。この時キーワードとして se に渡す文字列をシェルから保護するた めに必ず単一引用符で囲みます。また、

$ se '.*NIX' | more

などと正規表現を使ったり、検索結果をパイプに流すこともできます。

(2)追加

add というスクリプトが担当します。 追加したいデータが入っているファイル名を引数として与えます。 例えば /user/mybooks というファイルに追加したいデータが入っているとすると

$ add /user/mybooks

とします。引数として与えるファイル名は1つに限りませんが、指定したファイル すべてが見つからない場合はエラー中断しますのでデータの追加は行われません。

もし、引数を与えなかった場合は1件のみを手作業で入力するように動作します。 手作業入力の具体的な例は「メニューからの操作」部分を参照してください。

(3)全表示

スクリプト all を走らせると登録されているすべての項目を表示します。 すべての項目をプリンタに送りたい場合は

$ all | lpr

とすれば良いでしょう。画面で見たいのでしたら more にパイプでつないでください。

(4)更新

人間が入力したデータからキャビネットを構成するスクリプト類が操作しやすい形に 変換し、検索用のデータを更新します。引数なしで

$ upd

とするだけです。なお、 このスクリプトは最初に1度だけ使う ものです。 あとは add で データを追加すると自動的に upd が呼び出されます。

Cabinetの拡張

Cabinet には不必要になったデータを削除するためのスクリプトがありません。 腕試しにチャレンジしてみてはいかがでしょうか。大まかには

  • 削除したい項目を入力させる(引数として指定する)
  • それに一致する項目を見つけ
  • オペレータの確認を得たのちに
  • 該当項目を削除する
  • できればその際に元のデータのバックアップを取る

という手順行えば良いと思います。そのためにはこの研修用のディスクにある限られた ツールをどんなオプションで、どのよう組み合わせれば良いか? 等など、興味は尽き ません。また、この Cabinet は文献ファイルをアクセスする時に排他制御も行なって いませんのでこちらも試してみてください。

Bibliography

平林浩一/平林小枝子
UNIXのバックグラウンド. プロセッサNo.64 Aug 1990. 技術評論社

S.R. Bourne/三好・木下訳 
UNIXシステム. マイクロソフトウェア

copyright1987 Andrew S.Tanenbaum, Prentice Hall 
minix 1.6.24B, shell ソース・リスト