Powered by SmartDoc

コマンドを組み合わせる「のり」としてのシェルスクリプト

竹本 浩
take@pwave.sig.or.jp

UNIXの発展で大きく寄与をしたのは、シェルスクリプトである。シェルスクリプトによって既存のコマンドとユーザプログラムを組み合わせ、新たなプログラムを作成する過程は、ソフトウェアの「のり」と呼ぶにふさわしい。

シェルスクリプトでベースになった技術は、デバイスとファイルを区別しないファイルシステム、入出力のリダイレクト、forkとpipeによるストリームのパイプライン処理である。

ここでは、シェルスクリプトの「のり」としての役割に焦点を当てて解説する。

目次

Last modified: Wed Mar 30 22:21:21 JST 2005

1 そのとき歴史がかわった

UNIXの産みの親であるKen ThompsonとDennis RitchieがAT&Tのベル研究所でMulticsと呼ばれる大型のオペレーティングシステムを開発していた頃、「インタラクティブで便利なコンピュータサービス」が欲しいと言って作ったのが、現在のUNIXである。

彼らは、「ベル研の文書処理システムを作る」と言って予算を引き出し、PDP-11(システムメモリ16Kバイト、ユーザメモリ8Kバイト、ハードディスク512Kバイト)という現在のPDA以下のハードウェアを購入し、その上に現在のUNIXシステムのコマンド群とroffと呼ばれる文書処理システムを構築したのである。

1.1 ストリーミングとパイプライン

「インタラクティブで便利なコンピュータサービス」は、情報の流れである「ストリーミング」を複数のコマンド間でつなぎ合わせた「パイプライン」を形成することによって実現された。

再利用率の高いコマンドとシェルスクリプトによるカスタマイズコマンドを作成することによってユーザのニーズに応じた処理を短時間で提供できることがUNIXの最大の強みとなった。

1.1.1 デモ

「パイプライン処理」の例を実演を交えて紹介する。シェルとのやりとりは、キーボードからのコマンドライン入力によって行う。

デモで使用するコマンドを以下に示す。

take% xev | Event2Plot | Graph

コマンドライン入力の各パートの説明を表[コマンドライン解説]に示す。プロンプトの後に入力されたコマンド文字列によって、xev, Event2Plot, Graphの3つのプロセス(1)が起動され、同時に処理が行われる。

これによってxevが出力したイベント情報をEvent2PlotがX Yの座標に変換しGraphに渡し、別のウィンドウに同時にプロットすることができる。これがUNIXのパイプライン処理のすばらしいところである。

表 1.1.1.1 コマンドライン解説
パート文字列 内容
take% %とそれに先行する文字列はプロンプトと呼ばれる入力促進文字列。
xev Window内で発生したX-EVENTを標準出力に出力するXウィンドウコマンド
| コマンド間のストリーミングを結ぶパイプを示す。単に「パイプ」と呼ばれる。
Event2Plot X-EVENTの内マウスの動きを示すMotionNotifyイベントを選択し、それからマウスの座標を抽出するシェルスクリプト
Graph GNUのプロットデータ表示コマンドgraphのコマンドのオプションを一つにまとめたシェルスクリプト

デモの画面を図[パイプラインデモ画面]に示す。

図 1.1.1.1 パイプラインデモ画面

例に使用したシェルスクリプト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なのである。

  1. 「プロセス」はUNIXの処理の単位であり、すべてのプロセスに平等にCPUが割り当てられ、タイムシェアリングされている。

2 フォークとパイプ

UNIXでパイプラインを実現するための鍵になる部分をforkシステムコール(2)とpipeシステムコールの実装に見ることができる。

  1. UNIXでは、Cの関数呼び出しの内、カーネルで処理される関数を「システムコール」と呼ぶ。

2.1 forkシステムコール

