//! Parser de descenso recursivo sobre los tokens. La gramática es la de POSIX.1-2024 §2.10, //! recortada a lo que el producto usa (medido en `docs/plan-botar-busybox.md`): listas, tuberías, //! `&&`/`||`, `if`, `while`/`until`, `for`, `case`, funciones, grupos, subshells y redirecciones. //! //! Lo que NO hay, a propósito: alias (§2.3.1), `select`, `[[ ]]`, arrays. No son POSIX o no los usa //! nadie en el árbol — y un shell que es la raíz de confianza del arranque se mide por lo que NO //! trae. use crate::ast::*; use crate::lexer::{literal_desnudo, tokenizar, Tok}; pub struct Parser { ts: Vec, i: usize, cuerpos: Vec<(String, bool)>, } const RESERVADAS: &[&str] = &[ "if", "then", "elif", "else", "fi", "while", "until", "do", "done", "for", "in", "case", "esac", "{", "}", "!", ]; pub fn parsear(entrada: &str) -> Result { let (ts, cuerpos) = tokenizar(entrada)?; let mut p = Parser { ts, i: 0, cuerpos }; let l = p.lista(&[])?; p.saltos(); if p.i < p.ts.len() { return Err(format!("sobra entrada en el token {:?}", p.ts[p.i])); } Ok(l) } impl Parser { fn ver(&self) -> Option<&Tok> { self.ts.get(self.i) } fn es_op(&self, op: &str) -> bool { matches!(self.ver(), Some(Tok::Op(o)) if o == op) } fn es_palabra(&self, w: &str) -> bool { matches!(self.ver(), Some(Tok::Pal(p)) if literal_desnudo(p) == Some(w)) } fn comer_palabra(&mut self, w: &str) -> bool { if self.es_palabra(w) { self.i += 1; true } else { false } } fn exigir_palabra(&mut self, w: &str) -> Result<(), String> { if self.comer_palabra(w) { Ok(()) } else { Err(format!("falta `{w}` (hay {:?})", self.ver())) } } /// Salta saltos de línea (y los `;` sueltos donde la gramática los admite). fn saltos(&mut self) { while matches!(self.ver(), Some(Tok::Nueva)) { self.i += 1; } } // ───────────────────────────────────────────── listas /// Una lista, parando ante cualquiera de las palabras reservadas de `hasta` (sin consumirla). fn lista(&mut self, hasta: &[&str]) -> Result { let mut out: Lista = Vec::new(); loop { self.saltos(); if self.ver().is_none() || self.es_op(";;") || self.es_op(")") { break; } if hasta.iter().any(|w| self.es_palabra(w)) { break; } let yo = self.yo()?; // El separador se CONSUME acá, así que la decisión de seguir no puede volver a // mirarlo: hay que recordar que estuvo. Con el `;` ya comido, mirar el token actual // daba «falta `}`» en el idioma mismo de las cards (`…; exit 78; }`). let mut hubo_separador = false; let fin = if self.es_op("&") { self.i += 1; hubo_separador = true; Fin::SegundoPlano } else if self.es_op(";") { self.i += 1; hubo_separador = true; Fin::Secuencial } else { if matches!(self.ver(), Some(Tok::Nueva)) { hubo_separador = true; // lo come el `saltos()` de la vuelta siguiente } Fin::Secuencial }; out.push((yo, fin)); if !hubo_separador { break; } } if out.is_empty() { return Err("lista vacía".into()); } Ok(out) } fn yo(&mut self) -> Result { let primera = self.tuberia()?; let mut resto = Vec::new(); loop { let enlace = if self.es_op("&&") { Enlace::Y } else if self.es_op("||") { Enlace::O } else { break; }; self.i += 1; self.saltos(); resto.push((enlace, self.tuberia()?)); } Ok(YO { primera, resto }) } fn tuberia(&mut self) -> Result { let negada = self.comer_palabra("!"); let mut ordenes = vec![self.orden()?]; while self.es_op("|") { self.i += 1; self.saltos(); ordenes.push(self.orden()?); } Ok(Tuberia { negada, ordenes }) } // ───────────────────────────────────────────── órdenes fn orden(&mut self) -> Result { if self.es_palabra("if") { return self.si(); } if self.es_palabra("while") || self.es_palabra("until") { return self.bucle(); } if self.es_palabra("for") { return self.para(); } if self.es_palabra("case") { return self.caso(); } if self.es_palabra("{") { self.i += 1; let cuerpo = self.lista(&["}"])?; self.exigir_palabra("}")?; let redirs = self.redirs()?; return Ok(Orden::Grupo { cuerpo, redirs }); } if self.es_op("(") { self.i += 1; let cuerpo = self.lista(&[])?; if !self.es_op(")") { return Err(format!("falta `)` (hay {:?})", self.ver())); } self.i += 1; let redirs = self.redirs()?; return Ok(Orden::Subshell { cuerpo, redirs }); } // ¿definición de función? `nombre ( )` if let Some(Tok::Pal(p)) = self.ver() { if let Some(nombre) = literal_desnudo(p) { let nombre = nombre.to_string(); if !RESERVADAS.contains(&nombre.as_str()) && matches!(self.ts.get(self.i + 1), Some(Tok::Op(o)) if o == "(") && matches!(self.ts.get(self.i + 2), Some(Tok::Op(o)) if o == ")") { self.i += 3; self.saltos(); let cuerpo = self.orden()?; return Ok(Orden::Funcion { nombre, cuerpo: Box::new(cuerpo) }); } } } self.simple() } fn si(&mut self) -> Result { self.exigir_palabra("if")?; let mut ramas = Vec::new(); let cond = self.lista(&["then"])?; self.exigir_palabra("then")?; let cuerpo = self.lista(&["elif", "else", "fi"])?; ramas.push((cond, cuerpo)); let mut sino = None; loop { if self.comer_palabra("elif") { let c = self.lista(&["then"])?; self.exigir_palabra("then")?; let b = self.lista(&["elif", "else", "fi"])?; ramas.push((c, b)); continue; } if self.comer_palabra("else") { sino = Some(self.lista(&["fi"])?); } break; } self.exigir_palabra("fi")?; let redirs = self.redirs()?; Ok(Orden::Si { ramas, sino, redirs }) } fn bucle(&mut self) -> Result { let hasta = self.es_palabra("until"); self.i += 1; let cond = self.lista(&["do"])?; self.exigir_palabra("do")?; let cuerpo = self.lista(&["done"])?; self.exigir_palabra("done")?; let redirs = self.redirs()?; Ok(Orden::Bucle { hasta, cond, cuerpo, redirs }) } fn para(&mut self) -> Result { self.exigir_palabra("for")?; let var = match self.ver() { Some(Tok::Pal(p)) => match literal_desnudo(p) { Some(n) if !n.is_empty() => n.to_string(), _ => return Err("`for` sin nombre de variable".into()), }, _ => return Err("`for` sin nombre de variable".into()), }; self.i += 1; self.saltos(); let palabras = if self.comer_palabra("in") { let mut ps = Vec::new(); while let Some(Tok::Pal(p)) = self.ver() { if literal_desnudo(p) == Some("do") { break; } ps.push(p.clone()); self.i += 1; } if self.es_op(";") { self.i += 1; } self.saltos(); Some(ps) } else { if self.es_op(";") { self.i += 1; } self.saltos(); None }; self.exigir_palabra("do")?; let cuerpo = self.lista(&["done"])?; self.exigir_palabra("done")?; let redirs = self.redirs()?; Ok(Orden::Para { var, palabras, cuerpo, redirs }) } fn caso(&mut self) -> Result { self.exigir_palabra("case")?; let sujeto = match self.ver() { Some(Tok::Pal(p)) => p.clone(), t => return Err(format!("`case` sin sujeto (hay {t:?})")), }; self.i += 1; self.saltos(); self.exigir_palabra("in")?; let mut ramas = Vec::new(); loop { self.saltos(); if self.comer_palabra("esac") { break; } if self.ver().is_none() { return Err("`case` sin `esac`".into()); } // patrones: pal ('|' pal)* ')' if self.es_op("(") { self.i += 1; } let mut patrones = Vec::new(); loop { match self.ver() { Some(Tok::Pal(p)) => { patrones.push(p.clone()); self.i += 1; } t => return Err(format!("patrón de `case` inválido: {t:?}")), } if self.es_op("|") { self.i += 1; continue; } break; } if !self.es_op(")") { return Err(format!("falta `)` en el patrón de `case` (hay {:?})", self.ver())); } self.i += 1; self.saltos(); let cuerpo = if self.es_op(";;") || self.es_palabra("esac") { Vec::new() } else { self.lista(&["esac"])? }; ramas.push((patrones, cuerpo)); self.saltos(); if self.es_op(";;") { self.i += 1; } } let redirs = self.redirs()?; Ok(Orden::Caso { sujeto, ramas, redirs }) } fn simple(&mut self) -> Result { let mut asignaciones = Vec::new(); let mut palabras: Vec = Vec::new(); let mut redirs = Vec::new(); loop { if let Some(r) = self.redir()? { redirs.push(r); continue; } let Some(Tok::Pal(p)) = self.ver() else { break }; let p = p.clone(); if palabras.is_empty() { if let Some(a) = asignacion(&p) { asignaciones.push(a); self.i += 1; continue; } if let Some(w) = literal_desnudo(&p) { if RESERVADAS.contains(&w) { break; } } } palabras.push(p); self.i += 1; } if asignaciones.is_empty() && palabras.is_empty() && redirs.is_empty() { return Err(format!("orden vacía en {:?}", self.ver())); } Ok(Orden::Simple { asignaciones, palabras, redirs }) } fn redirs(&mut self) -> Result, String> { let mut out = Vec::new(); while let Some(r) = self.redir()? { out.push(r); } Ok(out) } fn redir(&mut self) -> Result, String> { let (fd_explicito, salto) = match self.ver() { Some(Tok::IoNum(n)) => (Some(*n), 1), _ => (None, 0), }; let op = match self.ts.get(self.i + salto) { Some(Tok::Op(o)) if matches!(o.as_str(), ">" | ">>" | "<" | "<<" | ">&" | "<&" | "<>") => { o.clone() } _ => return Ok(None), }; self.i += salto + 1; if op == "<<" { let id = match self.ver() { Some(Tok::AquiDoc(id)) => *id, t => return Err(format!("aquí-documento mal formado: {t:?}")), }; self.i += 1; let (texto, expande) = self.cuerpos[id].clone(); let cuerpo = if expande { cuerpo_expandible(&texto)? } else { vec![crate::ast::Seg::Lit { texto, comillado: true }] }; return Ok(Some(Redir::AquiDoc { fd: fd_explicito.unwrap_or(0), cuerpo, expande })); } let destino = match self.ver() { Some(Tok::Pal(p)) => p.clone(), t => return Err(format!("redirección `{op}` sin destino: {t:?}")), }; self.i += 1; let r = match op.as_str() { ">" => Redir::Salida { fd: fd_explicito.unwrap_or(1), destino, append: false }, ">>" => Redir::Salida { fd: fd_explicito.unwrap_or(1), destino, append: true }, "<" => Redir::Entrada { fd: fd_explicito.unwrap_or(0), origen: destino }, "<>" => Redir::LecturaEscritura { fd: fd_explicito.unwrap_or(0), ruta: destino }, ">&" | "<&" => { let escritura = op == ">&"; let dd = if literal_desnudo(&destino) == Some("-") { DupDestino::Cerrar } else { DupDestino::Fd(destino) }; Redir::Duplica { fd: fd_explicito.unwrap_or(if escritura { 1 } else { 0 }), otro: dd, escritura, } } _ => unreachable!(), }; Ok(Some(r)) } } /// El cuerpo de un aquí-documento que expande: se lexa como si estuviera entre comillas dobles /// (expansiones sí, `"` no especial). fn cuerpo_expandible(texto: &str) -> Result { let escapado: String = texto.replace('"', "\\\""); let (ts, _) = tokenizar(&format!("\"{escapado}\""))?; for t in ts { if let Tok::Pal(p) = t { return Ok(p); } } Ok(vec![crate::ast::Seg::Lit { texto: String::new(), comillado: true }]) } /// `NOMBRE=valor` al principio de una orden simple. fn asignacion(p: &Palabra) -> Option { let Seg::Lit { texto, comillado: false } = &p[0] else { return None }; let pos = texto.find('=')?; let nombre = &texto[..pos]; if nombre.is_empty() || !nombre.chars().next().map(|c| c == '_' || c.is_ascii_alphabetic())? { return None; } if !nombre.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) { return None; } let mut valor: Palabra = Vec::new(); let resto = &texto[pos + 1..]; if !resto.is_empty() { valor.push(Seg::Lit { texto: resto.to_string(), comillado: false }); } valor.extend(p[1..].iter().cloned()); if valor.is_empty() { valor.push(Seg::Lit { texto: String::new(), comillado: true }); } Some(Asignacion { nombre: nombre.to_string(), valor }) } #[cfg(test)] mod pruebas { use super::*; #[test] fn el_idioma_de_las_cards() { // La forma exacta que usan 18 de las 23 cards con shell. let l = parsear( "test -f /etc/x.conf || { echo 'falta' >&2; exit 78; }; exec /usr/bin/x --flag", ) .unwrap(); assert_eq!(l.len(), 2); } #[test] fn bucle_de_espera_de_las_cards_de_compat() { parsear( "i=0; while [ ! -S /run/dbus/system_bus_socket ] && [ $i -lt 100 ]; \ do i=$((i+1)); sleep 0.2; done; exec /usr/bin/arje-polkit-compat", ) .unwrap(); } #[test] fn el_for_y_el_case_del_montar_trabajo() { parsear( "for h in /home/*; do u=$(basename \"$h\"); uid=$(id -u \"$u\") || continue; \ mkdir -p \"/run/user/$uid\"; done; case $x in a|b) echo si ;; *) echo no ;; esac", ) .unwrap(); } #[test] fn funcion_y_subshell_y_tuberia() { parsear("f() { echo hola; }; ( f | cat ) > /dev/null 2>&1").unwrap(); } #[test] fn palabra_reservada_entrecomillada_no_es_reservada() { // `"if"` es una orden, no un condicional. let l = parsear("\"if\" x").unwrap(); match &l[0].0.primera.ordenes[0] { Orden::Simple { palabras, .. } => assert_eq!(palabras.len(), 2), o => panic!("{o:?}"), } } #[test] fn asignaciones_solo_antes_de_la_orden() { let l = parsear("A=1 B=2 cmd C=3").unwrap(); match &l[0].0.primera.ordenes[0] { Orden::Simple { asignaciones, palabras, .. } => { assert_eq!(asignaciones.len(), 2); assert_eq!(palabras.len(), 2); // `cmd` y `C=3` (que es un argumento) } o => panic!("{o:?}"), } } }