自作シェルをRustで実装するときのメモ
Tutorial - Write a Shell in C(以下チュートリアル)をRustで実装していきます。
Rustは先週から触り始めたばかりでお作法も何もわからないので、コメントいただけるとしあわせになります
2021/08/06 追記
GitHubのリポジトリをPublicにしました。
Basic loop of a shell
記念すべき最初のコードです!🎉🎉
Cで書かれているサンプルコードをRustで書いてみました。
おそらくこの部分はこれからの実装に合わせて書き直していくので、とりあえずこの状態で進みます。ちなみに今回作るシェルはshelly
と名付けたので、コードに出てくるshelly
はそういうことだと思ってください。
fn shelly_loop() {
let line: String;
let arguments: String;
let status: bool;
loop {
print!("> ");
line = read_line();
arguments = split_line();
status = execute(arguments);
if (!status) {
break;
}
}
}
Reading a line
途中までread_line
関数の実装を書いてたけど、C言語にもRustにも標準入力を受け取るメソッドあるみたい。それもそうか。
ということで先の関数に追加します。
+use std::io;
fn shelly_loop() {
- let line: String;
let arguments: String;
let status: bool;
loop {
print!("> ");
+ // Read line
+ let mut input_line = String::new();
+ io::stdin().read_line(&mut input_line)
+ .expect("Error: Failed to read a line.");
arguments = split_line();
status = execute(arguments);
if (!status) {
break;
}
}
}
これによって、標準入力をinput_line
にセットすることができるはずです。何らかの理由で標準入力が失敗したら、Error: Failed to read a line.
というメッセージが返ってきます。
参考
Parsing the line
受け取った文字列を空白で区切って配列に配置する操作です。これもメソッドで行けそうだったので以下のように追加します。エラー文の内容を少し変更していますが、本質に関わるものではないためdiffには含めません。
use std::io;
fn shelly_loop() {
- let arguments: String;
let status: bool;
loop {
print!("> ");
// Read line
let mut input_line = String::new();
io::stdin().read_line(&mut input_line)
.expect("Shelly error: Failed to read a line.");
+ // Separate strings with spaces.
+ let mut arguments: Vec<&str> = input_line.split(' ').collect();
status = execute(arguments);
if (!status) {
break;
}
}
}
参考
なんか訳分かんなくなったのでリセットします。
今回のシェルでは、
- コマンドラインから入力を受け取る。
- 入力をパースする。
- プロセスをforkする。
- 親プロセスでは、子プロセスが終了するまで待機する。
- 子プロセスではコマンドを実行する。実行が完了したら子プロセスは終了する。
の無限ループなので、上から順番に実装していく感じがよさそう。
Rust初心者がサンプルコードをそのままRustに移行とかアホ
コマンドの入力を変数に入れて出力することができました。
use std::io;
fn main() {
let mut input_line = String::new();
match io::stdin().read_line(&mut input_line) {
Ok(n) => {
println!("{} bytes read", n);
println!("Input: {}", &input_line);
}
Err(error) => {
println!("Error: {}", error);
}
}
}
参考
入力文字列を空白で区切ってベクターに入れることができました。
前の段階ではパターンマッチを使っていましたが、expect()
で中の値をそのまま取り出す方が簡潔に済みそう。
use std::io;
fn main() {
// Read input line
let mut input_line = String::new();
io::stdin().read_line(&mut input_line).expect("Shelly: Input error.");
// Parse input line
let command: Vec<&str> = input_line.split_whitespace().collect();
for argument in command {
println!("{:?}", argument);
}
}
参考
現在のプロセスからnix
クレートのfork()
を使って子プロセスをフォークします。なお、2021/07/31時点でnix
はunsafeとなっているので、unsafe
ブロックで囲んでいます。
use std::io;
use nix::unistd::{fork, getpid, getppid, ForkResult};
fn main() {
// Read input line
let mut input_line = String::new();
io::stdin().read_line(&mut input_line).expect("Shelly: Input error.");
// Parse input line
// "foo bar baz" => ["foo", "bar", "baz"]
let command: Vec<&str> = input_line.split_whitespace().collect();
for term in command {
println!("{:?}", term);
}
println!("Current process id: {}", getpid());
unsafe {
match fork() {
Ok(ForkResult::Parent {child}) => {
// I'm a parent process.
println!("Main({}) forked a child({})", getpid(), child);
}
Ok(ForkResult::Child) => {
// I'm a child process.
println!("Child({}) started. PPID is {}", getpid(), getppid());
}
Err(_) => {
println!("Fork failed.");
}
}
}
}
実行結果はこんな感じになります。pid
が親子で対応していますね。正常にフォークできています。
>>> cargo run
Compiling shelly v0.1.0 (/shelly)
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/shelly`
hoge fuga piyo
"hoge"
"fuga"
"piyo"
Current process id: 9144
Main(9144) forked a child(9208)
Child(9208) started. PPID is 9144
参考
親プロセスの中で、子プロセスが終了するのを待ちます。
1つ目のmatchでは、親プロセス上で変数pid
に自身(すなわち親プロセス)のプロセスIDを代入しています。一方、子プロセス上ではexit(0)
を呼び出し、子プロセスはすぐに終了します。2つ目のmatchでは最初にwaitpid()
が呼び出されます。これにより子プロセスが終了するまで親プロセスは動作を中断しますが、先のexit(0)
によって既に子プロセスは終了しているため、親プロセスは動作を再開します。そして同時に、子プロセスが終了したというメッセージを表示します。
use std::io;
use nix::sys::wait::waitpid;
use nix::unistd::{fork, getpid, getppid, ForkResult};
use std::process::exit;
fn main() {
// Read input line
let mut input_line = String::new();
io::stdin().read_line(&mut input_line).expect("Shelly: Input error.");
// Parse input line
// "foo bar baz" => ["foo", "bar", "baz"]
let command: Vec<&str> = input_line.split_whitespace().collect();
for term in command {
println!("{:?}", term);
}
println!("Current process id: {}", getpid());
unsafe {
let pid = match fork() {
Ok(ForkResult::Parent {child}) => {
// I'm a parent process.
println!("Main({}) forked a child({})", getpid(), child);
child
}
Ok(ForkResult::Child) => {
// I'm a child process.
println!("Child({}) started. PPID is {}", getpid(), getppid());
exit(0)
}
Err(_) => {
panic!("Fork failed.");
}
};
match waitpid(pid, None) {
Ok(status) => {
println!("Child exited {:?}.", status);
}
Err(_) => {
println!("Waitpid failed.");
}
}
}
}
実装結果は以下の通りになります。
>>> cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/shelly`
hoge fuga piyo
"hoge"
"fuga"
"piyo"
Current process id: 13325
Main(13325) forked a child(13341)
Child(13341) started. PPID is 13325
Child exited Exited(Pid(13341), 0).
参考
現在作ってるshellの名前をshelly
から coconush
に変えます🥥。
入力したコマンドを子プロセス上で実行することができました!!!🎉🎉🎉
現状ls -a
やcat Cargo.toml
のような2単語のコマンドのみ実行できます。
use std::io;
use nix::sys::wait::waitpid;
use nix::unistd::{execvp, fork, getpid, getppid, ForkResult};
use std::process::exit;
use std::ffi::CString;
use std::vec::Vec;
fn main() {
loop{
// Read input line
let mut input_line = String::new();
io::stdin().read_line(&mut input_line).expect("Shelly: Input error.");
// Parse input line
// "foo bar baz" => ["foo", "bar", "baz"]
let command: Vec<&str> = input_line.split_whitespace().collect();
let bin = CString::new(command[0].to_string()).unwrap();
let args = CString::new(command[1].to_string()).unwrap();
for term in command {
println!("{:?}", term);
}
println!("Current process id: {}", getpid());
unsafe {
match fork() {
Ok(ForkResult::Parent {child}) => {
println!("Main({}) forked a child({})", getpid(), child);
match waitpid(child, None) {
Ok(_pid) => {
println!("Child exited {:?}.", child);
}
Err(_) => {
println!("Waitpid failed.");
}
}
}
Ok(ForkResult::Child) => {
println!("Child({}) started. PPID is {}", getpid(), getppid());
execvp(&bin, &[&bin, &args]).expect("coconush error: failed exec.");
exit(0)
}
Err(_) => {
panic!("Fork failed.");
}
};
}
}
}
以下、ls -al
の実行結果です。
>>> cargo run
Compiling coconush v0.1.0 (/coconush)
Finished dev [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/coconush`
ls -la
"ls"
"-la"
Current process id: 7396
Main(7396) forked a child(7476)
Child(7476) started. PPID is 7396
total 36
drwxr-xr-x 5 o-xian o-xian 4096 Aug 1 00:33 .
drwxr-xr-x 20 o-xian o-xian 4096 Jul 29 18:31 ..
drwxr-xr-x 7 o-xian o-xian 4096 Aug 4 11:09 .git
-rw-r--r-- 1 o-xian o-xian 8 Jul 26 17:54 .gitignore
-rw-r--r-- 1 o-xian o-xian 1613 Aug 4 11:01 Cargo.lock
-rw-r--r-- 1 o-xian o-xian 192 Aug 4 11:01 Cargo.toml
-rw-r--r-- 1 o-xian o-xian 442 Aug 4 11:07 README.md
drwxr-xr-x 2 o-xian o-xian 4096 Aug 4 11:06 src
drwxr-xr-x 4 o-xian o-xian 4096 Jul 27 22:34 target
Child exited Pid(7476).
参考
入力部分をexpect()
で誤魔化すのはあまりよろしくない。
というわけで入力部分もちゃんとパターンマッチしていきます。Ok
の時は変数に文字を入力するだけにして、Err
の時はエラー文を表示します。
let mut input_line = String::new();
// io::stdin().read_line(&mut input_line).expect("coconush error: Input error")
match io::stdin().read_line(&mut input_line) {
Ok(_) => {}
Err(error) => {
println!("coconush error: {}", error);
}
}
Ok型の中の値は使わない(input_line()
では、入力文字列のバイト数をOk型に包んで返してくる)ので、_
で無視します。
値全体やパターンの一部の値を無視する方法はいくつかあります:
_
パターンを使用すること(もう見かけました)、 他のパターン内で_
パターンを使用すること、アンダースコアで始まる名前を使用すること、..
を使用して値の残りの部分を無視することです。
参考
GitHubをPublicにしました。もしよかったら見てってください。
nix::unistd::fork
のみがunsafe
であることが分かりました。したがって、fork()
のみunsafe
で囲むことにします。
// Only fork() is unsafe.
match unsafe{ fork() } {
Ok(ForkResult::Parent {child}) => {
println!("Main({}) forked a child({})", getpid(), child);
match waitpid(child, None) {
Ok(_pid) => {
println!("Child exited {:?}.", child);
}
Err(_) => {
println!("Waitpid failed.");
}
}
}
Ok(ForkResult::Child) => {
println!("Child({}) started. PPID is {}", getpid(), getppid());
execvp(&bin, &[&bin, &args]).expect("coconush error: failed exec.");
exit(0)
}
Err(_) => {
panic!("Fork failed.");
}
};
参照
rustfmt
がcargo fmt
で実行できることを知ったので、さっそく実行。かなり見やすくなりましたね。
use nix::sys::wait::waitpid;
use nix::unistd::{execvp, fork, getpid, getppid, ForkResult};
use std::ffi::CString;
use std::io;
use std::process::exit;
use std::vec::Vec;
fn main() {
loop {
// Read input line
let mut input_line = String::new();
match io::stdin().read_line(&mut input_line) {
Ok(_) => {}
Err(error) => {
println!("coconush error: {}", error);
}
}
// Parse input line
// "foo bar baz" => ["foo", "bar", "baz"]
let command: Vec<&str> = input_line.split_whitespace().collect();
let bin = CString::new(command[0].to_string()).unwrap();
let args = CString::new(command[1].to_string()).unwrap();
for term in command {
println!("{:?}", term);
}
println!("Current process id: {}", getpid());
match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => {
println!("Main({}) forked a child({})", getpid(), child);
match waitpid(child, None) {
Ok(_pid) => {
println!("Child exited {:?}.", child);
}
Err(_) => {
println!("Waitpid failed.");
}
}
}
Ok(ForkResult::Child) => {
println!("Child({}) started. PPID is {}", getpid(), getppid());
execvp(&bin, &[&bin, &args]).expect("coconush error: failed exec.");
exit(0)
}
Err(_) => {
panic!("Fork failed.");
}
};
}
}
参考
ちょっとハマったのでメモ。
以下のコードは、文字列を表示した状態で標準入力を受け付けるという意図があります。
fn main() {
print!("Input your command >>>");
let mut input_line = String::new();
match io::stdin().read_line(&mut input_line) {
Ok(_) => {
println!("Input: {}", &input_line);
}
Err(error) => {
println!("Input error: {}", error);
}
}
}
ですから、本来はこうなってほしいです。
Input your command >>>hogehoge
Input error: hogehoge
しかし、実際に動かしてみると、以下のように表示順が入れ替わります。
hogehoge
Input your command >>>Input: hogehoge
どうもこれは「ラインバッファ」という仕組みによるものみたいです。つまり、入出力を一旦バッファに蓄えておき、改行コードが入力されたら(すなわち、Enterを叩いたら)バッファ内のデータを開放するという仕組みによるものらしいです。したがって、順番に出力していくには、改行以外の操作でもバッファ内データを開放できればいい、ということになります。
use std::io::*;
fn main() {
print!("Input your command >>>");
match stdout().flush() {
Ok(_) => {}
Err(error) => {
eprintln!("{}", error);
}
}
let mut input_line = String::new();
match stdin().read_line(&mut input_line) {
Ok(_) => {
println!("Input: {}", &input_line);
}
Err(error) => {
println!("Input error: {}", error);
}
}
}
参考
全体的にコードをスッキリさせるため、ファイル分割します。とりあえずまずはプロンプト表示から。
use std::env;
use std::io::Result;
use whoami::{hostname, username};
pub fn display_prompt() -> Result<()> {
let current_path = env::current_dir()?;
print!(
"{} @{} :{} >",
username(),
hostname(),
current_path.display()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_current_directory() {
assert!(display_prompt().is_ok());
}
}
main
部分では変わらず無限ループさせます。
mod prompt;
use std::io::{stdin, stdout, Write};
use std::process::exit;
fn main() {
loop {
match prompt::display_prompt() {
Ok(_) => {}
Err(e) => {
println!("error: {}", e);
exit(0);
}
};
match stdout().flush() {
Ok(_) => {}
Err(e) => {
println!("error: {}", e);
exit(0);
}
};
// input command
let mut line = String::new();
match stdin().read_line(&mut line) {
Ok(_) => {}
Err(e) => {
println!("error: {}", e);
exit(0);
}
}
line.remove(line.len() - 1);
// debug
println!("{}", line);
}
}
参考
複数コマンドの実行ができるようになりました。現在ls
, ls -al
, pwd
などに対応しています。
use std::env;
use std::io::Result;
use whoami::{hostname, username};
use colored::*;
pub fn display_prompt() -> Result<()> {
let current_path = env::current_dir()?;
print!(
"{}{}{}:{}{}",
username().green().truecolor(222, 165, 132).bold(),
"@".truecolor(222, 165, 132).bold(),
hostname().truecolor(222, 165, 132).bold(),
current_path.display(),
">".truecolor(222, 165, 132).bold()
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_current_directory() {
assert!(display_prompt().is_ok());
}
}
mod prompt;
use std::io::{stdin, stdout, Write};
use std::process::Command;
fn main() {
loop {
// display status
match prompt::display_prompt() {
Ok(_) => {}
Err(e) => {
eprintln!("prompt error: {}", e);
}
}
match stdout().flush() {
Ok(_) => {}
Err(e) => {
eprintln!("buf error: {}", e);
}
}
// input line
let mut line = String::new();
match stdin().read_line(&mut line) {
Ok(_) => {}
Err(e) => {
eprintln!("read line error: {}", e);
}
}
// parse input
let mut parts = line.trim().split_whitespace();
let command = parts.next().unwrap_or("\n");
let args = parts;
// exec command
match Command::new(command).args(args).spawn() {
Ok(mut child) => match child.wait() {
Ok(_) => {}
Err(e) => {
eprintln!("wait error: {}", e);
}
},
Err(e) => {
eprintln!("exec error: {}", e);
}
}
}
}
参考
組み込みcd
, exit
を実装しました。
match
がいささか冗長だったため、if let
文に書き換えています。
mod prompt;
use std::env;
use std::io::{stdin, stdout, Write};
use std::path::Path;
use std::process::Command;
fn main() {
loop {
// display status
if let Err(e) = prompt::display_prompt() {
eprintln!("prompt error: {}", e);
}
if let Err(e) = stdout().flush() {
eprintln!("buf error: {}", e);
}
// input line
let mut line = String::new();
if let Err(e) = stdin().read_line(&mut line) {
eprintln!("read line error: {}", e);
}
// parse input
let mut parts = line.trim().split_whitespace();
let command = parts.next().unwrap_or("\n");
let args = parts;
// exec command
match command {
"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);
}
},
"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) => {
eprintln!("exec error: {}", e);
}
},
}
}
}
参考
ブログにまとめました。