Yash: yet another shell

概要

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

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

不具合の報告や機能改善の提案などは一応受け付けるが、期待しないこと。 筆者はこのプログラムの動作に関して保証をしないし、 一切責任をとらない。もし使いたいのであればそれを承知の上で すべて自己責任で使うこと。

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

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

要件

プログラムの中核部分は多くの Unix システムに共通のコードであるが、 付随的機能の実装に Linux 特有の機能を使用している。従って、このシェルを コンパイル・実行できるのは Linux だけだろう。

このプログラムをコンパイル・実行するには以下のものが必要である:

コンパイル

yash をコンパイルするには単に make を実行するだけである。 添付の Makefile には install ルールは定義していないので、 もしインストールする場合は /usr/local/bin 辺りに手動でコピーすること。

Readline ライブラリのリンクに関するエラーが出る場合は、Makefile の LDFLAGS 変数の設定で -ltermcap を -lncurses や -lcurses に変えてやってみると うまくゆくかもしれない。環境によっては -ltermcap がなくてもうまくいくかもしれない。

基本的な動作

プログラムを引数なしで起動すると、端末から一行ずつコマンドを読み取って 実行する対話的モードに入る。コマンドを入力すると、コマンドが実行され、 コマンドが終了すれば次のコマンドの入力を待つ。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 に戻る。

対話的モードで毎回入力の読み取りに入る前に、コマンド stty -inlcr -igncr icrnl icanon echo echoe echok の実行と同様にして端末がリセットされる。

コマンドの文法

コマンドの書式も大体 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 でない時のみ次のコマンドを実行する
( ... )
括弧内のコマンドをサブシェルで実行する。
#
これ以降をコメントとして無視する

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

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

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

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

変数は、環境変数のみ利用できる。パラメータ展開やコマンド置換などは ある程度利用できる。現在、$@ や $? などの特殊なパラメータは未実装である。

ジョブ制御

パイプラインでつながった一連のコマンド群をジョブという。ジョブは ジョブ番号とプロセスグループ 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 コマンドはパイプラインの中で使っては効果がない。
cd
作業ディレクトリを変更する。引数を省略すると環境変数 HOME の値に変更する。このコマンドは、環境変数 PWD, SPWD, OLDPWD の値も変更する。 PWD と SPWD には新しい作業ディレクトリのパスが入る。OLDPWD にはひとつ前の 作業ディレクトリのパスが入る。
umask
umask を表示・変更する。引数なしだと umask を表示する。 引数に八進数を指定するとそれに umask を変更する。
export
環境変数を設定する。引数に、NAME=VALUE の形式で環境変数の名前と値を指定する。-n オプションを付けて環境変数の名前だけ指定すると、その環境変数を削除する。
このコマンドには環境変数を表示する機能はない。適宜 printenv 外部コマンドなどを使用すること。
なお、このシェルにはシェル変数は存在しない。
.
source
引数に指定されたファイル名のファイルを読み込んで、 中に書いてあるコマンドを実行する。
history
履歴を操作する。引数なしだと、現在残っている全ての履歴を表示する。 数値を引数に指定すると、最近の履歴をその数だけ表示する。 その他、以下のオプションによって各種操作が可能。
-c
履歴を全て削除する。
-d n
履歴番号 n の履歴一件を削除する。
-r file
指定したファイルから履歴を読み込む。
-w file
指定したファイルに履歴を上書き保存する。
-s arg...
arg... を履歴に追加する。 この時、この history コマンド自身は履歴に残らない。
-r/-w オプションでファイルを指定しないと、histfile オプションのファイルを使う。
alias
エイリアスを設定・表示する。エイリアスは alias name=value のようにして設定する。設定してあるエイリアスを表示するには、 エイリアスの名前だけを引数に指定する。引数を一つも指定しないと全エイリアスを 表示する。引数は一度にいくつでも指定できる。エイリアスを設定する際に -g オプションを指定すると、それはグローバルエイリアスとなる。 グローバルエイリアスは、コマンド名に限らずコマンドライン上の任意の位置で 置換される。
unalias
引数で指定した名前のエイリアスを削除する。 引数を一つも指定しないと全エイリアスを削除する。
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)