シェルをフルスクラッチしてみようと思い,まずは手始めに簡易的なシェルとして紹介されていた LSH を実装してみました.
実装したソースコードはこちらにあります.
LSH に実装されている機能
LSH には次のコマンドが実装されています.
- cd
- help
- exit
- 指定したプログラムの実行
これだけのコマンドを簡単に実装するだけなので,システムプログラミングのいい入り口になるかなと思います.なので,興味のある方はぜひチャレンジしてみてください.
使用したライブラリ
- nix: これは以前にも紹介したことがありますが,今回はシステムコールの呼び出し部分をすべてこのライブラリで実装しています.Rust の unsafe な処理を safe になるようにラップしたライブラリで,unsafe ブロックを呼び出さずに済むメリットがあります.ただ,実装されていないコマンドも多く,今後どうなるかは動向が気になります.
全体の構成とシステムコールを使用した箇所に関する解説
元の実装は C 言語によるもので,若干 C 言語特有のワークアラウンドを Rust 向けに書き直すという作業が必要になります.また,型クラスや代数データを今回行った実装よりもハードめに使えば,もう少しスッキリした実装にできたかなとは思いますが,C 言語側の実装に関数名や流れを合わせるという意図から今回はそうしませんでした.
簡易的なシェルの作り方ですが,これはとてもシンプルです.文字列をパースしてパターンマッチし,マッチされた結果に該当する関数を呼び出すという処理を行うだけで実装可能です.
コマンドそのものの実装については,cd コマンドのようにシステムコールを1つ使用すれば実装できてしまうものもあれば,cat や ls コマンドのように,いくつかのシステムコールを組み合わせて実装するコマンドもあります.
cd コマンド
cd コマンドの実装は,拍子抜けかもしれませんがシステムコールの chdir
を使用していましたので,その通りに使用しました.
fn lsh_cd(dir: &str) -> Result<Status, LshError> { if dir.is_empty() { Err(LshError::new("lsh: expected argument to cd\n")) } else { chdir(Path::new(&dir)) .map(|_| Status::Success) .map_err(|err| LshError::new(&err.to_string())) } }
指定したプログラムの実行
よくあるプログラム実行です.プロセスを fork し,子プロセスにプログラムそのものの実行は任せ,親プロセスは子プロセスの結果を待つという流れです.各システムコールに関する詳しい解説については以前書いた記事にまとめております.
fn lsh_launch(args: Vec<String>) -> Result<Status, LshError> { let pid = fork().map_err(|_| LshError::new("fork failed"))?; match pid { ForkResult::Parent { child } => { let wait_pid_result = waitpid(child, None).map_err(|err| LshError::new(&format!("{}", err))); match wait_pid_result { Ok(WaitStatus::Exited(_, _)) => Ok(Status::Success), Ok(WaitStatus::Signaled(_, _, _)) => Ok(Status::Success), Err(err) => Err(LshError::new(&err.message)), _ => Ok(Status::Success), } } ForkResult::Child => { let path = CString::new(args[0].to_string()).unwrap(); let args = if args.len() > 1 { CString::new(args[1].to_string()).unwrap() } else { CString::new("").unwrap() }; execv(&path, &[path.clone(), args]) .map(|_| Status::Success) .map_err(|_| LshError::new("Child Process failed")) } } }
さて,実行についてですが,example/
配下にある C プログラムをコンパイルして,lsh シェルを実行し,次のようなコマンドを打つとうまくいくはずです.
C のコンパイルについてはお決まりの,
gcc -o main main.c
そして,lsh シェルを起動し,
cd ./example
./main
と実行するだけです.Hello, World!
とコンソールに出力されたら成功です.シェルを終了するには,
exit
と入力します.
まとめ
- LSH は簡易シェル作成のいい練習になるのでオススメです.
- nix を使うとシステムコールを容易に呼び出せる上に,後続処理を Rust らしい書き方にできるのでいいと思います.
- 時間があったら,
ls
コマンドやcat
コマンドも実装してみたいですね.とりあえずls
コマンドがないのは不便なので一番最初はそれかなと思います.