forkシステムコールは、UNIXでプロセスを生成する唯一の関数である。forkによってカーネルは呼び出し元のコピーを「子プロセス」(3)が生成される。forkによって生成された親子間では、ファイルテーブルやi-nodeテーブルとプログラムの命令コード(4)を共有する。これによって2つのプロセスが同じファイルにアクセスすることができるようになる。forkシステムコールの仕組みを図[forkの仕組み]に示す。

シェルによる「パイプライン処理」を実現するためには、プロセスによるファイルの動的切り替えが必要になるため、このような親子関係のプロセス生成メカニズムを導入したものと思われれる。

図 2.1.1 forkの仕組み
  1. forkによって生成されるプロセスは必ず親子関係を持つ
  2. テキストコードと呼ばれる

2.2 pipeシステムコール

pipeシステムコールは、本当に不思議な関数である。自分が出力したものを自分へ送るストリームの輪(パイプ)がpipeシステムによって生成される。pipeシステムコールの仕組みを図[pipeの仕組み]に示す。

図 2.2.1 pipeの仕組み

この不思議なパイプをforkシステムコールと組み合わせることよって「パイプライン」を形成することができる。図[forkとpipeの組み合わせ]にforkとpipeの組み合わせでパイプラインを形成するメカニズムを示す。

図 2.2.2 forkとpipeの組み合わせ
  1. 1個のpipeを持つ親プロセスが子プロセスを生成する
  2. 親プロセスのin、子プロセスのoutをクローズする
  3. 親プロセスのstdoutをcloseして、pipeのout記述子の複製を生成する
  4. 子プロセスのstdinをcloseして、pipeのin記述子の複製を生成する

ここで、UNIXのファイル記述の割り当て規則である「最後にcloseされた最も小さな記述子をdupで割り当てる」を利用している。これは、createやopen等のファイルを割り当てるシステムコールでも適応されている。共通ルールである。

3 ファイルシステム

パイプラインを形成するためには、もう一つの難関が存在していた。それが端末やハードディスク、テープ装置、プリンタ等のハードウェアデバイスである。UNIXのファイルシステムの特徴を挙げると

特にi-nodeとファイルテーブルを使って、ファイルとハードウェアデバイスを同様に扱うことができるようにしたのが大きな特徴である。

UNIXのファイルシステムの構成を図[UNIXのツリー階層ファイルシステム]に示す。

図 3.1 UNIXのツリー階層ファイルシステム
  1. Windowsではハードディスクごとに分かれた階層構造を持つが、UNIXでは単一のツリー構造しか許さない。これはi-nodeがマシン上での一意性を保証する唯一の手段として利用されているためである。

4 シェルとは何か

4.1 UNIXの構造

UNIXの構造を図[UNIX内部構造]に示す。

カーネルがハードウェアとコマンドの間に位置し、コマンドはシステムコールによってカーネルに対してハードウェアへのアクセス要求を送る。

シェルが一番外側に位置し、ユーザとコマンドの間に位置し、ユーザからの要求に応じてコマンドを実行し、その結果をユーザに返す役割をする。

図 4.1.1 UNIX内部構造

どのようにしてユーザがシェルと会話するかを次節で説明する。

4.2 コマンドの実行

デモで説明したようにシェルとの会話は、プロンプトが表示されたコマンドラインに実行したいコマンドと引数を渡すことによって行う。

コマンドは、プロンプトの直後に指定し、ブランクまたはタブで区切って、引数を指定する。通常マイナス"-"が先行した引数はオプションと呼ばれ、実行するコマンドのモードやデフォルトの設定値の変更等を行う。オプションに続いて引数(arg1, arg2)を指定する。

コマンドの引数の終わりは、セミコロン";"または改行キーである。一行のコマンドラインに複数のコマンドの実行を指定する場合には;を使用する。コマンドの実行は、改行キーを押すことによって行われる。

% command -option arg1 arg2 ...

4.3 リダイレクト

