Rustで学ぶシェルの簡単な構造
はじめに
いつも何気なく使っているシェル、中身どうなっているんだろう、、こんな感じなのかな〜みたいなのは前から想像を膨らませてながら仕事していた日々。
ふとRustでシェルが公開されているのを見て、「せっかくだし自分でもつくって、さわってみよ」という軽い気持ちで作ってみましたので何かの参考になれば幸いです。
開発環境
- MacOS Monterey 12.6
- docker 20.10.17
- rust 1.60 (dockerコンテナ内)
ディレクトリ, ファイル構成
下記のような構成になってます。
今回コンテナひとつなのでcomposeいらないかなとも思いましたが、軽い気持ちでコンテナ追加するかもしれないので一応用意しました。
.
├── docker-compose.yml -> compose設定
└── msh
├── Cargo.toml -> Rust設定
├── Dockerfile -> Rustのdocker設定
└── src
├── main.rs -> シェルのメイン処理
└── prompt.rs -> commandディスプレイ設定
プロセスのフロー
プロセスの処理は下記の内容を基本にしております。
Write Shell in Cの流れに則って作ってます。
- ループ処理
- 入力受付待ち
- Enter押されたタイミングでコマンド・引数抽出(read_list)
- コマンド実行
つくってく
メイン処理とコマンド群に分けて記載していきます。
メイン処理
Input読み込みは下記で行ってます。
(read_lineでの読み込みのため、カーソルなどの特殊キー受付がされないのでそのあたりはEventでハンドリングしたい。。)
読み込み後、後段の処理で前後空白をトリミングして、空白毎に分割します。
念の為unwrap_orでnullや空白系の入力の場合はEnter(”\n”)が入力されたとみなして、argsに格納します。
let mut line = String::new();
let stdin = stdin();
if let Err(e) = stdin.read_line(&mut line) {
eprintln!("read line error: {}", e);
}
let mut parts = line.trim().split_whitespace();
let command = parts.next().unwrap_or("\n");
let args = parts;
match処理を用いて、コマンド毎の処理を行います。
今はcd, exitコマンド以外は、rustの標準ライブラリのCommandを利用して実行を行うようにしていますが、この辺りもカスタマイズしたいです。
Enter(”\n”)のみの入力の場合は条件を無視します。
match command {
"\n" => {},
"cd" => {
// 詳細処理は後述
},
"exit" => return,
command => match Command::new(command).args(args).spawn() {
Ok(mut child) => {
if let Err(e) = child.wait() {
eprintln!("wait error: {}", e);
}
}
Err(e) => {
println!("exec : {}", command);
eprintln!("exec error: {}", e);
}
},
}
各コマンド群
cd
let new_dir = args.peekable().peek().map_or("/", |x| *x);
let root = Path::new(new_dir);
if let Err(e) = env::set_current_dir(&root) {
eprintln!("cd error: {}", e);
}
実行
下記のような形でコンパイル後、実行できます。
docker compose exec cargo watch -x run
つくってみて
今回作って見て、シェルの基本的な構造、Rustの基本的な?書き方など学べてよかったかなと思います。最近Rust製のアプリケーションから、カーネルレベルのものまで色々出てきていて、業務レベルでもいつかは触ってみたい今日この頃、、という気持ちではあります。
参考にはならないかもしれないですが、今回のコードはGithubにも載せております。そして、もしよければStarもらえると励みになります。
Discussion