mod fs;
mod text;
mod util;
use std::io::Write;
use tokio_util::sync::CancellationToken;
use crate::interp::Flow;
use crate::io::{CommandIo, OutTarget};
use crate::state::ShellState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Builtin {
Cd,
Pwd,
Exit,
True,
False,
Export,
Unset,
Echo,
Cat,
Cp,
Mv,
Rm,
Mkdir,
Touch,
Sleep,
Test,
Bracket,
}
pub(crate) const NAMES: &[&str] = &[
"cd", "pwd", "exit", "true", "false", "export", "unset", "echo", "cat", "cp", "mv", "rm",
"mkdir", "touch", "sleep", "test",
];
pub(crate) fn find(name: &str) -> Option<Builtin> {
match name {
"cd" => Some(Builtin::Cd),
"pwd" => Some(Builtin::Pwd),
"exit" => Some(Builtin::Exit),
"true" => Some(Builtin::True),
"false" => Some(Builtin::False),
"export" => Some(Builtin::Export),
"unset" => Some(Builtin::Unset),
"echo" => Some(Builtin::Echo),
"cat" => Some(Builtin::Cat),
"cp" => Some(Builtin::Cp),
"mv" => Some(Builtin::Mv),
"rm" => Some(Builtin::Rm),
"mkdir" => Some(Builtin::Mkdir),
"touch" => Some(Builtin::Touch),
"sleep" => Some(Builtin::Sleep),
"test" => Some(Builtin::Test),
"[" => Some(Builtin::Bracket),
_ => None,
}
}
pub(crate) fn run(
builtin: Builtin,
args: &[String],
state: &mut ShellState,
io: CommandIo,
) -> Flow {
if builtin == Builtin::Cat {
return text::cat(args, &state.cwd, io);
}
let CommandIo {
stdin: _,
stdout,
stderr,
} = io;
match builtin {
Builtin::True => Flow::Next(0),
Builtin::False => Flow::Next(1),
Builtin::Exit => exit(args, state, stderr),
Builtin::Pwd => {
write_line(stdout, state.cwd.display());
Flow::Next(0)
}
Builtin::Cd => cd(args, state, stderr),
Builtin::Export => export(args, state, stderr),
Builtin::Unset => {
for name in args {
state.unset(name);
}
Flow::Next(0)
}
Builtin::Echo => text::echo(args, stdout),
Builtin::Cp => fs::cp(args, &state.cwd, stderr),
Builtin::Mv => fs::mv(args, &state.cwd, stderr),
Builtin::Rm => fs::rm(args, &state.cwd, stderr),
Builtin::Mkdir => fs::mkdir(args, &state.cwd, stderr),
Builtin::Touch => fs::touch(args, &state.cwd, stderr),
Builtin::Test => util::test(args, &state.cwd, stderr),
Builtin::Bracket => util::test_bracket(args, &state.cwd, stderr),
Builtin::Cat => unreachable!("handled above, before `io` was destructured"),
Builtin::Sleep => unreachable!("dispatched asynchronously; see `run_sleep`"),
}
}
pub(crate) async fn run_sleep(args: &[String], io: CommandIo, cancel: &CancellationToken) -> Flow {
let CommandIo {
stdin: _,
stdout: _,
stderr,
} = io;
util::sleep(args, stderr, cancel).await
}
pub(crate) fn write_line(target: OutTarget, text: impl std::fmt::Display) {
let mut writer = target.writer();
let _ = writeln!(writer, "{text}");
}
pub(crate) fn usage_error(stderr: OutTarget, message: impl std::fmt::Display) -> Flow {
write_line(stderr, message);
Flow::Next(2)
}
pub(crate) fn command_error(stderr: OutTarget, message: impl std::fmt::Display) -> Flow {
write_line(stderr, message);
Flow::Next(1)
}
pub(crate) fn take_flags<'a, 'k>(
args: &'a [String],
known: &'k [&'k str],
) -> Result<(Vec<&'k str>, &'a [String]), String> {
let mut seen = Vec::new();
let mut rest = args;
while let Some(first) = rest.first() {
let Some(letters) = first.strip_prefix('-') else {
break;
};
if letters.is_empty() {
return Err(first.clone());
}
for letter in letters.chars() {
let spelled = format!("-{letter}");
match known.iter().copied().find(|flag| *flag == spelled) {
Some(flag) => seen.push(flag),
None => return Err(spelled),
}
}
rest = &rest[1..];
}
Ok((seen, rest))
}
fn exit(args: &[String], state: &ShellState, stderr: OutTarget) -> Flow {
match args.first() {
None => Flow::Exit(state.last_exit),
Some(code) => match code.parse() {
Ok(code) => Flow::Exit(code),
Err(_) => {
write_line(
stderr,
format_args!("exit: numeric argument required: {code}"),
);
Flow::Exit(2)
}
},
}
}
fn cd(args: &[String], state: &mut ShellState, stderr: OutTarget) -> Flow {
let target = match args.first() {
Some(path) => path.clone(),
None => match state.home() {
Some(home) => home.to_string(),
None => {
write_line(stderr, "cd: HOME not set");
return Flow::Next(1);
}
},
};
match state.cwd.join(&target).canonicalize() {
Ok(resolved) if resolved.is_dir() => {
state.export("PWD", Some(resolved.display().to_string()));
state.cwd = resolved;
Flow::Next(0)
}
_ => {
write_line(stderr, format_args!("cd: no such directory: {target}"));
Flow::Next(1)
}
}
}
fn export(args: &[String], state: &mut ShellState, stderr: OutTarget) -> Flow {
for arg in args {
let (name, value) = match arg.split_once('=') {
Some((name, value)) => (name, Some(value.to_string())),
None => (arg.as_str(), None),
};
if !is_valid_name(name) {
write_line(
stderr,
format_args!("export: not a valid identifier: {name}"),
);
return Flow::Next(2);
}
state.export(name, value);
}
Flow::Next(0)
}
fn is_valid_name(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}