Yash: yet another shell

概要

このプログラムは C 言語 (C99: JIS X 3010:2003) で書いた簡単なコマンドライン端末用シェルである。

これはもともと筆者が学科の課題で作ったプログラムであり、bash などの本格的な シェルを目指して作ったものではない。機能は少なくバグも多いと思われるが、 シェルのソースを見てみたいが bash のソースは複雑過ぎて読めない というような人にとっては参考になるかもしれない。

このシェルは POSIX.1-2001 準拠シェルとなることを目指して開発中である。

筆者はこのプログラムの動作に関して保証をしないし、 一切責任をとらない。もし使いたいのであればそれを承知の上で すべて自己責任で使うこと。

このプログラムは GNU General Public License (Version 2) の元で自由に再配布・変更などができる。

なお、yash という名前のシェルは他にも存在すると思われるが、特に関係はない。

要件

このシェルは任意の POSIX.1-2001 準拠システムでビルド・実行できるはずである。 ビルド・インストールのしかたの詳細は INSTALL ファイルを参照の事。

基本的な動作

プログラムを引数なしで起動すると、端末から一行ずつコマンドを読み取って 実行する対話的モードに入る。コマンドを入力すると、コマンドが実行され、 コマンドが終了すれば次のコマンドの入力を待つ。exit/logout コマンドを実行するか EOL (Ctrl-D) を入力すると、(未了のジョブがなければ) シェルを終了する。

シェルがログインシェルとして起動される (-l オプション付きで起動するか、 起動時のプログラム名が - で始まる) と、--noprofile オプションが ない限り、対話的モードに入る前に初期化ファイル ~/.yash_profile を読み取って、 ファイルの中に書いてあるコマンドを一行ずつ実行する。ログインシェルでない場合、 --norc オプションがない限り、対話的モードに入る前に ~/.yashrc を読み取って同様に実行する。この時実行するファイルは --rcfile filename オプションで変更できる。

起動時には、これらの他に次のオプションを指定できる:

--help
ヘルプを表示して終了する
--version
バージョン情報を表示して終了する
-l, --login
ログインシェルとして実行する。一部のコマンドの動作に影響する。
-i, --interactive
対話的モードを行う
-c command
コマンド command を実行して終了する。-i オプションより優先する。

対話的モードでのコマンドの読み取りは、GNU Readline ライブラリを用いている。 よって、大体 bash と同じように操作できる。option コマンドで履歴ファイルを 指定しておくと、シェルを起動・終了するときに履歴をファイルから読込み・保存する。

対話的モードでは、シェルのプロセスグループ ID はシェル自身のプロセス ID にリセットされる。シェルが停止・終了するとき、元のプロセスグループ ID に戻る。

コマンドの文法

コマンドの書式も大体 bash と同じであり、以下の記法 (記号) が使える:

n>file
ファイルディスクリプタ n に、ファイル file に書き込むためのリダイレクトを生成する。n を省略すると標準出力をリダイレクトする。
n<file
ファイルディスクリプタ n に、ファイル file から読み込むためのリダイレクトを生成する。n を省略すると標準入力をリダイレクトする。
n<>file
ファイルディスクリプタ n に、ファイル file に対する読み書き両用のリダイレクトを生成する。n を省略すると標準入力をリダイレクトする。
n<&m
ファイルディスクリプタ m をファイルディスクリプタ n にコピーする。n を省略すると標準入力にコピーする。
m- ならば、ファイルディスクリプタ n を閉じる。n を省略すると標準入力を閉じる。
n>&m
ファイルディスクリプタ m をファイルディスクリプタ n にコピーする。n を省略すると標準出力にコピーする。
m- ならば、ファイルディスクリプタ n を閉じる。n を省略すると標準出力を閉じる。
|
コマンドをパイプラインでつなぐ。 前のコマンドの出力を次のコマンドに入力する。
;
コマンドを順次実行する。前のコマンドが終わったら次のコマンドを実行する。
&
& の前に書かれたコマンドをバックグラウンドで起動する。
&&
&& の前に書かれたコマンドの終了ステータスが 0 の時のみ次のコマンドを実行する
||
|| の前に書かれたコマンドの終了ステータスが 0 でない時のみ次のコマンドを実行する
( ... )
括弧内のコマンドをサブシェルで実行する。
#
これ以降をコメントとして無視する

リダイレクトの処理はパイプラインよりも後に行う。よって、リダイレクトと パイプラインの指定が重複するときはリダイレクトがパイプラインを上書きする。

演算子の優先順位は、| が最も高く、続いて &&||、そして最も低いのが &; である。括弧 ( ) は演算子の優先順位を変更するのにも使える。