forkシステムコールで説明した親プロセスがオープンしていたファイルを子プロセスが読み込むことができる機能を使って動的にコマンドの入出力ファイルを切り替える機能が「リダイレクト」である。

UNIXのCプログラムでは、main関数がプログラムの開始部分(エントリポイント)であり、プログラムmainを呼び出す前に標準入力(stdin)、標準出力(stdout)、標準エラー出力(stderr)にそれぞれ、0, 1, 2のファイル記述子がオープンされた状態でmainが呼び出される。

従って標準入力から読み込み、標準出力に書き込むプログラムを書いておけば、実行時にリダイレクト機能を使って入力ファイルや出力先を動的に切り替えることが可能になる。

4.3.1 リダイレクト指定

入力リダイレクトには、"<"が使われ、出力のリダイレクトには">"と">>"が使用される。">>"が指定された場合には、ファイルの終わりに追加する。

リダイレクトの例をリファレンスから引用する。

$ 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の文字数をカウントする

4.4 ファイル展開

コマンドの引数にファイル名を指定する場合、メタ記号によって複数のファイルを一括して指定することができる。メタ記号の一覧を以下に示す。

メタ記号 説明
? 任意の一文字にマッチする
[abef] []で囲まれた文字のいずれかの一文字にマッチする
[a-z] a-zのように-の間に含まれる一文字にマッチする
[^abc] [の直後に^が指定されるとそれ以降に指定した文字列以外の文字にマッチする
* 任意の文字列にマッチする

例)ファイルが.bakで終わるすべてのファイルを削除する場合、

% rm *.bak

と指定する。(6)

  1. rmはファイルを削除(remove)するコマンドである。

4.5 バックグラウンド

コマンドを逐次処理するのではなく、並行して処理する場合には、バックグラウンド指定(&)を使用する。

$ rm -r . & ; # カレントディレクトリ以下のすべてのファイルをバックグランドで削除する

バックグラウンドで起動しているプロセスをフォアグラウンドに変更するには(7)、fgコマンドを使用する。

  1. 入力待ちでサスペンド状態になっている場合

4.6 よく使われるコマンド

シェルでよく使われるコマンドを表[基本コマンド]に示す。

各コマンドの使い方は、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
表 4.6.1 基本コマンド
コマンド名 機能
echo 引数を出力する
pwd カレントディレクトリを出力する
ls 指定されたファイルリストを出力する
cp ファイルをコピーする
cat 引数で指定された複数のファイルを結合して出力する
rm 指定されたファイルを削除する
grep 第1引数で指定されたパターンマッチする行を第2引数以降で指定されたファイルから検索する
cd 指定されたディレクトリに移動する
wc ファイルの行数、単語数、文字数をカウントする
test 引数で指定された条件を検証する
  1. キーワード検索を行う場合には、予め/usr/sbin/makewhatisコマンドでコマンドデータベースを作成しておく必要がある。

5 シェルスクリプトとは何か

5.1 デモ例題の説明

シェルスクリプトとは何かを、デモで使用したシェルスクリプトを例に説明する。

これは、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)

このようにシェルスクリプトでは、コマンドラインから入力するコマンドとその引数をファイルに記述し、一つのコマンドとして登録することができる。

  1. UNIXではCで記述され、コンパイルされた実行形式のコマンドとシェルスクリプトは全く同様に扱われる

5.2 コマンドの拡張

シェルは、コマンドを組み合わせるだけではなく、その使い方を拡張するためにも利用できる。

例)標準入力から第一引数に指定されたパターンを検索するプログラムtgrep(10)を複数ファイルの検索に拡張する場合の拡張方法を示す。

新しいシェルスクリプトをの名称をTgrep.shとし、その仕様は、引数が1個の場合には、標準入力から検索し、引数が2個以上の場合には、各ファイルに対してパターンに一致する行を検索するものとする。

  1. Cプログラミングの例題を使用

5.2.1 引数のチェック

引数のチェックで使われるシェルの文法が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.

