diff --git a/Cargo.lock b/Cargo.lock index 6c07357e..c7f876c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1152,6 +1152,13 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simi" +version = "0.0.1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 7d0dd34f..be5d6b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/takana-cli", "crates/hammerd", "crates/netup", + "crates/simi", ] [workspace.package] diff --git a/crates/simi/Cargo.toml b/crates/simi/Cargo.toml new file mode 100644 index 00000000..8fc86555 --- /dev/null +++ b/crates/simi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "simi" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +description = "simi — el /bin/sh del producto: POSIX, sin deps, para las cards de arje y los /init de la imagen." + +[[bin]] +name = "simi" +path = "src/main.rs" + +# Sólo libc, igual que netup y hammerd: un shell necesita fork/execve/waitpid/dup2/pipe y eso es +# ABI estable del kernel. Cero crates de terceros — ni clap, ni regex, ni tokio: el glob, la +# aritmética y el parseo son propios. La razón no es purismo: este binario es la raíz de confianza +# del frente C (ATTEST_PATHS), y cada dep suya entra en esa raíz. +[dependencies] +libc = "0.2" diff --git a/crates/simi/src/arith.rs b/crates/simi/src/arith.rs new file mode 100644 index 00000000..575767b9 --- /dev/null +++ b/crates/simi/src/arith.rs @@ -0,0 +1,389 @@ +//! `$(( … ))`. Aritmética entera con signo (POSIX §2.6.4: la misma que el C de `long`), con +//! precedencia completa, ternario y asignación. Propio: es un descenso recursivo de 150 líneas. +//! +//! La división por cero es un ERROR, no un 0 silencioso — un `$((x/0))` que devuelve 0 es +//! exactamente la clase de fallo que llega hasta el final diciendo que todo fue bien. + +type Num = i64; + +pub struct Contexto<'a> { + /// Lee una variable (vacía si no existe), como manda POSIX para la aritmética. + pub leer: &'a dyn Fn(&str) -> String, + /// Asigna (para `x = …`, `x += …`). + pub asignar: &'a mut dyn FnMut(&str, &str), +} + +pub fn evaluar(expr: &str, ctx: &mut Contexto) -> Result { + let mut p = P { cs: expr.chars().collect(), i: 0, ctx }; + p.espacios(); + let v = p.ternario()?; + p.espacios(); + if p.i < p.cs.len() { + return Err(format!("aritmética: sobra `{}`", p.cs[p.i..].iter().collect::())); + } + Ok(v) +} + +struct P<'a, 'b> { + cs: Vec, + i: usize, + ctx: &'a mut Contexto<'b>, +} + +impl P<'_, '_> { + fn espacios(&mut self) { + while matches!(self.cs.get(self.i), Some(c) if c.is_whitespace()) { + self.i += 1; + } + } + + fn come(&mut self, s: &str) -> bool { + let n = s.chars().count(); + if self.cs[self.i..].starts_with(&s.chars().collect::>()[..]) { + // no confundir `=` con `==`, ni `<` con `<<` + self.i += n; + self.espacios(); + true + } else { + false + } + } + + fn mira(&self, s: &str) -> bool { + self.cs[self.i..].starts_with(&s.chars().collect::>()[..]) + } + + fn ternario(&mut self) -> Result { + let c = self.o_logico()?; + if self.come("?") { + let a = self.ternario()?; + if !self.come(":") { + return Err("aritmética: falta `:` del ternario".into()); + } + let b = self.ternario()?; + return Ok(if c != 0 { a } else { b }); + } + Ok(c) + } + + fn o_logico(&mut self) -> Result { + let mut a = self.y_logico()?; + while self.mira("||") { + self.come("||"); + let b = self.y_logico()?; + a = ((a != 0) || (b != 0)) as Num; + } + Ok(a) + } + + fn y_logico(&mut self) -> Result { + let mut a = self.o_bits()?; + while self.mira("&&") { + self.come("&&"); + let b = self.o_bits()?; + a = ((a != 0) && (b != 0)) as Num; + } + Ok(a) + } + + fn o_bits(&mut self) -> Result { + let mut a = self.xor_bits()?; + while self.mira("|") && !self.mira("||") { + self.come("|"); + a |= self.xor_bits()?; + } + Ok(a) + } + + fn xor_bits(&mut self) -> Result { + let mut a = self.y_bits()?; + while self.mira("^") { + self.come("^"); + a ^= self.y_bits()?; + } + Ok(a) + } + + fn y_bits(&mut self) -> Result { + let mut a = self.igualdad()?; + while self.mira("&") && !self.mira("&&") { + self.come("&"); + a &= self.igualdad()?; + } + Ok(a) + } + + fn igualdad(&mut self) -> Result { + let mut a = self.relacional()?; + loop { + if self.mira("==") { + self.come("=="); + a = (a == self.relacional()?) as Num; + } else if self.mira("!=") { + self.come("!="); + a = (a != self.relacional()?) as Num; + } else { + return Ok(a); + } + } + } + + fn relacional(&mut self) -> Result { + let mut a = self.desplazamiento()?; + loop { + if self.mira("<=") { + self.come("<="); + a = (a <= self.desplazamiento()?) as Num; + } else if self.mira(">=") { + self.come(">="); + a = (a >= self.desplazamiento()?) as Num; + } else if self.mira("<") && !self.mira("<<") { + self.come("<"); + a = (a < self.desplazamiento()?) as Num; + } else if self.mira(">") && !self.mira(">>") { + self.come(">"); + a = (a > self.desplazamiento()?) as Num; + } else { + return Ok(a); + } + } + } + + fn desplazamiento(&mut self) -> Result { + let mut a = self.aditivo()?; + loop { + if self.mira("<<") { + self.come("<<"); + a = a.wrapping_shl(self.aditivo()? as u32); + } else if self.mira(">>") { + self.come(">>"); + a = a.wrapping_shr(self.aditivo()? as u32); + } else { + return Ok(a); + } + } + } + + fn aditivo(&mut self) -> Result { + let mut a = self.multiplicativo()?; + loop { + self.espacios(); + if self.mira("+") && !self.mira("++") { + self.come("+"); + a = a.wrapping_add(self.multiplicativo()?); + } else if self.mira("-") && !self.mira("--") { + self.come("-"); + a = a.wrapping_sub(self.multiplicativo()?); + } else { + return Ok(a); + } + } + } + + fn multiplicativo(&mut self) -> Result { + let mut a = self.unario()?; + loop { + self.espacios(); + if self.mira("*") { + self.come("*"); + a = a.wrapping_mul(self.unario()?); + } else if self.mira("/") { + self.come("/"); + let b = self.unario()?; + if b == 0 { + return Err("aritmética: división por cero".into()); + } + a = a.wrapping_div(b); + } else if self.mira("%") { + self.come("%"); + let b = self.unario()?; + if b == 0 { + return Err("aritmética: módulo por cero".into()); + } + a = a.wrapping_rem(b); + } else { + return Ok(a); + } + } + } + + fn unario(&mut self) -> Result { + self.espacios(); + if self.come("!") { + return Ok((self.unario()? == 0) as Num); + } + if self.come("~") { + return Ok(!self.unario()?); + } + if self.mira("-") && !self.mira("--") { + self.come("-"); + return Ok(self.unario()?.wrapping_neg()); + } + if self.mira("+") && !self.mira("++") { + self.come("+"); + return self.unario(); + } + self.primario() + } + + fn primario(&mut self) -> Result { + self.espacios(); + if self.come("(") { + let v = self.ternario()?; + if !self.come(")") { + return Err("aritmética: falta `)`".into()); + } + return Ok(v); + } + match self.cs.get(self.i) { + Some(c) if c.is_ascii_digit() => self.numero(), + Some(c) if *c == '_' || c.is_ascii_alphabetic() => { + let mut n = String::new(); + while let Some(d) = self.cs.get(self.i) { + if *d == '_' || d.is_ascii_alphanumeric() { + n.push(*d); + self.i += 1; + } else { + break; + } + } + self.espacios(); + // asignaciones + for (op, f) in [ + ("+=", 1i32), + ("-=", 2), + ("*=", 3), + ("/=", 4), + ("%=", 5), + ] { + if self.mira(op) { + self.come(op); + let b = self.ternario()?; + let a = self.valor_de(&n)?; + let v = match f { + 1 => a.wrapping_add(b), + 2 => a.wrapping_sub(b), + 3 => a.wrapping_mul(b), + 4 => { + if b == 0 { + return Err("aritmética: división por cero".into()); + } + a.wrapping_div(b) + } + _ => { + if b == 0 { + return Err("aritmética: módulo por cero".into()); + } + a.wrapping_rem(b) + } + }; + (self.ctx.asignar)(&n, &v.to_string()); + return Ok(v); + } + } + if self.mira("=") && !self.mira("==") { + self.come("="); + let v = self.ternario()?; + (self.ctx.asignar)(&n, &v.to_string()); + return Ok(v); + } + self.valor_de(&n) + } + Some(c) => Err(format!("aritmética: no esperaba `{c}`")), + None => Err("aritmética: expresión incompleta".into()), + } + } + + fn valor_de(&self, n: &str) -> Result { + let s = (self.ctx.leer)(n); + let s = s.trim(); + if s.is_empty() { + return Ok(0); + } + interpretar(s).ok_or_else(|| format!("aritmética: `{n}` no es un número (`{s}`)")) + } + + fn numero(&mut self) -> Result { + let inicio = self.i; + while matches!(self.cs.get(self.i), Some(c) if c.is_ascii_alphanumeric()) { + self.i += 1; + } + let s: String = self.cs[inicio..self.i].iter().collect(); + self.espacios(); + interpretar(&s).ok_or_else(|| format!("aritmética: `{s}` no es un número")) + } +} + +/// POSIX: decimal, octal con `0` delante, hexadecimal con `0x`. +fn interpretar(s: &str) -> Option { + let (negativo, s) = match s.strip_prefix('-') { + Some(r) => (true, r), + None => (false, s), + }; + let v = if let Some(h) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + Num::from_str_radix(h, 16).ok()? + } else if s.len() > 1 && s.starts_with('0') { + Num::from_str_radix(&s[1..], 8).ok()? + } else { + s.parse().ok()? + }; + Some(if negativo { -v } else { v }) +} + +#[cfg(test)] +mod pruebas { + use super::*; + use std::cell::RefCell; + use std::collections::HashMap; + + fn ev(expr: &str, vars: &[(&str, &str)]) -> Result { + let mapa: RefCell> = + RefCell::new(vars.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()); + let leer = |n: &str| mapa.borrow().get(n).cloned().unwrap_or_default(); + let mut asignar = |n: &str, v: &str| { + mapa.borrow_mut().insert(n.to_string(), v.to_string()); + }; + let mut ctx = Contexto { leer: &leer, asignar: &mut asignar }; + evaluar(expr, &mut ctx) + } + + #[test] + fn lo_que_usan_las_cards() { + // `i=$((i+1))` es la única aritmética de las cards medidas + assert_eq!(ev("i+1", &[("i", "0")]).unwrap(), 1); + assert_eq!(ev("i+1", &[("i", "99")]).unwrap(), 100); + // una variable sin definir vale 0 + assert_eq!(ev("n+1", &[]).unwrap(), 1); + } + + #[test] + fn precedencia_y_parentesis() { + assert_eq!(ev("2+3*4", &[]).unwrap(), 14); + assert_eq!(ev("(2+3)*4", &[]).unwrap(), 20); + assert_eq!(ev("1 << 4", &[]).unwrap(), 16); + assert_eq!(ev("7 % 4", &[]).unwrap(), 3); + assert_eq!(ev("-3 + 1", &[]).unwrap(), -2); + assert_eq!(ev("!0", &[]).unwrap(), 1); + assert_eq!(ev("1 < 2 && 3 >= 3", &[]).unwrap(), 1); + assert_eq!(ev("0 ? 10 : 20", &[]).unwrap(), 20); + } + + #[test] + fn bases() { + assert_eq!(ev("0x10", &[]).unwrap(), 16); + assert_eq!(ev("010", &[]).unwrap(), 8); + assert_eq!(ev("10", &[]).unwrap(), 10); + } + + #[test] + fn division_por_cero_es_error_no_cero() { + assert!(ev("1/0", &[]).is_err()); + assert!(ev("1%0", &[]).is_err()); + } + + #[test] + fn asigna() { + assert_eq!(ev("x = 3 + 4", &[]).unwrap(), 7); + assert_eq!(ev("x += 2", &[("x", "5")]).unwrap(), 7); + } +} diff --git a/crates/simi/src/ast.rs b/crates/simi/src/ast.rs new file mode 100644 index 00000000..36dc9aa9 --- /dev/null +++ b/crates/simi/src/ast.rs @@ -0,0 +1,129 @@ +//! El árbol. Nada de estado ni de ejecución acá: sólo la forma del lenguaje. + +/// Un trozo de palabra. La palabra se arma en el lexer y se expande en `expand`, en ese orden: +/// tilde → parámetro/orden/aritmética → división en campos → rutas → quita de comillas. +#[derive(Debug, Clone, PartialEq)] +pub enum Seg { + /// Texto literal. `comillado` = vino de comillas ⇒ **no** se divide en campos ni se globea. + Lit { texto: String, comillado: bool }, + /// `~` al principio de una palabra (o de un campo tras `:` en una asignación). + Tilde, + /// `$x`, `${x}`, `${x:-def}`, `${#x}`, `${x%%pat}`… `comillado` va aparte porque + /// `"$x"` no se divide y `$x` sí. + Param { nombre: String, op: ParamOp, arg: Vec, comillado: bool }, + /// `$( … )` y `` ` … ` ``. El cuerpo es shell otra vez y se parsea al expandir. + CmdSub { cuerpo: String, comillado: bool }, + /// `$(( … ))`. + Arith { expr: String, comillado: bool }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamOp { + /// `$x` / `${x}` + Valor, + /// `${#x}` + Largo, + /// `${x:-p}` · `${x-p}` (sin dos puntos: sólo si NO está definida) + Defecto { dos_puntos: bool }, + /// `${x:=p}` · `${x=p}` + Asigna { dos_puntos: bool }, + /// `${x:?p}` · `${x?p}` + Error { dos_puntos: bool }, + /// `${x:+p}` · `${x+p}` + Alterno { dos_puntos: bool }, + /// `${x#p}` · `${x##p}` + QuitaPrefijo { largo: bool }, + /// `${x%p}` · `${x%%p}` + QuitaSufijo { largo: bool }, +} + +pub type Palabra = Vec; + +#[derive(Debug, Clone, PartialEq)] +pub enum Redir { + /// `n> pal` (`trunca` falso ⇒ `>>`) + Salida { fd: i32, destino: Palabra, append: bool }, + /// `n< pal` + Entrada { fd: i32, origen: Palabra }, + /// `n<> pal` + LecturaEscritura { fd: i32, ruta: Palabra }, + /// `n>&m`, `n<&m`, y los cierres `n>&-` / `n<&-` + Duplica { fd: i32, otro: DupDestino, escritura: bool }, + /// `n<< DELIM` ya resuelto: el cuerpo viene del lexer. `expande` = el delimitador NO estaba + /// entrecomillado. + AquiDoc { fd: i32, cuerpo: Palabra, expande: bool }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum DupDestino { + Fd(Palabra), + Cerrar, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Asignacion { + pub nombre: String, + pub valor: Palabra, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Orden { + Simple { + asignaciones: Vec, + palabras: Vec, + redirs: Vec, + }, + /// `{ …; }` + Grupo { cuerpo: Lista, redirs: Vec }, + /// `( … )` + Subshell { cuerpo: Lista, redirs: Vec }, + Si { + ramas: Vec<(Lista, Lista)>, // (condición, cuerpo) + sino: Option, + redirs: Vec, + }, + /// `while`/`until` + Bucle { hasta: bool, cond: Lista, cuerpo: Lista, redirs: Vec }, + Para { + var: String, + /// `None` = `for x; do` ⇒ itera sobre `"$@"` + palabras: Option>, + cuerpo: Lista, + redirs: Vec, + }, + Caso { + sujeto: Palabra, + ramas: Vec<(Vec, Lista)>, + redirs: Vec, + }, + Funcion { nombre: String, cuerpo: Box }, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Tuberia { + /// `! cmd | cmd` + pub negada: bool, + pub ordenes: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Enlace { + Y, // && + O, // || +} + +#[derive(Debug, Clone, PartialEq)] +pub struct YO { + pub primera: Tuberia, + pub resto: Vec<(Enlace, Tuberia)>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Fin { + /// `;` o fin de línea + Secuencial, + /// `&` + SegundoPlano, +} + +pub type Lista = Vec<(YO, Fin)>; diff --git a/crates/simi/src/builtins.rs b/crates/simi/src/builtins.rs new file mode 100644 index 00000000..d60072a8 --- /dev/null +++ b/crates/simi/src/builtins.rs @@ -0,0 +1,865 @@ +//! 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`. +//! +//! `getopts` y `times` NO están todavía, y eso se dice en vez de fingirlo: ninguna card ni ningún +//! `/init` los usa; los guiones del hub sí, y ésos corren bajo bash. + +use std::io::{BufRead, Write}; + +use crate::exec; +use crate::shell::{Flujo, Shell, LLEGO_SENAL}; + +const BUILTINS: &[&str] = &[ + ":", ".", "break", "cd", "command", "continue", "echo", "eval", "exec", "exit", "export", + "false", "hash", "local", "printf", "pwd", "read", "return", "set", "shift", "test", "[", + "trap", "true", "type", "umask", "unset", "wait", +]; + +pub fn es_builtin(n: &str) -> bool { + BUILTINS.contains(&n) +} + +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 + } + "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 |"); + } +} diff --git a/crates/simi/src/exec.rs b/crates/simi/src/exec.rs new file mode 100644 index 00000000..77edddc0 --- /dev/null +++ b/crates/simi/src/exec.rs @@ -0,0 +1,864 @@ +//! Ejecución: redirecciones, tuberías, subshells, funciones y el `fork`/`execve`. +//! +//! Dos invariantes que este fichero sostiene a propósito, porque las dos se midieron rotas en el +//! candidato que se evaluó antes (ver `docs/plan-botar-busybox.md`): +//! +//! 1. **El `trap EXIT` de un subshell CORRE.** No sólo el del shell de arriba: también el de +//! `( … )`, el de `$( … )` y el de un componente de tubería. Es reubeno/brush#1396, y falla en +//! silencio: el script termina con `rc=0` y la limpieza no se hizo. +//! 2. **`exec` reemplaza el proceso.** Las cards de arje son `sh -c '…; exec '`: si el +//! shell forkea, arje supervisa al shell y no al daemon, y el reinicio apunta al proceso +//! equivocado. +//! +//! Sobre `fork` en Rust: este proceso es **de un solo hilo** a propósito (ni tokio ni threads), que +//! es lo que hace legítimo el `fork` — igual que dash en C. Entre el `fork` y el `execve` el hijo +//! no toma locks ajenos. + +use std::ffi::CString; +use std::io::{Read, Write}; +use std::os::unix::io::{AsRawFd, FromRawFd}; + +use crate::ast::*; +use crate::expand; +use crate::parser::parsear; +use crate::shell::{Flujo, Shell, LLEGO_SENAL}; + +// ───────────────────────────────────────────────────────── listas y enlaces + +/// Corre `f` con `set -e` suspendido. Ver `Shell::errexit_suspendido`. +fn suspendiendo_errexit(sh: &mut Shell, f: impl FnOnce(&mut Shell) -> T) -> T { + sh.errexit_suspendido += 1; + let r = f(sh); + sh.errexit_suspendido -= 1; + r +} + +/// Lo que hay que hacer al ENTRAR en un subshell, en los cuatro sitios que forkean. +/// +/// POSIX §2.12: los traps heredados que no estén ignorados se ponen en su acción por defecto. O +/// sea que un subshell **no** corre el `trap EXIT` del padre — sólo el que él mismo ponga. Las dos +/// mitades son distintas y confundirlas es fácil: la otra mitad (que el trap PROPIO del subshell sí +/// corre) es reubeno/brush#1396. Acá se rompió la primera versión de simi, al revés que brush, y no +/// lo vio el banco diferencial sino la prueba de regresión — los dos oráculos hacen falta. +fn entrar_en_subshell(sh: &mut Shell) { + sh.es_subshell = true; + sh.traps.clear(); +} + +fn debe_abortar(sh: &Shell) -> bool { + sh.opts.errexit && sh.errexit_suspendido == 0 && sh.estado != 0 +} + +pub fn correr_lista(sh: &mut Shell, l: &Lista) -> Flujo { + for (yo, fin) in l { + atender_senales(sh); + match fin { + Fin::SegundoPlano => { + lanzar_al_fondo(sh, yo); + } + Fin::Secuencial => { + let f = correr_yo(sh, yo, false); + if f != Flujo::Normal { + return f; + } + } + } + } + Flujo::Normal +} + +/// `permite_fallo`: estamos en una condición (`if`, `while`, lado izquierdo de `&&`, `!`), donde +/// un estado distinto de cero **no** dispara `set -e`. +pub fn correr_yo(sh: &mut Shell, yo: &YO, permite_fallo: bool) -> Flujo { + let cuerpo = |sh: &mut Shell| -> Flujo { + let hay_resto = !yo.resto.is_empty(); + // La primera de una cadena `&&`/`||` es una CONDICIÓN: su fallo no aborta. + let f = if hay_resto { + suspendiendo_errexit(sh, |sh| correr_tuberia(sh, &yo.primera)) + } else { + correr_tuberia(sh, &yo.primera) + }; + if f != Flujo::Normal { + return f; + } + for (n, (enlace, t)) in yo.resto.iter().enumerate() { + let ultima = n + 1 == yo.resto.len(); + let saltar = match enlace { + Enlace::Y => sh.estado != 0, + Enlace::O => sh.estado == 0, + }; + if saltar { + continue; + } + let f = if ultima { + correr_tuberia(sh, t) + } else { + suspendiendo_errexit(sh, |sh| correr_tuberia(sh, t)) + }; + if f != Flujo::Normal { + return f; + } + } + if debe_abortar(sh) { + return Flujo::Exit(sh.estado); + } + Flujo::Normal + }; + if permite_fallo { + suspendiendo_errexit(sh, cuerpo) + } else { + cuerpo(sh) + } +} + +fn correr_tuberia(sh: &mut Shell, t: &Tuberia) -> Flujo { + if t.ordenes.len() == 1 { + // `! cmd` es una condición: el fallo de `cmd` no aborta. + let f = if t.negada { + suspendiendo_errexit(sh, |sh| correr_orden(sh, &t.ordenes[0])) + } else { + correr_orden(sh, &t.ordenes[0]) + }; + if t.negada { + sh.estado = (sh.estado == 0) as i32; + } + if debe_abortar(sh) && f == Flujo::Normal { + return Flujo::Exit(sh.estado); + } + return f; + } + + let n = t.ordenes.len(); + let mut entrada: Option = None; + let mut pids: Vec = Vec::new(); + for (i, orden) in t.ordenes.iter().enumerate() { + let (lectura, escritura) = if i + 1 < n { + let mut fds = [0i32; 2]; + if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 { + sh.error("no pude crear la tubería"); + sh.estado = 1; + return Flujo::Normal; + } + (Some(fds[0]), Some(fds[1])) + } else { + (None, None) + }; + let pid = unsafe { libc::fork() }; + if pid == 0 { + // hijo + if let Some(e) = entrada { + unsafe { libc::dup2(e, 0) }; + unsafe { libc::close(e) }; + } + if let Some(w) = escritura { + unsafe { libc::dup2(w, 1) }; + unsafe { libc::close(w) }; + } + if let Some(r) = lectura { + unsafe { libc::close(r) }; + } + entrar_en_subshell(sh); + let f = correr_orden(sh, orden); + let estado = match f { + Flujo::Exit(c) => c, + _ => sh.estado, + }; + salir_de_subshell(sh, estado); + } + if let Some(e) = entrada { + unsafe { libc::close(e) }; + } + if let Some(w) = escritura { + unsafe { libc::close(w) }; + } + entrada = lectura; + pids.push(pid); + } + let mut ultimo = 0; + for pid in pids { + ultimo = esperar(pid); + } + sh.estado = if t.negada { (ultimo == 0) as i32 } else { ultimo }; + if debe_abortar(sh) { + return Flujo::Exit(sh.estado); + } + Flujo::Normal +} + +fn lanzar_al_fondo(sh: &mut Shell, yo: &YO) { + let pid = unsafe { libc::fork() }; + if pid == 0 { + entrar_en_subshell(sh); + let f = correr_yo(sh, yo, true); + let estado = match f { + Flujo::Exit(c) => c, + _ => sh.estado, + }; + salir_de_subshell(sh, estado); + } + sh.ultimo_fondo = Some(pid); + sh.estado = 0; +} + +// ───────────────────────────────────────────────────────── órdenes + +fn correr_orden(sh: &mut Shell, o: &Orden) -> Flujo { + match o { + Orden::Funcion { nombre, cuerpo } => { + sh.funcs.insert(nombre.clone(), (**cuerpo).clone()); + sh.estado = 0; + Flujo::Normal + } + Orden::Simple { asignaciones, palabras, redirs } => { + correr_simple(sh, asignaciones, palabras, redirs) + } + Orden::Grupo { cuerpo, redirs } => con_redirs(sh, redirs, |sh| correr_lista(sh, cuerpo)), + Orden::Subshell { cuerpo, redirs } => { + let pid = unsafe { libc::fork() }; + if pid == 0 { + entrar_en_subshell(sh); + let f = match aplicar(sh, redirs) { + Ok(_) => correr_lista(sh, cuerpo), + Err(e) => { + sh.error(&e); + sh.estado = 1; + Flujo::Normal + } + }; + let estado = match f { + Flujo::Exit(c) => c, + _ => sh.estado, + }; + salir_de_subshell(sh, estado); + } + sh.estado = esperar(pid); + Flujo::Normal + } + Orden::Si { ramas, sino, redirs } => con_redirs(sh, redirs, |sh| { + for (cond, cuerpo) in ramas { + let f = correr_lista_condicion(sh, cond); + if let Some(f) = f { + return f; + } + if sh.estado == 0 { + return correr_lista(sh, cuerpo); + } + } + match sino { + Some(l) => correr_lista(sh, l), + None => { + sh.estado = 0; + Flujo::Normal + } + } + }), + Orden::Bucle { hasta, cond, cuerpo, redirs } => con_redirs(sh, redirs, |sh| { + sh.bucles += 1; + let mut ultimo = 0; + let salida = loop { + if let Some(f) = correr_lista_condicion(sh, cond) { + break f; + } + let seguir = if *hasta { sh.estado != 0 } else { sh.estado == 0 }; + if !seguir { + break Flujo::Normal; + } + match correr_lista(sh, cuerpo) { + Flujo::Break(n) => { + if n > 1 { + break Flujo::Break(n - 1); + } + break Flujo::Normal; + } + Flujo::Continue(n) => { + if n > 1 { + break Flujo::Continue(n - 1); + } + } + Flujo::Normal => {} + f => break f, + } + ultimo = sh.estado; + }; + sh.bucles -= 1; + if salida == Flujo::Normal { + sh.estado = ultimo; + } + salida + }), + Orden::Para { var, palabras, cuerpo, redirs } => con_redirs(sh, redirs, |sh| { + let lista: Vec = match palabras { + None => sh.params.clone(), + Some(ps) => { + let mut v = Vec::new(); + for p in ps { + match expand::campos(sh, p) { + Ok(cs) => v.extend(cs), + Err(e) => { + sh.error(&e); + sh.estado = 1; + return Flujo::Normal; + } + } + } + v + } + }; + sh.bucles += 1; + sh.estado = 0; + let salida = 'bucle: { + for v in lista { + sh.asignar(var, &v); + match correr_lista(sh, cuerpo) { + Flujo::Break(n) => { + if n > 1 { + break 'bucle Flujo::Break(n - 1); + } + break 'bucle Flujo::Normal; + } + Flujo::Continue(n) => { + if n > 1 { + break 'bucle Flujo::Continue(n - 1); + } + } + Flujo::Normal => {} + f => break 'bucle f, + } + } + Flujo::Normal + }; + sh.bucles -= 1; + salida + }), + Orden::Caso { sujeto, ramas, redirs } => con_redirs(sh, redirs, |sh| { + let s = match expand::una(sh, sujeto) { + Ok(s) => s, + Err(e) => { + sh.error(&e); + sh.estado = 2; + return Flujo::Exit(2); + } + }; + for (patrones, cuerpo) in ramas { + for p in patrones { + let pat = match expand::patron(sh, p) { + Ok(p) => p, + Err(e) => { + sh.error(&e); + sh.estado = 1; + return Flujo::Normal; + } + }; + if crate::glob::encaja(&pat, &s) { + if cuerpo.is_empty() { + sh.estado = 0; + return Flujo::Normal; + } + return correr_lista(sh, cuerpo); + } + } + } + sh.estado = 0; + Flujo::Normal + }), + } +} + +/// Corre la lista de una condición (`if`/`while`). Devuelve `Some(flujo)` sólo si hay que abandonar +/// (un `exit` o un `return` de adentro); si no, deja el estado en `sh.estado`. +fn correr_lista_condicion(sh: &mut Shell, l: &Lista) -> Option { + suspendiendo_errexit(sh, |sh| { + for (yo, _) in l { + let f = correr_yo(sh, yo, false); + if f != Flujo::Normal { + return Some(f); + } + } + None + }) +} + +fn correr_simple( + sh: &mut Shell, + asignaciones: &[Asignacion], + palabras: &[Palabra], + redirs: &[Redir], +) -> Flujo { + // 1. expandir las palabras + let mut argv: Vec = Vec::new(); + for p in palabras { + match expand::campos(sh, p) { + Ok(cs) => argv.extend(cs), + Err(e) => { + // Un error de expansión (comilla sin cerrar en una sustitución, `${x:?}`, `set -u`) + // ABORTA con 2 en un shell no interactivo, no sigue con 1. Medido contra el ash. + sh.error(&e); + sh.estado = 2; + return Flujo::Exit(2); + } + } + } + + // 2. las asignaciones + let mut pares: Vec<(String, String)> = Vec::new(); + for a in asignaciones { + match expand::una(sh, &a.valor) { + Ok(v) => pares.push((a.nombre.clone(), v)), + Err(e) => { + // Un error de expansión (comilla sin cerrar en una sustitución, `${x:?}`, `set -u`) + // ABORTA con 2 en un shell no interactivo, no sigue con 1. Medido contra el ash. + sh.error(&e); + sh.estado = 2; + return Flujo::Exit(2); + } + } + } + + if argv.is_empty() { + // sólo asignaciones (y quizá redirecciones, que se abren y se cierran) + match aplicar(sh, redirs) { + Ok(g) => restaurar(g), + Err(e) => { + // Un error de expansión (comilla sin cerrar en una sustitución, `${x:?}`, `set -u`) + // ABORTA con 2 en un shell no interactivo, no sigue con 1. Medido contra el ash. + sh.error(&e); + sh.estado = 2; + return Flujo::Exit(2); + } + } + for (k, v) in pares { + sh.asignar(&k, &v); + } + sh.estado = 0; + return Flujo::Normal; + } + + if sh.opts.xtrace { + eprintln!("+ {}", argv.join(" ")); + } + + // 3. ¿función? + if let Some(cuerpo) = sh.funcs.get(&argv[0]).cloned() { + return con_redirs(sh, redirs, |sh| { + let previos = std::mem::replace(&mut sh.params, argv[1..].to_vec()); + sh.funcion_honda += 1; + sh.locales.push(Vec::new()); + let f = correr_orden(sh, &cuerpo); + if let Some(marco) = sh.locales.pop() { + for (nombre, previo) in marco.into_iter().rev() { + match previo { + Some(v) => { + sh.vars.insert(nombre, v); + } + None => { + sh.vars.remove(&nombre); + } + } + } + } + sh.funcion_honda -= 1; + sh.params = previos; + match f { + Flujo::Return => Flujo::Normal, + otro => otro, + } + }); + } + + // 4. ¿builtin? + if crate::builtins::es_builtin(&argv[0]) { + return match aplicar(sh, redirs) { + Ok(guardados) => { + // Las asignaciones de una orden con builtin quedan en el shell (POSIX lo deja + // sin especificar para los regulares; hacerlas persistir es lo que hace ash). + for (k, v) in &pares { + sh.asignar(k, v); + } + let f = crate::builtins::correr(sh, &argv); + restaurar(guardados); + f + } + Err(e) => { + sh.error(&e); + sh.estado = 2; + Flujo::Exit(2) + } + }; + } + + // 5. orden externa + lanzar_externo(sh, &argv, redirs, &pares, false) +} + +/// Aplica redirecciones, corre `cuerpo`, restaura. Para grupos y compuestas, que NO forkean. +fn con_redirs Flujo>(sh: &mut Shell, redirs: &[Redir], cuerpo: F) -> Flujo { + match aplicar(sh, redirs) { + Ok(guardados) => { + let f = cuerpo(sh); + restaurar(guardados); + f + } + Err(e) => { + sh.error(&e); + sh.estado = 1; + Flujo::Normal + } + } +} + +// ───────────────────────────────────────────────────────── redirecciones + +pub struct Guardado { + fd: i32, + copia: i32, +} + +pub fn aplicar(sh: &mut Shell, redirs: &[Redir]) -> Result, String> { + let mut guardados = Vec::new(); + for r in redirs { + let (fd, nuevo) = match r { + Redir::Salida { fd, destino, append } => { + let ruta = expand::una(sh, destino)?; + let banderas = if *append { + libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND + } else { + libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC + }; + (*fd, abrir(&ruta, banderas)?) + } + Redir::Entrada { fd, origen } => { + let ruta = expand::una(sh, origen)?; + (*fd, abrir(&ruta, libc::O_RDONLY)?) + } + Redir::LecturaEscritura { fd, ruta } => { + let ruta = expand::una(sh, ruta)?; + (*fd, abrir(&ruta, libc::O_RDWR | libc::O_CREAT)?) + } + Redir::Duplica { fd, otro, .. } => match otro { + DupDestino::Cerrar => { + guardados.push(Guardado { fd: *fd, copia: unsafe { libc::dup(*fd) } }); + unsafe { libc::close(*fd) }; + continue; + } + DupDestino::Fd(p) => { + let s = expand::una(sh, p)?; + let n: i32 = s.parse().map_err(|_| format!("{s}: no es un descriptor"))?; + let copia = unsafe { libc::dup(n) }; + if copia < 0 { + return Err(format!("{n}: descriptor no válido")); + } + (*fd, copia) + } + }, + Redir::AquiDoc { fd, cuerpo, expande } => { + let texto = if *expande { + expand::una(sh, cuerpo)? + } else { + match &cuerpo[0] { + Seg::Lit { texto, .. } => texto.clone(), + _ => String::new(), + } + }; + (*fd, fd_con_texto(&texto)?) + } + }; + let copia = unsafe { libc::dup(fd) }; + guardados.push(Guardado { fd, copia }); + unsafe { libc::dup2(nuevo, fd) }; + unsafe { libc::close(nuevo) }; + } + Ok(guardados) +} + +pub fn restaurar(guardados: Vec) { + for g in guardados.into_iter().rev() { + if g.copia >= 0 { + unsafe { libc::dup2(g.copia, g.fd) }; + unsafe { libc::close(g.copia) }; + } else { + unsafe { libc::close(g.fd) }; + } + } +} + +fn abrir(ruta: &str, banderas: i32) -> Result { + let c = CString::new(ruta).map_err(|_| format!("{ruta}: ruta con NUL"))?; + let fd = unsafe { libc::open(c.as_ptr(), banderas, 0o644 as libc::c_int) }; + if fd < 0 { + return Err(format!("{ruta}: no pude abrir")); + } + Ok(fd) +} + +/// Un descriptor de sólo lectura con `texto` adentro, para los aquí-documentos. +/// +/// Se usa `memfd_create` en vez de una tubería porque una tubería se **bloquea** si el cuerpo pasa +/// del buffer del kernel (64 K) y no hay nadie leyendo todavía. Si no hay memfd, cae a la tubería, +/// que para los cuerpos de este árbol (2 en 21 guiones, cortos) alcanza. +fn fd_con_texto(texto: &str) -> Result { + let nombre = CString::new("simi-aquidoc").unwrap(); + let fd = unsafe { libc::memfd_create(nombre.as_ptr(), 0) }; + if fd >= 0 { + let bytes = texto.as_bytes(); + let mut escrito = 0; + while escrito < bytes.len() { + let n = unsafe { + libc::write( + fd, + bytes[escrito..].as_ptr() as *const libc::c_void, + bytes.len() - escrito, + ) + }; + if n <= 0 { + unsafe { libc::close(fd) }; + return Err("aquí-documento: no pude escribir".into()); + } + escrito += n as usize; + } + unsafe { libc::lseek(fd, 0, libc::SEEK_SET) }; + return Ok(fd); + } + let mut fds = [0i32; 2]; + if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 { + return Err("aquí-documento: no pude crear la tubería".into()); + } + let bytes = texto.as_bytes(); + unsafe { libc::write(fds[1], bytes.as_ptr() as *const libc::c_void, bytes.len()) }; + unsafe { libc::close(fds[1]) }; + Ok(fds[0]) +} + +// ───────────────────────────────────────────────────────── procesos + +/// `reemplazar` = viene del builtin `exec`: NO se forkea, se sustituye este proceso. +pub fn lanzar_externo( + sh: &mut Shell, + argv: &[String], + redirs: &[Redir], + asignaciones: &[(String, String)], + reemplazar: bool, +) -> Flujo { + let ruta = match resolver(sh, &argv[0]) { + Some(r) => r, + None => { + sh.error(&format!("{}: no encontrado", argv[0])); + sh.estado = 127; + return Flujo::Normal; + } + }; + let mut entorno = sh.entorno(); + for (k, v) in asignaciones { + entorno.retain(|e| !e.starts_with(&format!("{k}="))); + entorno.push(format!("{k}={v}")); + } + + if reemplazar { + if let Err(e) = aplicar(sh, redirs) { + sh.error(&e); + sh.estado = 1; + return Flujo::Normal; + } + ejecutar(&ruta, argv, &entorno); + // si volvemos, el execve falló + sh.error(&format!("{}: no pude ejecutar", argv[0])); + return Flujo::Exit(126); + } + + let pid = unsafe { libc::fork() }; + if pid < 0 { + sh.error("no pude forkear"); + sh.estado = 1; + return Flujo::Normal; + } + if pid == 0 { + if let Err(e) = aplicar(sh, redirs) { + eprintln!("{}: {}", sh.nombre, e); + unsafe { libc::_exit(1) }; + } + // el hijo no hereda los traps atrapados (POSIX §2.11) + unsafe { + libc::signal(libc::SIGINT, libc::SIG_DFL); + libc::signal(libc::SIGQUIT, libc::SIG_DFL); + } + ejecutar(&ruta, argv, &entorno); + eprintln!("{}: {}: no pude ejecutar", sh.nombre, argv[0]); + unsafe { libc::_exit(126) }; + } + sh.estado = esperar(pid); + Flujo::Normal +} + +fn ejecutar(ruta: &str, argv: &[String], entorno: &[String]) { + let c_ruta = match CString::new(ruta) { + Ok(c) => c, + Err(_) => return, + }; + let c_argv: Vec = argv.iter().filter_map(|a| CString::new(a.as_str()).ok()).collect(); + let c_env: Vec = entorno.iter().filter_map(|e| CString::new(e.as_str()).ok()).collect(); + let mut p_argv: Vec<*const libc::c_char> = c_argv.iter().map(|c| c.as_ptr()).collect(); + p_argv.push(std::ptr::null()); + let mut p_env: Vec<*const libc::c_char> = c_env.iter().map(|c| c.as_ptr()).collect(); + p_env.push(std::ptr::null()); + unsafe { libc::execve(c_ruta.as_ptr(), p_argv.as_ptr(), p_env.as_ptr()) }; +} + +/// Resolución por `PATH`. Propia y no `execvp`, porque el `PATH` que manda es el del SHELL +/// (`sh.vars`), que no tiene por qué ser el del `environ` heredado. +fn resolver(sh: &Shell, nombre: &str) -> Option { + if nombre.contains('/') { + return Some(nombre.to_string()); + } + let path = sh.leer("PATH").unwrap_or_else(|| "/usr/bin:/bin".into()); + for dir in path.split(':') { + let dir = if dir.is_empty() { "." } else { dir }; + let cand = format!("{dir}/{nombre}"); + let c = CString::new(cand.clone()).ok()?; + if unsafe { libc::access(c.as_ptr(), libc::X_OK) } == 0 { + return Some(cand); + } + } + None +} + +fn esperar(pid: i32) -> i32 { + let mut estado = 0i32; + loop { + let r = unsafe { libc::waitpid(pid, &mut estado, 0) }; + if r < 0 { + let e = std::io::Error::last_os_error(); + if e.raw_os_error() == Some(libc::EINTR) { + continue; + } + return 1; + } + break; + } + if libc::WIFEXITED(estado) { + libc::WEXITSTATUS(estado) + } else if libc::WIFSIGNALED(estado) { + 128 + libc::WTERMSIG(estado) + } else { + 1 + } +} + +/// Salida de un subshell: **corre el `trap EXIT` antes de irse**. Es la invariante 1 del módulo. +pub fn salir_de_subshell(sh: &mut Shell, estado: i32) -> ! { + correr_trap_exit(sh, estado); + let _ = std::io::stdout().flush(); + let _ = std::io::stderr().flush(); + unsafe { libc::_exit(estado) } +} + +pub fn correr_trap_exit(sh: &mut Shell, estado: i32) { + if sh.trap_exit_corrido { + return; + } + sh.trap_exit_corrido = true; + if let Some(accion) = sh.traps.get("EXIT").cloned() { + sh.estado = estado; + if let Ok(l) = parsear(&accion) { + correr_lista(sh, &l); + } + sh.estado = estado; + } +} + +/// Entre órdenes: si llegó una señal atrapada, corre su acción. El handler sólo prende una +/// bandera (es lo único async-signal-safe que se puede hacer). +pub fn atender_senales(sh: &mut Shell) { + for n in 1..32usize { + if LLEGO_SENAL[n].swap(false, std::sync::atomic::Ordering::SeqCst) { + let nombre = crate::builtins::nombre_de_senal(n as i32); + if let Some(accion) = sh.traps.get(&nombre).cloned() { + if let Ok(l) = parsear(&accion) { + let previo = sh.estado; + correr_lista(sh, &l); + sh.estado = previo; + } + } + } + } +} + +// ───────────────────────────────────────────────────────── sustituciones + +/// `$( … )` y `` ` … ` ``: corre el cuerpo en un subshell y devuelve su stdout sin los saltos +/// finales. El subshell corre su `trap EXIT` como cualquier otro (invariante 1). +pub fn sustituir_orden(sh: &mut Shell, cuerpo: &str) -> Result { + let lista = parsear(cuerpo).map_err(|e| format!("sustitución: {e}"))?; + let mut fds = [0i32; 2]; + if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 { + return Err("no pude crear la tubería".into()); + } + let pid = unsafe { libc::fork() }; + if pid < 0 { + unsafe { libc::close(fds[0]) }; + unsafe { libc::close(fds[1]) }; + return Err("no pude forkear".into()); + } + if pid == 0 { + unsafe { libc::close(fds[0]) }; + unsafe { libc::dup2(fds[1], 1) }; + unsafe { libc::close(fds[1]) }; + entrar_en_subshell(sh); + let f = correr_lista(sh, &lista); + let estado = match f { + Flujo::Exit(c) => c, + _ => sh.estado, + }; + salir_de_subshell(sh, estado); + } + unsafe { libc::close(fds[1]) }; + let mut salida = String::new(); + { + let mut f = unsafe { std::fs::File::from_raw_fd(fds[0]) }; + let mut buf = Vec::new(); + let _ = f.read_to_end(&mut buf); + salida = String::from_utf8_lossy(&buf).into_owned(); + let _ = f.as_raw_fd(); + } + sh.estado = esperar(pid); + while salida.ends_with('\n') { + salida.pop(); + } + Ok(salida) +} + +pub fn evaluar_aritmetica(sh: &mut Shell, expr: &str) -> Result { + // Las variables de la expresión pueden traer expansiones: `$(( $x + 1 ))`. + let expr = expandir_texto(sh, expr)?; + let vars: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); + let leer_sh: Vec<(String, String)> = sh + .vars + .iter() + .map(|(k, (v, _))| (k.clone(), v.clone())) + .collect(); + let leer = |n: &str| { + leer_sh + .iter() + .find(|(k, _)| k == n) + .map(|(_, v)| v.clone()) + .unwrap_or_default() + }; + let mut asignar = |n: &str, v: &str| vars.borrow_mut().push((n.to_string(), v.to_string())); + let mut ctx = crate::arith::Contexto { leer: &leer, asignar: &mut asignar }; + let r = crate::arith::evaluar(&expr, &mut ctx); + for (k, v) in vars.into_inner() { + sh.asignar(&k, &v); + } + r.map(|n| n.to_string()) +} + +/// Expande las sustituciones de un texto suelto (el interior de `$(( ))`). +fn expandir_texto(sh: &mut Shell, texto: &str) -> Result { + if !texto.contains('$') && !texto.contains('`') { + return Ok(texto.to_string()); + } + let escapado = texto.replace('"', "\\\""); + let l = parsear(&format!(": \"{escapado}\"")).map_err(|e| format!("aritmética: {e}"))?; + let Orden::Simple { palabras, .. } = &l[0].0.primera.ordenes[0] else { + return Ok(texto.to_string()); + }; + expand::una(sh, &palabras[1]) +} diff --git a/crates/simi/src/expand.rs b/crates/simi/src/expand.rs new file mode 100644 index 00000000..2228ccea --- /dev/null +++ b/crates/simi/src/expand.rs @@ -0,0 +1,399 @@ +//! Expansiones, **en el orden que manda POSIX §2.6** — y el orden es el contrato, no un detalle: +//! +//! 1. tilde · 2. parámetro, orden y aritmética (de izquierda a derecha) · 3. división en campos +//! por `IFS` · 4. expansión de rutas · 5. quita de comillas. +//! +//! Un shell que divide antes de expandir, o que globea lo que vino entrecomillado, pasa igual +//! cualquier banco de casos sencillos y rompe el primer `for f in $LISTA` de verdad. + +use crate::ast::{Palabra, ParamOp, Seg}; +use crate::glob; +use crate::shell::Shell; + +/// Un trozo ya expandido. `comillado` decide si se divide en campos y si se globea; +/// `corte` fuerza un campo nuevo (lo necesita `"$@"`). +struct Trozo { + texto: String, + comillado: bool, + corte: bool, +} + +/// Campos finales de una palabra: expansión + división + rutas + quita de comillas. +pub fn campos(sh: &mut Shell, p: &Palabra) -> Result, String> { + let trozos = expandir(sh, p)?; + let crudos = armar(trozos, &sh.ifs()); + let mut out = Vec::new(); + for (campo, globeable) in crudos { + let texto: String = campo.iter().map(|(c, _)| *c).collect(); + if !sh.opts.noglob && globeable && hay_meta_sin_proteger(&campo) { + let hits = glob::expandir(&como_patron(&campo)); + if hits.is_empty() { + out.push(texto); + } else { + out.extend(hits); + } + } else { + out.push(texto); + } + } + Ok(out) +} + +/// Una palabra donde la división en campos NO aplica: asignaciones, destinos de redirección, +/// cuerpos de aquí-documento, sujeto de `case`. +pub fn una(sh: &mut Shell, p: &Palabra) -> Result { + let trozos = expandir(sh, p)?; + Ok(trozos.into_iter().map(|t| t.texto).collect()) +} + +/// Como `una`, pero devolviendo un PATRÓN: lo que vino entrecomillado queda escapado, así +/// `case "$x" in \*)` compara un asterisco literal. Lo usan `case` y `${x#pat}`. +pub fn patron(sh: &mut Shell, p: &Palabra) -> Result { + let trozos = expandir(sh, p)?; + let mut campo: Vec<(char, bool)> = Vec::new(); + for t in trozos { + for c in t.texto.chars() { + campo.push((c, t.comillado)); + } + } + Ok(como_patron(&campo)) +} + +/// ¿Hay un metacarácter de ruta SIN proteger? Los protegidos vinieron de comillas y son literales. +fn hay_meta_sin_proteger(campo: &[(char, bool)]) -> bool { + campo.iter().any(|(c, prot)| !prot && matches!(c, '*' | '?' | '[')) +} + +/// Texto de patrón para `glob`: los caracteres protegidos se escapan con `\`, y una barra +/// invertida NO protegida —la que salió de una expansión— se escapa para que siga siendo una +/// barra literal. +/// +/// ⚠ Acá estaba el bug que el banco diferencial destapó: la primera versión usaba `\` como marca +/// interna y después la quitaba con un `quitar_escapes` final, así que `$(echo \\$V)` perdía la +/// barra que la sustitución había producido (`ash` daba `\hola`, simi daba `hola`). **La quita de +/// comillas es sobre las comillas de la PALABRA, nunca sobre lo que produjo una expansión** — y eso +/// no se puede representar con un escape dentro del texto: hace falta la máscara. +fn como_patron(campo: &[(char, bool)]) -> String { + let mut out = String::new(); + for (c, prot) in campo { + if *prot { + if matches!(c, '*' | '?' | '[' | ']' | '\\') { + out.push('\\'); + } + } else if *c == '\\' { + out.push('\\'); + } + out.push(*c); + } + out +} + +fn expandir(sh: &mut Shell, p: &Palabra) -> Result, String> { + let mut out: Vec = Vec::new(); + for seg in p { + match seg { + Seg::Lit { texto, comillado } => { + out.push(Trozo { texto: texto.clone(), comillado: *comillado, corte: false }) + } + Seg::Tilde => { + let casa = sh.leer("HOME").unwrap_or_default(); + out.push(Trozo { texto: casa, comillado: true, corte: false }); + } + Seg::Arith { expr, comillado } => { + let texto = crate::exec::evaluar_aritmetica(sh, expr)?; + out.push(Trozo { texto, comillado: *comillado, corte: false }); + } + Seg::CmdSub { cuerpo, comillado } => { + let texto = crate::exec::sustituir_orden(sh, cuerpo)?; + out.push(Trozo { texto, comillado: *comillado, corte: false }); + } + Seg::Param { nombre, op, arg, comillado } => { + expandir_param(sh, nombre, *op, arg, *comillado, &mut out)?; + } + } + } + Ok(out) +} + +fn expandir_param( + sh: &mut Shell, + nombre: &str, + op: ParamOp, + arg: &Palabra, + comillado: bool, + out: &mut Vec, +) -> Result<(), String> { + // `$@` y `$*`: los únicos que producen varios campos. + if nombre == "@" || nombre == "*" { + let params = sh.params.clone(); + if op == ParamOp::Largo { + out.push(Trozo { texto: params.len().to_string(), comillado, corte: false }); + return Ok(()); + } + if nombre == "*" && comillado { + let sep = sh.ifs().chars().next().map(|c| c.to_string()).unwrap_or_default(); + out.push(Trozo { texto: params.join(&sep), comillado: true, corte: false }); + return Ok(()); + } + for (n, v) in params.iter().enumerate() { + out.push(Trozo { texto: v.clone(), comillado, corte: n > 0 }); + } + return Ok(()); + } + + let valor = sh.leer(nombre); + let definida_no_vacia = valor.as_deref().is_some_and(|v| !v.is_empty()); + let definida = valor.is_some(); + + let texto = match op { + ParamOp::Valor => { + if valor.is_none() && sh.opts.nounset { + return Err(format!("{nombre}: variable sin definir")); + } + valor.unwrap_or_default() + } + ParamOp::Largo => valor.unwrap_or_default().chars().count().to_string(), + ParamOp::Defecto { dos_puntos } => { + let usar = if dos_puntos { definida_no_vacia } else { definida }; + if usar { + valor.unwrap_or_default() + } else { + una(sh, arg)? + } + } + ParamOp::Asigna { dos_puntos } => { + let usar = if dos_puntos { definida_no_vacia } else { definida }; + if usar { + valor.unwrap_or_default() + } else { + let v = una(sh, arg)?; + sh.asignar(nombre, &v); + v + } + } + ParamOp::Error { dos_puntos } => { + let usar = if dos_puntos { definida_no_vacia } else { definida }; + if usar { + valor.unwrap_or_default() + } else { + let m = una(sh, arg)?; + let m = if m.is_empty() { "parámetro sin definir".to_string() } else { m }; + return Err(format!("{nombre}: {m}")); + } + } + ParamOp::Alterno { dos_puntos } => { + let usar = if dos_puntos { definida_no_vacia } else { definida }; + if usar { + una(sh, arg)? + } else { + String::new() + } + } + ParamOp::QuitaPrefijo { largo } => { + let v = valor.unwrap_or_default(); + let pat = patron(sh, arg)?; + quitar(&v, &pat, true, largo) + } + ParamOp::QuitaSufijo { largo } => { + let v = valor.unwrap_or_default(); + let pat = patron(sh, arg)?; + quitar(&v, &pat, false, largo) + } + }; + out.push(Trozo { texto, comillado, corte: false }); + Ok(()) +} + +/// `${x#pat}` / `${x%pat}`. `prefijo` elige el lado; `largo` elige la coincidencia más larga. +fn quitar(valor: &str, pat: &str, prefijo: bool, largo: bool) -> String { + let cs: Vec = valor.chars().collect(); + let mut mejor: Option = None; + let rango: Vec = (0..=cs.len()).collect(); + for n in rango { + let trozo: String = if prefijo { + cs[..n].iter().collect() + } else { + cs[cs.len() - n..].iter().collect() + }; + if glob::encaja(pat, &trozo) { + match mejor { + None => mejor = Some(n), + Some(m) => { + if (largo && n > m) || (!largo && n < m) { + mejor = Some(n); + } + } + } + if !largo { + break; // la más corta: la primera que encaja + } + } + } + match mejor { + None => valor.to_string(), + Some(n) => { + if prefijo { + cs[n..].iter().collect() + } else { + cs[..cs.len() - n].iter().collect() + } + } + } +} + +/// División en campos por `IFS`. Devuelve `(campo con máscara, se_puede_globear)`. +fn armar(trozos: Vec, ifs: &str) -> Vec<(Vec<(char, bool)>, bool)> { + let mut campos: Vec<(Vec<(char, bool)>, bool)> = Vec::new(); + let mut actual: Vec<(char, bool)> = Vec::new(); + let mut globeable = false; + let mut hay_algo = false; + + for t in trozos { + if t.corte { + campos.push((std::mem::take(&mut actual), globeable)); + globeable = false; + hay_algo = true; + } + if t.comillado { + actual.extend(t.texto.chars().map(|c| (c, true))); + hay_algo = true; + continue; + } + globeable = true; + hay_algo = true; + let mut primero = true; + for parte in dividir(&t.texto, ifs) { + if !primero { + campos.push((std::mem::take(&mut actual), globeable)); + globeable = true; + } + actual.extend(parte.chars().map(|c| (c, false))); + primero = false; + } + if !t.texto.is_empty() && t.texto.chars().next_back().is_some_and(|c| ifs.contains(c)) { + campos.push((std::mem::take(&mut actual), globeable)); + globeable = true; + } + } + if hay_algo { + campos.push((actual, globeable)); + } + // POSIX: los campos vacíos que salen de la división se descartan; los que vienen de comillas + // se quedan. Un campo vacío no-globeable venía de comillas. + campos.retain(|(t, g)| !t.is_empty() || !*g); + campos +} + +/// Divide por `IFS`: los blancos de `IFS` colapsan, los no-blancos delimitan de a uno. +fn dividir(s: &str, ifs: &str) -> Vec { + if ifs.is_empty() { + return vec![s.to_string()]; + } + let blancos: Vec = ifs.chars().filter(|c| matches!(c, ' ' | '\t' | '\n')).collect(); + let otros: Vec = ifs.chars().filter(|c| !matches!(c, ' ' | '\t' | '\n')).collect(); + let mut out: Vec = Vec::new(); + let mut act = String::new(); + let mut i = 0; + let cs: Vec = s.chars().collect(); + while i < cs.len() { + let c = cs[i]; + if blancos.contains(&c) { + out.push(std::mem::take(&mut act)); + while i < cs.len() && blancos.contains(&cs[i]) { + i += 1; + } + continue; + } + if otros.contains(&c) { + out.push(std::mem::take(&mut act)); + i += 1; + continue; + } + act.push(c); + i += 1; + } + out.push(act); + // los blancos del principio y del final no crean campos vacíos + while out.first().is_some_and(|f| f.is_empty()) && out.len() > 1 { + out.remove(0); + } + out +} + +#[cfg(test)] +mod pruebas { + use super::*; + use crate::parser::parsear; + + fn campos_de(sh: &mut Shell, entrada: &str) -> Vec { + let l = parsear(entrada).unwrap(); + let orden = &l[0].0.primera.ordenes[0]; + let crate::ast::Orden::Simple { palabras, .. } = orden else { panic!() }; + let mut out = Vec::new(); + for p in &palabras[1..] { + out.extend(campos(sh, p).unwrap()); + } + out + } + + #[test] + fn comillas_no_dividen_y_desnudo_si() { + let mut sh = Shell::nuevo(); + sh.asignar("x", "a b"); + assert_eq!(campos_de(&mut sh, "cmd \"$x\""), vec!["a b"]); + assert_eq!(campos_de(&mut sh, "cmd $x"), vec!["a", "b"]); + } + + #[test] + fn expansiones_de_parametro() { + let mut sh = Shell::nuevo(); + sh.asignar("v", "arch.tar.gz"); + assert_eq!(campos_de(&mut sh, "cmd ${v%%.*}"), vec!["arch"]); + assert_eq!(campos_de(&mut sh, "cmd ${v##*.}"), vec!["gz"]); + assert_eq!(campos_de(&mut sh, "cmd ${v%.gz}"), vec!["arch.tar"]); + assert_eq!(campos_de(&mut sh, "cmd ${v#arch.}"), vec!["tar.gz"]); + assert_eq!(campos_de(&mut sh, "cmd ${#v}"), vec!["11"]); + assert_eq!(campos_de(&mut sh, "cmd ${nada:-def}"), vec!["def"]); + assert_eq!(campos_de(&mut sh, "cmd ${v:+si}"), vec!["si"]); + } + + #[test] + fn posicionales() { + let mut sh = Shell::nuevo(); + sh.params = vec!["uno".into(), "dos tres".into()]; + assert_eq!(campos_de(&mut sh, "cmd \"$@\""), vec!["uno", "dos tres"]); + assert_eq!(campos_de(&mut sh, "cmd $@"), vec!["uno", "dos", "tres"]); + assert_eq!(campos_de(&mut sh, "cmd \"$*\""), vec!["uno dos tres"]); + assert_eq!(campos_de(&mut sh, "cmd $#"), vec!["2"]); + } + + #[test] + fn ifs_con_separador_no_blanco() { + let mut sh = Shell::nuevo(); + sh.asignar("IFS", ":"); + sh.asignar("x", "a:b:c"); + assert_eq!(campos_de(&mut sh, "cmd $x"), vec!["a", "b", "c"]); + } + + #[test] + fn lo_entrecomillado_no_se_globea() { + let mut sh = Shell::nuevo(); + // `"/dev/nul*"` es literal; `/dev/nul*` expande + assert_eq!(campos_de(&mut sh, "cmd \"/dev/nul*\""), vec!["/dev/nul*"]); + assert_eq!(campos_de(&mut sh, "cmd /dev/nul*"), vec!["/dev/null"]); + } + + #[test] + fn glob_sin_coincidencia_queda_tal_cual() { + let mut sh = Shell::nuevo(); + assert_eq!(campos_de(&mut sh, "cmd /no-existe-jamas*"), vec!["/no-existe-jamas*"]); + } + + #[test] + fn campo_vacio_entrecomillado_sobrevive() { + let mut sh = Shell::nuevo(); + sh.asignar("vacia", ""); + assert_eq!(campos_de(&mut sh, "cmd \"$vacia\""), vec![""]); + assert!(campos_de(&mut sh, "cmd $vacia").is_empty()); + } +} diff --git a/crates/simi/src/glob.rs b/crates/simi/src/glob.rs new file mode 100644 index 00000000..aee842ae --- /dev/null +++ b/crates/simi/src/glob.rs @@ -0,0 +1,207 @@ +//! Coincidencia de patrones POSIX (§2.13) y expansión de rutas. Propio, sin `regex` ni `glob`: +//! son ~150 líneas y evitan dos deps en la raíz de confianza del arranque. +//! +//! Sirve para dos cosas distintas con las mismas reglas: expandir `*.txt` a ficheros, y decidir +//! si `abc` encaja en el patrón de una rama de `case`. + +/// ¿`texto` encaja en `patron`? `*`, `?`, `[abc]`, `[!abc]`, `[a-z]`, y `\` escapa. +pub fn encaja(patron: &str, texto: &str) -> bool { + coincide(&patron.chars().collect::>(), &texto.chars().collect::>()) +} + +fn coincide(p: &[char], t: &[char]) -> bool { + if p.is_empty() { + return t.is_empty(); + } + match p[0] { + '*' => { + // voraz con retroceso: `*` puede comerse de 0 a todo + for corte in 0..=t.len() { + if coincide(&p[1..], &t[corte..]) { + return true; + } + } + false + } + '?' => !t.is_empty() && coincide(&p[1..], &t[1..]), + '[' => { + if t.is_empty() { + return false; + } + match clase(p) { + Some((acepta, largo)) => acepta(t[0]) && coincide(&p[largo..], &t[1..]), + // `[` sin cerrar es un literal + None => t[0] == '[' && coincide(&p[1..], &t[1..]), + } + } + '\\' if p.len() > 1 => !t.is_empty() && t[0] == p[1] && coincide(&p[2..], &t[1..]), + c => !t.is_empty() && t[0] == c && coincide(&p[1..], &t[1..]), + } +} + +/// Analiza `[…]` desde `p[0] == '['`. Devuelve `(predicado, cuántos chars ocupa)`. +#[allow(clippy::type_complexity)] +fn clase(p: &[char]) -> Option<(Box bool>, usize)> { + let mut i = 1; + let negada = matches!(p.get(i), Some('!') | Some('^')); + if negada { + i += 1; + } + let mut sueltos: Vec = Vec::new(); + let mut rangos: Vec<(char, char)> = Vec::new(); + let mut primero = true; + loop { + let mut c = *p.get(i)?; + if c == ']' && !primero { + i += 1; + break; + } + primero = false; + // `\` dentro de la clase escapa: `[\\\`\"\$]` es el conjunto de los cuatro caracteres, + // y es literalmente el que usa el `config.status` que genera cualquier `configure`. + if c == '\\' { + if let Some(d) = p.get(i + 1) { + c = *d; + sueltos.push(c); + i += 2; + continue; + } + } + if p.get(i + 1) == Some(&'-') && p.get(i + 2).is_some_and(|d| *d != ']') { + rangos.push((c, p[i + 2])); + i += 3; + } else { + sueltos.push(c); + i += 1; + } + } + let pred = move |c: char| { + let dentro = sueltos.contains(&c) || rangos.iter().any(|(a, b)| c >= *a && c <= *b); + dentro != negada + }; + Some((Box::new(pred), i)) +} + +/// ¿La palabra tiene algún metacarácter de ruta sin escapar? Si no, no hay nada que expandir. +pub fn tiene_meta(s: &str) -> bool { + let cs: Vec = s.chars().collect(); + let mut i = 0; + while i < cs.len() { + match cs[i] { + '\\' => i += 2, + '*' | '?' | '[' => return true, + _ => i += 1, + } + } + false +} + +/// Expande un patrón de ruta. Devuelve las coincidencias ORDENADAS, o vacío si no hay ninguna +/// (quien llama deja entonces la palabra tal cual, que es lo que manda POSIX). +pub fn expandir(patron: &str) -> Vec { + let absoluto = patron.starts_with('/'); + let partes: Vec<&str> = patron.split('/').filter(|p| !p.is_empty()).collect(); + let mut actuales: Vec = vec![if absoluto { "/".into() } else { String::new() }]; + for (n, parte) in partes.iter().enumerate() { + let ultima = n + 1 == partes.len(); + let mut siguientes = Vec::new(); + for base in &actuales { + if !tiene_meta(parte) { + let cand = unir(base, parte); + let existe = std::path::Path::new(&cand).symlink_metadata().is_ok(); + if existe || !ultima { + // sin meta, no hace falta listar el directorio + if existe { + siguientes.push(cand); + } + } + continue; + } + let dir = if base.is_empty() { ".".to_string() } else { base.clone() }; + let Ok(entradas) = std::fs::read_dir(&dir) else { continue }; + let mut nombres: Vec = entradas + .filter_map(|e| e.ok()) + .map(|e| e.file_name().to_string_lossy().into_owned()) + .filter(|n| { + // un patrón no matchea los que empiezan con punto salvo que el patrón lo diga + !(n.starts_with('.') && !parte.starts_with('.')) + }) + .filter(|n| encaja(parte, n)) + .collect(); + nombres.sort(); + for n in nombres { + let cand = unir(base, &n); + if ultima || std::path::Path::new(&cand).is_dir() { + siguientes.push(cand); + } + } + } + actuales = siguientes; + if actuales.is_empty() { + return Vec::new(); + } + } + actuales +} + +fn unir(base: &str, hoja: &str) -> String { + if base.is_empty() { + hoja.to_string() + } else if base == "/" { + format!("/{hoja}") + } else { + format!("{base}/{hoja}") + } +} + +#[cfg(test)] +mod pruebas { + use super::*; + + #[test] + fn asterisco_y_signo() { + assert!(encaja("*.txt", "a.txt")); + assert!(encaja("*", "")); + assert!(!encaja("*.txt", "a.txtx")); + assert!(encaja("a?c", "abc")); + assert!(!encaja("a?c", "ac")); + assert!(encaja("loop*", "loop0")); + } + + #[test] + fn clases() { + assert!(encaja("[abc]x", "bx")); + assert!(!encaja("[abc]x", "dx")); + assert!(encaja("[!abc]x", "dx")); + assert!(encaja("[a-f]1", "c1")); + assert!(!encaja("[a-f]1", "g1")); + // `]` como primer carácter es literal + assert!(encaja("[]a]", "]")); + } + + #[test] + fn el_patron_de_case_del_init() { + // `case $n in proc|sys|dev|run|tmp|store|init) continue ;;` — patrones sin meta + for n in ["proc", "sys", "dev"] { + assert!(encaja(n, n)); + } + assert!(!encaja("proc", "procfs")); + } + + #[test] + fn barra_escapa() { + assert!(encaja(r"a\*b", "a*b")); + assert!(!encaja(r"a\*b", "axb")); + } + + #[test] + fn sin_coincidencia_devuelve_vacio() { + assert!(expandir("/no-existe-jamas-*/x").is_empty()); + } + + #[test] + fn expande_en_el_arbol_real() { + let hits = expandir("/dev/nul*"); + assert_eq!(hits, vec!["/dev/null".to_string()]); + } +} diff --git a/crates/simi/src/lexer.rs b/crates/simi/src/lexer.rs new file mode 100644 index 00000000..321dfa44 --- /dev/null +++ b/crates/simi/src/lexer.rs @@ -0,0 +1,780 @@ +//! Tokenizador. Primero se tokeniza TODO, después se parsea: así los cuerpos de los aquí-documentos +//! —que viven en las líneas siguientes al operador— ya están resueltos cuando el parser los necesita. +//! +//! El sitio delicado de todo el shell está acá: **la barra invertida dentro de `` ` ` ``** +//! (POSIX.1-2024 §2.6.3). Es lo que rompió a brush (reubeno/brush#1394) y se lleva 25 líneas del +//! `libtool` que genera cualquier `configure`. La regla, que NO es la de `$( )`: +//! +//! > dentro de `` ` ` `` la barra invertida conserva su significado literal, **salvo** cuando +//! > precede a `$`, `` ` `` o `\`, donde se ELIMINA antes de parsear el texto de la orden. +//! +//! Ver `pruebas::comillas_invertidas_posix_2_6_3`, que es el caso de brush calcado. + +use crate::ast::{Palabra, ParamOp, Seg}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Tok { + Pal(Palabra), + Op(String), + IoNum(i32), + /// Referencia al cuerpo del aquí-documento, en la tabla que devuelve `tokenizar`. + AquiDoc(usize), + Nueva, +} + +pub struct Lexer { + src: Vec, + i: usize, + toks: Vec, + /// `(cuerpo, expande)` + cuerpos: Vec<(String, bool)>, + /// aquí-documentos anunciados en esta línea y pendientes de leer su cuerpo + pendientes: Vec<(usize, String, bool, bool)>, // (id, delim, expande, quitar_tabs) +} + +/// `(tokens, cuerpos de aquí-documento)` +pub fn tokenizar(entrada: &str) -> Result<(Vec, Vec<(String, bool)>), String> { + let mut lx = Lexer { + src: entrada.chars().collect(), + i: 0, + toks: Vec::new(), + cuerpos: Vec::new(), + pendientes: Vec::new(), + }; + lx.correr()?; + Ok((lx.toks, lx.cuerpos)) +} + +const OPERADORES: &[&str] = &[ + "<<-", ";;", "&&", "||", ">>", "<<", ">&", "<&", "<>", ";", "&", "|", "(", ")", "<", ">", +]; + +fn es_meta(c: char) -> bool { + matches!(c, ' ' | '\t' | '\n' | ';' | '&' | '|' | '(' | ')' | '<' | '>') +} + +impl Lexer { + fn ver(&self, n: usize) -> Option { + self.src.get(self.i + n).copied() + } + + fn correr(&mut self) -> Result<(), String> { + loop { + // espacios (sin newline) y continuación de línea + while let Some(c) = self.ver(0) { + if c == ' ' || c == '\t' { + self.i += 1; + } else if c == '\\' && self.ver(1) == Some('\n') { + self.i += 2; + } else if c == '#' && self.al_principio_de_palabra() { + while self.ver(0).is_some() && self.ver(0) != Some('\n') { + self.i += 1; + } + } else { + break; + } + } + let Some(c) = self.ver(0) else { break }; + + if c == '\n' { + self.i += 1; + self.leer_cuerpos_pendientes()?; + self.toks.push(Tok::Nueva); + continue; + } + if let Some(op) = self.operador_en_curso() { + self.i += op.chars().count(); + if op == "<<" || op == "<<-" { + self.anunciar_aqui_doc(&op)?; + } else { + self.toks.push(Tok::Op(op)); + } + continue; + } + let pal = self.leer_palabra()?; + // `2>` y `2<`: un número pegado a una redirección es el descriptor, no una palabra. + if let Some(n) = numero_de_fd(&pal) { + if matches!(self.ver(0), Some('>') | Some('<')) { + self.toks.push(Tok::IoNum(n)); + continue; + } + } + self.toks.push(Tok::Pal(pal)); + } + self.leer_cuerpos_pendientes()?; + Ok(()) + } + + fn al_principio_de_palabra(&self) -> bool { + // El carácter INMEDIATAMENTE anterior, sin saltarse los blancos: `a#b` no es comentario + // (el `#` va pegado a `a`) y `a #b` sí. Saltarse los blancos invertía las dos. + match self.i.checked_sub(1).map(|n| self.src[n]) { + None => true, + Some(c) => es_meta(c), + } + } + + fn operador_en_curso(&self) -> Option { + let resto: String = self.src[self.i..].iter().take(3).collect(); + OPERADORES.iter().find(|op| resto.starts_with(**op)).map(|op| op.to_string()) + } + + fn anunciar_aqui_doc(&mut self, op: &str) -> Result<(), String> { + while matches!(self.ver(0), Some(' ') | Some('\t')) { + self.i += 1; + } + // El delimitador: si trae comillas, el cuerpo NO se expande. + let mut delim = String::new(); + let mut comillado = false; + while let Some(c) = self.ver(0) { + if es_meta(c) { + break; + } + match c { + '\'' | '"' => { + comillado = true; + let cierre = c; + self.i += 1; + while let Some(d) = self.ver(0) { + if d == cierre { + break; + } + delim.push(d); + self.i += 1; + } + self.i += 1; + } + '\\' => { + comillado = true; + self.i += 1; + if let Some(d) = self.ver(0) { + delim.push(d); + self.i += 1; + } + } + _ => { + delim.push(c); + self.i += 1; + } + } + } + if delim.is_empty() { + return Err("aquí-documento sin delimitador".into()); + } + let id = self.cuerpos.len(); + self.cuerpos.push((String::new(), !comillado)); + self.pendientes.push((id, delim, !comillado, op == "<<-")); + self.toks.push(Tok::Op("<<".into())); + self.toks.push(Tok::AquiDoc(id)); + Ok(()) + } + + fn leer_cuerpos_pendientes(&mut self) -> Result<(), String> { + let pendientes: Vec<_> = self.pendientes.drain(..).collect(); + for (id, delim, _expande, quitar_tabs) in pendientes { + let mut cuerpo = String::new(); + loop { + if self.i >= self.src.len() { + break; // EOF sin delimitador: POSIX avisa; acá se acepta lo leído + } + let mut linea = String::new(); + while let Some(c) = self.ver(0) { + self.i += 1; + if c == '\n' { + break; + } + linea.push(c); + } + let comparar = if quitar_tabs { linea.trim_start_matches('\t') } else { &linea[..] }; + if comparar == delim { + break; + } + cuerpo.push_str(comparar); + cuerpo.push('\n'); + } + self.cuerpos[id].0 = cuerpo; + } + Ok(()) + } + + // ───────────────────────────────────────────── palabras + + fn leer_palabra(&mut self) -> Result { + let mut segs: Vec = Vec::new(); + // El literal en curso y SU estado de comillado. Cuando el estado cambia hay que cerrar el + // seg: si no, `lt_y=\"hola\"` queda como UN seg entrecomillado y el parser ya no ve la + // asignación —el prefijo antes del `=` tiene que ser desnudo— así que la ejecuta como una + // orden. Es lo que rompía el bucle de `config.status` de libtool, medido con el banco. + let mut lit = String::new(); + let mut lit_q: Option = None; + let mut vacio_comillado = false; + + macro_rules! volcar { + () => { + if !lit.is_empty() { + segs.push(Seg::Lit { + texto: std::mem::take(&mut lit), + comillado: lit_q.unwrap_or(false), + }); + } + lit_q = None; + }; + } + macro_rules! empujar { + ($c:expr, $q:expr) => {{ + let q: bool = $q; + if lit_q.is_some() && lit_q != Some(q) { + volcar!(); + } + lit_q = Some(q); + lit.push($c); + }}; + } + + if self.ver(0) == Some('~') { + segs.push(Seg::Tilde); + self.i += 1; + } + + while let Some(c) = self.ver(0) { + if es_meta(c) { + break; + } + match c { + '\\' => { + self.i += 1; + match self.ver(0) { + Some('\n') => self.i += 1, // continuación de línea: desaparece + Some(d) => { + empujar!(d, true); + self.i += 1; + } + None => empujar!('\\', true), + } + } + '\'' => { + self.i += 1; + let mut vacia = true; + let mut cerrada = false; + while let Some(d) = self.ver(0) { + self.i += 1; + if d == '\'' { + cerrada = true; + break; + } + empujar!(d, true); + vacia = false; + } + if !cerrada { + return Err("comilla simple sin cerrar".into()); + } + if vacia && lit.is_empty() && segs.is_empty() { + vacio_comillado = true; + } + } + '"' => { + self.i += 1; + let mut vacia = true; + let mut cerrada = false; + loop { + let Some(d) = self.ver(0) else { break }; + if d == '"' { + self.i += 1; + cerrada = true; + break; + } + vacia = false; + match d { + '\\' => { + self.i += 1; + match self.ver(0) { + // Dentro de comillas dobles la barra sólo escapa a estos cuatro. + Some(e @ ('$' | '`' | '"' | '\\')) => { + empujar!(e, true); + self.i += 1; + } + Some('\n') => self.i += 1, + Some(e) => { + empujar!('\\', true); + empujar!(e, true); + self.i += 1; + } + None => empujar!('\\', true), + } + } + '$' => { + volcar!(); + let s = self.leer_dolar(true)?; + segs.push(s); + } + '`' => { + volcar!(); + let cuerpo = self.leer_backtick(true)?; + segs.push(Seg::CmdSub { cuerpo, comillado: true }); + } + _ => { + empujar!(d, true); + self.i += 1; + } + } + } + if !cerrada { + return Err("comilla doble sin cerrar".into()); + } + if vacia && lit.is_empty() && segs.is_empty() { + vacio_comillado = true; + } + } + '$' => { + volcar!(); + let s = self.leer_dolar(false)?; + segs.push(s); + } + '`' => { + volcar!(); + let cuerpo = self.leer_backtick(false)?; + segs.push(Seg::CmdSub { cuerpo, comillado: false }); + } + _ => { + empujar!(c, false); + self.i += 1; + } + } + } + volcar!(); + if segs.is_empty() && vacio_comillado { + segs.push(Seg::Lit { texto: String::new(), comillado: true }); + } + if segs.is_empty() { + return Err("palabra vacía".into()); + } + Ok(segs) + } + + /// `$x`, `${…}`, `$(( … ))`, `$( … )`, `$?`, `$1`, `$@`… + fn leer_dolar(&mut self, comillado: bool) -> Result { + self.i += 1; // $ + match self.ver(0) { + Some('(') if self.ver(1) == Some('(') => { + self.i += 2; + let expr = self.leer_hasta_parentesis_doble()?; + Ok(Seg::Arith { expr, comillado }) + } + Some('(') => { + self.i += 1; + let cuerpo = self.leer_hasta_parentesis()?; + Ok(Seg::CmdSub { cuerpo, comillado }) + } + Some('{') => { + self.i += 1; + self.leer_llaves(comillado) + } + Some(c) if c.is_ascii_digit() => { + self.i += 1; + Ok(Seg::Param { + nombre: c.to_string(), + op: ParamOp::Valor, + arg: vec![], + comillado, + }) + } + Some(c @ ('@' | '*' | '#' | '?' | '$' | '!' | '-' | '0')) => { + self.i += 1; + Ok(Seg::Param { nombre: c.to_string(), op: ParamOp::Valor, arg: vec![], comillado }) + } + Some(c) if c == '_' || c.is_ascii_alphabetic() => { + let mut n = String::new(); + while let Some(d) = self.ver(0) { + if d == '_' || d.is_ascii_alphanumeric() { + n.push(d); + self.i += 1; + } else { + break; + } + } + Ok(Seg::Param { nombre: n, op: ParamOp::Valor, arg: vec![], comillado }) + } + // `$` solo: literal + _ => Ok(Seg::Lit { texto: "$".into(), comillado }), + } + } + + fn leer_llaves(&mut self, comillado: bool) -> Result { + let largo = if self.ver(0) == Some('#') { + // `${#x}`; ojo con `${#}` que es el número de posicionales + if matches!(self.ver(1), Some('}')) { + self.i += 2; + return Ok(Seg::Param { + nombre: "#".into(), + op: ParamOp::Valor, + arg: vec![], + comillado, + }); + } + self.i += 1; + true + } else { + false + }; + let mut nombre = String::new(); + if let Some(c @ ('@' | '*' | '#' | '?' | '$' | '!' | '-')) = self.ver(0) { + nombre.push(c); + self.i += 1; + } else { + while let Some(d) = self.ver(0) { + if d == '_' || d.is_ascii_alphanumeric() { + nombre.push(d); + self.i += 1; + } else { + break; + } + } + } + if nombre.is_empty() { + return Err("${} sin nombre".into()); + } + if largo { + if self.ver(0) != Some('}') { + return Err("${#x} mal cerrado".into()); + } + self.i += 1; + return Ok(Seg::Param { nombre, op: ParamOp::Largo, arg: vec![], comillado }); + } + let op = match self.ver(0) { + Some('}') => { + self.i += 1; + return Ok(Seg::Param { nombre, op: ParamOp::Valor, arg: vec![], comillado }); + } + Some(':') => { + self.i += 1; + match self.ver(0) { + Some('-') => ParamOp::Defecto { dos_puntos: true }, + Some('=') => ParamOp::Asigna { dos_puntos: true }, + Some('?') => ParamOp::Error { dos_puntos: true }, + Some('+') => ParamOp::Alterno { dos_puntos: true }, + _ => return Err("operador ${x:…} no soportado".into()), + } + } + Some('-') => ParamOp::Defecto { dos_puntos: false }, + Some('=') => ParamOp::Asigna { dos_puntos: false }, + Some('?') => ParamOp::Error { dos_puntos: false }, + Some('+') => ParamOp::Alterno { dos_puntos: false }, + Some('#') => { + if self.ver(1) == Some('#') { + self.i += 1; + ParamOp::QuitaPrefijo { largo: true } + } else { + ParamOp::QuitaPrefijo { largo: false } + } + } + Some('%') => { + if self.ver(1) == Some('%') { + self.i += 1; + ParamOp::QuitaSufijo { largo: true } + } else { + ParamOp::QuitaSufijo { largo: false } + } + } + _ => return Err("operador ${…} no soportado".into()), + }; + self.i += 1; // el carácter del operador + // el argumento, hasta la `}` que cierra (contando anidamiento y comillas) + let arg_txt = self.leer_hasta_llave()?; + let arg = if arg_txt.is_empty() { + vec![] + } else { + let (ts, _) = tokenizar(&arg_txt)?; + match ts.into_iter().find_map(|t| match t { + Tok::Pal(p) => Some(p), + _ => None, + }) { + Some(p) => p, + None => vec![Seg::Lit { texto: String::new(), comillado: true }], + } + }; + Ok(Seg::Param { nombre, op, arg, comillado }) + } + + fn leer_hasta_llave(&mut self) -> Result { + let mut prof = 1; + let mut out = String::new(); + while let Some(c) = self.ver(0) { + if c == '\\' { + // barra invertida: se copia con lo que escapa, sin interpretarlo + out.push(c); + self.i += 1; + if let Some(d) = self.ver(0) { + out.push(d); + self.i += 1; + } + continue; + } + match c { + '{' => prof += 1, + '}' => { + prof -= 1; + if prof == 0 { + self.i += 1; + return Ok(out); + } + } + '\'' | '"' => { + let cierre = c; + out.push(c); + self.i += 1; + while let Some(d) = self.ver(0) { + out.push(d); + self.i += 1; + if d == cierre { + break; + } + if d == '\\' { + if let Some(e) = self.ver(0) { + out.push(e); + self.i += 1; + } + } + } + continue; + } + _ => {} + } + out.push(c); + self.i += 1; + } + Err("${…} sin cerrar".into()) + } + + fn leer_hasta_parentesis(&mut self) -> Result { + let mut prof = 1; + let mut out = String::new(); + while let Some(c) = self.ver(0) { + if c == '\\' { + // barra invertida: se copia con lo que escapa, sin interpretarlo + out.push(c); + self.i += 1; + if let Some(d) = self.ver(0) { + out.push(d); + self.i += 1; + } + continue; + } + match c { + '(' => prof += 1, + ')' => { + prof -= 1; + if prof == 0 { + self.i += 1; + return Ok(out); + } + } + '\'' | '"' => { + let cierre = c; + out.push(c); + self.i += 1; + while let Some(d) = self.ver(0) { + out.push(d); + self.i += 1; + if d == cierre { + break; + } + if d == '\\' && cierre == '"' { + if let Some(e) = self.ver(0) { + out.push(e); + self.i += 1; + } + } + } + continue; + } + _ => {} + } + out.push(c); + self.i += 1; + } + Err("$( sin cerrar".into()) + } + + fn leer_hasta_parentesis_doble(&mut self) -> Result { + let mut prof = 0; + let mut out = String::new(); + while let Some(c) = self.ver(0) { + if c == '\\' { + out.push(c); + self.i += 1; + if let Some(d) = self.ver(0) { + out.push(d); + self.i += 1; + } + continue; + } + if c == '(' { + prof += 1; + } else if c == ')' { + if prof == 0 { + if self.ver(1) == Some(')') { + self.i += 2; + return Ok(out); + } + return Err("$(( sin cerrar".into()); + } + prof -= 1; + } + out.push(c); + self.i += 1; + } + Err("$(( sin cerrar".into()) + } + + /// El cuerpo de `` ` … ` ``, aplicando POSIX §2.6.3 a la barra invertida. + /// + /// Dentro de las comillas invertidas la barra es LITERAL salvo ante `$`, `` ` `` y `\`, donde + /// se elimina. brush aplicaba acá las reglas de `$( )` y por eso el `config.status` de libtool + /// generaba 25 líneas con un nivel de escapado de menos. + fn leer_backtick(&mut self, en_comillas_dobles: bool) -> Result { + self.i += 1; // ` + let mut out = String::new(); + while let Some(c) = self.ver(0) { + match c { + '`' => { + self.i += 1; + return Ok(out); + } + '\\' => { + // Fuera de comillas la barra se elimina ante `$`, `` ` `` y `\`. DENTRO de + // comillas dobles el cuerpo del backtick sigue estando entre comillas, así que + // el `"` se suma al conjunto: `"`echo \"`"` deja el cuerpo `echo "`, que es un + // error de sintaxis — y es lo que hace el ash. Medido con el banco. + let quitar = matches!( + self.ver(1), + Some('$') | Some('`') | Some('\\') + ) || (en_comillas_dobles && self.ver(1) == Some('"')); + if quitar { + out.push(self.ver(1).unwrap()); + self.i += 2; + } else { + out.push('\\'); + self.i += 1; + } + } + _ => { + out.push(c); + self.i += 1; + } + } + } + Err("` sin cerrar".into()) + } +} + +fn numero_de_fd(p: &Palabra) -> Option { + if p.len() != 1 { + return None; + } + match &p[0] { + Seg::Lit { texto, comillado: false } if !texto.is_empty() => { + if texto.chars().all(|c| c.is_ascii_digit()) { + texto.parse().ok() + } else { + None + } + } + _ => None, + } +} + +/// Si la palabra es exactamente un literal sin comillas, devuelve su texto. Lo usa el parser para +/// reconocer palabras reservadas: `"if"` entrecomillado NO es la reservada `if`. +pub fn literal_desnudo(p: &Palabra) -> Option<&str> { + match p.as_slice() { + [Seg::Lit { texto, comillado: false }] => Some(texto), + _ => None, + } +} + +#[cfg(test)] +mod pruebas { + use super::*; + + fn cmdsub(entrada: &str) -> String { + let (ts, _) = tokenizar(entrada).unwrap(); + for t in ts { + if let Tok::Pal(p) = t { + for s in p { + if let Seg::CmdSub { cuerpo, .. } = s { + return cuerpo; + } + } + } + } + panic!("no había sustitución en {entrada:?}"); + } + + /// POSIX.1-2024 §2.6.3 — el bug de brush (reubeno/brush#1394), calcado del reproductor. + #[test] + fn comillas_invertidas_posix_2_6_3() { + // \$ ⇒ se elimina la barra: el cuerpo pide `echo $V`… NO: la barra ante `$` se ELIMINA, + // así que el cuerpo es `echo $V` y la expansión la hace la orden de adentro. + assert_eq!(cmdsub(r#"echo "a: [`echo \$V`]""#), "echo $V"); + // \\ ⇒ una sola barra + assert_eq!(cmdsub(r#"echo "b: [`echo \\`]""#), r"echo \"); + // \x ⇒ la barra NO se toca (x no es $ ` ni \) + assert_eq!(cmdsub(r#"echo "[`echo \x`]""#), r"echo \x"); + // en $( ) no hay regla especial: el cuerpo va tal cual + let (ts, _) = tokenizar(r#"echo "[$(echo \$V)]""#).unwrap(); + let cuerpo = ts + .into_iter() + .find_map(|t| match t { + Tok::Pal(p) => p.into_iter().find_map(|s| match s { + Seg::CmdSub { cuerpo, .. } => Some(cuerpo), + _ => None, + }), + _ => None, + }) + .unwrap(); + assert_eq!(cuerpo, r"echo \$V"); + } + + #[test] + fn aqui_doc_con_y_sin_comillas() { + let (_, cuerpos) = tokenizar("cat </dev/null").unwrap(); + assert!(ts.contains(&Tok::IoNum(2))); + assert!(ts.contains(&Tok::Op(">".into()))); + } + + #[test] + fn comentario_solo_al_principio_de_palabra() { + let (ts, _) = tokenizar("echo a#b # comentario").unwrap(); + let pals: Vec<_> = ts + .iter() + .filter_map(|t| match t { + Tok::Pal(p) => literal_desnudo(p), + _ => None, + }) + .collect(); + assert_eq!(pals, vec!["echo", "a#b"]); + } + + #[test] + fn comillas_vacias_son_un_campo() { + let (ts, _) = tokenizar("echo ''").unwrap(); + match &ts[1] { + Tok::Pal(p) => assert_eq!(p, &vec![Seg::Lit { texto: String::new(), comillado: true }]), + t => panic!("{t:?}"), + } + } +} diff --git a/crates/simi/src/main.rs b/crates/simi/src/main.rs new file mode 100644 index 00000000..a216b1cf --- /dev/null +++ b/crates/simi/src/main.rs @@ -0,0 +1,144 @@ +//! simi — el `/bin/sh` del producto. +//! +//! «simi» es *boca, lengua* en quechua: es cómo se le habla a la máquina. +//! +//! **Por qué existe.** El frente C del ADR 0008 pone a busybox en la raíz de confianza del arranque +//! (`STAGE1_COMPONENTS`, `ATTEST_PATHS`) y en el `exec` de cada card de arje. Botarlo pedía un `sh`, +//! y el candidato que se midió —brush 0.4.0— pesa 6,9 MB, arranca 3,7× más lento que el ash de +//! busybox y trae dos divergencias medidas contra POSIX (reubeno/brush#1394 y #1396). +//! +//! **Alcance, y está MEDIDO, no supuesto** (`docs/plan-botar-busybox.md`): +//! +//! | consumidor | qué usa | +//! |---|---| +//! | las 23 cards de arje con shell | 8 builtins, 13 construcciones, ninguna función, ningún `if` | +//! | los 21 `/init` empotrados de las imágenes | + `if`, funciones, `case`, aquí-documentos, `${…}`, posicionales | +//! | `takana-live-install.sh` (corre en la imagen viva) | POSIX entero | +//! +//! Lo que NO está y se dice en vez de fingirlo: control de trabajos (`jobs`/`fg`/`bg`), edición de +//! línea, historial, `alias`, `getopts`, `[[ ]]`, arrays. El shell interactivo de `getty` es otro +//! consumidor y no está cubierto todavía. +//! +//! Uso: `simi [-c ORDEN | FICHERO] [-e] [-u] [-x] [-n] [ARGS…]`, y por stdin si no hay ninguno. + +mod arith; +mod ast; +mod builtins; +mod exec; +mod expand; +mod glob; +mod lexer; +mod parser; +mod shell; + +use std::io::Read; + +use shell::{Flujo, Shell}; + +fn main() -> std::process::ExitCode { + let args: Vec = std::env::args().collect(); + let mut sh = Shell::nuevo(); + // argv[0] con guión delante = shell de login; hoy sólo cambia el nombre en los mensajes. + let argv0 = args.first().cloned().unwrap_or_else(|| "simi".into()); + sh.nombre = argv0.trim_start_matches('-').rsplit('/').next().unwrap_or("simi").to_string(); + + let mut i = 1; + let mut orden: Option = None; + let mut fichero: Option = None; + while i < args.len() { + let a = &args[i]; + match a.as_str() { + "-c" => { + i += 1; + match args.get(i) { + Some(c) => orden = Some(c.clone()), + None => { + eprintln!("{}: -c necesita una orden", sh.nombre); + return std::process::ExitCode::from(2); + } + } + } + "-s" => {} + "--" => { + i += 1; + break; + } + _ if a.starts_with('-') && a.len() > 1 && !a.starts_with("--") => { + for c in a[1..].chars() { + match c { + 'e' => sh.opts.errexit = true, + 'u' => sh.opts.nounset = true, + 'x' => sh.opts.xtrace = true, + 'n' => sh.opts.noexec = true, + 'f' => sh.opts.noglob = true, + 'i' | 'l' => {} + otro => { + eprintln!("{}: -{otro}: opción no soportada", sh.nombre); + return std::process::ExitCode::from(2); + } + } + } + } + _ => { + if orden.is_none() { + fichero = Some(a.clone()); + } + break; + } + } + i += 1; + } + + // Los argumentos que quedan son los posicionales. Con `-c`, el primero es `$0`. + let resto: Vec = args[i..].to_vec(); + let texto = match (&orden, &fichero) { + (Some(c), _) => { + if !resto.is_empty() { + sh.nombre = resto[0].clone(); + sh.params = resto[1..].to_vec(); + } + c.clone() + } + (None, Some(f)) => { + sh.nombre = f.clone(); + sh.params = resto[1..].to_vec(); + match std::fs::read_to_string(f) { + Ok(t) => t, + Err(e) => { + eprintln!("{}: {}: {}", argv0, f, e); + return std::process::ExitCode::from(127); + } + } + } + (None, None) => { + let mut t = String::new(); + if std::io::stdin().read_to_string(&mut t).is_err() { + eprintln!("{}: no pude leer la entrada", sh.nombre); + return std::process::ExitCode::from(2); + } + t + } + }; + + let lista = match parser::parsear(&texto) { + Ok(l) => l, + Err(e) => { + eprintln!("{}: error de sintaxis: {}", sh.nombre, e); + return std::process::ExitCode::from(2); + } + }; + // `-n`: parsear y no ejecutar. Es lo que usa el banco diferencial, y lo que hace que un guion + // roto se detecte sin correrlo. + if sh.opts.noexec { + return std::process::ExitCode::SUCCESS; + } + + let flujo = exec::correr_lista(&mut sh, &lista); + let estado = match flujo { + Flujo::Exit(c) => c, + _ => sh.estado, + }; + exec::correr_trap_exit(&mut sh, estado); + let final_ = if sh.trap_exit_corrido { estado } else { sh.estado }; + std::process::ExitCode::from((final_ & 0xff) as u8) +} diff --git a/crates/simi/src/parser.rs b/crates/simi/src/parser.rs new file mode 100644 index 00000000..87f6a162 --- /dev/null +++ b/crates/simi/src/parser.rs @@ -0,0 +1,515 @@ +//! Parser de descenso recursivo sobre los tokens. La gramática es la de POSIX.1-2024 §2.10, +//! recortada a lo que el producto usa (medido en `docs/plan-botar-busybox.md`): listas, tuberías, +//! `&&`/`||`, `if`, `while`/`until`, `for`, `case`, funciones, grupos, subshells y redirecciones. +//! +//! Lo que NO hay, a propósito: alias (§2.3.1), `select`, `[[ ]]`, arrays. No son POSIX o no los usa +//! nadie en el árbol — y un shell que es la raíz de confianza del arranque se mide por lo que NO +//! trae. + +use crate::ast::*; +use crate::lexer::{literal_desnudo, tokenizar, Tok}; + +pub struct Parser { + ts: Vec, + i: usize, + cuerpos: Vec<(String, bool)>, +} + +const RESERVADAS: &[&str] = &[ + "if", "then", "elif", "else", "fi", "while", "until", "do", "done", "for", "in", "case", "esac", + "{", "}", "!", +]; + +pub fn parsear(entrada: &str) -> Result { + let (ts, cuerpos) = tokenizar(entrada)?; + let mut p = Parser { ts, i: 0, cuerpos }; + let l = p.lista(&[])?; + p.saltos(); + if p.i < p.ts.len() { + return Err(format!("sobra entrada en el token {:?}", p.ts[p.i])); + } + Ok(l) +} + +impl Parser { + fn ver(&self) -> Option<&Tok> { + self.ts.get(self.i) + } + + fn es_op(&self, op: &str) -> bool { + matches!(self.ver(), Some(Tok::Op(o)) if o == op) + } + + fn es_palabra(&self, w: &str) -> bool { + matches!(self.ver(), Some(Tok::Pal(p)) if literal_desnudo(p) == Some(w)) + } + + fn comer_palabra(&mut self, w: &str) -> bool { + if self.es_palabra(w) { + self.i += 1; + true + } else { + false + } + } + + fn exigir_palabra(&mut self, w: &str) -> Result<(), String> { + if self.comer_palabra(w) { + Ok(()) + } else { + Err(format!("falta `{w}` (hay {:?})", self.ver())) + } + } + + /// Salta saltos de línea (y los `;` sueltos donde la gramática los admite). + fn saltos(&mut self) { + while matches!(self.ver(), Some(Tok::Nueva)) { + self.i += 1; + } + } + + // ───────────────────────────────────────────── listas + + /// Una lista, parando ante cualquiera de las palabras reservadas de `hasta` (sin consumirla). + fn lista(&mut self, hasta: &[&str]) -> Result { + let mut out: Lista = Vec::new(); + loop { + self.saltos(); + if self.ver().is_none() || self.es_op(";;") || self.es_op(")") { + break; + } + if hasta.iter().any(|w| self.es_palabra(w)) { + break; + } + let yo = self.yo()?; + // El separador se CONSUME acá, así que la decisión de seguir no puede volver a + // mirarlo: hay que recordar que estuvo. Con el `;` ya comido, mirar el token actual + // daba «falta `}`» en el idioma mismo de las cards (`…; exit 78; }`). + let mut hubo_separador = false; + let fin = if self.es_op("&") { + self.i += 1; + hubo_separador = true; + Fin::SegundoPlano + } else if self.es_op(";") { + self.i += 1; + hubo_separador = true; + Fin::Secuencial + } else { + if matches!(self.ver(), Some(Tok::Nueva)) { + hubo_separador = true; // lo come el `saltos()` de la vuelta siguiente + } + Fin::Secuencial + }; + out.push((yo, fin)); + if !hubo_separador { + break; + } + } + if out.is_empty() { + return Err("lista vacía".into()); + } + Ok(out) + } + + fn yo(&mut self) -> Result { + let primera = self.tuberia()?; + let mut resto = Vec::new(); + loop { + let enlace = if self.es_op("&&") { + Enlace::Y + } else if self.es_op("||") { + Enlace::O + } else { + break; + }; + self.i += 1; + self.saltos(); + resto.push((enlace, self.tuberia()?)); + } + Ok(YO { primera, resto }) + } + + fn tuberia(&mut self) -> Result { + let negada = self.comer_palabra("!"); + let mut ordenes = vec![self.orden()?]; + while self.es_op("|") { + self.i += 1; + self.saltos(); + ordenes.push(self.orden()?); + } + Ok(Tuberia { negada, ordenes }) + } + + // ───────────────────────────────────────────── órdenes + + fn orden(&mut self) -> Result { + if self.es_palabra("if") { + return self.si(); + } + if self.es_palabra("while") || self.es_palabra("until") { + return self.bucle(); + } + if self.es_palabra("for") { + return self.para(); + } + if self.es_palabra("case") { + return self.caso(); + } + if self.es_palabra("{") { + self.i += 1; + let cuerpo = self.lista(&["}"])?; + self.exigir_palabra("}")?; + let redirs = self.redirs()?; + return Ok(Orden::Grupo { cuerpo, redirs }); + } + if self.es_op("(") { + self.i += 1; + let cuerpo = self.lista(&[])?; + if !self.es_op(")") { + return Err(format!("falta `)` (hay {:?})", self.ver())); + } + self.i += 1; + let redirs = self.redirs()?; + return Ok(Orden::Subshell { cuerpo, redirs }); + } + // ¿definición de función? `nombre ( )` + if let Some(Tok::Pal(p)) = self.ver() { + if let Some(nombre) = literal_desnudo(p) { + let nombre = nombre.to_string(); + if !RESERVADAS.contains(&nombre.as_str()) + && matches!(self.ts.get(self.i + 1), Some(Tok::Op(o)) if o == "(") + && matches!(self.ts.get(self.i + 2), Some(Tok::Op(o)) if o == ")") + { + self.i += 3; + self.saltos(); + let cuerpo = self.orden()?; + return Ok(Orden::Funcion { nombre, cuerpo: Box::new(cuerpo) }); + } + } + } + self.simple() + } + + fn si(&mut self) -> Result { + self.exigir_palabra("if")?; + let mut ramas = Vec::new(); + let cond = self.lista(&["then"])?; + self.exigir_palabra("then")?; + let cuerpo = self.lista(&["elif", "else", "fi"])?; + ramas.push((cond, cuerpo)); + let mut sino = None; + loop { + if self.comer_palabra("elif") { + let c = self.lista(&["then"])?; + self.exigir_palabra("then")?; + let b = self.lista(&["elif", "else", "fi"])?; + ramas.push((c, b)); + continue; + } + if self.comer_palabra("else") { + sino = Some(self.lista(&["fi"])?); + } + break; + } + self.exigir_palabra("fi")?; + let redirs = self.redirs()?; + Ok(Orden::Si { ramas, sino, redirs }) + } + + fn bucle(&mut self) -> Result { + let hasta = self.es_palabra("until"); + self.i += 1; + let cond = self.lista(&["do"])?; + self.exigir_palabra("do")?; + let cuerpo = self.lista(&["done"])?; + self.exigir_palabra("done")?; + let redirs = self.redirs()?; + Ok(Orden::Bucle { hasta, cond, cuerpo, redirs }) + } + + fn para(&mut self) -> Result { + self.exigir_palabra("for")?; + let var = match self.ver() { + Some(Tok::Pal(p)) => match literal_desnudo(p) { + Some(n) if !n.is_empty() => n.to_string(), + _ => return Err("`for` sin nombre de variable".into()), + }, + _ => return Err("`for` sin nombre de variable".into()), + }; + self.i += 1; + self.saltos(); + let palabras = if self.comer_palabra("in") { + let mut ps = Vec::new(); + while let Some(Tok::Pal(p)) = self.ver() { + if literal_desnudo(p) == Some("do") { + break; + } + ps.push(p.clone()); + self.i += 1; + } + if self.es_op(";") { + self.i += 1; + } + self.saltos(); + Some(ps) + } else { + if self.es_op(";") { + self.i += 1; + } + self.saltos(); + None + }; + self.exigir_palabra("do")?; + let cuerpo = self.lista(&["done"])?; + self.exigir_palabra("done")?; + let redirs = self.redirs()?; + Ok(Orden::Para { var, palabras, cuerpo, redirs }) + } + + fn caso(&mut self) -> Result { + self.exigir_palabra("case")?; + let sujeto = match self.ver() { + Some(Tok::Pal(p)) => p.clone(), + t => return Err(format!("`case` sin sujeto (hay {t:?})")), + }; + self.i += 1; + self.saltos(); + self.exigir_palabra("in")?; + let mut ramas = Vec::new(); + loop { + self.saltos(); + if self.comer_palabra("esac") { + break; + } + if self.ver().is_none() { + return Err("`case` sin `esac`".into()); + } + // patrones: pal ('|' pal)* ')' + if self.es_op("(") { + self.i += 1; + } + let mut patrones = Vec::new(); + loop { + match self.ver() { + Some(Tok::Pal(p)) => { + patrones.push(p.clone()); + self.i += 1; + } + t => return Err(format!("patrón de `case` inválido: {t:?}")), + } + if self.es_op("|") { + self.i += 1; + continue; + } + break; + } + if !self.es_op(")") { + return Err(format!("falta `)` en el patrón de `case` (hay {:?})", self.ver())); + } + self.i += 1; + self.saltos(); + let cuerpo = if self.es_op(";;") || self.es_palabra("esac") { + Vec::new() + } else { + self.lista(&["esac"])? + }; + ramas.push((patrones, cuerpo)); + self.saltos(); + if self.es_op(";;") { + self.i += 1; + } + } + let redirs = self.redirs()?; + Ok(Orden::Caso { sujeto, ramas, redirs }) + } + + fn simple(&mut self) -> Result { + let mut asignaciones = Vec::new(); + let mut palabras: Vec = Vec::new(); + let mut redirs = Vec::new(); + loop { + if let Some(r) = self.redir()? { + redirs.push(r); + continue; + } + let Some(Tok::Pal(p)) = self.ver() else { break }; + let p = p.clone(); + if palabras.is_empty() { + if let Some(a) = asignacion(&p) { + asignaciones.push(a); + self.i += 1; + continue; + } + if let Some(w) = literal_desnudo(&p) { + if RESERVADAS.contains(&w) { + break; + } + } + } + palabras.push(p); + self.i += 1; + } + if asignaciones.is_empty() && palabras.is_empty() && redirs.is_empty() { + return Err(format!("orden vacía en {:?}", self.ver())); + } + Ok(Orden::Simple { asignaciones, palabras, redirs }) + } + + fn redirs(&mut self) -> Result, String> { + let mut out = Vec::new(); + while let Some(r) = self.redir()? { + out.push(r); + } + Ok(out) + } + + fn redir(&mut self) -> Result, String> { + let (fd_explicito, salto) = match self.ver() { + Some(Tok::IoNum(n)) => (Some(*n), 1), + _ => (None, 0), + }; + let op = match self.ts.get(self.i + salto) { + Some(Tok::Op(o)) if matches!(o.as_str(), ">" | ">>" | "<" | "<<" | ">&" | "<&" | "<>") => { + o.clone() + } + _ => return Ok(None), + }; + self.i += salto + 1; + if op == "<<" { + let id = match self.ver() { + Some(Tok::AquiDoc(id)) => *id, + t => return Err(format!("aquí-documento mal formado: {t:?}")), + }; + self.i += 1; + let (texto, expande) = self.cuerpos[id].clone(); + let cuerpo = if expande { + cuerpo_expandible(&texto)? + } else { + vec![crate::ast::Seg::Lit { texto, comillado: true }] + }; + return Ok(Some(Redir::AquiDoc { fd: fd_explicito.unwrap_or(0), cuerpo, expande })); + } + let destino = match self.ver() { + Some(Tok::Pal(p)) => p.clone(), + t => return Err(format!("redirección `{op}` sin destino: {t:?}")), + }; + self.i += 1; + let r = match op.as_str() { + ">" => Redir::Salida { fd: fd_explicito.unwrap_or(1), destino, append: false }, + ">>" => Redir::Salida { fd: fd_explicito.unwrap_or(1), destino, append: true }, + "<" => Redir::Entrada { fd: fd_explicito.unwrap_or(0), origen: destino }, + "<>" => Redir::LecturaEscritura { fd: fd_explicito.unwrap_or(0), ruta: destino }, + ">&" | "<&" => { + let escritura = op == ">&"; + let dd = if literal_desnudo(&destino) == Some("-") { + DupDestino::Cerrar + } else { + DupDestino::Fd(destino) + }; + Redir::Duplica { + fd: fd_explicito.unwrap_or(if escritura { 1 } else { 0 }), + otro: dd, + escritura, + } + } + _ => unreachable!(), + }; + Ok(Some(r)) + } +} + +/// El cuerpo de un aquí-documento que expande: se lexa como si estuviera entre comillas dobles +/// (expansiones sí, `"` no especial). +fn cuerpo_expandible(texto: &str) -> Result { + let escapado: String = texto.replace('"', "\\\""); + let (ts, _) = tokenizar(&format!("\"{escapado}\""))?; + for t in ts { + if let Tok::Pal(p) = t { + return Ok(p); + } + } + Ok(vec![crate::ast::Seg::Lit { texto: String::new(), comillado: true }]) +} + +/// `NOMBRE=valor` al principio de una orden simple. +fn asignacion(p: &Palabra) -> Option { + let Seg::Lit { texto, comillado: false } = &p[0] else { return None }; + let pos = texto.find('=')?; + let nombre = &texto[..pos]; + if nombre.is_empty() || !nombre.chars().next().map(|c| c == '_' || c.is_ascii_alphabetic())? { + return None; + } + if !nombre.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) { + return None; + } + let mut valor: Palabra = Vec::new(); + let resto = &texto[pos + 1..]; + if !resto.is_empty() { + valor.push(Seg::Lit { texto: resto.to_string(), comillado: false }); + } + valor.extend(p[1..].iter().cloned()); + if valor.is_empty() { + valor.push(Seg::Lit { texto: String::new(), comillado: true }); + } + Some(Asignacion { nombre: nombre.to_string(), valor }) +} + +#[cfg(test)] +mod pruebas { + use super::*; + + #[test] + fn el_idioma_de_las_cards() { + // La forma exacta que usan 18 de las 23 cards con shell. + let l = parsear( + "test -f /etc/x.conf || { echo 'falta' >&2; exit 78; }; exec /usr/bin/x --flag", + ) + .unwrap(); + assert_eq!(l.len(), 2); + } + + #[test] + fn bucle_de_espera_de_las_cards_de_compat() { + parsear( + "i=0; while [ ! -S /run/dbus/system_bus_socket ] && [ $i -lt 100 ]; \ + do i=$((i+1)); sleep 0.2; done; exec /usr/bin/arje-polkit-compat", + ) + .unwrap(); + } + + #[test] + fn el_for_y_el_case_del_montar_trabajo() { + parsear( + "for h in /home/*; do u=$(basename \"$h\"); uid=$(id -u \"$u\") || continue; \ + mkdir -p \"/run/user/$uid\"; done; case $x in a|b) echo si ;; *) echo no ;; esac", + ) + .unwrap(); + } + + #[test] + fn funcion_y_subshell_y_tuberia() { + parsear("f() { echo hola; }; ( f | cat ) > /dev/null 2>&1").unwrap(); + } + + #[test] + fn palabra_reservada_entrecomillada_no_es_reservada() { + // `"if"` es una orden, no un condicional. + let l = parsear("\"if\" x").unwrap(); + match &l[0].0.primera.ordenes[0] { + Orden::Simple { palabras, .. } => assert_eq!(palabras.len(), 2), + o => panic!("{o:?}"), + } + } + + #[test] + fn asignaciones_solo_antes_de_la_orden() { + let l = parsear("A=1 B=2 cmd C=3").unwrap(); + match &l[0].0.primera.ordenes[0] { + Orden::Simple { asignaciones, palabras, .. } => { + assert_eq!(asignaciones.len(), 2); + assert_eq!(palabras.len(), 2); // `cmd` y `C=3` (que es un argumento) + } + o => panic!("{o:?}"), + } + } +} diff --git a/crates/simi/src/shell.rs b/crates/simi/src/shell.rs new file mode 100644 index 00000000..ee5fa661 --- /dev/null +++ b/crates/simi/src/shell.rs @@ -0,0 +1,168 @@ +//! El estado del shell y el flujo de control. Nada de sintaxis acá. + +use std::collections::HashMap; +use std::sync::atomic::AtomicBool; + +use crate::ast::Orden; + +/// Señales atrapadas que llegaron y todavía no se ejecutaron. Un handler de señal sólo puede tocar +/// cosas async-signal-safe, así que **lo único que hace es prender una bandera**; la acción del +/// `trap` corre entre órdenes, que es donde POSIX dice que corre. +pub static LLEGO_SENAL: [AtomicBool; 32] = [const { AtomicBool::new(false) }; 32]; + +#[derive(Debug, Clone, PartialEq)] +pub enum Flujo { + Normal, + Break(u32), + Continue(u32), + /// `return` dentro de una función o de un `.` + Return, + /// `exit` — se propaga hasta arriba + Exit(i32), +} + +#[derive(Default, Clone)] +pub struct Opciones { + /// `set -e` + pub errexit: bool, + /// `set -u` + pub nounset: bool, + /// `set -x` + pub xtrace: bool, + /// `set -n`: parsear y no ejecutar + pub noexec: bool, + /// `set -f`: sin expansión de rutas + pub noglob: bool, +} + +pub struct Shell { + /// Variables del shell. `exportada` decide si viaja en el `environ` de los hijos. + pub vars: HashMap, + pub funcs: HashMap, + /// `$1`, `$2`… + pub params: Vec, + /// `$0` + pub nombre: String, + pub estado: i32, + pub opts: Opciones, + /// `trap` por señal: `"EXIT"` o el nombre/número de la señal. + pub traps: HashMap, + /// PID del último `&` + pub ultimo_fondo: Option, + /// profundidad de bucles, para `break`/`continue` + pub bucles: u32, + /// profundidad de funciones, para `return` y `local` + pub funcion_honda: u32, + /// pila de variables locales a restaurar al salir de cada función + pub locales: Vec)>>, + /// Profundidad de contextos donde un estado ≠ 0 NO dispara `set -e`: condiciones de `if` y + /// `while`, operandos no finales de `&&`/`||`, y `!`. Es un contador DINÁMICO a propósito: así + /// la suspensión entra sola en el cuerpo de una función llamada desde una condición, que es lo + /// que POSIX manda y lo que el banco diferencial midió roto en la primera versión + /// (`f() { false; echo sigo; }; f || true` con `set -e`: el ash sigue, simi salía). + pub errexit_suspendido: u32, + /// ¿este proceso es un subshell? Decide si el `trap EXIT` ya corrió. + pub es_subshell: bool, + pub trap_exit_corrido: bool, +} + +impl Shell { + pub fn nuevo() -> Self { + let mut vars = HashMap::new(); + for (k, v) in std::env::vars() { + vars.insert(k, (v, true)); + } + vars.entry("IFS".into()).or_insert((" \t\n".into(), false)); + if let Ok(d) = std::env::current_dir() { + vars.insert("PWD".into(), (d.to_string_lossy().into_owned(), true)); + } + vars.insert("PPID".into(), (unsafe { libc::getppid() }.to_string(), false)); + Shell { + vars, + funcs: HashMap::new(), + params: Vec::new(), + nombre: "simi".into(), + estado: 0, + opts: Opciones::default(), + traps: HashMap::new(), + ultimo_fondo: None, + bucles: 0, + funcion_honda: 0, + locales: Vec::new(), + errexit_suspendido: 0, + es_subshell: false, + trap_exit_corrido: false, + } + } + + pub fn leer(&self, nombre: &str) -> Option { + match nombre { + "?" => return Some(self.estado.to_string()), + "#" => return Some(self.params.len().to_string()), + "$" => return Some(unsafe { libc::getpid() }.to_string()), + "!" => return Some(self.ultimo_fondo.unwrap_or(0).to_string()), + "0" => return Some(self.nombre.clone()), + "-" => { + let mut s = String::new(); + if self.opts.errexit { + s.push('e'); + } + if self.opts.nounset { + s.push('u'); + } + if self.opts.xtrace { + s.push('x'); + } + return Some(s); + } + _ => {} + } + if let Ok(n) = nombre.parse::() { + if n >= 1 { + return self.params.get(n - 1).cloned(); + } + } + self.vars.get(nombre).map(|(v, _)| v.clone()) + } + + pub fn ifs(&self) -> String { + self.leer("IFS").unwrap_or_else(|| " \t\n".to_string()) + } + + pub fn asignar(&mut self, nombre: &str, valor: &str) { + let exportada = self.vars.get(nombre).map(|(_, e)| *e).unwrap_or(false); + self.vars.insert(nombre.to_string(), (valor.to_string(), exportada)); + } + + pub fn exportar(&mut self, nombre: &str) { + if let Some(e) = self.vars.get_mut(nombre) { + e.1 = true; + } else { + self.vars.insert(nombre.to_string(), (String::new(), true)); + } + } + + /// Marca una variable como local a la función en curso, guardando su valor previo. + pub fn declarar_local(&mut self, nombre: &str) { + if let Some(marco) = self.locales.last_mut() { + let previo = self.vars.get(nombre).cloned(); + marco.push((nombre.to_string(), previo)); + } + } + + /// El `environ` para un hijo: sólo las exportadas. + pub fn entorno(&self) -> Vec { + let mut v: Vec = self + .vars + .iter() + .filter(|(_, (_, e))| *e) + .map(|(k, (val, _))| format!("{k}={val}")) + .collect(); + v.sort(); + v + } + + pub fn error(&self, msg: &str) { + eprintln!("{}: {}", self.nombre, msg); + } +} diff --git a/crates/simi/tests/divergencias_medidas.rs b/crates/simi/tests/divergencias_medidas.rs new file mode 100644 index 00000000..2871d835 --- /dev/null +++ b/crates/simi/tests/divergencias_medidas.rs @@ -0,0 +1,198 @@ +//! Las divergencias que el banco diferencial MIDIÓ, convertidas en pruebas. +//! +//! Cada una está acá porque se rompió de verdad —en brush 0.4.0, o en la primera versión de simi— +//! y ninguna se veía sin comparar contra un shell de control. La referencia de todas es el ash de +//! busybox 1.36.1, medido en la misma corrida (`docs/plan-botar-busybox.md`). +//! +//! Corren el binario de verdad, no la librería: lo que importa es el comportamiento observable. + +use std::process::Command; + +fn correr(guion: &str) -> (i32, String, String) { + let salida = Command::new(env!("CARGO_BIN_EXE_simi")) + .arg("-c") + .arg(guion) + .output() + .expect("no pude ejecutar simi"); + ( + salida.status.code().unwrap_or(-1), + String::from_utf8_lossy(&salida.stdout).into_owned(), + String::from_utf8_lossy(&salida.stderr).into_owned(), + ) +} + +fn salida(guion: &str) -> String { + correr(guion).1 +} + +/// reubeno/brush#1394 · POSIX.1-2024 §2.6.3. La barra invertida dentro de `` ` ` `` es literal +/// salvo ante `$`, `` ` `` y `\`. Es el bug que hacía que el `libtool` generado saliera con un +/// nivel de escapado de menos y que 0 de 3 recetas autotools construyeran. +#[test] +fn barra_invertida_en_comillas_invertidas() { + let g = r#"V=hola +echo "a: [`echo \$V`]" +echo "b: [`echo \\`]" +echo "c: [`echo \\$V`]" +echo "d: [$(echo \\$V)]""#; + assert_eq!(salida(g), "a: [hola]\nb: [\\]\nc: [$V]\nd: [\\hola]\n"); +} + +/// reubeno/brush#1396 · el `trap EXIT` de un subshell CORRE. brush no lo ejecuta nunca —ni con +/// `exit` explícito— y falla en silencio: `rc=0` y la limpieza sin hacer. +#[test] +fn trap_exit_de_subshell() { + assert_eq!( + salida(r#"trap 'echo "[padre]"' EXIT; ( trap 'echo "[sub]"' EXIT; echo dentro ); echo fuera"#), + "dentro\n[sub]\nfuera\n[padre]\n" + ); + // con `exit` explícito y con el estado preservado + let (rc, out, _) = correr(r#"( trap 'echo T' EXIT; exit 3 ); echo rc=$?"#); + assert_eq!((rc, out.as_str()), (0, "T\nrc=3\n")); + // y en una sustitución de orden, que también es un subshell + assert_eq!(salida(r#"x=$( trap 'echo T' EXIT; echo v ); echo "[$x]""#), "[v\nT]\n"); +} + +/// El trap heredado NO se dispara en el subshell (POSIX §2.12): se resetea al entrar. Es el caso +/// complementario del anterior, y confundirlos es fácil. +#[test] +fn trap_heredado_no_se_dispara_en_el_subshell() { + assert_eq!( + salida(r#"trap 'echo "[exit]"' EXIT; echo padre; ( echo sub ); echo vuelta"#), + "padre\nsub\nvuelta\n[exit]\n" + ); +} + +/// `shift` más allá de `$#`: estado 1 y los posicionales intactos. brush devolvía 2. +#[test] +fn shift_mas_alla_de_argc() { + assert_eq!(salida("set --; shift 2>/dev/null; echo rc=$?"), "rc=1\n"); + assert_eq!(salida("set -- a b; shift 5 2>/dev/null; echo rc=$?-$#"), "rc=1-2\n"); + assert_eq!(salida("set -- a b c; shift 2; echo $1"), "c\n"); +} + +/// El bucle REAL del `config.status` que genera cualquier `configure` de autotools. Es el +/// consumidor que decide, y el que ningún banco sintético reproduce: la primera versión de simi +/// devolvía las tres variables VACÍAS, igual que brush. +#[test] +fn bucle_de_config_status_de_libtool() { + let g = r#"ECHO="printf %s\\n" +SED=sed +GREP=grep +sed_quote_subst='s/\([`"$\\]\)/\\\1/g' +for var in ECHO SED GREP; do + case `eval \\$ECHO \\""\\$$var"\\"` in + *[\\\`\"\$]*) eval "lt_$var=\\\"\`\$ECHO \"\$$var\" | \$SED \"\$sed_quote_subst\"\`\\\"" ;; + *) eval "lt_$var=\\\"\$$var\\\"" ;; + esac +done +echo "ECHO=[$lt_ECHO]" +echo "SED=[$lt_SED]" +echo "GREP=[$lt_GREP]""#; + // La expectativa es lo que da el ash de busybox, medido en la misma corrida — no lo que uno + // cree que debería dar: la primera versión de esta prueba escribió `%s\n` de memoria y el + // control dice `%s\\n`. + assert_eq!( + salida(g), + "ECHO=[\"printf %s\\\\n\"]\nSED=[\"sed\"]\nGREP=[\"grep\"]\n" + ); +} + +/// Una asignación cuyo valor trae comillas o escapes sigue siendo una ASIGNACIÓN. Si el lexer +/// funde el prefijo desnudo con el valor entrecomillado, `x="a b"` se ejecuta como si fuera una +/// orden — y el fallo se lee como «no encontrado», que manda a buscar al lugar equivocado. +#[test] +fn asignaciones_con_comillas_y_escapes() { + assert_eq!(salida(r#"x="a b"; echo "[$x]"; set -- $x; echo $#"#), "[a b]\n2\n"); + assert_eq!(salida(r#"eval "y=\\\"hola\\\""; echo "[$y]""#), "[\"hola\"]\n"); + assert_eq!(salida(r#"z=a'b'c; echo "[$z]""#), "[abc]\n"); +} + +/// `set -e` se SUSPENDE dentro de una condición, y la suspensión entra en el cuerpo de la función +/// llamada desde ahí. Es la semántica más divergente entre shells y la que rompe los scripts de +/// limpieza cuando está mal. +#[test] +fn set_e_se_suspende_en_condiciones() { + let g = r#"set -e +( false ) || echo or-salva +if false; then echo no; fi +f() { false; echo en-funcion-tras-false; } +f || echo funcion-abortada +echo fin"#; + assert_eq!(salida(g), "or-salva\nen-funcion-tras-false\nfin\n"); + // …y sigue abortando cuando toca + let (rc, out, _) = correr("set -e\necho antes\nfalse\necho despues"); + assert_eq!((rc, out.as_str()), (1, "antes\n")); +} + +/// El orden de las expansiones (§2.6): lo entrecomillado no se divide ni se globea, y una barra +/// invertida que SALE de una expansión es un carácter literal, no un escape. +#[test] +fn orden_de_expansiones() { + assert_eq!(salida(r#"echo "/dev/nul*""#), "/dev/nul*\n"); + assert_eq!(salida("echo /dev/nul*"), "/dev/null\n"); + assert_eq!(salida("echo /no-existe-jamas*"), "/no-existe-jamas*\n"); + assert_eq!(salida(r#"V=hola; echo [$(echo \\$V)]"#), "[\\hola]\n"); + assert_eq!(salida("IFS=:; x=a:b:c; set -- $x; echo $#"), "3\n"); + assert_eq!(salida(r#"v=arch.tar.gz; echo ${v%%.*} ${v##*.} ${#v}"#), "arch gz 11\n"); +} + +/// El idioma de las 23 cards de arje, entero: `test … || { echo >&2; exit 78; }; exec …`. +#[test] +fn el_idioma_de_las_cards_de_arje() { + let (rc, out, err) = correr( + "test -f /no-existe || { echo 'falta la config' >&2; exit 78; }; exec /bin/true", + ); + assert_eq!(rc, 78); + assert_eq!(out, ""); + assert_eq!(err, "falta la config\n"); + // el camino bueno: el `exec` REEMPLAZA el proceso + let g = "echo $$ > /dev/null; exec /bin/echo reemplazado; echo NO-DEBERIA-LLEGAR"; + assert_eq!(salida(g), "reemplazado\n"); +} + +/// El bucle de espera de las cards de compatibilidad (`arje-polkit-compat`, `arje-logind-compat`). +#[test] +fn bucle_de_espera_con_aritmetica() { + let g = "i=0; while [ ! -S /no-existe ] && [ $i -lt 5 ]; do i=$((i+1)); done; echo $i"; + assert_eq!(salida(g), "5\n"); +} + +/// Los `/init` empotrados de las imágenes: `for`, `case`, funciones, aquí-documentos. +#[test] +fn lo_que_usan_los_init_empotrados() { + let g = r#"say() { echo "==> $*"; } +say hola +for d in proc sys dev raro; do + case $d in proc|sys|dev) echo "salto $d" ;; *) echo "monto $d" ;; esac +done +cat < hola\nsalto proc\nsalto sys\nsalto dev\nmonto raro\nliteral raro\nsin expandir $d\n" + ); +} + +/// Un error de expansión en un shell no interactivo ABORTA con 2, no sigue con 1. +#[test] +fn errores_de_expansion_abortan_con_2() { + let (rc, out, _) = correr("echo antes; echo ${nada:?falta}; echo despues"); + assert_eq!((rc, out.as_str()), (2, "antes\n")); + let (rc, _, _) = correr("set -u; echo $NODEFINIDA"); + assert_eq!(rc, 2); + let (rc, _, _) = correr("echo [`echo \"`]"); + assert_eq!(rc, 2, "una comilla sin cerrar en una sustitución es error de sintaxis"); +} + +/// `$(( ))`: la división por cero es un error, no un cero silencioso. +#[test] +fn division_por_cero_no_es_cero() { + let (rc, out, _) = correr("echo $((1/0)); echo despues"); + assert_eq!(rc, 2); + assert_eq!(out, ""); +} diff --git a/scripts/fixtures/sh-casos/08-trap-exit-y-estado.sh b/scripts/fixtures/sh-casos/08-trap-exit-y-estado.sh index d93ff6b4..655a789a 100644 --- a/scripts/fixtures/sh-casos/08-trap-exit-y-estado.sh +++ b/scripts/fixtures/sh-casos/08-trap-exit-y-estado.sh @@ -5,3 +5,8 @@ trap 'echo "trap-exit rc=$?"' EXIT ( trap 'echo "trap-subshell"' EXIT; true ) false echo "ultimo=$?" +( trap 'echo "[heredado-no-debe-sonar]"' EXIT; true ) >/dev/null 2>&1 +# Y la otra mitad, que es la que POSIX §2.12 manda al revés: el trap HEREDADO se resetea al entrar +# al subshell, así que el de arriba NO tiene que sonar acá. simi lo tenía mal en su primera versión +# —lo disparaba— y el banco no lo veía porque ningún caso lo probaba. Ahora sí. +( echo "subshell sin trap propio" )