UNIX 講習会 (3)

目次へ

Last modified: Tue Jun 19 22:14:04 2001

2001/5/8


3. シェルの使い方 (1)

前回、シェルはログインして最初に立ち上がるプロセスであり、 ユーザのプロセスはすべてここから起動すると説明した (図1)。

  1. ユーザがログインする。
  2. UNIX がそのユーザ所有のシェルを起動させる。
  3. ユーザは、そのシェルからいろんなプロセスを起動して仕事する。
  4. ユーザがシェルを終了させる (exit)。
  5. 2. で起動したシェルが終了すると、UNIX はそのユーザを ログアウトさせる。


図1・UNIX のプロセス木 [再掲]

シェルはべつに特別なプロセスではない。Windows や Mac では、 シェル (Explorer や Finder) の動作はほとんどシステムと統合されており、 ユーザはシェルの存在をあまり意識する必要がなかった。 しかし、UNIX では「シェルもひとつのプロセス」ということを常に頭に 入れておく必要がある。

また、Windows や Mac では、シェルはグラフィカルなものだった。 しかし UNIX でメインに使われているのはテキストベースのシェル、 つまりユーザが言語によって命令を与えるタイプのシェルである。 したがって、UNIX におけるシェルは言語インタプリタの一種である。

UNIX のシェルには 2つの系統があり、これらは互いに文法 (と機能) が すこしちがう。

子プロセスと終了状態

UNIX では、じつは「子プロセスの起動」は関数呼び出しとみなせる。

% ls -l /
ls(-l, /)
という2引数の関数呼び出しと考える。 関数だから、当然返り値をもたねばならない。

じつはプロセスは終了するとき、親プロセスにシグナルを送り、 そのプロセスの関数返り値としてある整数を送ることができる。 これをプロセスの 終了状態 という。

次のような状況を考えてみよう (図 1)。


図1・プロセスの呼び出し

このような状況の場合、プロセス C の終了状態はプロセス B に 渡される。プロセス B の終了状態はプロセス A に渡される。 これは以下のようなプログラムに似ている。

function A(x,y) {
  ...
  B(x) を呼び出す。
  ...
}

function B(x) {
  ...
  C( ) を呼び出す。
  ...
}

function C( ) {
  ...
}

子プロセスは親に終了状態を渡して、はじめて本当に「死ぬ」ことができる。 すでに終了して(死んで)いるのに、まだ親にその返り値を受けとって もらっていないプロセスをゾンビという。これは ps すると <defunct> と表示される。親あるいは init がその返り値を受けとらない かぎり、そのプロセスはメモリ上から消えない。

終了状態の意味は、そのプログラムによって違う。 しかし UNIX では慣例として、終了状態 0 はそのプログラムが成功裏に 終了したことを表すことになっている。それ以外の値は、なんらかの理由で うまくいかなかったことを表している。直前のコマンドの終了状態は、 (あとで説明するけど) echo $? というコマンドによって見ることが できる。

% ls /
% echo $?
0 (ls は成功)
% ls oaeirjfoearjf
ls: oaeirjfoearjf: No such file or directory
% echo $?
1 (ls は失敗)
% echo $?
0 (echo は成功 - もう変わってしまっている)

C 言語でのインターフェイス:

UNIX では、基本的なプログラムはすべて C 言語で書かれる。 UNIX における C の main() 関数呼び出しは、プロセスの 実行と密接に関係している。

UNIX における C プログラムの例:

#include <stdio.h>

int main(int argc, char *argv[])
{
  int i;
  printf("argc=%d\n", argc);
  for(i = 0; i < argc; i++) {
    printf("argv[%d]=%s\n", i, argv[i]);
  }
  return(0);
}

C の main 関数は int argcchar *argv[] という 引数をとり、かならず int 型の返り値をもつことが決められている。 ここで main に渡される argv[] がシェルからプロセスに渡される 文字列引数であり、main の返り値がシェルに返される終了状態となっている。

プロセスへの引数

