address Logo

rc - the Plan9 shell -

目次

履歴

Plan9 の 標準 shell は rc である。rc は UNIX の Bourne shell に相当する Plan9 の shell である。以下の解説において、読者は Bourne shell に関して十分な知識を有していると想定されている。(unix でも rc の移植版が使用可能である。もっとも rc は Plan9 を想定して設計されているのだが ... )

特徴

以下に思い付くままに rc の特徴を挙げる。

履歴機能は持たない。Plan9 では履歴機能は shellではなく、ウィンドウマネージャがサポートする。

rc は 言語 C に似ている

rc は言語 C に似ている。csh が C に似ている以上に似ている。
C の様に

	if(  ){    } 
	while(  ){     } 

の構文が使えるのだ。
例えば次の様に書く。

	------------------- dup -------------- 
	#!/bin/rc 
	# coded by Kenar 
	# First we should check the validity of the operation 
	for(f in $*){ 
		if(test -e $f.orig){ 
			echo $f.orig already exist! 
			exit 
		} 
	} 
	for(f in $*) 
		cp -x $f $f.orig 
	-------------------------------------- 

ここに cp-x オプションは日付とアクセスモードを保存するオプションである。 dup はまず foo.orig が存在しない事を確認する。もし存在すれば、その事を指摘し、中断する。さらに dup は複数のファイルを処理する。

