rc - the Plan9 shell -
履歴- 2016/05/12 update
- 2005/05/03 update
- 2005/04/28 update
- 2002/09/02 update
- 2001/07/11 update
- 2001/07/09 update
- 2000/03/04 update
- 1998/08/28
特徴
以下に思い付くままに rc の特徴を挙げる。- 言語 C に似ている
- パターンマッチングが強力
- 分かりやすく易しい
- シェル変数(環境変数)が配列化されて強力
- 安全性への配慮がされている
- 小さくてシンプル
rc は 言語 C に似ている
rc は言語 C に似ている。csh が C に似ている以上に似ている。C の様に
if( ){ } while( ){ }
例えば次の様に書く。
#!/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
譜 dup
ここに cp
の -x
オプションは日付とアクセスモードを保存するオプションである。 dup
はまず foo.orig
が存在しない事を確認する。もし存在すれば、その事を指摘し、中断する。さらに dup
は複数のファイルを処理する。
if [ ... ]; then
の [ ]
は Bourne shell の構文の一部ではない。記号 [
はコマンド名であり、この変なコマンドは test
へのリンクである。[
コマンドは最後の引数に ]
を要求し、]
に続く ;
は [
コマンドのセパレータである。[ ]
の中は一つ一つがコマンドの引数で、if [ "$a" = "$b" ]; then
if(~ $a $b){}
~
はコマンドではない。)
rc は文字列のマッチング演算が使い易い
rc では if 文や while 文の( )
の中でマッチング演算子 ~
を使用できる。例を挙げよう。#!/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 では実はこれらは
a=(alice)
複数の要素を扱う時には丸括弧を使う。
u=(alice bob carol) u=(alice (bob carol))
u(1), u(2), u(3)
しかし上の
u
はv='alice bob carol'
'alice bob carol'
だけを持つ配列でv=('alice bob carol')
X
を shell 変数とする時、配列の要素の個数は $#X
で表し、配列の値は $X
で表す。
また配列の要素 1 の値は $X(1)
で表す。
また shell 変数の取り消しは
X=()
シェル名 | 代入法 | 要素の個数 | 変数の値 | 変数の要素の値 |
---|---|---|---|---|
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] |
部分配列
2016/05/12
rcの部分配列の範囲は "-
" を使って表現する。例えば
term% a=(alice bob carol david) term% echo $a alice bob carol david term% echo $a(1-3) alice bob carol term% echo $a(2-3) bob carol term% echo $a(2-) bob carol david term%
変数 *
また $*
は引数の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
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}
`{....}
{ }
内のコマンドの実行結果を意味する。( 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
ifs='&' q=`{echo -n $QUERY}
$q
の内容は('alice=18' 'bob=20')
for(x in $q){ ifs='=' y=`{echo -n $x} $y(1)=$y(2) }
alice
と bob
には各々 18
と 20
が代入される。このことは実際にecho $alice echo $bob
eval
を使わなくてもよいのである。QUERY='mail someone</etc/passwd;alice=16&bob=20'
'mail someone</etc/passwd;alice'
18
が代入されているのである。この事は
echo $'mail someone</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
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]
プログラム 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
echo one>a echo 1>a
2つのプログラム foo
と bar
の出力を比較したい場合には rc では
cmp <{foo} <{bar}
<{program}
さて 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 { .... }
function
を省く形も許している。関数引数は何れも$1
, $2
,... あるいは全て纏めて $*
で参照できる。
局所変数 x
, y
が必要な時には
bash: foo(){ local x y; .... } bash: function foo(){ local x y; .... } rc: fn foo { x=x0 y=y0 { .... }}
x0
と y0
は初期値であり、必ず与えなければならない。
筆者は局所変数を bash 流にキーワード local
で指定する方が好きである。では rc は何故 local
を導入しなかったのであろうか? rc の設計者は不要なものは作らない主義なのである。実際、 rc における関数の局所変数は以下のように自然にでてくる。
次のコマンドを sh, bash, rc で実行してみよう。
a=aaa b=bbb; a=alice b=bob echo $a $b; echo $a $b
sh: aaa bbb alice bob
bash: aaa bbb aaa bbb
rc: alice bob aaa bbb
{ }
{
と }
は (
と )
と同様にコマンドのグループ化記号である。( ... )
はサブシェルで実行するが { ... }
はそうではない。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
rc は小さくても sh と同等あるいはそれ以上の機能を持っている。
なお rc の UNIX 版が存在する。
最初の2つは今となっては古い。Russ の Plan 9 Port が推奨される。ここには Plan 9 で生まれた多数の UNIX 用ソフトウェアが含まれている。
rc は sh と同様、履歴機能を持たない。Plan 9 ではこのことは全く問題にならない。Plan 9 では過去に現れた入出力データを再利用する問題はシェルではなくてウィンドウが担っているからだ。しかし Unix ではつらい。スクリプト用として使えば良いであろう。
rc は sh と同様に外部コマンドでやって行けるものは内部で処理されない。(rc の read
は外部コマンドである。sh と異なり外部コマンドでやって行ける仕様だから。)
read
や echo
の様に頻繁に使うコマンドを外部処理すれば速度が犠牲になる。これは哲学の問題である。筆者は美しさよりも処理速度の方が気になり、「read
や echo
は内部の方が...コードの増加はたかが知れている...」と思う方である。もっとも rc はこのままでも軽快に使える。
次の引用は bash のマニュアルからである。
It's too big and too slow.
文献
[1] Lowell Jay Arthur 著, 伊藤正安監訳、千吉良英毅他訳「UNIX シェルプログラミング」(オーム社、1993)[2] G.Anderson, P.Anderson 著, 落合浩一郎・大木敦雄訳「UNIX SHELL フィールドガイド」(パーソナルメディア、1987)
[3] 砂原秀樹、石井秀治、植原啓介、林周志共著「プロフェッショナル シェルプログラミング」(アスキー出版局、1996)