5.2.2 各ファイルに対する繰り返し処理

引数のチェックが完了したところで、各ファイルに対する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が実際に活用できるコマンドとなる。

5.3 シェルスクリプトTIP集

シェルスクリプトを作成する上で役に立つTIPをいくつか紹介する。

5.3.1 縦をよこにする

ファイルに指定されたファイルに対して、特定のコマンドを実行したいとき、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`
  1. この方法の欠点として、コマンドラインで指定できる文字数に制限(通常2Kバイト)があることである。
  2. 私は、一時ファイル名を数字で始めることによって通常のファイルと区別できるようにしている。このようにすれば、一時ファイルの削除はrm -f [0-9]*でできるので、一時ファイルの後かたづけが簡単になる。

5.3.2 一意なファイル名の生成

シェルスクリプト中で一時ファイルを作成する場合、一時ファイル名が固定ファイルの場合、同じコマンドが同時に使用された場合、一時ファイルが上書きされ、壊れてしまうことがある。このような場合、マシン上で一意な一時ファイル名を作成できるとこのような心配はなくなる。シェルスクリプトではプロセス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.

となる。この例のようにシェルスクリプトを実行する方法として、起動シェルの引数としてシェルスクリプトとその引数を指定するやり方がある。特に一時的なスクリプトの場合にこの方式が使われる。

5.3.3 クォーティング

シェル変数にクォーティング文字としてダブルクォート、シングルクォート、バックスラッシュがある。これまで何の説明もなくダブルクォートやシングルクォートを使ってきたが、その使い分けを説明する。

どのような時にダブルクォートを使うのか

空白やタブを含む複数の単語を1個の引数としてコマンドに渡す場合で、かつシェル変数の展開を行う場合

コマンドのラインでのダブルクォートは、シェルで取り除かれ、コマンドにはシェル変数を展開して、ダブルクォートを除いた文字列渡される。

どのような時にシングルクォートを使うのか

シェル変数の展開を行わない場合と、ダブルクォートも含めてコマンドに渡す必要がある場合

ダブルクォートと同様にシングルクォートを除いた文字列がコマンドに渡される。

6 シェルスクリプト・リファレンス

6.1 コマンドの実行

シェルでは、空白(ブランク、タブ)で区切られた最初の引数がコマンド名として扱われ、その後の引数がコマンドへの引数として渡されます。コマンドの区切りは、セミコロン(;)または改行とパイプ(|)、括弧()、バックグランド指定(&)で区切られる。

複数のコマンドを一行に記述したいとき、if thenを1行に記述したいとき、明示的にコマンドの終わりを示したいときにセミコロンを付ける。

通常、ハイフン(-)で始まる引数はコマンドのオプションとして扱われ、コマンド オプション コマンド引数の順で指定するのが慣例となっている。

$ echo hello world # 改行をコマンドラインの終わりに使用している
hello world
$ wc -l test.c; # コマンドの終わりを明示的に示すために、セミコロンを付けている
test.c 120

6.1.1 バックグラウンド

コマンドを逐次処理するのではなく、並行して処理する場合には、バックグラウンド指定(&)を使用する。

$ rm -r . & ; # カレントディレクトリ以下のすべてのファイルをバックグランドで削除する

バックグラウンドで起動しているプロセスをフォアグラウンドに変更するには(13)、fgコマンドを使用する。ログインシェルがshの場合、logoutするとバックグラウンドプロセスが、終了してしまう。このため、nohupコマンドを使用することによってlogout後もバックグラウンドプロセスが継続して稼働する。

  1. 入力待ちでサスペンド状態になっている場合

6.1.2 入出力リダイレクト

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の文字数をカウントする

6.1.3 パイプ処理

コマンドの出力ストリームを次のコマンドの入力ストリームにすることを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にコピーする

6.1.4 グループ処理

一連の処理の入出力をまとめ、別シェルプロセスで実行したい場合、括弧()で括ることによってグループ処理を指定することができる。

$ (export LANG=ja_JP.SJIS; ovstart) # 別プロセスで環境変数LANGを変更し、ovstartコマンドを実行する
$ (sleep 10; echo hello) & # バックグランドで5秒待ってから"hello"と表示する。(プロンプトはすぐにでる)

6.2 シェルスクリプト

一連のシェルコマンドをファイルに列記し、一度に実行することができる。このようにコマンドの処理を記述したファイルをシェルスクリプトと呼ぶ。

helloと出力し、5秒待って、再度hello againと出力するスクリプトをリスト[test.sh]に示す。シェルスクリプトでは#から行末までをコメントとして扱う。ただし、最初の行の#!と記述したコメントは特殊な意味を持ち、起動シェル(コマンドでも可)を指定する行である。

リスト 6.2.1 test.sh
# #以降はコメントとして扱われる。
echo hello
sleep 5
echo hello again

このプログラムを実行するためには、

$ chmod +x test.sh

とtest.shに実行権を設定すればよい。

6.3 ファイル展開

UNIXのファイル名を指定する場合に、ファイルの正規表現を使用することができる。シェルで使用できる正規表現は、*, ?, []がある。

*
.で始まる文字列を除くすべてのファイルにマッチする。ピリオド(.)がカレントディレクトリを表すので、マッチしないようにしている。
?
任意の1文字にマッチする。
[]
[]内で指定されている文字にマッチする。a-zのように"-"で範囲を指定することができる。[^A-Z]のように[の直後に"^"が指定されると指定された文字以外にマッチする。

ファイル展開の例を示す。

$ ls a*.doc  # aで始まり、拡張子がdocのファイルとマッチする
$ ls a?b.txt # aで始まり、3文字目がbで拡張子が.txtのファイルにマッチする
$ rm [0-9]*  # 私は、数字で始まるファイルを一時ファイル名に使用する。これは一時ファイルの削除を意味する
$ ls [^0-9]* # 一時ファイルを除くファイル名を表示する

6.4 シェル変数

shでは、シェル変数と環境変数の2つの変数が使用される。シェル変数はカレントシェル内のみで有効な変数であり、環境変数はシェルから起動されるすべての子プロセスに引き継がれる変数である。シェルスクリプトでは、これらの変数を使ってコマンド間での情報交換を行っている。

6.4.1 シェル変数のセット

シェル変数に値をセットするには、変数名と値の間に空白を入れずに=を入れる。代入する値の中に空白が含まれる場合シングルクォート(')またはダブルクォート(")で括る。シェル変数名にはアルファベット、数字、アンダースコア(_)からが使用できる。

簡単なシェル変数の例を示す。

$ HOST_NAME=IES00           # シェル変数HOST_NAMEに"IES00"をセットする
$ IN_FILE=""       # シェル変数IN_FILEに空文字をセットする
$ PS="ps ef"       # シェル変数PSに"ps ef"をセットする

6.4.2 環境変数のセット

シェル変数を環境変数として宣言するには、exportコマンドを使用する。

$ export TERM

のようにexportの後に環境変数とする既存の変数名を指定する方法と、

$ export NEW_TERM=vt100

のようにNEW_TERMに値を設定して、新規環境変数NEW_TERMを宣言する方法の2通りがある。

6.4.3 シェル変数/環境変数の参照

シェル変数は$変数名で参照することができる。ただし変数名の区切りが認識しづらい場合、ダブルクォート(")(14)や、${}で括って参照する。

$ VAR=hello
$ echo $VAR
hello
$ echo "${VAR}_again"
hello_again

シェル変数には配列指定も可能であり、変数名[添え字]の形式で指定する。すべての配列要素を参照する場合には、$配列名[@]とする。

$ LINE[1]=foo
$ LINE[2]=bar
$ echo $LINE[1] $LINE[2]
foo bar
  1. シングルクォートの場合にはシェル変数の展開が行われないので注意を要する

6.4.4 特殊変数

shには、位置変数指定、特殊変数参照、特殊置換指定がある。

位置変数指定
位置変数は、$0, $1, $2, ...$nのように引数の位置を数字nで指定する参照方法である。
特殊変数参照
プロセスID、引数の個数などの特殊な情報を参照する。特殊変数の内、$*, $?, $$, $#がよく使われる。
$*
$1から始まるすべての位置変数を展開する
$#
引数の個数
$$
スクリプトのプロセスID
$?
直前のコマンドの終了ステータス
特殊置換
${変数:=省略値}の形式で指定し、変数が定義されていない場合には省略値を参照することができる。

6.4.5 バッククォート

コマンドの実行結果をシェル変数に代入するためにバッククォート(`)指定がある。

