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