お題:"test"コマンドの中身を追跡せよ。
シェルスクリプトで良く使われる"test"コマンド(="["コマンド)の仕組みを調べてみる。
※なお、今回はBNF(バッカス・ナウア記法)や字句解析・構文解析などをある程度既知のものとして、駆け足で通していきます。
これら前提とする知識については、「デーモン君のソース探検」を参照するなり、専門書(コンパイラ関係)やWikipediaなどを参照して下さい。
プログラム本体について調べてみる。
$ which test /bin/test $ which [ /bin/[ $ ls -l /bin/test /bin/[ -r-xr-xr-x 2 root wheel 48648 Sep 9 2002 /bin/[* -r-xr-xr-x 2 root wheel 48648 Sep 9 2002 /bin/test*
リンク数が2になっていることから、ハードリンクであることが予想される。i-node数を表示させてみる。
$ ls -i /bin/[ /bin/test 92192 /bin/[* 92192 /bin/test*
同じ、よってハードリンクであることが判明した。
なおmanについては、以下で参照出来る。
man 1 test
or
man 1 [
ソースコードの場所を調べる。
$ locate test (... 長すぎるのでもう少し絞ってみる ...) $ locate test.c ... /usr/src/bin/test/test.c ...
ソースコードの位置が判明したので、ソースの解析に進む。
まず冒頭からいきなりBNF(バッカス・ナウア記法)でtestコマンドで使える評価パラメータの文法が定義されている。
/* test(1) accepts the following grammar: oexpr ::= aexpr | aexpr "-o" oexpr ; aexpr ::= nexpr | nexpr "-a" aexpr ; nexpr ::= primary | "!" primary primary ::= unary-operator operand | operand binary-operator operand | operand | "(" oexpr ")" ; unary-operator ::= "-r"|"-w"|"-x"|"-f"|"-d"|"-c"|"-b"|"-p"| "-u"|"-g"|"-k"|"-s"|"-t"|"-z"|"-n"|"-o"|"-O"|"-G"|"-L"|"-S"; binary-operator ::= "="|"!="|"-eq"|"-ne"|"-ge"|"-gt"|"-le"|"-lt"| "-nt"|"-ot"|"-ef"; operand ::= <any legal UNIX file name> */
トップダウンでつなげれば、
oexpr -> aexpr -> nexpr -> primary
の順になり、primaryは次の4種類に分類される。
1. "unary"形式 : "-r ファイル名" など、一つの値について評価する形式 2. "binary"形式 : "値1 -eq 値2" など、二つの値を比較して評価する形式 3. "operand"形式 : "値" だけの形式(値の文字列長が0以上なら真) 4. "(" + ... + ")"形式 : 括弧でくくった中を優先評価する形式
このBNFに続き、字句解析で使うトークンの識別番号をenumで定義している。
enum token { EOI, FILRD, FILWR, ... BOR, LPAREN, RPAREN, OPERAND };
続いてトークンの形式を表すenumが定義されている。
enum token_types { UNOP, BINOP, BUNOP, BBINOP, PAREN };
そして演算子を表す構造体と、字句解析で使う実際の演算子(予約語)の定義。
static struct t_op { const char *op_text; short op_num, op_type; } const ops [] = { {"-r", FILRD, UNOP}, {"-w", FILWR, UNOP}, ... {"-a", BAND, BBINOP}, {"-o", BOR, BBINOP}, {"(", LPAREN, PAREN}, {")", RPAREN, PAREN}, {0, 0, 0} };
宣言・定義の締めくくりはファイル内グローバル変数の定義:
static char **t_wp; static struct t_op const *t_wp_op;
"t_wp"は文字列ポインタへのポインタ。"t_wp_op"は上で配列で定義された"ops"の内、現在処理中の演算子を表すポインタになっている。
その後main()まではプロトタイプ宣言が続くが、省略する。
main()関数は短く書かれている。まず戻り値用のint型変数が宣言される。
int res;
続いて"["として起動された場合は対になる"]"がargvの最後にあるかチェックする。
setprogname(argv[0]); if (strcmp(argv[0], "[") == 0) { if (strcmp(argv[--argc], "]")) error("missing ]"); argv[argc] = NULL; }
"t_wp"をargv[1]へのポインタ、つまり評価用パラメータの最初の1個を指すように初期化する。
t_wp = &argv[1];
ex:
$ test abc -o def → "t_wp" は "abc" へのポインタに初期化される
そしてBNFでの一番の根本、"oexpr"の分析に進む。
res = !oexpr(t_lex(*t_wp));
"t_lex()"関数はひとまず置いておいて、oexpr()の中を見てみる。
static int oexpr(enum token n) { int res; res = aexpr(n); if (t_lex(*++t_wp) == BOR) return oexpr(t_lex(*++t_wp)) || res; t_wp--; return res; }
BNFと重ねてみると、
oexpr ::= aexpr | aexpr "-o" oexpr ; = oexpr ::= aexpr | aexpr "-o" oexpr ;
で、
res = aexpr(n);
の部分が
oexpr ::= aexpr |
に当たり、続く
if (t_lex(*++t_wp) == BOR) return oexpr(t_lex(*++t_wp)) || res;
がBNFで後半の
aexpr "-o" oexpr ;
の処理に相当している。
この流れでBNFの通りに進んでいく。
oexpr() → aexpr() → nexpr() → { primary() | binop() | filestat() }
primary()/binop()/filstat()まで進めば、あとはそれぞれの評価が戻り値として返され、逆方向に伝播してmain()の
res = !oexpr(t_lex(*t_wp));
まで戻ってくる仕掛けになっている。
それぞれの関数の詳細まではここには載せない。冒頭でも書いたとおり字句解析・構文解析の知識があれば読みこなせる作りになっていると思うし、このメモ書きで特に書き留めておくようなトリッキーな処理は使われていない。
最後に t_lex() だけ見てお仕舞いとする。
static enum token t_lex(char *s) { struct t_op const *op; op = ops; if (s == 0) { t_wp_op = (struct t_op *)0; return EOI; } while (op->op_text) { if (strcmp(s, op->op_text) == 0) { if ((op->op_type == UNOP && isoperand()) || (op->op_num == LPAREN && *(t_wp+1) == 0)) break; t_wp_op = op; return op->op_num; } op++; } t_wp_op = (struct t_op *)0; return OPERAND; }
かなりシンプルな作りになっている。test.cは字句解析を行うにもかかわらず、比較的簡単で読みやすいソースになっている。
これは、コマンドライン引数が argv という形で、空白文字で分割済の状態で取り出せるおかげだと思われる。
このお陰でt_lex()も大幅に簡素化されている。t_lex()はトークンの識別番号を返すが、
struct t_op const *op; op = ops;
これでまず定義済の演算子トークンの先頭でループ用の変数を初期化し、続くwhileループで現在処理中の文字列が"-r"や"-ne"などの演算子にマッチする場合はそのトークン番号を返す。
whileループをまわりきってしまい、最後の
} const ops [] = { ... {0, 0, 0} };
まで到達した場合は、primary()での処理用に
t_wp_op = (struct t_op *)0; return OPERAND;
とする。
なお "t_wp" が "argv[1]" で初期化されているお陰で、「次のトークンに進む」が
++t_wp
と書ける。また「一つ先のトークン」(=トークンの先読み)も
t_wp[1]
と書くことができ、これもtest.cが読みやすい理由の一つになっている。
今回のお題については、ここまで。