rust-shell - 解答

実装コード

メインループ

// src/main.rs
use std::io::{self, Write};

mod shell;
mod parser;
mod executor;

use shell::Shell;

fn main() {
    let mut shell = Shell::new();

    loop {
        print!("minishell> ");
        io::stdout().flush().unwrap();

        let mut input = String::new();
        match io::stdin().read_line(&mut input) {
            Ok(0) => break,  // EOF
            Ok(_) => {
                let input = input.trim();
                if input.is_empty() {
                    continue;
                }
                if let Err(e) = shell.execute_line(input) {
                    eprintln!("Error: {}", e);
                }
            }
            Err(e) => {
                eprintln!("Read error: {}", e);
                break;
            }
        }
    }
}

シェルコア

// src/shell.rs
use std::collections::HashMap;
use std::env;
use std::path::PathBuf;

pub struct Shell {
    pub cwd: PathBuf,
    pub env: HashMap<String, String>,
    pub last_exit_code: i32,
}

impl Shell {
    pub fn new() -> Self {
        let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
        let env: HashMap<String, String> = env::vars().collect();

        Shell {
            cwd,
            env,
            last_exit_code: 0,
        }
    }

    pub fn execute_line(&mut self, line: &str) -> Result<(), String> {
        let commands = crate::parser::parse(line)?;

        if commands.is_empty() {
            return Ok(());
        }

        self.last_exit_code = crate::executor::execute(self, commands)?;
        Ok(())
    }

    // 組み込みコマンド
    pub fn builtin_cd(&mut self, args: &[String]) -> Result<i32, String> {
        let path = if args.is_empty() {
            self.env.get("HOME")
                .map(|s| s.as_str())
                .unwrap_or("/")
        } else {
            &args[0]
        };

        let new_path = if path.starts_with('/') {
            PathBuf::from(path)
        } else {
            self.cwd.join(path)
        };

        match env::set_current_dir(&new_path) {
            Ok(_) => {
                self.cwd = new_path.canonicalize().unwrap_or(new_path);
                Ok(0)
            }
            Err(e) => {
                eprintln!("cd: {}: {}", path, e);
                Ok(1)
            }
        }
    }

    pub fn builtin_pwd(&self) -> Result<i32, String> {
        println!("{}", self.cwd.display());
        Ok(0)
    }

    pub fn builtin_echo(&self, args: &[String]) -> Result<i32, String> {
        println!("{}", args.join(" "));
        Ok(0)
    }

    pub fn builtin_env(&self) -> Result<i32, String> {
        for (key, value) in &self.env {
            println!("{}={}", key, value);
        }
        Ok(0)
    }
}

パーサー

// src/parser.rs
use std::path::PathBuf;

#[derive(Debug, Clone)]
pub struct Command {
    pub program: String,
    pub args: Vec<String>,
    pub stdin_redirect: Option<PathBuf>,
    pub stdout_redirect: Option<Redirect>,
}

#[derive(Debug, Clone)]
pub enum Redirect {
    Overwrite(PathBuf),
    Append(PathBuf),
}

pub fn parse(line: &str) -> Result<Vec<Command>, String> {
    let mut commands = Vec::new();
    let parts: Vec<&str> = line.split('|').collect();

    for part in parts {
        let command = parse_single_command(part.trim())?;
        commands.push(command);
    }

    Ok(commands)
}

