Closed20

自作シェルをRustで実装するときのメモ

ONOYAMA ShodaiONOYAMA Shodai

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;
        }
    }
}
ONOYAMA ShodaiONOYAMA Shodai

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.というメッセージが返ってきます。

参考

ONOYAMA ShodaiONOYAMA Shodai

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;
         }
     }
 }

参考

ONOYAMA ShodaiONOYAMA Shodai

なんか訳分かんなくなったのでリセットします。
今回のシェルでは、

  • コマンドラインから入力を受け取る。
  • 入力をパースする。
  • プロセスをforkする。
  • 親プロセスでは、子プロセスが終了するまで待機する。
  • 子プロセスではコマンドを実行する。実行が完了したら子プロセスは終了する。

の無限ループなので、上から順番に実装していく感じがよさそう。
Rust初心者がサンプルコードをそのままRustに移行とかアホ

ONOYAMA ShodaiONOYAMA Shodai

コマンドの入力を変数に入れて出力することができました。

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);
        }
    }
}

参考

ONOYAMA ShodaiONOYAMA Shodai

入力文字列を空白で区切ってベクターに入れることができました。
前の段階ではパターンマッチを使っていましたが、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);
    }
    
}

参考

ONOYAMA ShodaiONOYAMA Shodai

現在のプロセスから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

参考

ONOYAMA ShodaiONOYAMA Shodai

親プロセスの中で、子プロセスが終了するのを待ちます。
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).

参考

ONOYAMA ShodaiONOYAMA Shodai

入力したコマンドを子プロセス上で実行することができました!!!🎉🎉🎉
現状ls -acat 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).

参考

ONOYAMA ShodaiONOYAMA Shodai

入力部分を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型に包んで返してくる)ので、_で無視します。

値全体やパターンの一部の値を無視する方法はいくつかあります: _パターンを使用すること(もう見かけました)、 他のパターン内で_パターンを使用すること、アンダースコアで始まる名前を使用すること、..を使用して値の残りの部分を無視することです。

参考

ONOYAMA ShodaiONOYAMA Shodai

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.");
    }
};

参照

ONOYAMA ShodaiONOYAMA Shodai

rustfmtcargo 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.");
            }
        };
    }
}

参考

ONOYAMA ShodaiONOYAMA Shodai

ちょっとハマったのでメモ。
以下のコードは、文字列を表示した状態で標準入力を受け付けるという意図があります。

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);
    }
  }
}

参考

ONOYAMA ShodaiONOYAMA Shodai

全体的にコードをスッキリさせるため、ファイル分割します。とりあえずまずはプロンプト表示から。

prompt.rs
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部分では変わらず無限ループさせます。

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);
    }
}

参考

ONOYAMA ShodaiONOYAMA Shodai

複数コマンドの実行ができるようになりました。現在ls, ls -al, pwdなどに対応しています。

parse.rs
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());
    }
}

main.rs
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);
            }
        }
    }
}

参考

ONOYAMA ShodaiONOYAMA Shodai

組み込みcd, exitを実装しました。
matchがいささか冗長だったため、if let文に書き換えています。

main.rs
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);
                }
            },
        }
    }
}

参考

このスクラップは2021/09/12にクローズされました