use crate::env::ShellEnv;
use crate::error::{RuntimeErrorKind, ShellError};
use nix::unistd::Pid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CdMode {
Logical,
Physical,
}
pub fn builtin_cd(args: &[String], env: &mut ShellEnv) -> Result<i32, ShellError> {
let (mode, operand) = parse_cd_options(args)?;
let (target, from_cdpath) = resolve_target(operand.as_deref(), env)?;
let old_pwd = env
.vars
.get("PWD")
.map(|s| s.to_string())
.or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned())
})
.unwrap_or_else(|| "/".to_string());
let new_pwd = match mode {
CdMode::Logical => {
let candidate = lexical_canonicalize(&target, &old_pwd);
if let Err(e) = std::env::set_current_dir(&candidate) {
return Err(ShellError::runtime(
RuntimeErrorKind::IoError,
format!("cd: {}: {}", target, e),
));
}
candidate
}
CdMode::Physical => {
if let Err(e) = std::env::set_current_dir(&target) {
return Err(ShellError::runtime(
RuntimeErrorKind::IoError,
format!("cd: {}: {}", target, e),
));
}
match std::env::current_dir() {
Ok(p) => p.to_string_lossy().into_owned(),
Err(e) => {
return Err(ShellError::runtime(
RuntimeErrorKind::IoError,
format!("cd: {}: {}", target, e),
));
}
}
}
};
let _ = env.vars.set("OLDPWD", old_pwd);
let _ = env.vars.set("PWD", new_pwd.clone());
if from_cdpath {
println!("{}", new_pwd);
}
Ok(0)
}
pub fn builtin_echo(args: &[String]) -> Result<i32, ShellError> {
if args.first().map(|a| a.as_str()) == Some("-n") {
print!("{}", args[1..].join(" "));
} else {
println!("{}", args.join(" "));
}
Ok(0)
}
pub fn builtin_alias(args: &[String], env: &mut ShellEnv) -> Result<i32, ShellError> {
if args.is_empty() {
for (name, value) in env.aliases.sorted_iter() {
println!("alias {}='{}'", name, value);
}
return Ok(0);
}
let mut status = 0;
for arg in args {
if let Some(pos) = arg.find('=') {
let name = &arg[..pos];
let value = &arg[pos + 1..];
env.aliases.set(name, value);
} else {
match env.aliases.get(arg) {
Some(value) => println!("alias {}='{}'", arg, value),
None => {
eprintln!("yosh: alias: {}: not found", arg);
status = 1;
}
}
}
}
Ok(status)
}
pub fn builtin_unalias(args: &[String], env: &mut ShellEnv) -> Result<i32, ShellError> {
if args.is_empty() {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
"unalias: usage: unalias name [name ...]",
));
}
let mut status = 0;
for arg in args {
if arg == "-a" {
env.aliases.clear();
} else if !env.aliases.remove(arg) {
eprintln!("yosh: unalias: {}: not found", arg);
status = 1;
}
}
Ok(status)
}
pub fn builtin_kill(args: &[String], shell_pgid: Pid) -> Result<i32, ShellError> {
if args.is_empty() {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
"kill: usage: kill [-s sigspec | -signum] pid...",
));
}
if args[0] == "-l" {
return kill_list(&args[1..]);
}
let (sig_num, pid_args) = match parse_kill_signal(args) {
Ok(v) => v,
Err(msg) => {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("kill: {}", msg),
));
}
};
let mut status = 0;
for pid_str in pid_args {
let pid: i32 = match pid_str.parse() {
Ok(n) => n,
Err(_) => {
eprintln!("yosh: kill: {}: invalid pid", pid_str);
status = 1;
continue;
}
};
let target = if pid == 0 {
let gpid = shell_pgid.as_raw();
if gpid <= 1 {
eprintln!("yosh: kill: invalid shell process group");
status = 1;
continue;
}
Pid::from_raw(-gpid)
} else {
Pid::from_raw(pid)
};
if let Err(e) =
nix::sys::signal::kill(target, nix::sys::signal::Signal::try_from(sig_num).ok())
{
eprintln!("yosh: kill: ({}) - {}", pid_str, e);
status = 1;
}
}
Ok(status)
}
fn parse_kill_signal(args: &[String]) -> Result<(i32, &[String]), String> {
if args[0] == "-s" {
if args.len() < 3 {
return Err("option requires an argument -- s".to_string());
}
let sig = crate::signal::signal_name_to_number(&args[1])?;
Ok((sig, &args[2..]))
} else if args[0] == "--" {
Ok((libc::SIGTERM, &args[1..]))
} else if args[0].starts_with('-') && args[0].len() > 1 {
let spec = &args[0][1..];
if let Ok(num) = spec.parse::<i32>() {
Ok((num, &args[1..]))
} else {
let sig = crate::signal::signal_name_to_number(spec)?;
Ok((sig, &args[1..]))
}
} else {
Ok((libc::SIGTERM, args))
}
}
fn kill_list(args: &[String]) -> Result<i32, ShellError> {
if args.is_empty() {
let names: Vec<&str> = crate::signal::SIGNAL_TABLE
.iter()
.map(|&(_, name)| name)
.collect();
println!("{}", names.join(" "));
return Ok(0);
}
for arg in args {
if let Ok(num) = arg.parse::<i32>() {
let sig = if num > 128 { num - 128 } else { num };
match crate::signal::signal_number_to_name(sig) {
Some(name) => println!("{}", name),
None => {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("kill: {}: invalid signal number", arg),
));
}
}
} else {
match crate::signal::signal_name_to_number(arg) {
Ok(num) => println!("{}", num),
Err(e) => {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("kill: {}", e),
));
}
}
}
}
Ok(0)
}
pub fn builtin_umask(args: &[String]) -> Result<i32, ShellError> {
if args.is_empty() {
let current = unsafe { libc::umask(0) };
unsafe { libc::umask(current) };
println!("{:04o}", current);
return Ok(0);
}
if args[0] == "-S" {
let current = unsafe { libc::umask(0) };
unsafe { libc::umask(current) };
println!("{}", umask_to_symbolic(current));
return Ok(0);
}
if args[0].chars().all(|c| c.is_ascii_digit()) {
return umask_set_octal(&args[0]);
}
umask_set_symbolic(&args[0])
}
fn umask_to_symbolic(mask: libc::mode_t) -> String {
let perms = 0o777 & !mask;
let fmt = |bits: libc::mode_t| -> String {
let mut s = String::new();
if bits & 4 != 0 {
s.push('r');
}
if bits & 2 != 0 {
s.push('w');
}
if bits & 1 != 0 {
s.push('x');
}
s
};
format!(
"u={},g={},o={}",
fmt((perms >> 6) & 7),
fmt((perms >> 3) & 7),
fmt(perms & 7),
)
}
fn umask_set_octal(s: &str) -> Result<i32, ShellError> {
for c in s.chars() {
if !('0'..='7').contains(&c) {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("umask: {}: invalid octal number", s),
));
}
}
match libc::mode_t::from_str_radix(s, 8) {
Ok(mode) => {
unsafe { libc::umask(mode) };
Ok(0)
}
Err(_) => Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("umask: {}: invalid octal number", s),
)),
}
}
pub(crate) fn parse_cd_options(args: &[String]) -> Result<(CdMode, Option<String>), ShellError> {
let mut mode = CdMode::Logical;
let mut iter = args.iter();
let operand: Option<String>;
loop {
match iter.next() {
None => {
operand = None;
break;
}
Some(a) if a == "--" => {
operand = iter.next().cloned();
if iter.next().is_some() {
return Err(ShellError::runtime(
RuntimeErrorKind::IoError,
"cd: too many arguments",
));
}
break;
}
Some(a) if a == "-" => {
operand = Some(a.clone());
if iter.next().is_some() {
return Err(ShellError::runtime(
RuntimeErrorKind::IoError,
"cd: too many arguments",
));
}
break;
}
Some(a) if a.starts_with('-') && a.len() >= 2 => {
for ch in a[1..].chars() {
match ch {
'L' => mode = CdMode::Logical,
'P' => mode = CdMode::Physical,
other => {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("cd: -{}: invalid option", other),
));
}
}
}
}
Some(a) => {
operand = Some(a.clone());
if iter.next().is_some() {
return Err(ShellError::runtime(
RuntimeErrorKind::IoError,
"cd: too many arguments",
));
}
break;
}
}
}
Ok((mode, operand))
}
pub(crate) fn lexical_canonicalize(path: &str, pwd: &str) -> String {
let combined: String = if path.starts_with('/') {
path.to_string()
} else if path.is_empty() {
pwd.to_string()
} else {
format!("{}/{}", pwd.trim_end_matches('/'), path)
};
let mut stack: Vec<&str> = Vec::new();
for comp in combined.split('/') {
match comp {
"" | "." => continue,
".." => {
if stack.last().map(|s| *s != "..").unwrap_or(false) {
stack.pop();
} else if !combined.starts_with('/') {
stack.push("..");
}
}
other => stack.push(other),
}
}
if stack.is_empty() {
"/".to_string()
} else {
let mut out = String::new();
for c in &stack {
out.push('/');
out.push_str(c);
}
out
}
}
pub(crate) fn resolve_target(
operand: Option<&str>,
env: &ShellEnv,
) -> Result<(String, bool), ShellError> {
let op = match operand {
None => {
return match env.vars.get("HOME") {
Some(h) if !h.is_empty() => Ok((h.to_string(), false)),
_ => Err(ShellError::runtime(
RuntimeErrorKind::IoError,
"cd: HOME not set",
)),
};
}
Some(o) => o,
};
if op == "-" {
return match env.vars.get("OLDPWD") {
Some(p) if !p.is_empty() => Ok((p.to_string(), true)),
_ => Err(ShellError::runtime(
RuntimeErrorKind::IoError,
"cd: OLDPWD not set",
)),
};
}
if op.starts_with('/') {
return Ok((op.to_string(), false));
}
if op == "." || op == ".." || op.starts_with("./") || op.starts_with("../") {
return Ok((op.to_string(), false));
}
if let Some(cdpath) = env.vars.get("CDPATH") {
for entry in cdpath.split(':') {
let dir = if entry.is_empty() { "." } else { entry };
let candidate = format!("{}/{}", dir.trim_end_matches('/'), op);
if let Ok(meta) = std::fs::metadata(&candidate)
&& meta.is_dir()
{
return Ok((candidate, true));
}
}
}
Ok((op.to_string(), false))
}
fn umask_set_symbolic(s: &str) -> Result<i32, ShellError> {
let current = unsafe { libc::umask(0) };
unsafe { libc::umask(current) };
let mut mask = current;
for clause in s.split(',') {
let bytes = clause.as_bytes();
if bytes.is_empty() {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("umask: {}: invalid symbolic mode", s),
));
}
let mut i = 0;
let mut who_mask: libc::mode_t = 0;
while i < bytes.len() && matches!(bytes[i], b'u' | b'g' | b'o' | b'a') {
match bytes[i] {
b'u' => who_mask |= 0o700,
b'g' => who_mask |= 0o070,
b'o' => who_mask |= 0o007,
b'a' => who_mask |= 0o777,
_ => unreachable!(),
}
i += 1;
}
if who_mask == 0 {
who_mask = 0o777;
}
if i >= bytes.len() || !matches!(bytes[i], b'=' | b'+' | b'-') {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("umask: {}: invalid symbolic mode", s),
));
}
let op = bytes[i] as char;
i += 1;
let mut perm_bits: libc::mode_t = 0;
while i < bytes.len() {
match bytes[i] {
b'r' => perm_bits |= 0o444,
b'w' => perm_bits |= 0o222,
b'x' => perm_bits |= 0o111,
_ => {
return Err(ShellError::runtime(
RuntimeErrorKind::InvalidArgument,
format!("umask: {}: invalid symbolic mode", s),
));
}
}
i += 1;
}
let effective_perms = perm_bits & who_mask;
match op {
'=' => {
mask = (mask & !who_mask) | (who_mask & !effective_perms);
}
'+' => {
mask &= !effective_perms;
}
'-' => {
mask |= effective_perms;
}
_ => unreachable!(),
}
}
unsafe { libc::umask(mask) };
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|x| x.to_string()).collect()
}
#[test]
fn parse_no_args_defaults_to_logical_none() {
let (mode, op) = parse_cd_options(&[]).unwrap();
assert_eq!(mode, CdMode::Logical);
assert_eq!(op, None);
}
#[test]
fn parse_dash_is_operand_not_option() {
let (mode, op) = parse_cd_options(&s(&["-"])).unwrap();
assert_eq!(mode, CdMode::Logical);
assert_eq!(op.as_deref(), Some("-"));
}
#[test]
fn parse_l_flag() {
let (mode, op) = parse_cd_options(&s(&["-L"])).unwrap();
assert_eq!(mode, CdMode::Logical);
assert_eq!(op, None);
}
#[test]
fn parse_p_flag() {
let (mode, op) = parse_cd_options(&s(&["-P"])).unwrap();
assert_eq!(mode, CdMode::Physical);
assert_eq!(op, None);
}
#[test]
fn parse_flag_with_operand() {
let (mode, op) = parse_cd_options(&s(&["-P", "/tmp"])).unwrap();
assert_eq!(mode, CdMode::Physical);
assert_eq!(op.as_deref(), Some("/tmp"));
}
#[test]
fn parse_combined_flags_last_wins() {
let (mode, _) = parse_cd_options(&s(&["-LP"])).unwrap();
assert_eq!(mode, CdMode::Physical);
let (mode, _) = parse_cd_options(&s(&["-PL"])).unwrap();
assert_eq!(mode, CdMode::Logical);
}
#[test]
fn parse_separate_flags_last_wins() {
let (mode, op) = parse_cd_options(&s(&["-L", "-P", "foo"])).unwrap();
assert_eq!(mode, CdMode::Physical);
assert_eq!(op.as_deref(), Some("foo"));
}
#[test]
fn parse_double_dash_terminates_options() {
let (mode, op) = parse_cd_options(&s(&["--", "-foo"])).unwrap();
assert_eq!(mode, CdMode::Logical);
assert_eq!(op.as_deref(), Some("-foo"));
}
#[test]
fn parse_invalid_option_errors() {
let err = parse_cd_options(&s(&["-x"])).unwrap_err();
assert_eq!(err.exit_code(), 2);
assert!(err.to_string().contains("invalid option"));
}
#[test]
fn parse_too_many_operands_errors() {
let err = parse_cd_options(&s(&["a", "b"])).unwrap_err();
assert!(err.to_string().contains("too many arguments"));
}
#[test]
fn lex_absolute_returned_as_is() {
assert_eq!(lexical_canonicalize("/tmp", "/Users/foo"), "/tmp");
}
#[test]
fn lex_absolute_with_dotdot() {
assert_eq!(lexical_canonicalize("/tmp/../etc", "/"), "/etc");
}
#[test]
fn lex_relative_resolves_against_pwd() {
assert_eq!(lexical_canonicalize("../bar", "/tmp/foo"), "/tmp/bar");
}
#[test]
fn lex_single_dots_skipped() {
assert_eq!(lexical_canonicalize("./foo/./bar", "/tmp"), "/tmp/foo/bar");
}
#[test]
fn lex_repeated_slashes_collapsed() {
assert_eq!(lexical_canonicalize("/tmp//foo", "/"), "/tmp/foo");
}
#[test]
fn lex_dotdot_above_root_stays_at_root() {
assert_eq!(lexical_canonicalize("/..", "/"), "/");
}
#[test]
fn lex_multiple_dotdots_pop_correctly() {
assert_eq!(lexical_canonicalize("a/b/../..", "/tmp/x"), "/tmp/x");
}
#[test]
fn lex_empty_operand_returns_pwd() {
assert_eq!(lexical_canonicalize("", "/tmp"), "/tmp");
}
#[test]
fn lex_root_stays_root() {
assert_eq!(lexical_canonicalize("/", "/tmp"), "/");
}
#[test]
fn lex_trailing_slash_dropped() {
assert_eq!(lexical_canonicalize("/tmp/", "/"), "/tmp");
}
use crate::env::ShellEnv;
fn make_env(pairs: &[(&str, &str)]) -> ShellEnv {
let mut env = ShellEnv::new("yosh", vec![]);
for name in &["HOME", "OLDPWD", "PWD", "CDPATH"] {
let _ = env.vars.unset(name);
}
for (k, v) in pairs {
let _ = env.vars.set(k, (*v).to_string());
}
env
}
#[test]
fn resolve_none_uses_home() {
let env = make_env(&[("HOME", "/home/x")]);
let (target, from_cdpath) = resolve_target(None, &env).unwrap();
assert_eq!(target, "/home/x");
assert!(!from_cdpath);
}
#[test]
fn resolve_none_home_unset_errors() {
let env = make_env(&[]);
let err = resolve_target(None, &env).unwrap_err();
assert!(err.to_string().contains("HOME not set"));
}
#[test]
fn resolve_dash_uses_oldpwd_and_sets_from_cdpath() {
let env = make_env(&[("OLDPWD", "/prev")]);
let (target, from_cdpath) = resolve_target(Some("-"), &env).unwrap();
assert_eq!(target, "/prev");
assert!(from_cdpath, "cd - must print the new PWD");
}
#[test]
fn resolve_dash_oldpwd_unset_errors() {
let env = make_env(&[]);
let err = resolve_target(Some("-"), &env).unwrap_err();
assert!(err.to_string().contains("OLDPWD not set"));
}
#[test]
fn resolve_absolute_passes_through() {
let env = make_env(&[("CDPATH", "/etc")]);
let (target, from_cdpath) = resolve_target(Some("/tmp"), &env).unwrap();
assert_eq!(target, "/tmp");
assert!(!from_cdpath, "absolute paths skip CDPATH");
}
#[test]
fn resolve_dot_prefix_skips_cdpath() {
let env = make_env(&[("CDPATH", "/etc")]);
let (target, from_cdpath) = resolve_target(Some("./foo"), &env).unwrap();
assert_eq!(target, "./foo");
assert!(!from_cdpath);
}
#[test]
fn resolve_dotdot_prefix_skips_cdpath() {
let env = make_env(&[("CDPATH", "/etc")]);
let (target, from_cdpath) = resolve_target(Some("../foo"), &env).unwrap();
assert_eq!(target, "../foo");
assert!(!from_cdpath);
}
#[test]
fn resolve_cdpath_hit() {
let tmp = tempfile::tempdir().unwrap();
let sub = tmp.path().join("sub");
std::fs::create_dir(&sub).unwrap();
let cdpath = tmp.path().to_string_lossy().to_string();
let env = make_env(&[("CDPATH", cdpath.as_str())]);
let (target, from_cdpath) = resolve_target(Some("sub"), &env).unwrap();
assert_eq!(target, sub.to_string_lossy());
assert!(from_cdpath);
}
#[test]
fn resolve_cdpath_miss_falls_through() {
let tmp = tempfile::tempdir().unwrap();
let cdpath = tmp.path().to_string_lossy().to_string();
let env = make_env(&[("CDPATH", cdpath.as_str())]);
let (target, from_cdpath) = resolve_target(Some("nonexistent_xyz"), &env).unwrap();
assert_eq!(target, "nonexistent_xyz");
assert!(!from_cdpath);
}
#[test]
fn resolve_cdpath_empty_entry_is_dot() {
let tmp = tempfile::tempdir().unwrap();
let sub = tmp.path().join("sub");
std::fs::create_dir(&sub).unwrap();
std::env::set_current_dir(tmp.path()).unwrap();
let env = make_env(&[("CDPATH", ":/nonexistent")]);
let (target, from_cdpath) = resolve_target(Some("sub"), &env).unwrap();
assert!(
target.ends_with("sub") || target == "./sub",
"got: {}",
target
);
assert!(from_cdpath);
}
#[test]
fn resolve_cdpath_skips_non_directory_entries() {
let tmp = tempfile::tempdir().unwrap();
let file_path = tmp.path().join("regular_file");
std::fs::write(&file_path, "x").unwrap();
let cdpath = tmp.path().to_string_lossy().to_string();
let env = make_env(&[("CDPATH", cdpath.as_str())]);
let (target, from_cdpath) = resolve_target(Some("regular_file"), &env).unwrap();
assert_eq!(target, "regular_file");
assert!(!from_cdpath);
}
}