😺

Rustで作った自作Shellの話

2020/12/16に公開

RustでShellを作ったので作り方という紹介というかなんというか。

使ったライブラリは以下です。

[dependencies]
nix = "0.19.0"
whoami = "0.9.0"
dirs = "3.0"

標準入力

とりあえずShellっぽさを出すためにユーザと現在のディレクトリを表示して標準入力から文字列を受け取ります。

use std::env;
use std::io::{stdin, stdout, Write};

use whoami;

fn main() {
    sh_loop();
}

fn sh_loop() {
    loop {
        print!(
            "{}@{}:{} > ",
            whoami::username(),
            whoami::hostname(),
            env::current_dir().unwrap().display()
        );
        stdout().flush().unwrap();

        //文字列を取得
        let mut line = String::new();
        stdin().read_line(&mut line).expect("Faild to read line");
        line.remove(line.len() - 1);
    }
}

パーサー

まず構造体を定義します。

pub struct Command {
  command: String,
  sub_command: String,
  option: Vec<String>,
  path: String,
  index: usize,
  pipe: Option<Box<CommandParse>>,
  redirect: Option<Redirect>,
}

受け取った文字列をパースします。

fn judge_loop(&mut self, mut line_split: &mut Vec<&str>) {
  self.index += 1;
  let line_index = line_split.len();
  //配列の先頭は実行するコマンド
  self.command = line_split[0].to_string();
  loop {
    if line_index <= self.index {
      break;
    }
    self.judge(&mut line_split);
    self.index += 1;
  }
  }

fn judge(&mut self, args: &mut Vec<&str>) {
  let arg = args[self.index];

  //リダイレクト
  if arg.chars().nth(0).unwrap() == '>' {
    self.index += 1;
    let arg = args[self.index];
    if arg.len() == 2 && arg.chars().nth(1).unwrap() == '>' {
      self.redirect = Some(Redirect::new(arg, true));
      return;
    }
    self.redirect = Some(Redirect::new(arg, false));
    return;
  }
  
  //オプション
  if arg.chars().nth(0).unwrap() == '-' {
    self.option.push(arg.to_string());
    return;
  }

  //パス
  if arg.contains("/") || arg.contains(".") {
    self.path = arg.to_string();
    return;
  }

  //パイプ
  if arg == "|" {
    let mut command = CommandParse::new();
    let mut args_split: Vec<&str> = Vec::new();
    for index in self.index + 1..args.len() {
      let split = args[index];
      args_split.push(split);
    }
    command.judge_loop(&mut args_split);
    self.pipe = Some(Box::new(command));
    self.index += args.len() - self.index;
    return;
  }

  self.sub_command = arg.to_string();
}

受け取った文字列を空白で分割して一番最初の文字列をコマンド、'-'が含まれていればオプション、"/"か"."が含まれていればパス、'|'と'>'それぞれパイプとリダイレクト、その他はサブコマンドとしてパースしています。

ビルトインコマンド

ビルトインコマンドとはShellに最初から組み込まれているコマンドことです。
cdコマンドとexitコマンドあたりをとりあえず実装します。

cd コマンド

  
use std::env;

use dirs;

use super::super::parser::parser::CommandParse;

pub fn run_cd(commands: &CommandParse) -> Result<(), String> {
    let is_path = set_current(&commands.get_path());
    let is_subcommand = set_current(&commands.get_sub_command());
    let is_path_empty = commands.get_path().trim().is_empty();
    let is_subcommand_empty = commands.get_sub_command().trim().is_empty();

    if is_path_empty && is_subcommand_empty {
      env::set_current_dir(dirs::home_dir().unwrap()).unwrap();
      return Ok(());
    }

    if !(is_path || is_subcommand) {
      if !is_path_empty {
        return Err(format!("cd : {} No such file or directory", &commands.get_path()));
      } else if !is_subcommand_empty {
        return Err(format!(
          "cd : {} No such file or directory",
          &commands.get_sub_command()
        ));
      }
    }
  return Ok(());
}

fn set_current(path: &str) -> bool {
  env::set_current_dir(path).is_ok()
}

解析したコマンドがcdなら次のサブコマンドかパスはディレクトリになのでそこに移動します。

exit コマンド

use std::process::exit;

use super::super::parser::parser::CommandParse;

pub fn run_exit(command: &CommandParse) -> Result<(), String> {
  if command.get_index() == 1 {
    exit(1);
  } else {
    return Err(format!("exit has no subcommands, options and path"));
  }
}