注釈: Bourne shell の if [ ... ]; then[ ] は Bourne shell の構文の一部ではない。記号 [ はコマンド名であり、この変なコマンドは test へのリンクである。[ コマンドは最後の引数に ] を要求し、] に続く ;[ コマンドのセパレータである。[ ] の中は一つ一つがコマンドの引数で、

	if [ "$a" = "$b" ]; then 

の様に空白に注意して書く必要がある。ちなみに rc では

	if(~ $a $b){} 

と書く。(~ はコマンドではない。)

rc は文字列のマッチング演算が使い易い

rc では if 文や while 文の ( ) の中でマッチング演算子 ~ を使用できる。例を挙げよう。

	------------------------ pack ---------------------------- 
	#!/bin/rc -e 
	fn usage { 
		echo 'pack source source ...' >[1=2] 
		exit usage 
	} 
	# 
	#   foo   -> foo.gz          in case of file. 
	#   foo   -> foo.tar.gz      in case of dir. 
	#   foo.tar -> foo.tar.gz    in case of file. 
	#   foo.tar.gz -> DO NOTHING in case of file. 
	# 
	#   code by Kenar 
	# 
	while (~ $1 -*){ 
		switch($1){ 
		case -* 
			usage 
		} 
		shift 
	} 
	while (! ~ $#* 0){ 
		x=$1 
		if (test -d $x ){ 
			tar -cf $x.tar $x 
			x=$x.tar 
		} 
		if (test -e $x){ 
			if(! ~ $x *.gz) gzip $x 
		} 
		shift 
	} 
	----------------------------------------------------------- 

この例の

	~ $1 -* 

では $1-* にマッチした場合に真となる。但し rc のマッチングは正規表現のマッチングではなくシェルスタイルのマッチングである。また $#*$* (コマンド引数の残り)の要素数を表し、

	! ~ $#1 0 

の中の `!' は否定演算子である。(つまりこの場合 $1 の長さが 0 ではない場合に真となる。)

switch 文の case ラベルでも Bourne shell と同様にパターンマッチングが使える。

switch 文のケースラベルにせよ、while 文や if 文のマッチング式にせよ、
パターンは複数指定できる。その場合

	switch($user){ 
	case alice bob 
		.... 
	case carol 
		.... 
	} 

の様にパターンは空白で区切る。

switch 文の書き方は Bourne shell や C-shell に比べて簡単になっている事に注意せよ。例えば 各 case の終わりに、

	;; 		# Bourne shell 


	breadksw	# C-shell 

を必要としない。

shell 変数は1次元の配列である

代入と参照

rc における shell 変数への代入法は Bourne shell と似ている。
変数 a へ 'alice' を代入するには、

	a=alice 

あるいは、もっと丁寧に

	a='alice' 

を実行する。(二重引用符 " は使用しない。rc では二重引用符は単なる文字である。)
rc では実はこれらは

	a=(alice) 

の省略形である。丸括弧は配列を表す。つまり rc の shell 変数は1次元配列なのである。

複数の要素を扱う時には丸括弧を使う。

	u=(alice bob carol) 
	u=(alice (bob carol)) 

これらは3つの要素を持つ同一の1次元の配列であり、各要素は、

	u(1), u(2), u(3) 

で表される。
しかし上の u

	v='alice bob carol' 

と同じではない。これは1つの要素 'alice bob carol' だけを持つ配列で

	v=('alice bob carol') 

と同じ意味である。

X を shell 変数とする時、配列の要素の個数は $#X で表し、配列の値は $X で表す。
また配列の要素 1 の値は $X(1) で表す。
また shell 変数の取り消しは

	X=() 

で行う。これは空の配列である。

注: shell 変数に関して rc は csh から多くを取り入れている。以下に両者の変数の設定法、要素数の表現、変数値の表現、要素の値の表現を比較してみる。(なお csh に関しては文献[2] が詳しい。)
シェル名 代入法 要素の個数 変数の値 変数の要素の値
rc u=(alice bob carol) $#u $u $u(1) $u(2) $u(3)
csh set u=(alice bob carol) $#u $u $u[1] $u[2] $u[3]

変数 *

また $* は引数の1次元配列の値を表している。従って rc では * は特殊な shell 変数に過ぎない。実際 rc では

	*=(alice bob carol) 

のように代入できる。$1 $2 ... は $*(1) $*(2) ... の省略形である。

whatis

shell 変数の値を表示し確認するのに echo では不十分である。実際、

	w=('alice bob' carol) 
	v='alice bob carol' 


	echo $w 
	echo $v 

によって共に

	alice bob carol 

を表示し、内部の構造が見えない。そこで rc の内部コマンド whatis が威力を発揮する。例えば、

	term% whatis w 
	w=('alice bob' carol) 
	term% wahtis v 
	v='alice bob carol' 

の様に内部構造が見える様に出力し、おまけにそのまま代入コマンドとして再利用できる。

配列から文字列への変換

u を配列 (alice bob carol) とする。このの要素を纏めて、文字列 'alice bob carol' として扱いたい場合がある。この場合には演算子 $" を使用する。

	v=$"u 

この逆問題に関しては後に解説する*。

* 「安全性への配慮」を見よ

配列の合成

	x=(alice bob) 
	y=(carol david) 

としよう。この下で

	z=(alice bob carol david) 

を得るには

	z=($x $y) 

を実行すればよい。特に x の要素に 'carol' を付け加えたいだけであれば

	x=($x carol) 

である。

演算子 ^

rc には文字列の連結のために結合演算子 ^ が用意されている。実行例を挙げよう。

	term% a=alice 
	term% b=bob 
	term% x=$a^$b 
	term% whatis x 
	x=alicebob 

これは文字列同士の単純な連結である。

配列と文字列の場合には

	term% c=(alice bob carol) 
	term% x=$c^.z 
	term% whatis x 
	x=(alice.z bob.z carol.z) 

即ち、数学的な言い方をすると、ベクトルとスカラーの積のような振る舞いをする。

2つの配列の場合には、ベクトルの内積のように振る舞う。但し配列要素の個数が異なるとエラーになる。

	term% d=('Miss ' Mr. Mrs.) 
	term% x=$d^$c 
	term% whatis x 
	x=('Miss alice' Mr.bob Mrs.carol) 

結合演算子 ^ は、これが無くても区切りが明確な場合には省略できる。
例えば $a^$b$a$b と書いてもよく、また $c^.z$c.z と書いてもよい。しかしながら、$c^z$cz と書く事はできない。(cz が変数名と見做される。)

shell 変数は連想配列である

rc では shell 変数は連想配列である。次のコードを実行して見よう。

	Jul=7 
	a=Jul 
	echo $$a 

この結果は
sh では 291a
bash では 10018a
となる。いずれもユーザにとって意味のない数字である。(たぶんシステムに依存するであろう)
rc ではちゃんと 7 になってくれる。
以下は筆者のサーバで使用されているスクリプトの抜き書きである。

	Jan=01; Feb=02; Mar=03; Apr=04; May=05; Jun=06 
	Jul=07; Aug=08; Sep=09; Oct=10; Nov=11; Dec=12 
	c=`{date} 
	yy=`{echo $c(6) | sed s/^20//} 
	mm=$$c(2) 
	mbox=9fans.$yy$mm 

このクスリプトの目標は date の出力、即ち、

	Mon Jul  9 07:55:40 JST 2001 

を基に

	mbox=9fans.0107 

を実行する事にある。

一時的な shell 変数

1つのコマンドに対してのみ使用できる shell 変数の使い方もある。

	term% name=alice 
	term% name=bob echo $name 
	bob 
	term% echo $name 
	alice 

即ち変数への代入の後にコマンドが来れば、その代入はコマンドに対してローカルであると見なされる。代入は

	name=alice age=18 echo $name $age 

のように複数あっても構わない。空白で区切る。
コマンドが複数の行に渡る場合には

	name=alice age=18 { 
		echo name age 
		echo $name $age 
	} 

のように、グルーピング記号 { } を使って処理できる。

安全性への配慮

shell 変数が1次元配列なので

	a=`{command} 

で変数 a への複数の要素への代入が可能となる。ここに

	`{....} 

{ } 内のコマンドの実行結果を意味する。( Bourne shell のスタイルで書くと `....` である。)
rc では {... } を使った shell 変数への代入に対しては(そしてこの場合にのみ) ifs (input field separator) が使用される。

	b=alice!bob!carol 
	ifs=! a=`{echo -n $b} 

を実行すると

	a=(alice bob carol) 

となるであろう*。但し、これでは ifs の値が変化する。従って副作用をさけるためには

	ifs_save=ifs 
	ifs=! a=`{echo -n $b} 
	ifs=ifs_save 

としてもよいが、

	ifs=! {a=`{echo -n $b}} 

