Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
pub stderr: Vec<u8>,
}

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<u8>) -> Self {
Self {
status,
stdout,
stderr: Vec::new(),
}
}

fn stderr(status: i32, stderr: Vec<u8>) -> Self {
Self {
status,
stdout: Vec::new(),
stderr,
}
}
}

pub(crate) fn run(
shell: &mut Shell,
name: &str,
argv: &[String],
env_overlay: &HashMap<String, String>,
) -> Result<Option<BuiltinResult>> {
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))
}
22 changes: 22 additions & 0 deletions src/commands/cd.rs
Original file line number Diff line number Diff line change
@@ -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<BuiltinResult> {
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(),
)),
}
}
9 changes: 9 additions & 0 deletions src/commands/echo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::commands::BuiltinResult;
use anyhow::Result;
use std::io::Write;

pub(crate) fn run(argv: &[String]) -> Result<BuiltinResult> {
let mut stdout = Vec::new();
writeln!(stdout, "{}", argv.join(" "))?;
Ok(BuiltinResult::stdout(0, stdout))
}
40 changes: 40 additions & 0 deletions src/commands/environment.rs
Original file line number Diff line number Diff line change
@@ -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<String, String>,
) -> 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<BuiltinResult> {
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))
}
9 changes: 9 additions & 0 deletions src/commands/exit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::commands::BuiltinResult;

pub(crate) fn run(argv: &[String]) -> BuiltinResult {
let code = argv
.first()
.and_then(|s| s.parse::<i32>().ok())
.unwrap_or(0);
BuiltinResult::status(code)
}
11 changes: 11 additions & 0 deletions src/commands/pwd.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use crate::commands::BuiltinResult;
use crate::path::display_path;
use anyhow::Result;
use std::env;

pub(crate) fn run() -> Result<BuiltinResult> {
Ok(BuiltinResult::stdout(
0,
format!("{}\n", display_path(&env::current_dir()?)).into_bytes(),
))
}
14 changes: 14 additions & 0 deletions src/commands/source.rs
Original file line number Diff line number Diff line change
@@ -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<BuiltinResult> {
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())?,
))
}
9 changes: 9 additions & 0 deletions src/commands/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use crate::commands::BuiltinResult;

pub(crate) fn true_() -> BuiltinResult {
BuiltinResult::success()
}

pub(crate) fn false_() -> BuiltinResult {
BuiltinResult::status(1)
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod commands;
mod parser;
mod path;
mod runtime;
Expand Down
83 changes: 10 additions & 73 deletions src/runtime.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,7 +14,7 @@ pub struct RunOptions {}

#[derive(Debug)]
pub struct Shell {
vars: HashMap<String, String>,
pub(crate) vars: HashMap<String, String>,
last_status: i32,
}

Expand Down Expand Up @@ -150,79 +151,15 @@ impl Shell {
command: &AstCommand,
capture_stdout: bool,
) -> Result<Option<CommandOutput>> {
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::<i32>().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(
Expand Down
Loading