$ DIR=`pwd`

とするとシェル変数DIRにpwdの実行結果(カレントディレクトリの値)がセットされる。

例えば、変数iの値を1プラスする指定は、

$ i=`expr $i + 1`

とすることで算術計算をシェルスクリプト内で実現することができる。

6.5 制御コマンド

シェルスクリプト内で、処理の流れを変える制御コマンドとして、if, case, for, while文がある。処理の終わりは、fi, esac, doneで表し、ネスト構造を取ることも可能である。

6.5.1 if文

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

6.5.2 testコマンド

通常if文の条件判定にはtestコマンドを使用する。shでは、慣例として"["(testのシンボリックリンク)を利用する。この場合には終わりを"]"で囲む。よく使われるtestコマンドオプションを以下に示す。(15)

=
文字列が等しい場合に、真を返す
!=
文字列が等しくない場合、真を返す
-eq
数値が等しい場合に、真を返す
-ne
数値が等しくない場合に、真を返す
-d
ディレクトリの場合に、真を返す
-e
ファイルが存在する場合に、真を返す
-z
文字列の長さが0の場合に、真を返す。シェル変数に値がセットされているか否かを調べるときに使用する。
-n
文字列の長さが0でない場合に、真を返す
  1. [および]はコマンドであるため、expr_listとの間に空白を入れる必要がある。