さて、以上のことから、プロセスとはあたかもひとつの「関数」のように みなせることがわかった。シェルはプロセスを起動するときに、その 引数をわたす。シェルの最初の引数 (UNIX 的には 0 番目の引数とよぶ) だけは 特別で、これは呼びだす関数 (=プログラム) の名前になっている。

% ls        -l        /
  プログラム名  1番目の引数  2番目の引数

シェルはスペースで区切られているものをひとつの引数とみなす。 しかし、引数として「スペース」をふくんだ文字列を渡したいときは どうするのか? たとえば「this is a pen」という文字列を「ひとつの引数」として 渡すことはできるのだろうか?

ふつうにやると、これは 4つの引数があることになってしまう。

% ls        this       is        a      pen
  プログラム名  1番目の引数 2番目の引数 3番目の引数 4番目の引数
this: No such file or directory
is: No such file or directory
a: No such file or directory
pen: No such file or directory

この場合は引数 '〜' で囲むと、囲んだところは ひとつの引数として扱われる。

% ls        'this is a pen'
  プログラム名  1番目の引数

あるいは \ (バックスラッシュ) を使って、スペースを 区切り文字ではなくただの文字として扱うこともできる。これを「スペースの機能を エスケープ (escape, 免除) する」と呼ぶ。

% ls        this\ is\ a\ pen
  プログラム名  1番目の引数

これを使うと、たとえばスペースの入ったファイル名だって作れる。

% cp /etc/motd 'this is a pen'
% ls
this is a pen (←ひとつのファイル名)

もっとこわいこともできる。

% cp /etc/motd  ' '
% ls
	(← 空白のファイル名)

問: こうすると、ファイル名にはどんな文字でも使えるように見える。 しかし UNIX では、実際には 1つだけファイル名に使えない文字が存在する。 それはなにか。

引数の展開

引数にある文字を含ませると、シェルは その文字を特別に解釈し、ひとつの引数を複数の引数に展開する。 このような特殊文字をふくんだ文字列を「パターン (pattern)」と呼ぶ (ちなみに、特殊文字でないふつうの文字は「リテラル (literal)」と呼ぶ)。 引数の展開をうまく使えるようになると、作業がぐっと楽になる。 正規表現とは微妙にちがうので注意。

たとえばカレントディレクトリに、 以下のようなファイルがあるとする。

*
0文字以上のファイル名あるいはディレクトリ名に一致する。 一致したファイル名はリストに展開される。 この場合、このパターンは次のように展開される。

* (カレントディレクトリのすべての項目)
LOCAL Mail ar bin pub tmp work

*a* (カレントディレクトリで a が含まれている項目)
Mail ar

/etc/m* (/etc/の m で始まるすべての項目)
/etc/magic /etc/mailcap /etc/mime.types /etc/minicom.users /etc/minirc.dfl /etc/modules.conf /etc/motd /etc/msgs /etc/mtab /etc/mtools.conf

