//! Los builtins. Son los que el producto usa de verdad: la medición de las cards dio ocho //! (`exit` `echo` `exec` `test` `[` `cd` `command` `continue`) y la de los `/init` empotrados suma //! `set`, `read`, `printf`, `export`, `trap`, `shift`, `break`, `return`, `.`, `eval`, `local`, //! `unset`, `pwd`, `true`, `false`, `:`, `wait`, `umask`, `type`, `hash`. //! //! ⚠ Ese criterio —«los que el producto usa de verdad»— es correcto para una shell de ARRANQUE y //! deja fuera lo INTERACTIVO, que es lo que simi también es: `/bin/sh` de takana es la shell de //! login de la consola. Medido el 2026-10-23: con `alias` sin implementar, cada atajo de //! `/etc/simirc` moría con `simi: alias: no encontrado` — dieciséis errores al abrir la sesión, y //! el usuario sin un solo alias. `alias`/`unalias` están desde entonces, y son POSIX, no un lujo. //! //! `times` NO está todavía, y eso se dice en vez de fingirlo: ninguna card ni ningún `/init` lo //! usa. `getopts` SÍ está desde el 2026-10-23: era el último hueco POSIX que marcaba el banco, y //! un script que parsea sus propias opciones es lo más normal del mundo en una shell de login. use std::io::{BufRead, Write}; use crate::exec; use crate::shell::{Flujo, Shell, LLEGO_SENAL}; const BUILTINS: &[&str] = &[ ":", ".", "alias", "break", "cd", "command", "continue", "echo", "eval", "exec", "exit", "export", "false", "getopts", "hash", "local", "printf", "pwd", "read", "return", "set", "shift", "test", "[", "trap", "true", "type", "umask", "unalias", "unset", "wait", ]; pub fn es_builtin(n: &str) -> bool { BUILTINS.contains(&n) } /// Para el autocompletado de `linea`: completar una ORDEN tiene que ofrecer los builtins, que no /// están en el `PATH` y si no se nombran acá se quedarían fuera. pub fn nombres() -> &'static [&'static str] { BUILTINS } pub fn correr(sh: &mut Shell, argv: &[String]) -> Flujo { let args = &argv[1..]; match argv[0].as_str() { ":" | "true" => { sh.estado = 0; Flujo::Normal } "false" => { sh.estado = 1; Flujo::Normal } "alias" => { // Sin operandos: lista TODO en forma reutilizable (`alias n='v'`), que es lo que POSIX // pide — la salida tiene que poder volver a leerse como entrada. if args.is_empty() { let mut o = std::io::stdout().lock(); for (n, v) in &sh.alias { let _ = writeln!(o, "alias {}={}", n, entrecomillar(v)); } sh.estado = 0; return Flujo::Normal; } let mut rc = 0; for a in args { match a.split_once('=') { // `alias n=v` define. El nombre vacío (`=v`) no es un alias válido. Some((n, v)) if !n.is_empty() => { sh.alias.insert(n.to_string(), v.to_string()); } // `alias n` consulta. _ => match sh.alias.get(a.as_str()) { Some(v) => { println!("alias {}={}", a, entrecomillar(v)); } None => { sh.error(&format!("alias: {a}: no existe")); rc = 1; } }, } } sh.estado = rc; Flujo::Normal } "getopts" => { // `getopts optstring nombre [arg…]` if args.len() < 2 { sh.error("getopts: uso: getopts cadena nombre [arg…]"); sh.estado = 2; return Flujo::Normal; } let spec_crudo = args[0].clone(); let nombre = args[1].clone(); // Un `:` al principio = modo SILENCIOSO: los errores se cuentan por la variable en vez // de escribirse en stderr. Es lo que usa un script que quiere dar su propio mensaje. let callado = spec_crudo.starts_with(':'); let spec: Vec = if callado { spec_crudo[1..].chars().collect() } else { spec_crudo.chars().collect() }; // Los operandos explícitos ganan sobre los posicionales, como manda POSIX. let lista: Vec = if args.len() > 2 { args[2..].to_vec() } else { sh.params.clone() }; let mut ind: i64 = sh.leer("OPTIND").and_then(|v| v.parse().ok()).unwrap_or(1); if ind < 1 { ind = 1; } // Si el script reasignó `OPTIND` a mano, el `optpos` que guardábamos ya no vale. if ind != sh.optind_escrito { sh.optpos = 0; } // Terminar: devolver >0 y dejar `OPTIND` donde apunta el primer operando. macro_rules! terminar { () => {{ sh.asignar("OPTIND", &ind.to_string()); sh.optind_escrito = ind; sh.optpos = 0; sh.estado = 1; return Flujo::Normal; }}; } if ind as usize > lista.len() { terminar!(); } let arg: Vec = lista[ind as usize - 1].chars().collect(); if sh.optpos == 0 { // Arranca un argumento nuevo: sólo sigue si parece opción. if arg.len() < 2 || arg[0] != '-' { terminar!(); } if arg[1] == '-' && arg.len() == 2 { // `--` cierra las opciones y se consume. ind += 1; terminar!(); } sh.optpos = 1; } let c = arg[sh.optpos]; sh.optpos += 1; let mut agotado = sh.optpos >= arg.len(); if agotado { sh.optpos = 0; } let pos = spec.iter().position(|&x| x == c && x != ':'); let necesita = pos.map(|i| spec.get(i + 1) == Some(&':')).unwrap_or(false); if pos.is_none() { // Opción desconocida. if agotado { ind += 1; } sh.asignar("OPTIND", &ind.to_string()); sh.optind_escrito = ind; sh.asignar(&nombre, "?"); if callado { sh.asignar("OPTARG", &c.to_string()); } else { sh.error(&format!("getopts: opción no válida -- {c}")); sh.vars.remove("OPTARG"); } sh.estado = 0; return Flujo::Normal; } if necesita { if !agotado { // El valor es el RESTO del mismo argumento: `-fx`. let resto: String = arg[sh.optpos..].iter().collect(); sh.asignar("OPTARG", &resto); sh.optpos = 0; agotado = true; ind += 1; } else { // El valor es el argumento SIGUIENTE: `-f x`. ind += 1; if ind as usize > lista.len() { sh.asignar("OPTIND", &ind.to_string()); sh.optind_escrito = ind; if callado { sh.asignar(&nombre, ":"); sh.asignar("OPTARG", &c.to_string()); } else { sh.error(&format!("getopts: la opción -- {c} necesita un argumento")); sh.asignar(&nombre, "?"); sh.vars.remove("OPTARG"); } sh.estado = 0; return Flujo::Normal; } sh.asignar("OPTARG", &lista[ind as usize - 1]); ind += 1; } } else { sh.vars.remove("OPTARG"); if agotado { ind += 1; } } let _ = agotado; sh.asignar("OPTIND", &ind.to_string()); sh.optind_escrito = ind; sh.asignar(&nombre, &c.to_string()); sh.estado = 0; Flujo::Normal } "unalias" => { if args.iter().any(|a| a == "-a") { sh.alias.clear(); sh.estado = 0; return Flujo::Normal; } if args.is_empty() { sh.error("unalias: falta el nombre"); sh.estado = 2; return Flujo::Normal; } let mut rc = 0; for a in args { if sh.alias.remove(a).is_none() { sh.error(&format!("unalias: {a}: no existe")); rc = 1; } } sh.estado = rc; Flujo::Normal } "echo" => bi_echo(sh, args), "printf" => bi_printf(sh, args), "pwd" => { let d = sh.leer("PWD").unwrap_or_default(); println!("{d}"); sh.estado = 0; Flujo::Normal } "cd" => bi_cd(sh, args), "exit" => { let c = args.first().and_then(|s| s.parse().ok()).unwrap_or(sh.estado); Flujo::Exit(c) } "return" => { let c = args.first().and_then(|s| s.parse().ok()).unwrap_or(sh.estado); sh.estado = c; if sh.funcion_honda == 0 { // `return` fuera de una función: POSIX lo deja sin especificar; ash sale. return Flujo::Exit(c); } Flujo::Return } "break" | "continue" => { let n: u32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1); if sh.bucles == 0 { sh.estado = 0; return Flujo::Normal; } sh.estado = 0; if argv[0] == "break" { Flujo::Break(n.max(1)) } else { Flujo::Continue(n.max(1)) } } "shift" => { let n: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(1); // ⚠ Más allá de `$#` el estado es 1 y los posicionales NO se tocan. brush devolvía 2 // acá (medido en el banco POSIX); POSIX, busybox y bash devuelven 1. if n > sh.params.len() { sh.estado = 1; } else { sh.params.drain(..n); sh.estado = 0; } Flujo::Normal } "export" => { for a in args { match a.split_once('=') { Some((k, v)) => { sh.asignar(k, v); sh.exportar(k); } None => sh.exportar(a), } } sh.estado = 0; Flujo::Normal } "local" => { if sh.funcion_honda == 0 { sh.error("local: fuera de una función"); sh.estado = 1; return Flujo::Normal; } for a in args { match a.split_once('=') { Some((k, v)) => { sh.declarar_local(k); sh.asignar(k, v); } None => { sh.declarar_local(a); sh.vars.insert(a.clone(), (String::new(), false)); } } } sh.estado = 0; Flujo::Normal } "unset" => { for a in args { if a == "-f" { continue; } sh.vars.remove(a); sh.funcs.remove(a); } sh.estado = 0; Flujo::Normal } "set" => bi_set(sh, args), "eval" => { let texto = args.join(" "); match crate::parser::parsear(&texto) { Ok(l) => exec::correr_lista(sh, &l), Err(e) => { sh.error(&format!("eval: {e}")); sh.estado = 2; Flujo::Normal } } } "." => bi_punto(sh, args), "exec" => { if args.is_empty() { sh.estado = 0; return Flujo::Normal; } exec::lanzar_externo(sh, args, &[], &[], true) } "command" => { let resto: Vec = args.iter().filter(|a| a.as_str() != "-p").cloned().collect(); if resto.first().map(|s| s.as_str()) == Some("-v") { let Some(n) = resto.get(1) else { sh.estado = 2; return Flujo::Normal; }; if es_builtin(n) || sh.funcs.contains_key(n) { println!("{n}"); sh.estado = 0; } else if let Some(r) = ruta_de(sh, n) { println!("{r}"); sh.estado = 0; } else { sh.estado = 1; } return Flujo::Normal; } if resto.is_empty() { sh.estado = 0; return Flujo::Normal; } if es_builtin(&resto[0]) { return correr(sh, &resto); } exec::lanzar_externo(sh, &resto, &[], &[], false) } "type" => { for a in args { if es_builtin(a) { println!("{a} is a shell builtin"); } else if sh.funcs.contains_key(a) { println!("{a} is a function"); } else if let Some(r) = ruta_de(sh, a) { println!("{a} is {r}"); } else { println!("{a}: not found"); sh.estado = 1; return Flujo::Normal; } } sh.estado = 0; Flujo::Normal } "hash" => { sh.estado = 0; Flujo::Normal } "umask" => { match args.first() { None => { let previa = unsafe { libc::umask(0o022) }; unsafe { libc::umask(previa) }; println!("{previa:04o}"); } Some(v) => match u32::from_str_radix(v, 8) { Ok(m) => { unsafe { libc::umask(m as libc::mode_t) }; } Err(_) => { sh.error(&format!("umask: {v}: no es octal")); sh.estado = 1; return Flujo::Normal; } }, } sh.estado = 0; Flujo::Normal } "wait" => { // sin argumentos: esperar a todos los hijos loop { let r = unsafe { libc::wait(std::ptr::null_mut()) }; if r < 0 { break; } } sh.estado = 0; Flujo::Normal } "trap" => bi_trap(sh, args), "read" => bi_read(sh, args), "test" | "[" => { let mut a: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); if argv[0] == "[" { if a.last() != Some(&"]") { sh.error("[: falta `]`"); sh.estado = 2; return Flujo::Normal; } a.pop(); } match evaluar_test(&a) { Ok(v) => sh.estado = !v as i32, Err(e) => { sh.error(&format!("test: {e}")); sh.estado = 2; } } Flujo::Normal } otro => { sh.error(&format!("{otro}: builtin sin implementar")); sh.estado = 127; Flujo::Normal } } } fn ruta_de(sh: &Shell, n: &str) -> Option { if n.contains('/') { return Some(n.to_string()); } let path = sh.leer("PATH").unwrap_or_else(|| "/usr/bin:/bin".into()); for dir in path.split(':') { let cand = format!("{}/{n}", if dir.is_empty() { "." } else { dir }); if std::fs::metadata(&cand).is_ok() { return Some(cand); } } None } fn bi_echo(sh: &mut Shell, args: &[String]) -> Flujo { // POSIX no da banderas a `echo`; `-n` es universal y el árbol lo usa. `-e` NO: para eso está // `printf`, y fingirlo sería adoptar una divergencia. let (sin_salto, resto) = match args.first().map(|s| s.as_str()) { Some("-n") => (true, &args[1..]), _ => (false, args), }; let salida = resto.join(" "); let mut out = std::io::stdout().lock(); let _ = out.write_all(salida.as_bytes()); if !sin_salto { let _ = out.write_all(b"\n"); } let _ = out.flush(); sh.estado = 0; Flujo::Normal } fn bi_printf(sh: &mut Shell, args: &[String]) -> Flujo { let Some(formato) = args.first() else { sh.error("printf: falta el formato"); sh.estado = 2; return Flujo::Normal; }; let datos = &args[1..]; let mut i = 0; let mut salida = String::new(); loop { let consumidos = formatear(formato, datos, i, &mut salida); match consumidos { Err(e) => { sh.error(&format!("printf: {e}")); sh.estado = 1; return Flujo::Normal; } Ok(0) => break, Ok(n) => { i += n; if i >= datos.len() { break; } } } } let mut out = std::io::stdout().lock(); let _ = out.write_all(salida.as_bytes()); let _ = out.flush(); sh.estado = 0; Flujo::Normal } /// Una pasada del formato. Devuelve cuántos argumentos consumió (0 = el formato no tiene /// conversiones, o sea que no hay que repetirlo). fn formatear( formato: &str, datos: &[String], desde: usize, salida: &mut String, ) -> Result { let cs: Vec = formato.chars().collect(); let mut i = 0; let mut usados = 0; while i < cs.len() { match cs[i] { '\\' if i + 1 < cs.len() => { salida.push_str(&escape(cs[i + 1])); i += 2; } '%' if i + 1 < cs.len() => { if cs[i + 1] == '%' { salida.push('%'); i += 2; continue; } // banderas, ancho y precisión let mut j = i + 1; let mut spec = String::from("%"); while j < cs.len() && "-+ #0".contains(cs[j]) { spec.push(cs[j]); j += 1; } let mut ancho = String::new(); while j < cs.len() && cs[j].is_ascii_digit() { ancho.push(cs[j]); j += 1; } let mut precision = String::new(); if j < cs.len() && cs[j] == '.' { j += 1; while j < cs.len() && cs[j].is_ascii_digit() { precision.push(cs[j]); j += 1; } } let Some(conv) = cs.get(j) else { return Err("formato incompleto".into()) }; let arg = datos.get(desde + usados).cloned().unwrap_or_default(); usados += 1; let texto = match conv { 's' => arg, 'b' => { let mut s = String::new(); let a: Vec = arg.chars().collect(); let mut k = 0; while k < a.len() { if a[k] == '\\' && k + 1 < a.len() { s.push_str(&escape(a[k + 1])); k += 2; } else { s.push(a[k]); k += 1; } } s } 'd' | 'i' => { let n: i64 = arg.trim().parse().unwrap_or(0); n.to_string() } 'u' => { let n: i64 = arg.trim().parse().unwrap_or(0); (n.max(0)).to_string() } 'x' => format!("{:x}", arg.trim().parse::().unwrap_or(0)), 'X' => format!("{:X}", arg.trim().parse::().unwrap_or(0)), 'o' => format!("{:o}", arg.trim().parse::().unwrap_or(0)), 'c' => arg.chars().next().map(|c| c.to_string()).unwrap_or_default(), c => return Err(format!("conversión `%{c}` no soportada")), }; let texto = match precision.parse::() { Ok(p) if matches!(conv, 's' | 'b') => texto.chars().take(p).collect(), _ => texto, }; let ancho_n: usize = ancho.parse().unwrap_or(0); let izquierda = spec.contains('-'); let cero = spec.contains('0') && !izquierda; if texto.chars().count() < ancho_n { let relleno: String = std::iter::repeat(if cero { '0' } else { ' ' }) .take(ancho_n - texto.chars().count()) .collect(); if izquierda { salida.push_str(&texto); salida.push_str(&relleno); } else { salida.push_str(&relleno); salida.push_str(&texto); } } else { salida.push_str(&texto); } i = j + 1; } c => { salida.push(c); i += 1; } } } Ok(usados) } fn escape(c: char) -> String { match c { 'n' => "\n".into(), 't' => "\t".into(), 'r' => "\r".into(), '\\' => "\\".into(), '0' => "\0".into(), 'a' => "\x07".into(), 'b' => "\x08".into(), 'f' => "\x0c".into(), 'v' => "\x0b".into(), otro => format!("\\{otro}"), } } fn bi_cd(sh: &mut Shell, args: &[String]) -> Flujo { let destino = match args.first() { Some(d) if d == "-" => sh.leer("OLDPWD").unwrap_or_default(), Some(d) => d.clone(), None => sh.leer("HOME").unwrap_or_default(), }; if destino.is_empty() { sh.error("cd: HOME sin definir"); sh.estado = 1; return Flujo::Normal; } let previo = sh.leer("PWD").unwrap_or_default(); match std::env::set_current_dir(&destino) { Ok(()) => { let nuevo = std::env::current_dir() .map(|d| d.to_string_lossy().into_owned()) .unwrap_or(destino); sh.asignar("OLDPWD", &previo); sh.exportar("OLDPWD"); sh.asignar("PWD", &nuevo); sh.exportar("PWD"); sh.estado = 0; } Err(e) => { sh.error(&format!("cd: {destino}: {e}")); sh.estado = 1; } } Flujo::Normal } fn bi_set(sh: &mut Shell, args: &[String]) -> Flujo { if args.is_empty() { let mut nombres: Vec<&String> = sh.vars.keys().collect(); nombres.sort(); for n in nombres { println!("{n}={}", sh.vars[n].0); } sh.estado = 0; return Flujo::Normal; } let mut i = 0; while i < args.len() { let a = &args[i]; if a == "--" { i += 1; break; } let (prender, letras) = match a.strip_prefix('-') { Some(l) => (true, l), None => match a.strip_prefix('+') { Some(l) => (false, l), None => break, }, }; for c in letras.chars() { match c { 'e' => sh.opts.errexit = prender, 'u' => sh.opts.nounset = prender, 'x' => sh.opts.xtrace = prender, 'n' => sh.opts.noexec = prender, 'f' => sh.opts.noglob = prender, otro => { sh.error(&format!("set: -{otro}: opción no soportada")); sh.estado = 2; return Flujo::Normal; } } } i += 1; } if i < args.len() || args.iter().any(|a| a == "--") { sh.params = args[i..].to_vec(); } sh.estado = 0; Flujo::Normal } fn bi_punto(sh: &mut Shell, args: &[String]) -> Flujo { let Some(ruta) = args.first() else { sh.error(".: falta el fichero"); sh.estado = 2; return Flujo::Normal; }; let ruta = if ruta.contains('/') { ruta.clone() } else { ruta_de(sh, ruta).unwrap_or_else(|| ruta.clone()) }; let texto = match std::fs::read_to_string(&ruta) { Ok(t) => t, Err(e) => { sh.error(&format!(".: {ruta}: {e}")); sh.estado = 1; return Flujo::Normal; } }; match crate::parser::parsear(&texto) { Ok(l) => { let f = exec::correr_lista(sh, &l); match f { Flujo::Return => Flujo::Normal, otro => otro, } } Err(e) => { sh.error(&format!(".: {ruta}: {e}")); sh.estado = 2; Flujo::Normal } } } fn bi_trap(sh: &mut Shell, args: &[String]) -> Flujo { if args.is_empty() { let mut ks: Vec<&String> = sh.traps.keys().collect(); ks.sort(); for k in ks { println!("trap -- '{}' {}", sh.traps[k], k); } sh.estado = 0; return Flujo::Normal; } let (accion, senales) = (&args[0], &args[1..]); for s in senales { let nombre = normalizar_senal(s); if accion.is_empty() || accion == "-" { sh.traps.remove(&nombre); if let Some(n) = numero_de_senal(&nombre) { unsafe { libc::signal(n, libc::SIG_DFL) }; } continue; } sh.traps.insert(nombre.clone(), accion.clone()); if let Some(n) = numero_de_senal(&nombre) { unsafe { libc::signal(n, manejar as libc::sighandler_t) }; } } sh.estado = 0; Flujo::Normal } extern "C" fn manejar(n: libc::c_int) { if (n as usize) < LLEGO_SENAL.len() { LLEGO_SENAL[n as usize].store(true, std::sync::atomic::Ordering::SeqCst); } } const SENALES: &[(&str, i32)] = &[ ("HUP", libc::SIGHUP), ("INT", libc::SIGINT), ("QUIT", libc::SIGQUIT), ("TERM", libc::SIGTERM), ("USR1", libc::SIGUSR1), ("USR2", libc::SIGUSR2), ("PIPE", libc::SIGPIPE), ("ALRM", libc::SIGALRM), ]; fn normalizar_senal(s: &str) -> String { let limpio = s.trim_start_matches("SIG").to_uppercase(); if limpio == "EXIT" || limpio == "0" { return "EXIT".into(); } if let Ok(n) = limpio.parse::() { if let Some((nombre, _)) = SENALES.iter().find(|(_, v)| *v == n) { return (*nombre).to_string(); } } limpio } fn numero_de_senal(nombre: &str) -> Option { SENALES.iter().find(|(n, _)| *n == nombre).map(|(_, v)| *v) } pub fn nombre_de_senal(n: i32) -> String { SENALES .iter() .find(|(_, v)| *v == n) .map(|(nombre, _)| (*nombre).to_string()) .unwrap_or_else(|| n.to_string()) } fn bi_read(sh: &mut Shell, args: &[String]) -> Flujo { let sin_escapes = args.first().map(|s| s.as_str()) == Some("-r"); let nombres: Vec = args[if sin_escapes { 1 } else { 0 }..].iter().cloned().collect(); let nombres = if nombres.is_empty() { vec!["REPLY".to_string()] } else { nombres }; let mut linea = String::new(); let leidos = std::io::stdin().lock().read_line(&mut linea).unwrap_or(0); if leidos == 0 { sh.estado = 1; return Flujo::Normal; } while linea.ends_with('\n') || linea.ends_with('\r') { linea.pop(); } if !sin_escapes { linea = linea.replace("\\\n", ""); } let ifs = sh.ifs(); let sep: Vec = ifs.chars().collect(); let campos: Vec<&str> = linea .split(|c| sep.contains(&c)) .filter(|s| !s.is_empty() || sep.iter().any(|c| !c.is_whitespace())) .collect(); for (n, nombre) in nombres.iter().enumerate() { let valor = if n + 1 == nombres.len() { // el último se come el resto campos[n.min(campos.len())..].join(" ") } else { campos.get(n).copied().unwrap_or("").to_string() }; sh.asignar(nombre, &valor); } sh.estado = 0; Flujo::Normal } // ───────────────────────────────────────────────────────── test /// `test` con el algoritmo por CANTIDAD de argumentos de POSIX §test, que es el que hace que /// `test "$x" = y` funcione con `$x` vacío o con `$x` valiendo `-f`. pub fn evaluar_test(a: &[&str]) -> Result { match a.len() { 0 => Ok(false), 1 => Ok(!a[0].is_empty()), 2 => { if a[0] == "!" { return Ok(a[1].is_empty()); } unario(a[0], a[1]) } 3 => { if let Some(v) = binario(a[0], a[1], a[2])? { return Ok(v); } if a[0] == "!" { return Ok(!evaluar_test(&a[1..])?); } if a[0] == "(" && a[2] == ")" { return evaluar_test(&a[1..2]); } Err(format!("expresión inválida: {a:?}")) } 4 => { if a[0] == "!" { return Ok(!evaluar_test(&a[1..])?); } if a[0] == "(" && a[3] == ")" { return evaluar_test(&a[1..3]); } general(a) } _ => general(a), } } /// `-a` / `-o` / `!` / paréntesis, con `-a` más fuerte que `-o`. fn general(a: &[&str]) -> Result { // partir por el `-o` de más afuera if let Some(i) = buscar_al_nivel(a, "-o") { return Ok(general(&a[..i])? || general(&a[i + 1..])?); } if let Some(i) = buscar_al_nivel(a, "-a") { return Ok(general(&a[..i])? && general(&a[i + 1..])?); } if a.first() == Some(&"!") { return Ok(!general(&a[1..])?); } if a.first() == Some(&"(") && a.last() == Some(&")") { return general(&a[1..a.len() - 1]); } evaluar_test(a) } fn buscar_al_nivel(a: &[&str], op: &str) -> Option { let mut prof = 0; for (i, t) in a.iter().enumerate() { match *t { "(" => prof += 1, ")" => prof -= 1, _ if *t == op && prof == 0 => return Some(i), _ => {} } } None } fn unario(op: &str, arg: &str) -> Result { let meta = std::fs::metadata(arg); let lmeta = std::fs::symlink_metadata(arg); Ok(match op { "-e" => lmeta.is_ok(), "-f" => meta.map(|m| m.is_file()).unwrap_or(false), "-d" => meta.map(|m| m.is_dir()).unwrap_or(false), "-h" | "-L" => lmeta.map(|m| m.file_type().is_symlink()).unwrap_or(false), "-s" => meta.map(|m| m.len() > 0).unwrap_or(false), "-r" => acceso(arg, libc::R_OK), "-w" => acceso(arg, libc::W_OK), "-x" => acceso(arg, libc::X_OK), "-n" => !arg.is_empty(), "-z" => arg.is_empty(), "-t" => { let fd: i32 = arg.parse().unwrap_or(-1); fd >= 0 && unsafe { libc::isatty(fd) } == 1 } "-p" | "-S" | "-b" | "-c" | "-g" | "-u" | "-k" | "-O" | "-G" => { use std::os::unix::fs::FileTypeExt; match lmeta { Err(_) => false, Ok(m) => match op { "-p" => m.file_type().is_fifo(), "-S" => m.file_type().is_socket(), "-b" => m.file_type().is_block_device(), "-c" => m.file_type().is_char_device(), _ => false, }, } } otro => return Err(format!("operador unario `{otro}` no soportado")), }) } fn acceso(ruta: &str, modo: i32) -> bool { match std::ffi::CString::new(ruta) { Ok(c) => (unsafe { libc::access(c.as_ptr(), modo) }) == 0, Err(_) => false, } } fn binario(a: &str, op: &str, b: &str) -> Result, String> { let num = |s: &str| -> Result { s.trim().parse().map_err(|_| format!("`{s}`: se esperaba un entero")) }; Ok(Some(match op { "=" | "==" => a == b, "!=" => a != b, "<" => a < b, ">" => a > b, "-eq" => num(a)? == num(b)?, "-ne" => num(a)? != num(b)?, "-lt" => num(a)? < num(b)?, "-le" => num(a)? <= num(b)?, "-gt" => num(a)? > num(b)?, "-ge" => num(a)? >= num(b)?, "-nt" | "-ot" => { let ma = std::fs::metadata(a).and_then(|m| m.modified()).ok(); let mb = std::fs::metadata(b).and_then(|m| m.modified()).ok(); match (ma, mb) { (Some(x), Some(y)) => { if op == "-nt" { x > y } else { x < y } } _ => false, } } "-ef" => { use std::os::unix::fs::MetadataExt; match (std::fs::metadata(a), std::fs::metadata(b)) { (Ok(x), Ok(y)) => x.ino() == y.ino() && x.dev() == y.dev(), _ => false, } } _ => return Ok(None), })) } #[cfg(test)] mod pruebas { use super::*; #[test] fn test_por_cantidad_de_argumentos() { assert!(evaluar_test(&["x"]).unwrap()); assert!(!evaluar_test(&[""]).unwrap()); assert!(evaluar_test(&["-n", "x"]).unwrap()); assert!(evaluar_test(&["-z", ""]).unwrap()); assert!(evaluar_test(&["!", ""]).unwrap()); assert!(evaluar_test(&["a", "=", "a"]).unwrap()); assert!(evaluar_test(&["1", "-lt", "2"]).unwrap()); assert!(!evaluar_test(&["2", "-lt", "2"]).unwrap()); // el caso que justifica el algoritmo: `$x` vacío con `=` assert!(evaluar_test(&["", "=", ""]).unwrap()); // `-f` como OPERANDO, no como operador assert!(evaluar_test(&["-f", "=", "-f"]).unwrap()); } #[test] fn test_ficheros_reales() { assert!(evaluar_test(&["-e", "/dev/null"]).unwrap()); assert!(evaluar_test(&["-d", "/"]).unwrap()); assert!(!evaluar_test(&["-f", "/no-existe-jamas"]).unwrap()); assert!(evaluar_test(&["-c", "/dev/null"]).unwrap()); } #[test] fn test_y_o_y_parentesis() { assert!(evaluar_test(&["-d", "/", "-a", "-e", "/dev/null"]).unwrap()); assert!(evaluar_test(&["-f", "/nope", "-o", "-d", "/"]).unwrap()); assert!(!evaluar_test(&["!", "-d", "/"]).unwrap()); assert!(evaluar_test(&["(", "-d", "/", ")"]).unwrap()); } #[test] fn printf_formatea() { let mut s = String::new(); formatear("%03d\\n", &["7".into()], 0, &mut s).unwrap(); assert_eq!(s, "007\n"); let mut s = String::new(); formatear("%s|%s\\n", &["a".into(), "b".into()], 0, &mut s).unwrap(); assert_eq!(s, "a|b\n"); let mut s = String::new(); formatear("%-4s|", &["x".into()], 0, &mut s).unwrap(); assert_eq!(s, "x |"); } } /// Entrecomilla un valor de alias para que `alias` sin argumentos produzca algo que se pueda /// volver a leer. Comilla simple, y una comilla simple dentro se cierra, se escapa y se reabre /// (`'\''`) — que es el único truco que funciona dentro de comillas simples. fn entrecomillar(v: &str) -> String { format!("'{}'", v.replace('\'', "'\\''")) }