6.5.3 for文

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

6.5.4 case文

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

(16)

  1. getoptは、例えば"tar -cvbf 20 /dev/rmt ."を"tar -c -v -b 20 -f /dev/rmt ."に変換してくれるので、case文の引数チェックを簡単に実現することができる。

6.5.5 while文

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

6.5.6 関数定義

よく使う処理を関数として定義することによって、コマンドやシェルスクリプトと同様に扱うことができる。

name() {
  block_stmt;
}

関数名nameの後に()を付けて、処理を中括弧{}で括ることで関数が定義できる。関数への引数は、位置変数$nで取得することができる。環境変数およびシェルスクリプト中のシェル変数は、関数内でも参照することができる。ただし、関数内部で定義されたシェル変数は、その関数内部でのみ参照することができる。

6.6 その他の便利な機能

6.6.1 heredocument

heredocumentを日本語で何と言えばよいか分からないが、スクリプト中にコマンドへの標準入力を記述できる機能と言えば、理解できるだろうか。heredocument中のシェル変数は、置換可能であるので、テンプレートファイルとして使用したり、awk等のプログラムを一時的に作成する場合に使用する。

heredocumentは、<<の後に終端文字列を指定し、その終端文字列で始まる行までをheredocumentの入力とする。

heredocumentの例を以下に示す。

DATE=`date`
cat <<EOF
Today is $DATE.
EOF

とすると、

Today is Wed Mar 14 14:29:32 JST 2001. 

のように表示される。

6.6.2 出力の結合

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"を指定することで出力を結合することができる。

6.6.3 特殊ファイル/dev/null

出力内容を捨てたい時やファイルを空にしたい時に特殊ファイル/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. アスキー出版,