Unixシェルスクリプト

新しいコマンドを作る

シェルスクリプトは、シェルのプログラミング機能を用いて、既存のコマンドを部品として新しいコマンドを作成する方法の一つです。

パイプやコマンド出力の置換を用いて、複数のコマンドを組み合わせて使うことができることを既に見てきましたが、繰り返し使うコマンド群がある場合、普通のコマンドと同等に使えるように、それを別の名前を持った新しいコマンドにすることができたら便利です。

話を具体的にするために、次のパイプラインを考えます。
$ du -S -k | sort -nr | head -10
(問 これは何を行っているか?実行し、マニュアルページを参照して考えてください。)

この仕事のためにtop10というシェルスクリプトを作ることにします。

まず最初に必要なことは、このパイプラインの内容を持つ普通のファイルを作成することです。
エディタで作成してもよいし、この程度なら次のようにして作成することもできます。
$ echo 'du -S -k | sort -nr | head -10' > top10
(問 このときもし引用符を忘れたらtop10の中には何が入る?)

又は
$ cat > top10
du -S -k | sort -nr | head -10
^D
(最後の行は、コントロールD: コントロールキーを押しながらDを押す。入力終了のシグナル。)

こうして作成したtop10を実行するのにはいくつかの方法があります。

まず、リダイレクトを使ってシェル自身の入力をキーボードからファイルに切り替えることができます。
$ bash < top10
皆さんが使っているシェルはbashという名前だったことを思い出してください。

シェルはまた、引数にファイル名を指定するとそのファイルから入力を取り込みます。
$ bash top10
しかし、実際にはこのいずれの方法もほとんど使いません。Unixの特徴の一つは、シェルでプログラムを結合して書いたプログラムと、例えばC言語で書かれ、機械語に翻訳されたプログラムをほとんど区別せずに扱うことができることです。top10を実行可能にするには、次の操作をします。
$ chmod +x top10
(問 chmodコマンドは何をするもので、この操作は何をしている?)

これを一度だけ行っておけば、次からは
$ ./top10
と入力すると実行できます。

'.'がカレントディレクトリを表すことを思い出すと、この記法はファイルの名前を指定しているにすぎないことに思い当たります。そこで、次のようにする とどうなるでしょうか。
$ pwd
/home/ichii
$ /home/ichii/top10
最初にカレントディレクトリを確認し、次にファイル名"top10"にディレクトリ名を加えたものを入力しました。このような指定方法を絶対パス名と呼ぶのでした。

(問  環境変数PATHを参照し、カレントディレクトリがサーチパスに含まれていないことを確認せよ。)

しかし、これではまだtop10は他のコマンドと全く同等に使えるようにはなっていません。コマンド名だけを入力して実行できるようにするには、 top10を個人用のコマンドをおさめたディレクトリ(~/binにすることが 多い)に移動し、PATHにそれを含めるようにするのがよいでしょう。
$ cd
$ pwd
/home/ichii
$ mkdir bin
$ mv top10 bin
$ ls top10
ls: top10: そのようなファイルやディレクトリはありません
$ PATH=~/bin:$PATH
$ echo $PATH
/home/ichii/bin:/usr/bin:/bin:...            # 実際の値は個人の設定によります。先頭に新たに追加した自分用のbinディレクトリが入っていることを確認してください。
$ top10
236 ./.kde/share/config
188 ./.gimp-1.2/palettes
...
(「シェル 初期設定ファイル」参照。)

コマンドの引数とパラメータ

次に、上でも行った作業である、ファイルのモードを実行可能に変えるためのcxというコマンドを作りたいとします。つまり、
$ cx top10
$ chmod +x top10
の簡略表現になるようにしようというわけです。このためには、cxという名前の、実行したいコマンドを含むシェルスクリプトを作ればよい訳ですが、今度は top10とは異なり、コマンドに実行のたび異なる引数を与える必要があります。シェルはその目的の為に特別なシェル変数を用意しています。シェルはシェルスクリプトの中で$1が現れるたびに1番目の引数、$2が現れるたびに2番目の引数という風に置換して行き、順次$9まで置換して行きます。従って、ファイルcxの内容が
chmod +x $1
となっていれば、
$ cx top10
が実行されたとき、$1はtop10に置換され目的を達することができます。

一連の作業は次のようになります。(やってみてください。)
 $ echo 'chmod +x $1' > cx
 $ bash cx cx
 $ echo echo Hi, there! > hello
 $ ./hello
 bash: ./hello: 許可がありません
 $ ./cx hello
 $ ./hello
 Hi, there!
 $ mv cx ~/bin
 $ rm hello
(2行目で、$ chmod +x cxとする代わりに、$ bash cx cxとしています。もちろん両者は等価ですが、上のようにした方が少し気が利いているような気がします。好みですが。)

cxが他のフィルタ等と同じように複数のファイルを引数に指定して一度に処理できるようにするにはどうしたらよいでしょうか?一つの考え方は
chmod +x $1 $2 $3 $4 $5 $6 $7 $8 $9
とする方法です。シェルは、もし引数が9未満のときには、指定されなかった引数には「長さ0の文字列」を与えるので、chmodには実際に指定された引数だけが与えられることになり、この方法でも一応は動きます。が、これはいかにも不格好です。(この方法で10個以上の引数を処理できるだろうか?)

この種の問題に対応するため、シェルには「全ての引数」を意味する$*という簡略表記が用意されています。これを使うと、cxは
chmod +x $*
とすればよいことになります。こうすれば、任意の個数の引数に対してうまく動きます。