の方が簡明である。

注意: echo-n オプションは改行を出力しない事を意味する。このオプションが無いと $a(3) は 'carol' にはならずに末尾に改行コードが付加されてしまう。

rc では ifs が利用される場面は Bourne shell に比べて厳しく制限されている。( Bourne shell の IFS は思わぬ落とし穴をもたらす事があり、セキュリティホールの原因になる。)

Web の QUERY でお目にかかる問題について考えよう。

	QUERY='alice=16&bob=20' 

変数 QUERY を基に

	alice=18 
	bob=20 

が安全に実行されるようにするにはどうするか? rc では次のように行えばよい。

	ifs='&' q=`{echo -n $QUERY} 

を実行すると $q の内容は

	('alice=18' 'bob=20') 

となる。そこで

	for(x in $q){ 
		ifs='=' y=`{echo -n $x} 
		$y(1)=$y(2) 
	} 

を実行すると alicebob には各々 1820 が代入される。このことは実際に

	echo $alice 
	echo $bob 

を実行して確認するがよい。危険な eval を使わなくてもよいのである。

	QUERY='mail someone&lt/etc/passwd;alice=16&bob=20' 

と書き換えられる事があっても

	'mail someone&lt/etc/passwd;alice' 

と言う名前の shell 変数が生成され、その変数に 18 が代入されているのである。
この事は

	echo $'mail someone&lt/etc/passwd;alice' 

を実行する事によって確認できる。

shell変数は自動的に環境変数になる

Plan9 の環境変数は /env に置かれている。環境変数 u へ値 alice を代入するには

	echo -n alice > /env/u 

を実行し、環境変数 u の値は

	cat /env/u 

で知る事ができる。
rc では shell変数は自動的に環境変数となる。

	u=alice 

によっても環境変数 u へ代入が行われる。また shell変数 u が存在しなければ、

	echo $u 

は環境変数の値を表示する。(従って $u が shell 変数 u の値であると言うのは正しくない。)

読者は shell 変数と環境変数が一致しない状況を

	u=alice 
	echo -n bob >/env/u 
	echo $u			# alice を出力する 
	cat /env/u		# bob を出力する 

を実行する事によって確認する事ができるだろう。

以上のルールは巧く働いている。
子プロセスへは shell 変数は引き継がれない。Plan9 では(UNIX と異なり)親子で環境変数を共有する事ができる。その場合も子プロセスにおける環境変数や shell 変数の変化は親プロセスの shell 変数に影響を与えない。shell の再帰的実行が巧く行くと言う訳だ。

rc の二重引用符の扱いはスクリプトを易しくしている

既に述べた様に、rc では二重引用符 " には特別の意味はない。ただの文字である。

Bourne shell では "' とは似ているが、"...." の中ではシェル変数は展開され、また` によってコマンドの実行結果を展開できた。しかし Bourne shell に於てもこの機能は要らないのである。