| でつないだパイプラインにおいて、最初のコマンドの前にも | を指定すると、最後のコマンドの出力が最初のコマンドの入力に繋がれ、 ループ状のパイプラインができる。

括弧 ( ) で囲まれたコマンドはサブシェル (元のシェルから分岐した 子プロセス) で実行される。

外部コマンドは、PATH 環境変数で指定したパスから検索される。PATH が未設定の 場合はカレントディレクトリから検索する。パイプラインやサブシェルを使わずに直接 実行した外部コマンドは、フルパスがコマンドハッシュテーブルに登録され、二回目 以降は PATH を検索せずに直接実行する。Readline でコマンド名の補完を行ったときも フルパスが登録される。フルパスがハッシュテーブルに登録された後で新しいコマンドを インストールしたりすると、正しくコマンドが検索されないことがある。このような時は rehash 組込みコマンドでハッシュテーブルをリセットするとよい。PATH 環境変数を 変更したときもハッシュテーブルがリセットされる。

コマンドの始めに name=value の形式で 変数への代入を行うことができる。このとき変数は環境変数としてコマンドに渡される。 コマンドを指定せずに変数の代入だけ行うと、シェル変数に代入したことになる。 シェル変数は代入しただけでは環境変数としてコマンドには渡されない。 シェル変数を環境変数としてコマンドに渡すには、export 組込みコマンドを使う。 シェルが起動するときに起動元から渡された環境変数は、最初から export が適用された シェル変数となる。

特殊組込みコマンドを実行するときに変数代入を行うと、その変数は自動的に export され、元のシェル環境に残る。従って例えば HOME=/ . script.sh というコマンドは export HOME=/ してから . script.sh を実行するのと同じである。これ以外のコマンドでは、変数はコマンドに環境変数として 渡されるものの、元のシェルの変数は変わらない。

パラメータ

コマンドライン中の $ はパラメータ・変数の展開に使える。 例えば ${PWD} と書くとそれを PWD 変数の値に置換する。 また ${!VAR} と書くと VAR 変数の値を名前とする変数の値に置換する。 指定した変数が存在しなければ空文字列に置換する。

置換する文字列を操作するために、変数名と } の間に 特殊な書式の文字列を指定することができる。以下の書式が使える。

