Don't Repeat Yourself

Don't Repeat Yourself (DRY) is a principle of software development aimed at reducing repetition of all kinds. -- wikipedia

簡易シェル LSH を Rust で実装してみた

シェルをフルスクラッチしてみようと思い,まずは手始めに簡易的なシェルとして紹介されていた LSH を実装してみました.

実装したソースコードはこちらにあります.

github.com

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 コマンドがないのは不便なので一番最初はそれかなと思います.