例えば Bourne shell では

	"alice $u carol" 


	'alice '$u' carol' 

は同じ結果を与える。同様に

	"alice `date` carol" 

と書く必要は無く、

	'alice '`date`' carol' 

でやっていける。即ち $` の展開は全て '...' の外で行うのである。

rc では二重引用符をシェルの制御から外された事によって awk などが非常に書き易くなった。例えば、Bourne shell のスクリプトの中でファイル名とデータ alice を引数 $1 , $2 としてawk に渡したい時には

	#!/bin/sh 
	awk "{if(\$4==\"$2\" && \$5!=\"*\") printf\"%s %4s %s %s\\n\", \ 
	\$1,\$5,\$2,\$3}" $1 

と書く。つまり \ のお化けができ上がるのである。
同じ事は rc では簡単に(そして読み易く)以下の様に書く事ができる。

	#!/bin/rc 
	awk '{if($4=='"$2"' && $5!="*") printf "%s %4s %s %s\n", \ 
	$1,$5,$2,$3}' $1 
注: この awk のスクリプトは次の問題を処理するためのものである。

以下の内容のファイルから第4項が alice の行をとりだしかつ第5項が * の行を捨て、そして項を $1,$5,$2,$3 の順に並べ変えよ。

	arisawa 95/08/24 20:27:21 alice  66% 
	arisawa 95/08/25 16:52:25 unix     * 
	arisawa 95/08/25 16:54:53 alice    * 
	arisawa 95/08/25 16:55:44 unix   50% 
	arisawa 95/08/25 16:57:12 alice  83% 
	arisawa 95/08/25 16:57:37 alice 100% 

分かりやすくなった I/O の切り替え

rc は Bourne shell と同様に(同じ方法で)

	> 
	>> 
	< 
	<< 
	| 

をサポートする。
もっと高度な I/O 切り替えになると Bourne shell とは多少異なる。
シェルスクリプトの内部でエラーメッセージを出力するには

	sh:		echo .... 1>&2 
	rc:		echo .... >[1=2] 
注釈: エラーメッセージは重要ではあるが csh ではできない。(そのための外部コマンドを準備せざるをえない。

プログラム foo の標準出力と標準エラーを共にファイル a へ出力したい場合には、

	sh:		(2>&1 foo) > a 
	sh:		(foo 2>&1) >a 
	rc:		{foo >[2=1]} >a 
	rc:		{>[2=1] foo} >a 

プログラム foo の標準出力と標準エラーを共に他のプログラム bar の入力として渡したい場合には、

	sh:		2>&1 foo | bar 
	sh:		foo 2>&1 | bar 
	rc:		foo >[2=1] | bar 
	rc:		{>[2=1] foo} | bar 

プログラム foo の標準エラーをファイル a へ出力したい場合には

	sh:		foo 2> a 
	rc:		foo >[2] a 

即ち UNIX のシステムコールである dup を伴う I/O 切り替えに関しては、rc の方が dup の行動を忠実に表現しているが、両者は同じ能力と見做して良い。但し最後に挙げた例は sh の紛らわしさを表している。

	echo one>a 
	echo 1>a 

sh では、この2つは良く似た事を行っているのではない。全く異なる構文規則に基づいているのだ。rc は sh のこの問題を嫌ったのであろう。

2つのプログラム foobar の出力を比較したい場合には rc では

	cmp <{foo} <{bar} 

を実行すれば良い。このように rc では

	<{program} 

を任意のコマンドの引数に指定できる。sh ではこれはできない。

さて rc では >[2] の自然な拡張として |[2] によってエラーメッセージだけをパイプに渡す事ができる。つぎはその実行例である。

term% ls aaa |[2] tee /tmp/x
ls: aaa: 'aaa' file does not exist
term% cat /tmp/x
ls: aaa: 'aaa' file does not exist
term% ls -l x |[2] tee /tmp/y
--rw-rw-r-- M 9 arisawa arisawa 35 May  3 00:23 x
term% cat  /tmp/y
term%

関数

rc の関数定義は bash と同様に局所変数を指定でき、再帰的に使用できる。
局所変数が現われない場合の sh, bash, rc の関数定義を比較しよう。

	sh:		foo(){ .... } 
	bash:		foo(){ .... } 
	bash:		function foo(){ .... } 
	rc:		fn foo { .... } 

bash は sh との互換性の為に function を省く形も許している。関数引数は何れも
$1, $2,... あるいは全て纏めて $* で参照できる。

局所変数 x, y が必要な時には

	bash:		foo(){ local x y; .... } 
	bash:		function foo(){ local x y; .... } 
	rc:		fn foo { x=x0 y=y0 { .... }} 

となる。ここに x0y0 は初期値であり、必ず与えなければならない。

筆者は局所変数を bash 流にキーワード local で指定する方が好きである。では rc は何故 local を導入しなかったのであろうか? rc の設計者は不要なものは作らない主義なのである。実際、 rc における関数の局所変数は以下のように自然にでてくる。

次のコマンドを sh, bash, rc で実行してみよう。

	a=aaa b=bbb; a=alice b=bob echo $a $b; echo $a $b 

結果は以下の様になる。(文献[1]には sh のこのような使い方が紹介されいる。しかしこの本を読む限り、この本の著者の sh と私の sh は異なると考えざるをえない。なお 実験した bash は 1.14.7 である)

	sh: 
	aaa bbb 
	alice bob 
	bash: 
	aaa bbb 
	aaa bbb 
	rc: 
	alice bob 
	aaa bbb 

入力のシンタックスは正しいのだが sh や bash ではこの場合の意味が定義されていない。rc では定義されており、シェル変数の代入のの後にコマンドが来た場合には、このシェル変数はこのコマンドに対して局所的であるとされている。そして rc では

	{ } 

はコマンドをグループ化する記号として定義されているので、関数の局所変数はこの応用にすぎない。

注: Bourne shell においても {}() と同様にコマンドのグループ化記号である。( ... ) はサブシェルで実行するが { ... } はそうではない。rc の { ... } もその点では Bourne shell と同じである。なお rc に於てサブシェルでの実行を指示するには { ... } の前に @ を付ける。

Bourne shell における { ... } は冷や飯を食わされている。活用されていないばかりか(文献[3])、

	(foo;bar) 

と書く事ができるのに

	{foo;bar} 

とはできず

	{foo;bar;} 

と書く必要がある。それでもマニュアルに載っているコマンドシンタックスは両者は同じである。

rc は小さくてシンプルだ

rc は小さいシンプルなシェルだ。大きさを Bourne shell と比較すると以下の通りである。

	name              rc             sh 
	size(bin)	102k           299k 
	size(src)	 90k           380k 

比較した sh は FreeBSD 2.2.5 添付のものである。(ドキュメントはサイズ計算に含めていない。)
rc は小さくても sh と同等あるいはそれ以上の機能を持っている。
なお rc の UNIX 版が存在する。

最初の2つは今となっては古い。Russ の Plan 9 Port が推奨される。ここには Plan 9 で生まれた多数の UNIX 用ソフトウェアが含まれている。

注意: 筆者の NEXTSTEP版の rc-1.7 は if 文が機能していない。従って switch 文で代用せざるを得ない。Linux 版は正しく働いているので移植の仕方が悪かったのであろう。(そのうち NEXTSTEP ユーザのために正しく移植したものをアップします。)

rc は sh と同様、履歴機能を持たない。Plan 9 ではこのことは全く問題にならない。Plan 9 では過去に現れた入出力データを再利用する問題はシェルではなくてウィンドウが担っているからだ。しかし Unix ではつらい。スクリプト用として使えば良いであろう。

rc は sh と同様に外部コマンドでやって行けるものは内部で処理されない。(rc の read は外部コマンドである。sh と異なり外部コマンドでやって行ける仕様だから。)
readecho の様に頻繁に使うコマンドを外部処理すれば速度が犠牲になる。これは哲学の問題である。筆者は美しさよりも処理速度の方が気になり、「readecho は内部の方が...コードの増加はたかが知れている...」と思う方である。もっとも rc はこのままでも軽快に使える。
次の引用は bash のマニュアルからである。

BUGS
It's too big and too slow.

文献

[1] Lowell Jay Arthur 著, 伊藤正安監訳、千吉良英毅他訳「UNIX シェルプログラミング」(オーム社、1993)
[2] G.Anderson, P.Anderson 著, 落合浩一郎・大木敦雄訳「UNIX SHELL フィールドガイド」(パーソナルメディア、1987)
[3] 砂原秀樹、石井秀治、植原啓介、林周志共著「プロフェッショナル シェルプログラミング」(アスキー出版局、1996)