${name-word}
変数 name が未定義なら、空文字列の代わりに word に置換する。
${name+word}
変数 name が定義済なら、その値の代わりに word に置換する。
${name=word}
変数 name が未定義なら、word をその変数に 代入し、そしてその値に置換する。この記法では特殊パラメータや 位置パラメータには代入できない。
${name?word}
変数 name が未定義なら、エラーメッセージとして word を出力し、非対話的シェルならそのまま終了する。 word が空文字列の場合はデフォルトのエラーメッセージを出す。
以上の四つの書式では、-, +, =, ? の直前に : を置くと、変数が未定義の 場合だけでなく変数の内容が空文字列の場合にも同様の各動作を行う。
${#name}
変数 name の内容の文字数に置換する。変数が未定義なら 0 になる。
name が特殊パラメータ * または @ の場合は、${#} に同じ。
${name#pat}
変数 name の内容が pat で始まっているなら、 その部分を削除したものに置換する。pat は、ファイル名展開と同様に *, ?, [ が特殊な意味を持つ。 * に当てはまる文字数に複数の可能性がある場合、削除する部分が できるだけ短くなるようにする。
${name##pat}
${name#pat} とほぼ同じで、当てはまる文字数に 複数の可能性がある場合に削除する部分ができるだけ長くなるようにする点だけが 異なる。
${name%pat}
${name%%pat}
${name#pat}, ${name##pat} とほぼ同じで、変数の内容の始めの部分ではなく末尾の部分を削除する点だけが 異なる。
${name/pat/sub}
変数 name の内容で最初に pat に当てはまる部分を sub に置き換えたものに置換する。当てはまる部分はできるだけ長く なるようにする。sub が空文字列なら、sub の直前の / は省略できる。
${name/#pat/sub}
${name/%pat/sub}
${name/pat/sub} とほぼ同じで、 それぞれ変数の内容の始めの部分・末尾の部分だけを置換する点だけが異なる。
${name//pat/sub}
${name/pat/sub} とほぼ同じで、 最初に pat に当てはまる部分だけではなくて当てはまる部分全てを sub に置き換える点だけが異なる。
${name:/pat/sub}
変数 name の内容全体が pat に当てはまるなら sub に置換する。

以下の名前の特殊パラメータが実装済である。

*, @
全ての位置パラメータに展開する。
引用符の中でこのパラメータが展開されるとき、@ では位置パラメータごとに 単語分割を行う。* では他のパラメータに同じく、単語分割はしない (各位置パラメータは IFS 変数の最初の文字で区切られる)。 引用符の外では、* も @ も同じである。
#
位置パラメータの個数を表す十進数に展開する。
?
最後に実行したコマンドの終了コードを表す十進数に展開する。 コマンドがシグナルによって終了した場合は、(シグナル番号 + 384) となる。 kill -l $? を実行すると実際のシグナル名が分かる。
$
このシェルのプロセス番号を表す十進数に展開する。サブシェル内では 元のシェルのプロセス番号となる。
!
最後に起動したバックグラウンドジョブのプロセス番号を表す十進数に展開 する。ジョブがパイプになっている場合はパイプ内の最後のプロセス番号となる。 ジョブが && または || による条件付き実行を 指示されている場合など、サブシェル環境で実行される場合は、サブシェルの プロセス番号となる。
0
シェルを起動する際にコマンドライン引数でシェルスクリプトファイルを与えて シェルスクリプトを実行させているときは、そのシェルスクリプトのファイル名に 展開する。対話的シェルではシェルを起動するときに与えられた実行ファイル名に 展開する。

ジョブ制御

パイプラインでつながった一連のコマンド群をジョブという。ジョブは ジョブ番号とプロセスグループ ID を持つ。プロセスグループ ID はジョブ内の最初のコマンドのプロセス ID に等しく、ジョブ内の全てのプロセスは そのプロセスグループに属する。ジョブ番号とプロセスグループ ID は jobs 組込みコマンドで確認できる。

対話的モードでは、端末からコマンドを読み取る前に自動的に jobs -n コマンドが実行され、ジョブの状態に変化があれば報告する。

組込みコマンド

組込みコマンドは以下の通り。(特殊) と書いてあるものは特殊組込みコマンドである。

:
true
何もしない。(終了ステータスは 0)
false
何もしない。(終了ステータスは 1)
exit (特殊)
シェルを終了する。-f オプションを付けない場合、 未了のジョブが残っていれば警告を表示し、終了しない。
logout
exit と同様だが、ログインシェルとして実行されているときしか使えない。
kill
プロセス (グループ) にシグナルを送る。-s signal オプションで送信するシグナルを指定する。省略すると -s TERM とみなす。 引数としてシグナルを送るプロセスの ID を指定する。 プロセスグループに属する全てのプロセスにシグナルを送るには、プロセスグループ ID に負号 - を付けたものを引数として指定する。引数に % を付けると、プロセス ID の代わりにジョブ番号を指定できる。
wait
ジョブの終了又は停止を待つ。引数を指定しないと、全ジョブを待つ。 wait %1 のようにしてジョブ番号を指定すると、そのジョブを待つ。 wait 1234 のようにしてプロセス ID を指定すると、そのプロセスが属する ジョブを待つ。
このコマンドを中止するには Ctrl-C を押して SIGINT シグナルを送ればよい。
suspend
シェルをサスペンドする (シェル自身に SIGSTOP シグナルを送る)。 ログインシェルとして実行されている場合、-f オプションを付けないと警告を表示し、サスペンドしない。
jobs
ジョブの一覧を表示する。-n オプションを付けると、 最近に実行状況が変化したジョブのみ表示する。-l オプションを付けるとジョブの 各プロセスのプロセス ID も表示する。 引数にジョブ番号を指定するとその番号のジョブのみ表示する。
disown
ジョブのプロセスを終了しないままジョブの登録を抹消する。 -a オプションを付けるか何もジョブを指定しないと、全てのジョブを対象とする。 -r オプションを付けると、実行中のジョブのみを対象とする。 -h オプションを付けると、登録を抹消するのではなく SIGHUP の再送を行わないようにする。(→シグナル)
fg
bg
ジョブをフォアグラウンド (fg) またはバックグラウンド (bg) で実行を再開する。ジョブ番号を引数として指定する。 省略すると最後に使ったジョブ (カレントジョブ) を再開する。 カレントジョブがないときはジョブ番号が最も大きいジョブを選択する。
exec (特殊)
このシェルのプロセスを引数として指定したコマンドで置き換える。-f オプションを付けない場合、未了のジョブが残っていれば警告を表示し、 プロセスは置き換えない。 -c オプションを指定すると、新しいコマンドは環境変数なしで実行される。(-c オプションなしでは、新しいコマンドはシェルの環境変数を受け継ぐ) -l オプションを指定すると、コマンドはログインシェルとして実行される。 -a NAME オプションでコマンドの main 関数に渡される最初の引数 (argv[0]) の値を指定できる。
コマンドを何も指定しない場合、プロセスは置き換わらない。 コマンドを指定せずにリダイレクトだけ指示すると、シェル自身のプロセス内で リダイレクトが開かれる。このリダイレクトは、今後シェルで起動する全ての コマンドに受け継がれる。
exec コマンドはパイプラインの中で使っては効果がない。
type
指定した引数は組込みコマンドなのか、外部コマンドなのか、エイリアスなのか ……といった情報を表示する。
cd
作業ディレクトリを変更する。引数を省略すると環境変数 HOME の値に変更する。このコマンドは、環境変数 PWD, SPWD, OLDPWD の値も変更する。 PWD と SPWD には新しい作業ディレクトリのパスが入る。OLDPWD にはひとつ前の 作業ディレクトリのパスが入る。
umask
umask を表示・変更する。引数なしだと umask を表示する。 引数に八進数を指定するとそれに umask を変更する。
export (特殊)
環境変数を設定する。引数にシェル変数名を指定すると、そのシェル変数が それ以降起動するコマンドに環境変数として渡されるようになる。引数に、NAME=VALUE の形式を指定すると同時に変数の値を代入できる。 -n オプションを付けて環境変数の名前だけ指定すると、その変数をそれ以降 起動するコマンドに環境変数として渡さないようにする。
このコマンドには環境変数を表示する機能はない。適宜 env 外部コマンドなどを使用すること。
unset
シェル変数を削除する。引数としてシェル変数名を指定する。
. (特殊)
source (特殊)
引数に指定されたファイル名のファイルを読み込んで、 中に書いてあるコマンドを実行する。ファイル名が / を含んでなければ、 ファイルは作業ディレクトリではなく PATH で指定したディレクトリから検索する。 ファイル名の後ろに指定した引数はファイルの内容を実行する際の 位置パラメータとなる。
eval (特殊)
引数をコマンドとみなして実行する。
history
履歴を操作する。引数なしだと、現在残っている全ての履歴を表示する。 数値を引数に指定すると、最近の履歴をその数だけ表示する。 その他、以下のオプションによって各種操作が可能。
-c
履歴を全て削除する。
-d n
履歴番号 n の履歴一件を削除する。
-r file
指定したファイルから履歴を読み込む。
-w file
指定したファイルに履歴を上書き保存する。
-s arg...
arg... を履歴に追加する。 この時、この history コマンド自身は履歴に残らない。
-r/-w オプションでファイルを指定しないと、histfile オプションのファイルを使う。
alias
エイリアスを設定・表示する。エイリアスは alias name=value のようにして設定する。設定してあるエイリアスを表示するには、 エイリアスの名前だけを引数に指定する。引数を一つも指定しないと全エイリアスを 表示する。引数は一度にいくつでも指定できる。エイリアスを設定する際に -g オプションを指定すると、それはグローバルエイリアスとなる。 グローバルエイリアスは、コマンド名に限らずコマンドライン上の任意の位置で 置換される。
unalias
引数で指定した名前のエイリアスを削除する。 -a を指定すると全エイリアスを削除する。
hash
引数無しだと、コマンドハッシュテーブルの内容を全て表示する。 -r オプションを付けると、コマンドハッシュテーブルの内容を空にする。 コマンド名を引数として与えると、そのコマンドをコマンドハッシュテーブルに 登録する。
rehash
hash -r に同じ。
option
シェルのオプションを設定する。例えば option histsize 300 とすると histsize オプションの値が 300 に設定される。-d オプションを用いて option -d histsize のようにすると、オプション設定がデフォルトに戻る。値を指定せずに option histsize のようにすると、オプションの現在値を表示する。
設定できるオプションは以下の通り:
histsize
シェルが記憶するコマンド履歴の数 (デフォルト: 500)
histfile
コマンド履歴を保存するファイル名 (デフォルト: なし)
histfilesize
ファイルに保存するコマンド履歴の数 (デフォルト: 500)
ps1
コマンドを入力するときのプロンプト
promptcommand
毎回プロンプトを出す前に自動的に実行するコマンド
huponexit
yes なら、シェルの終了時に全てのジョブに SIGHUP シグナルを送る。 (デフォルト: no)
このコマンドは将来廃止予定である。

組込みコマンドは基本的にシェルのプロセス内でそのまま実行するが、 パイプラインや & 記号を使ったときはサブシェル内で実行する。 この場合、一部の組込みコマンドは期待した働きをしないことがあるだろう。

シグナル

このシェルは、状況にかかわらず SIGQUIT, SIGTERM, SIGTSTP, SIGTTIN, SIGTTOU シグナルを常に無視する。wait 組込みコマンド実行中以外は、SIGINT も無視する。

シェルが SIGHUP を受け取ると、全てのジョブに対して SIGHUP を転送する。 停止しているジョブには SIGCONT も送る。その後シェル自身に SIGHUP を送り直して自滅する。

magicant (magicant.starmen@nifty.com)