*/* (カレントディレクトリの下のすべてディレクトリの 下のすべての項目)
Mail/ADDRESS Mail/admin Mail/djb Mail/draft Mail/inbox Mail/m Mail/outbox Mail/p Mail/postponed Mail/ref Mail/ssh ...

?
ファイル名あるいはディレクトリ名中の 任意の1文字に一致する。一致したファイル名はリストに展開される。

t?p (tとpの間に1文字ある項目)
tmp

??? (3文字の項目)
bin pub tmp

??????? (7文字の項目)
エラー (該当する項目がない)

[ ]
この中に入っている文字のどれか1文字に一致する。 一致したファイル名はリストに展開される。

[abc]r (abc のどれかで始まり、つぎに r がくる項目)
ar

[a-p][a-p][a-p] (a〜p のどれか 3文字からなる項目)
bin

これを試すためには echo を使う。これはシェルの内部コマンド (シェルの内部だけで処理する、子プロセスをあらたに起動しないコマンド) で、 与えられた引数をたた表示するだけのもの。でもこれを使うと、 引数がどのように展開されるか確認できる。

% echo *a*
% echo /etc/m*[a-z]*
% echo */*/*
% echo /etc/m???

問: 以下のコマンドを展開すると引数はいくつになるか。

% echo *b*

問: 次のコマンドは何をするものか。

% chmod 755 *
% cp work/* .
% cp *
% cp work/'*' .

問: なぜ、chmod は

chmod パーミッション ファイル名
で、
chmod ファイル名 パーミッション
じゃないのか。

問: 「2桁の数字.txt (23.txt など)」というファイル名をすべて 削除するにはどう入力すればよいか。

問: 「1〜3桁の数字.txt (0.txt, 198.txt など)」というファイル名をすべて 自分のホームディレクトリにコピーするにはどう入力すればよいか。

注意してほしいのは、上のパターンは「存在するファイル名にだけ展開される」 こと。パターンに一致するファイル名がひとつもないと (c シェルでは) エラーになる。このほかにもファイル名の存在とは 関係なく次のようなパターンが使える。

{ }
カンマで区切ったものをすべて展開する。該当する ファイル名が存在しているかどうかは関係ない。

{a,b,c}x
→ ax bx cx

hogehoge{a,b,c}{,2}
→ hogehogea hogehogea2 hogehogeb hogehogeb2 hogehogec hogehogec2

これを使うと、ファイルのコピーや移動のときに少ないタイプですむ。 有効に使うべし。

問: veryverylongfilename.txt というファイルを、 末尾に .bak という文字をつけたファイル名でバックアップしておきたい。 なるべく少ないタイプ数でタイプするとしたら、どう入力するか。

問: veryverylongfilename1.txt というファイルを quiteshortfilename1.txt というファイル名に変えたい。どう入力するか。

変数の展開

じつはシェルには変数がある。これは set コマンドで 設定でき、「$なんとか」によって展開できる。 変数はいくらでも設定できる。

% set a=work
% ls $a   ($a → work に展開される)
% echo $a

変数の内容を書きかえることもできる。 "〜" で囲まれた文字列は '〜' のときと 似ているが、中の $ 記号 (変数) は展開される。

% set a="$a pub"
% ls $a   (引数は 2つ)
% ls "$a" (引数は 1つ)

ただ単に「set」だけを実行すると、いま現在定義されている すべてのシェル変数の一覧が表示される。

次のものは特別な変数である。

prompt
現在のシェルのプロンプト文字列が入っている。
$$
そのシェルのプロセス ID に展開される。
$?
直前に実行が終了したコマンドの終了状態に展開される。
$!
直前にバックグラウンド状態になったコマンドのプロセスIDに展開される。

環境変数とシェル変数

前回、プロセスにくっついているものとして 「環境変数」があった。これはシェルではなく、各「プロセスに」 くっついている変数である。これはいろいろな情報 (たとえばホームディレクトリとかデフォルトのプリンタ名とか) を 親プロセスから子プロセスへと継承してくために使われる。

環境変数はふつう大文字のみの変数名になっている。 シェルでは、環境変数もシェル変数と同様に $ で展開できる。 しかし設定するコマンドは別。

% echo $USER
% echo $HOME
% echo $PRINTER

だいじな環境変数として、PATH がある。 これはシェル変数 path とつねに同期しており、 コマンドを検索するのに使う。

% echo $PATH
% echo $path

環境変数を変えるには setenv を使う。

% setenv HOGE aaa
% kterm

% echo $HOGE
aaa (受けつがれている)

プロセス固有の入出力機能

ところで、プロセスは自分の入出力すべき端末をどうやって知るのだろうか。 つまり、ある kterm で起動したプロセスは、なぜその kterm のみに 文字を表示し、別の kterm には表示しないのだろうか?

実は、2章で挙げた以外にもプロセスにくっついているものがある。

各プロセスは、ファイルに読み書きするのと同じように、 標準入力や標準出力を読み書きできる。ただしこれらはファイルそのものではなく 「ファイルに結びつけられているポインタ」であるところが違う。 また、これらは切り替えることができる。

端末からプログラムを起動したとき、これらは最初 すべて端末に結びついている。これによってプロセスは端末 (ユーザのキーボードによる入力) から文字を読みこんだり、 端末に出力したりすることができる (図2)。


図2・端末と標準入出力 カレントディレクトリと同様、標準入力や標準出力も各プロセスごとに 独立している。これを変更できるのは自分のプロセスだけ。 けれども新しくプロセスが作られるときは親と同じものを受けつぐ。

ブロック型ファイルとキャラクタ型ファイル:

「端末に文字を読み書きする」という言いかたには 違和感を覚えるかもしれない。UNIX の特徴は「なんでもファイルとして 扱える OS であることである。つまり、UNIX では、あらゆる入出力装置 (ディスク、端末(ディスプレイとキーボード)、ネットワーク、スピーカそして メモリなど) はファイルとして抽象化されている (このような抽象化を おこなうのが OS の役割であることを思い出そう)。

しかし、どうやっても端末が通常の意味でのファイルとは違うところが ある。端末は記憶装置ではないため、端末に対する入出力は 「その時点でしか」意味をもたない。ファイルへの入出力とは、 ディスク上のある一定範囲にわたって溜まっている静的なデータを 変更することであるのに対し、端末への入出力はつねに変化する データを読みとったり変えたりすることである。このような違いのため、 UNIX ではファイルを 2つの種類に分類している。ひとつは ある程度の大きさをもち、その中の好きな場所を自由に読み書きできる (これを「シーク (seek) が可能である」とか 「ランダムアクセス (random access) が可能である」と呼ぶ)。 「ブロック型」ファイル。通常のファイルはこれにあたる。 もうひとつは端末やネットワーク、スピーカなどの「キャラクタ (ストリーム) 型ファイル」で、これは時系列で変化し、その瞬間のデータにしか アクセスできない。一度読みこんでしまったものをもう一回読みこんだり、 これから読みこまれるべきデータを変更する機能はサポートされていない。 したがって、一般に標準入力や また、後で述べる「パイプ」もこのような キャラクタ型ファイルの一種である。

プロセスの stdin, stdout, stderr を変える

シェルを使うと、プロセスを起動するときに標準入力や標準出力を 別のものに切りかえることができる。

% ls > output1 (標準出力をファイル output1 に切りかえる。)
% sort < output1 (標準入力をファイル output1 に切りかえる。)


図3・標準出力をファイル output1 に切りかえる。

/dev/null は特殊なファイルで、つねに空。 ここに出力することは、捨てるのといっしょ。


図4・標準出力を /dev/null に振りわける。

ちなみに以下の方法は B シェルでのもの。 ふだんみんなが使っている C シェルでは、こういう機能は 不完全にしかない (ので、プログラミングのときは B シェルを使え、ということ)。

> ファイル名
標準出力を指定されたファイル名に振りわける。
2> ファイル名
標準エラー出力を指定されたファイル名に振りわける。
>> ファイル名
標準出力を指定されたファイル名に追加する。
< ファイル名
標準入力を指定されたファイル名から振りわける。

はまり例:

% sort > output1 < output1

さて、「あるプロセスの標準出力を別のプロセスの標準入力に 通したい」というとき、いちいち中間ファイルを作るのはめんどくさい。 これを解決するのがパイプである。


図6・パイプを使う

% ls | sort

C シェルのコマンド

内部コマンド (C shell)

set 変数名=値
echo
set
exec
setenv
unsetenv
alias
source
rehash

シェルの設定

set path = ( ~/bin /usr/local/bin/stwmutils /usr/local/bin /usr/ucb \
	/usr/bsd /usr/bin /usr/bin/X11 /bin /usr/local/bin/mh /usr/sbin )

setenv	PRINTER		pst

set prompt = "`whoami`@`hostname`% " # Prompt string

complete

alias	ls	'ls -F'
alias	ll	'ls -l'
alias	g	'grep -i'

^ front   新山 祐介 euske@cl.cs.titech.ac.jp