これは単純にexit(0)で処理を抜けているだけです。

コマンドの実行

execvpシステムコールを使いコマンドを実行するのですが、そのまま使うと実行中のものがコマンドに変換されてしまい、終了しても次のコマンドを受け取れないので、forkシステムコールを使い子プロセスを生成します。
そして子プロセスをコマンドに変換します。
最後に子プロセスを抜けて親に戻って最後にwaitで子プロセスの終了を待っています。

use std::ffi::{CStr, CString};
use std::process::exit;

use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use nix::unistd::*;

use super::super::built_in_command;
use super::super::parser;
use super::process::Process;

impl Process {
    pub fn argvs_execute(&mut self) -> Result<(), String> {
        self.signal_action();
        let command = self.get_run_command();
        let commands = self.get_run_command().get_command();

        if commands == "" {
            return Ok(());
        }

        if commands == "cd" {
            match built_in_command::cd::run_cd(command) {
                Ok(_) => {}
                Err(e) => {
                    return Err(e);
                }
            }
        } else if commands == "exit" {
            match built_in_command::exit::run_exit(command) {
                Ok(_) => {}
                Err(e) => {
                    return Err(e);
                }
            }
        } else {
            let command = self.get_run_command().clone();
	    //コマンドの実行
            match self.sh_launch(&command) {
                Ok(_) => {
                    for pid in self.get_process() {
			//プロセスを待つ
                        match self.wait_process(*pid) {
                            Ok(_) => {}
                            Err(e) => {
                                return Err(e);
                            }
                        }
                    }
                }
                Err(e) => {
                    return Err(e);
                }
            }
        }
        return Ok(());
    }

    fn sh_launch(&mut self, command: &parser::parser::CommandParse) -> Result<(), String> {
        let is_empty = !self.is_empty_pipes();
        match command.get_pipe() {
            Some(_) => match pipe() {
                Ok(pipe) => {
                    self.push_pipe(pipe);
                }
                Err(_) => {
                    return Err(format!("Pipe error"));
                }
            },
            None => {}
        }
        //プロセスの生成
        match unsafe { fork() } {
            //親プロセス
            Ok(ForkResult::Parent { child, .. }) => {
                self.push_process(child);
                if is_empty {
                    match self.pearent_connect_end() {
                        Ok(_) => {}
                        Err(e) => {
                            return Err(e);
                        }
                    }
                }
                match command.get_pipe() {
                    Some(pipe) => match self.sh_launch(&pipe) {
                        Ok(()) => {}
                        Err(e) => {
                            return Err(e);
                        }
                    },
                    None => {}
                }
            }
            //子プロセス
            Ok(ForkResult::Child) => unsafe {
                let cstring = CString::new(command.get_command()).expect("CString::new failed");
                let cstr = CStr::from_bytes_with_nul_unchecked(cstring.to_bytes_with_nul());
                let mut argv: Vec<CString> = Vec::new();
                self.push_argv(&mut argv);
                let result = execvp(cstr, &argv);
                match result {
                    Ok(_) => {
                        exit(0);
                    }

                    Err(_) => {
                        println!("{}: command not found", command.get_command());
                        exit(-1);
                    }
                }
            },

            Err(_) => {
                return Err(format!("Fork Failed"));
            }
        }
        return Ok(());
    }

    fn push_argv(&self, argvs: &mut Vec<CString>) {
        let command = self.get_run_command();
        argvs.push(CString::new(command.get_command()).expect("CString::new failed"));
        if command.get_sub_command() != "" {
            argvs.push(CString::new(command.get_sub_command()).expect("CString::new failed"));
        } else if command.get_path() != "" {
            argvs.push(CString::new(command.get_path()).expect("CString::new failed"));
        }
        for option in command.get_options() {
            argvs.push(CString::new(option.to_string()).expect("CString::new failed"));
        }
    }

    fn wait_process(&self, child: Pid) -> Result<(), String> {
        match waitpid(child, Some(WaitPidFlag::WCONTINUED)) {
            Ok(status) => match status {
                WaitStatus::Exited(_, _) => {}

                WaitStatus::Stopped(_, _) => {}
                _ => {
                    return Err(format!("Waiprocess EOF"));
                }
            },
            Err(_) => {}
        }
        return Ok(());
    }
}

ここまで実行は結構すぐにできました。
現在はリダイレクトとパイプが実装されています。
GitHub https://github.com/garebareDA/g_shell

Discussion