fn parse_single_command(input: &str) -> Result<Command, String> {
    let mut tokens: Vec<String> = Vec::new();
    let mut stdin_redirect = None;
    let mut stdout_redirect = None;

    let mut chars = input.chars().peekable();
    let mut current_token = String::new();
    let mut in_quotes = false;

    while let Some(c) = chars.next() {
        match c {
            '"' | '\'' => {
                in_quotes = !in_quotes;
            }
            ' ' if !in_quotes => {
                if !current_token.is_empty() {
                    tokens.push(current_token.clone());
                    current_token.clear();
                }
            }
            '>' if !in_quotes => {
                if !current_token.is_empty() {
                    tokens.push(current_token.clone());
                    current_token.clear();
                }
                let append = chars.peek() == Some(&'>');
                if append { chars.next(); }

                // ファイル名を取得
                while chars.peek() == Some(&' ') { chars.next(); }
                let filename: String = chars.by_ref()
                    .take_while(|&c| c != ' ' && c != '>' && c != '<')
                    .collect();

                stdout_redirect = Some(if append {
                    Redirect::Append(PathBuf::from(filename))
                } else {
                    Redirect::Overwrite(PathBuf::from(filename))
                });
            }
            '<' if !in_quotes => {
                if !current_token.is_empty() {
                    tokens.push(current_token.clone());
                    current_token.clear();
                }
                while chars.peek() == Some(&' ') { chars.next(); }
                let filename: String = chars.by_ref()
                    .take_while(|&c| c != ' ' && c != '>' && c != '<')
                    .collect();
                stdin_redirect = Some(PathBuf::from(filename));
            }
            _ => {
                current_token.push(c);
            }
        }
    }

    if !current_token.is_empty() {
        tokens.push(current_token);
    }

    if tokens.is_empty() {
        return Err("Empty command".to_string());
    }

    Ok(Command {
        program: tokens[0].clone(),
        args: tokens[1..].to_vec(),
        stdin_redirect,
        stdout_redirect,
    })
}

実行エンジン

// src/executor.rs
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::process::{Command as ProcessCommand, Stdio};

use crate::parser::{Command, Redirect};
use crate::shell::Shell;

pub fn execute(shell: &mut Shell, commands: Vec<Command>) -> Result<i32, String> {
    if commands.len() == 1 {
        execute_single(shell, &commands[0])
    } else {
        execute_pipeline(shell, commands)
    }
}

fn execute_single(shell: &mut Shell, cmd: &Command) -> Result<i32, String> {
    // 組み込みコマンドをチェック
    match cmd.program.as_str() {
        "cd" => return shell.builtin_cd(&cmd.args),
        "pwd" => return shell.builtin_pwd(),
        "echo" => return shell.builtin_echo(&cmd.args),
        "env" => return shell.builtin_env(),
        "exit" => std::process::exit(0),
        _ => {}
    }

    // 外部コマンド
    let mut process = ProcessCommand::new(&cmd.program);
    process.args(&cmd.args);

    // 入力リダイレクト
    if let Some(ref path) = cmd.stdin_redirect {
        let file = File::open(path)
            .map_err(|e| format!("Cannot open {}: {}", path.display(), e))?;
        process.stdin(Stdio::from(file));
    }

    // 出力リダイレクト
    if let Some(ref redirect) = cmd.stdout_redirect {
        let file = match redirect {
            Redirect::Overwrite(path) => File::create(path),
            Redirect::Append(path) => OpenOptions::new().append(true).create(true).open(path),
        }.map_err(|e| format!("Cannot open file: {}", e))?;
        process.stdout(Stdio::from(file));
    }

    let status = process.status()
        .map_err(|e| format!("{}: {}", cmd.program, e))?;

    Ok(status.code().unwrap_or(1))
}

fn execute_pipeline(shell: &mut Shell, commands: Vec<Command>) -> Result<i32, String> {
    let mut prev_stdout: Option<std::process::ChildStdout> = None;
    let mut children = Vec::new();

    for (i, cmd) in commands.iter().enumerate() {
        let mut process = ProcessCommand::new(&cmd.program);
        process.args(&cmd.args);

        // 前のコマンドの出力を入力に
        if let Some(stdout) = prev_stdout.take() {
            process.stdin(Stdio::from(stdout));
        }

        // 最後のコマンド以外はパイプ出力
        if i < commands.len() - 1 {
            process.stdout(Stdio::piped());
        }

        let mut child = process.spawn()
            .map_err(|e| format!("{}: {}", cmd.program, e))?;

        prev_stdout = child.stdout.take();
        children.push(child);
    }

    // 全てのプロセスの終了を待つ
    let mut last_status = 0;
    for mut child in children {
        let status = child.wait().map_err(|e| e.to_string())?;
        last_status = status.code().unwrap_or(1);
    }

    Ok(last_status)
}