diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..11baafa --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,71 @@ +use crate::runtime::Shell; +use anyhow::Result; +use std::collections::HashMap; + +mod cd; +mod echo; +mod environment; +mod exit; +mod pwd; +mod source; +mod status; + +#[derive(Debug)] +pub(crate) struct BuiltinResult { + pub status: i32, + pub stdout: Vec, + pub stderr: Vec, +} + +impl BuiltinResult { + fn success() -> Self { + Self::status(0) + } + + fn status(status: i32) -> Self { + Self { + status, + stdout: Vec::new(), + stderr: Vec::new(), + } + } + + fn stdout(status: i32, stdout: Vec) -> Self { + Self { + status, + stdout, + stderr: Vec::new(), + } + } + + fn stderr(status: i32, stderr: Vec) -> Self { + Self { + status, + stdout: Vec::new(), + stderr, + } + } +} + +pub(crate) fn run( + shell: &mut Shell, + name: &str, + argv: &[String], + env_overlay: &HashMap, +) -> Result> { + let result = match name { + "cd" => cd::run(shell, argv)?, + "pwd" => pwd::run()?, + "exit" => exit::run(argv), + "export" => environment::export(shell, argv, env_overlay), + "unset" => environment::unset(shell, argv), + "set" => environment::set(shell)?, + "true" => status::true_(), + "false" => status::false_(), + "echo" => echo::run(argv)?, + "." | "source" => source::run(shell, argv)?, + _ => return Ok(None), + }; + + Ok(Some(result)) +} diff --git a/src/commands/cd.rs b/src/commands/cd.rs new file mode 100644 index 0000000..edab2d9 --- /dev/null +++ b/src/commands/cd.rs @@ -0,0 +1,22 @@ +use crate::commands::BuiltinResult; +use crate::path::shell_path; +use crate::runtime::Shell; +use anyhow::{Context, Result}; +use std::env; + +pub(crate) fn run(shell: &Shell, argv: &[String]) -> Result { + let target = argv + .first() + .cloned() + .or_else(|| shell.vars.get("HOME").cloned()) + .or_else(|| shell.vars.get("USERPROFILE").cloned()) + .context("cd: missing destination and HOME/USERPROFILE is unset")?; + + match env::set_current_dir(shell_path(&target)) { + Ok(()) => Ok(BuiltinResult::success()), + Err(err) => Ok(BuiltinResult::stderr( + 1, + format!("cd: {err}\n").into_bytes(), + )), + } +} diff --git a/src/commands/echo.rs b/src/commands/echo.rs new file mode 100644 index 0000000..a60ac42 --- /dev/null +++ b/src/commands/echo.rs @@ -0,0 +1,9 @@ +use crate::commands::BuiltinResult; +use anyhow::Result; +use std::io::Write; + +pub(crate) fn run(argv: &[String]) -> Result { + let mut stdout = Vec::new(); + writeln!(stdout, "{}", argv.join(" "))?; + Ok(BuiltinResult::stdout(0, stdout)) +} diff --git a/src/commands/environment.rs b/src/commands/environment.rs new file mode 100644 index 0000000..ecfb365 --- /dev/null +++ b/src/commands/environment.rs @@ -0,0 +1,40 @@ +use crate::commands::BuiltinResult; +use crate::runtime::Shell; +use anyhow::Result; +use std::collections::HashMap; + +pub(crate) fn export( + shell: &mut Shell, + argv: &[String], + env_overlay: &HashMap, +) -> BuiltinResult { + for arg in argv { + if let Some((name, value)) = arg.split_once('=') { + shell.vars.insert(name.to_string(), value.to_string()); + } else if let Some(value) = env_overlay.get(arg).cloned() { + shell.vars.insert(arg.to_string(), value); + } + } + + BuiltinResult::success() +} + +pub(crate) fn unset(shell: &mut Shell, argv: &[String]) -> BuiltinResult { + for arg in argv { + shell.vars.remove(arg); + } + + BuiltinResult::success() +} + +pub(crate) fn set(shell: &Shell) -> Result { + let mut pairs: Vec<_> = shell.vars.iter().collect(); + pairs.sort_by(|a, b| a.0.cmp(b.0)); + + let mut stdout = Vec::new(); + for (key, value) in pairs { + stdout.extend_from_slice(format!("{key}={value}\n").as_bytes()); + } + + Ok(BuiltinResult::stdout(0, stdout)) +} diff --git a/src/commands/exit.rs b/src/commands/exit.rs new file mode 100644 index 0000000..c1bf54d --- /dev/null +++ b/src/commands/exit.rs @@ -0,0 +1,9 @@ +use crate::commands::BuiltinResult; + +pub(crate) fn run(argv: &[String]) -> BuiltinResult { + let code = argv + .first() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + BuiltinResult::status(code) +} diff --git a/src/commands/pwd.rs b/src/commands/pwd.rs new file mode 100644 index 0000000..ee69c6b --- /dev/null +++ b/src/commands/pwd.rs @@ -0,0 +1,11 @@ +use crate::commands::BuiltinResult; +use crate::path::display_path; +use anyhow::Result; +use std::env; + +pub(crate) fn run() -> Result { + Ok(BuiltinResult::stdout( + 0, + format!("{}\n", display_path(&env::current_dir()?)).into_bytes(), + )) +} diff --git a/src/commands/source.rs b/src/commands/source.rs new file mode 100644 index 0000000..ffad074 --- /dev/null +++ b/src/commands/source.rs @@ -0,0 +1,14 @@ +use crate::commands::BuiltinResult; +use crate::path::shell_path; +use crate::runtime::{RunOptions, Shell}; +use anyhow::{Context, Result}; + +pub(crate) fn run(shell: &mut Shell, argv: &[String]) -> Result { + let path = argv.first().context(".: missing script path")?; + let source = std::fs::read_to_string(shell_path(path)) + .with_context(|| format!("failed to read script {}", path))?; + + Ok(BuiltinResult::status( + shell.run_script(&source, RunOptions::default())?, + )) +} diff --git a/src/commands/status.rs b/src/commands/status.rs new file mode 100644 index 0000000..bfc2882 --- /dev/null +++ b/src/commands/status.rs @@ -0,0 +1,9 @@ +use crate::commands::BuiltinResult; + +pub(crate) fn true_() -> BuiltinResult { + BuiltinResult::success() +} + +pub(crate) fn false_() -> BuiltinResult { + BuiltinResult::status(1) +} diff --git a/src/lib.rs b/src/lib.rs index 83176cc..96d4656 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +mod commands; mod parser; mod path; mod runtime; diff --git a/src/runtime.rs b/src/runtime.rs index e536135..15567a1 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,5 +1,6 @@ +use crate::commands; use crate::parser::{Command as AstCommand, ListItem, Pipeline, RedirectKind, Word, parse}; -use crate::path::{display_path, is_explicit_path, shell_path}; +use crate::path::{is_explicit_path, shell_path}; use anyhow::{Context, Result, bail}; use std::collections::HashMap; use std::env; @@ -13,7 +14,7 @@ pub struct RunOptions {} #[derive(Debug)] pub struct Shell { - vars: HashMap, + pub(crate) vars: HashMap, last_status: i32, } @@ -150,79 +151,15 @@ impl Shell { command: &AstCommand, capture_stdout: bool, ) -> Result> { - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let status = match name { - "cd" => { - let target = argv - .first() - .cloned() - .or_else(|| self.vars.get("HOME").cloned()) - .or_else(|| self.vars.get("USERPROFILE").cloned()) - .context("cd: missing destination and HOME/USERPROFILE is unset")?; - match env::set_current_dir(shell_path(&target)) { - Ok(()) => 0, - Err(err) => { - writeln!(stderr, "cd: {err}")?; - 1 - } - } - } - "pwd" => { - writeln!(stdout, "{}", display_path(&env::current_dir()?))?; - 0 - } - "exit" => { - let code = argv - .first() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0); - return Ok(Some(CommandOutput { - status: code, - stdout, - })); - } - "export" => { - for arg in argv { - if let Some((name, value)) = arg.split_once('=') { - self.vars.insert(name.to_string(), value.to_string()); - } else if let Some(value) = env_overlay.get(arg).cloned() { - self.vars.insert(arg.to_string(), value); - } - } - 0 - } - "unset" => { - for arg in argv { - self.vars.remove(arg); - } - 0 - } - "set" => { - let mut pairs: Vec<_> = self.vars.iter().collect(); - pairs.sort_by(|a, b| a.0.cmp(b.0)); - for (key, value) in pairs { - writeln!(stdout, "{key}={value}")?; - } - 0 - } - "true" => 0, - "false" => 1, - "echo" => { - writeln!(stdout, "{}", argv.join(" "))?; - 0 - } - "." | "source" => { - let path = argv.first().context(".: missing script path")?; - let source = std::fs::read_to_string(shell_path(path)) - .with_context(|| format!("failed to read script {}", path))?; - self.run_script(&source, RunOptions::default())? - } - _ => return Ok(None), + let Some(result) = commands::run(self, name, argv, env_overlay)? else { + return Ok(None); }; - write_builtin_streams(command, capture_stdout, &stdout, &stderr)?; - Ok(Some(CommandOutput { status, stdout })) + write_builtin_streams(command, capture_stdout, &result.stdout, &result.stderr)?; + Ok(Some(CommandOutput { + status: result.status, + stdout: result.stdout, + })) } fn run_external(