ついでに説明しておくと、$0は実行するプログラムの名前になります。
$ echo 'echo $0' > name
$ cx name
$ ./name
./name
$ mv name ~/bin
$ ./name
./bin/name
これをうまく使うと、同一のファイルに複数の名前をつける(リンクする)ことで、一つのシェルスクリプトに異なった動作をさせることができます。

シェルスクリプトにおけるループ

いくつかのファイルについて同じ仕事を繰り返し実行することはよくあることです。プログラミング言語では、繰り返しはループと呼ばれ、基本的な制御構造の 一つです。Bashなどのシェルでは繰り返しはfor文を用いて行われます。シェルスクリプトだけでなく、端末からの入力でもよく用います。

for文の構文は次のようになっています。
for 変数 in 単語のリスト
do
コマンド1
コマンド2
done
例:複数のファイル名を一行に一つずつ表示するには

$; for i in * 
> do
> echo $i
> done
ここでは変数としてiを使いましたが、任意の文字列(空白を含まない)が使えます。(ただし、他のシェル変数とぶつかる値は使わない方がいいでしょう。)

例:複数のファイルのバックアップコピーを作る
$ for i in *
> do
> echo making backup of $;i
> cp $i $;i.bak
> done
cpをmvに変えれば名前の変更ができます。

ついでに一言:

ファイルのバックアップを作るのに、上ではcpを使いましたが、本当はタイムスタンプ(ファイル変更の日時。$ ls -l で出る)を保存するために、mvを使った方がいいと思います。また、バックアップファイルの名前の付け方は、自分なりに工夫して一貫した付け方をする習慣を付けると混乱しなくてすみます。.bakは、他のアプリケーションが使っていることがあるので、あまりおすすめできません。私(一井)は、".ORIG" ".good"(ちゃんと動くことが分かっているものをとっておくときなど)、または日付を20120601のように表したものを使っています。もっとも、ちゃんとした開発をするとき(論文を書くときも含む)は、バージョン管理システムというものを使い、このようなアドホックで不格好なことはしません。興味ある人はgitとかsubversionとかを調べてみてください。

forループは、複数のコマンドを実行するときとか、組み込まれている引数の処理が今やりたい仕事に対して適当でないときに重宝しますが、それぞれのコマンドが既にファイル名をループしているとき、つまり複数のファイル名を引数に与えれば、それぞれのファイルについて処理を行ってくれるコマンドに対しては forループを使うのは無駄であり、得策ではありません。

あまりよくない例:
$ for i in *
> do
> chmod +x $i
> done
(問 これはなぜいけないか)

シェルスクリプト内で用いるときには
for i in *
for i in $*
の違いに注意してください。(問 違いは何?)

実習

上のことを確かめる。
$ cat > looptest
echo first case
for i in *
do
echo 9 $i
done
echo second case
for i in $*
do
echo $i
done
^D
$ cx looptest
$ mv looptest ~/bin
$ looptest
$ looptest *
$ looptest /tmp/*
forに対する引数のリストは別のコマンドの出力を``でとりこんで作ることも多く、また、そもそもファイル名である必要もありません(単なる文字列として処理されるだけなので)。

課題

1. forループを使って、九九の表を生成してください。出力は
1 x 1 = 1
1 x 2 = 2
...
9 x 9 = 81
となるようにしてください。(ヒント:exprコマンドが使える。bash組み込みの演算を使ってもよい。)

これをシェルスクリプトにし、さらに、引数としてnを与えるとnの段の九九の練習ができるようにしてください。

2. PATHに含まれるディレクトリの中で、現実にいくつのコマンドがシェルスクリプトなのか確かめよ。この作業自体を、一つのコマンド行で実行できるだろう か。(ヒント:fileコマンド)
私(一井)のやり方は「fileコマンドを使ってシェルスクリプトを探す」にあります。

補足

ここまで、コマンドを単に書き並べるだけで簡単にシェルスクリプトを作ってきました。しかし、今日のUnix/Linuxでは様々なスクリプト言語が使われることもあり、スクリプトの第1行には、どのプログラムで処理するスクリプトかを明示することが普通です。これまで扱って来たシェルスクリプトの場合
 #!/bin/sh
を第1行に書いておけば大丈夫です。第2行からはこれまでのようにコマンドを書き並べて行けばよいことになります。

なお、2行目以降、#があるとそこから行末まではコメントとしてプログラムの実行に影響を与えません。

上のおまじないが気になる人へ:#! の後にはスクリプトを処理するプログラム名を書きます。上の書き方だと、/bin/shというプログラムで処理していることになります。実はこれは古くからあるBourneシェルというものをさしていて、こうしておくとおよそどのようなUnixシステムに持って行っても基本的には動くはず(もちろん使われているコマンドがないとだめですが、シェルの文法自体は共通)なのです。Bashを使っていたはずなのに、と思うかもしれませんが、Linuxではshは bashの別名になっています。実際、bashはshに対して上位互換(shの機能はそっくりそのまま持っているということ)であり、これで問題はありません。

シェルの制御構造

シェルにはforループの他、普通のプログラミング言語に備わっているif文等の制御構造があります。これについては、「シェル制御構造の補足」を参照して実習を行ってください。

参考文献

このページの作成にあたっては

Brian W. Kernighan & Rob Pike, 『UNIXプログラミング環境』(アスキー出版局)

を参照しました。

この本は、具体的な記述の多くが古くなってしまっていますが、UNIXの精神を伝えるものとして依然として一読の価値ある名著です。計算機室の本棚にもあります。