Yash: yet another shell
概要
このプログラムは C 言語で書いた簡単なコマンドライン端末用シェルである。
これはもともと筆者が学科の課題で作ったプログラムであり、bash などの本格的な
シェルを目指して作ったものではない。機能は少なくシェルスクリプトすら未対応で、
バグも多いと思われる。しかしシェルのソースを見てみたいが bash
のソースは複雑過ぎて読めない
というような人にとっては参考になるかもしれない。
不具合の報告や機能改善の提案などは一応受け付けるが、期待しないこと。
筆者はこのプログラムの動作に関して保証をしないし、
一切責任をとらない。もし使いたいのであればそれを承知の上で
すべて自己責任で使うこと。
このプログラムは GNU General
Public License (Version 2) の元で自由に再配布・変更などができる。
なお、yash という名前のシェルは他にも存在すると思われるが、特に関係はない。
要件
プログラムの中核部分は多くの Unix システムに共通のコードであるが、
付随的機能の実装に Linux 特有の機能を使用している。従って、このシェルを
コンパイル・実行できるのは Linux だけだろう。
このプログラムをコンパイル・実行するには以下のものが必要である:
- Linux 2.6.16 以降
- GNU make
- GNU Complier Collection (GCC)
- GNU Readline Library
コンパイル
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
でない時のみ次のコマンドを実行する
( ... )
- 括弧内のコマンドをサブシェルで実行する。
#
- これ以降をコメントとして無視する
リダイレクトの処理はパイプラインよりも後に行う。よって、リダイレクトと
パイプラインの指定が重複するときはリダイレクトがパイプラインを上書きする。
演算子の優先順位は、|
が最も高く、続いて &&
と ||
、そして最も低いのが &
と ;
である。括弧 ( )
は演算子の優先順位を変更するのにも使える。
|
でつないだパイプラインにおいて、最後のコマンドの後にも
|
を指定すると (別のいい方をすると、最後の |
の後に
コマンドを書かないと)、最後のコマンドの出力が最初のコマンドの入力に繋がれ、
ループ状のパイプラインができる。
括弧 ( )
で囲まれたコマンドはサブシェル (元のシェルから分岐した
子プロセス) で実行される。
コマンドの始めに 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 コマンドはパイプラインの中で使っては効果がない。
- cd
- 作業ディレクトリを変更する。引数を省略すると環境変数 HOME
の値に変更する。このコマンドは、環境変数 PWD, SPWD, OLDPWD の値も変更する。
PWD と SPWD には新しい作業ディレクトリのパスが入る。OLDPWD にはひとつ前の
作業ディレクトリのパスが入る。
- umask
- umask を表示・変更する。引数なしだと umask を表示する。
引数に八進数を指定するとそれに umask を変更する。
- export (特殊)
- 環境変数を設定する。引数にシェル変数名を指定すると、そのシェル変数が
それ以降起動するコマンドに環境変数として渡されるようになる。引数に、NAME=VALUE の形式を指定すると同時に変数の値を代入できる。
-n オプションを付けて環境変数の名前だけ指定すると、その変数をそれ以降
起動するコマンドに環境変数として渡さないようにする。
- このコマンドには環境変数を表示する機能はない。適宜 env
外部コマンドなどを使用すること。
- unset
- シェル変数を削除する。引数としてシェル変数名を指定する。
- . (特殊)
- 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)