simi: el /bin/sh propio del producto — 4448 líneas, sólo libc, y CERO divergencias contra el ash
«simi» es boca/lengua en quechua. Es el sh que el frente C necesitaba: busybox está en la raíz de confianza del arranque (STAGE1_COMPONENTS, ATTEST_PATHS) y en el exec de cada card, y el candidato ajeno que se midió —brush 0.4.0— pesa 6,9 MB, arranca 4x más lento que el ash y trae dos divergencias medidas (#1394, #1396). Alcance MEDIDO, no supuesto: las 23 cards con shell (8 builtins, 13 construcciones), los 21 /init empotrados de las imágenes (+ if, funciones, case, aquí-documentos) y el POSIX de takana-live-install.sh. Resultado del banco diferencial, 159 casos contra el ash de busybox 1.36.1 (10 fixtures de regresión, 107 de tortura de comillas, 23 fragmentos de cards, 19 /init empotrados): simi DIVERGE=0 bash DIVERGE=8 brush DIVERGE=35 Incluye el bucle REAL de config.status que genera cualquier configure de autotools — el que tumbó a brush. 499 KB release, 1,4x la latencia del ash (brush: 4,1x). 47 pruebas propias en verde. Seis bugs que el banco destapó, y ninguno se veía sin un control al lado: 1. El lexer fundía lo entrecomillado con lo desnudo, así que x="a b" se ejecutaba como ORDEN y el error decía «no encontrado» — mandando a buscar al lugar equivocado. Ahora el comillado es por carácter. 2. Usaba la barra invertida como marca interna y la quitaba al final, así que una barra PRODUCIDA por una sustitución se perdía. La quita de comillas es sobre las comillas de la palabra, nunca sobre el resultado de una expansión: hace falta una máscara de protección, no un escape dentro del texto. 3. set -e no se suspendía dentro del cuerpo de una función llamada desde una condición. Ahora es un contador dinámico, que entra solo donde corresponde. 4. Los delimitadores de la sustitución de orden y de ${ } no respetaban la barra invertida: un paréntesis escapado cerraba la sustitución. 5. Las clases entre corchetes no entendían la barra invertida — y la del config.status de libtool es literalmente una clase con cuatro escapes. 6. Dentro de comillas dobles el cuerpo de un backtick sigue entre comillas, así que la barra también se elimina ante la comilla doble. Y uno que el banco NO vio y sí vio la prueba de regresión: el trap EXIT HEREDADO se disparaba en el subshell. POSIX §2.12 manda resetearlo — es la mitad complementaria del #1396 de brush, y al revés. Los dos oráculos hacen falta; el caso quedó agregado al fixture 08.
This commit is contained in:
Generated
+7
@@ -1152,6 +1152,13 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simi"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ members = [
|
|||||||
"crates/takana-cli",
|
"crates/takana-cli",
|
||||||
"crates/hammerd",
|
"crates/hammerd",
|
||||||
"crates/netup",
|
"crates/netup",
|
||||||
|
"crates/simi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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<Num, String> {
|
||||||
|
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::<String>()));
|
||||||
|
}
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct P<'a, 'b> {
|
||||||
|
cs: Vec<char>,
|
||||||
|
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::<Vec<_>>()[..]) {
|
||||||
|
// 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::<Vec<_>>()[..])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ternario(&mut self) -> Result<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
let mut a = self.y_bits()?;
|
||||||
|
while self.mira("^") {
|
||||||
|
self.come("^");
|
||||||
|
a ^= self.y_bits()?;
|
||||||
|
}
|
||||||
|
Ok(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y_bits(&mut self) -> Result<Num, String> {
|
||||||
|
let mut a = self.igualdad()?;
|
||||||
|
while self.mira("&") && !self.mira("&&") {
|
||||||
|
self.come("&");
|
||||||
|
a &= self.igualdad()?;
|
||||||
|
}
|
||||||
|
Ok(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn igualdad(&mut self) -> Result<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num, String> {
|
||||||
|
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<Num> {
|
||||||
|
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<Num, String> {
|
||||||
|
let mapa: RefCell<HashMap<String, String>> =
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Seg>, 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<Seg>;
|
||||||
|
|
||||||
|
#[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<Asignacion>,
|
||||||
|
palabras: Vec<Palabra>,
|
||||||
|
redirs: Vec<Redir>,
|
||||||
|
},
|
||||||
|
/// `{ …; }`
|
||||||
|
Grupo { cuerpo: Lista, redirs: Vec<Redir> },
|
||||||
|
/// `( … )`
|
||||||
|
Subshell { cuerpo: Lista, redirs: Vec<Redir> },
|
||||||
|
Si {
|
||||||
|
ramas: Vec<(Lista, Lista)>, // (condición, cuerpo)
|
||||||
|
sino: Option<Lista>,
|
||||||
|
redirs: Vec<Redir>,
|
||||||
|
},
|
||||||
|
/// `while`/`until`
|
||||||
|
Bucle { hasta: bool, cond: Lista, cuerpo: Lista, redirs: Vec<Redir> },
|
||||||
|
Para {
|
||||||
|
var: String,
|
||||||
|
/// `None` = `for x; do` ⇒ itera sobre `"$@"`
|
||||||
|
palabras: Option<Vec<Palabra>>,
|
||||||
|
cuerpo: Lista,
|
||||||
|
redirs: Vec<Redir>,
|
||||||
|
},
|
||||||
|
Caso {
|
||||||
|
sujeto: Palabra,
|
||||||
|
ramas: Vec<(Vec<Palabra>, Lista)>,
|
||||||
|
redirs: Vec<Redir>,
|
||||||
|
},
|
||||||
|
Funcion { nombre: String, cuerpo: Box<Orden> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Tuberia {
|
||||||
|
/// `! cmd | cmd`
|
||||||
|
pub negada: bool,
|
||||||
|
pub ordenes: Vec<Orden>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)>;
|
||||||
@@ -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<String> = 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<String> {
|
||||||
|
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<usize, String> {
|
||||||
|
let cs: Vec<char> = 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<char> = 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::<i64>().unwrap_or(0)),
|
||||||
|
'X' => format!("{:X}", arg.trim().parse::<i64>().unwrap_or(0)),
|
||||||
|
'o' => format!("{:o}", arg.trim().parse::<i64>().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::<usize>() {
|
||||||
|
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::<i32>() {
|
||||||
|
if let Some((nombre, _)) = SENALES.iter().find(|(_, v)| *v == n) {
|
||||||
|
return (*nombre).to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
limpio
|
||||||
|
}
|
||||||
|
|
||||||
|
fn numero_de_senal(nombre: &str) -> Option<i32> {
|
||||||
|
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<String> =
|
||||||
|
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<char> = 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<bool, String> {
|
||||||
|
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<bool, String> {
|
||||||
|
// 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<usize> {
|
||||||
|
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<bool, String> {
|
||||||
|
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<Option<bool>, String> {
|
||||||
|
let num = |s: &str| -> Result<i64, String> {
|
||||||
|
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 |");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <daemon>'`: 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<T>(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<i32> = None;
|
||||||
|
let mut pids: Vec<i32> = 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<String> = 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<Flujo> {
|
||||||
|
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<String> = 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<F: FnOnce(&mut Shell) -> 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<Vec<Guardado>, 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<Guardado>) {
|
||||||
|
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<i32, String> {
|
||||||
|
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<i32, String> {
|
||||||
|
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<CString> = argv.iter().filter_map(|a| CString::new(a.as_str()).ok()).collect();
|
||||||
|
let c_env: Vec<CString> = 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<String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
// Las variables de la expresión pueden traer expansiones: `$(( $x + 1 ))`.
|
||||||
|
let expr = expandir_texto(sh, expr)?;
|
||||||
|
let vars: std::cell::RefCell<Vec<(String, String)>> = 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<String, String> {
|
||||||
|
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])
|
||||||
|
}
|
||||||
@@ -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<Vec<String>, 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<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<Vec<Trozo>, String> {
|
||||||
|
let mut out: Vec<Trozo> = 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<Trozo>,
|
||||||
|
) -> 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<char> = valor.chars().collect();
|
||||||
|
let mut mejor: Option<usize> = None;
|
||||||
|
let rango: Vec<usize> = (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<Trozo>, 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<String> {
|
||||||
|
if ifs.is_empty() {
|
||||||
|
return vec![s.to_string()];
|
||||||
|
}
|
||||||
|
let blancos: Vec<char> = ifs.chars().filter(|c| matches!(c, ' ' | '\t' | '\n')).collect();
|
||||||
|
let otros: Vec<char> = ifs.chars().filter(|c| !matches!(c, ' ' | '\t' | '\n')).collect();
|
||||||
|
let mut out: Vec<String> = Vec::new();
|
||||||
|
let mut act = String::new();
|
||||||
|
let mut i = 0;
|
||||||
|
let cs: Vec<char> = 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<String> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::<Vec<_>>(), &texto.chars().collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
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<dyn Fn(char) -> bool>, usize)> {
|
||||||
|
let mut i = 1;
|
||||||
|
let negada = matches!(p.get(i), Some('!') | Some('^'));
|
||||||
|
if negada {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
let mut sueltos: Vec<char> = 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<char> = 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<String> {
|
||||||
|
let absoluto = patron.starts_with('/');
|
||||||
|
let partes: Vec<&str> = patron.split('/').filter(|p| !p.is_empty()).collect();
|
||||||
|
let mut actuales: Vec<String> = 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<String> = 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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<char>,
|
||||||
|
i: usize,
|
||||||
|
toks: Vec<Tok>,
|
||||||
|
/// `(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<Tok>, 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<char> {
|
||||||
|
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<String> {
|
||||||
|
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<Palabra, String> {
|
||||||
|
let mut segs: Vec<Seg> = 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<bool> = 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<Seg, String> {
|
||||||
|
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<Seg, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<String, String> {
|
||||||
|
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<i32> {
|
||||||
|
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 <<EOF\nuno $V\nEOF\ncat <<'FIN'\ndos $V\nFIN\n").unwrap();
|
||||||
|
assert_eq!(cuerpos.len(), 2);
|
||||||
|
assert_eq!(cuerpos[0], ("uno $V\n".to_string(), true));
|
||||||
|
assert_eq!(cuerpos[1], ("dos $V\n".to_string(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn aqui_doc_guion_come_tabulaciones() {
|
||||||
|
let (_, cuerpos) = tokenizar("cat <<-EOF\n\t\tsin-tab\n\tEOF\n").unwrap();
|
||||||
|
assert_eq!(cuerpos[0].0, "sin-tab\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn descriptor_pegado_a_redireccion() {
|
||||||
|
let (ts, _) = tokenizar("cmd 2>/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:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> = 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<String> = None;
|
||||||
|
let mut fichero: Option<String> = 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<String> = 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)
|
||||||
|
}
|
||||||
@@ -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<Tok>,
|
||||||
|
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<Lista, String> {
|
||||||
|
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<Lista, String> {
|
||||||
|
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<YO, String> {
|
||||||
|
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<Tuberia, String> {
|
||||||
|
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<Orden, String> {
|
||||||
|
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<Orden, String> {
|
||||||
|
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<Orden, String> {
|
||||||
|
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<Orden, String> {
|
||||||
|
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<Orden, String> {
|
||||||
|
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<Orden, String> {
|
||||||
|
let mut asignaciones = Vec::new();
|
||||||
|
let mut palabras: Vec<Palabra> = 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<Vec<Redir>, String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
while let Some(r) = self.redir()? {
|
||||||
|
out.push(r);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redir(&mut self) -> Result<Option<Redir>, 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<Palabra, String> {
|
||||||
|
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<Asignacion> {
|
||||||
|
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:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, (String, bool)>,
|
||||||
|
pub funcs: HashMap<String, Orden>,
|
||||||
|
/// `$1`, `$2`…
|
||||||
|
pub params: Vec<String>,
|
||||||
|
/// `$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<String, String>,
|
||||||
|
/// PID del último `&`
|
||||||
|
pub ultimo_fondo: Option<i32>,
|
||||||
|
/// 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<Vec<(String, Option<(String, bool)>)>>,
|
||||||
|
/// 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<String> {
|
||||||
|
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::<usize>() {
|
||||||
|
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<String> {
|
||||||
|
let mut v: Vec<String> = 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <<FIN
|
||||||
|
literal $d
|
||||||
|
FIN
|
||||||
|
cat <<'CRUDO'
|
||||||
|
sin expandir $d
|
||||||
|
CRUDO"#;
|
||||||
|
assert_eq!(
|
||||||
|
salida(g),
|
||||||
|
"==> 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, "");
|
||||||
|
}
|
||||||
@@ -5,3 +5,8 @@ trap 'echo "trap-exit rc=$?"' EXIT
|
|||||||
( trap 'echo "trap-subshell"' EXIT; true )
|
( trap 'echo "trap-subshell"' EXIT; true )
|
||||||
false
|
false
|
||||||
echo "ultimo=$?"
|
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" )
|
||||||
|
|||||||
Reference in New Issue
Block a user