//! Ejecución: redirecciones, tuberías, subshells, funciones y el `fork`/`execve`. //! //! Dos invariantes que este fichero sostiene a propósito, porque las dos se midieron rotas en el //! candidato que se evaluó antes (ver `docs/plan-botar-busybox.md`): //! //! 1. **El `trap EXIT` de un subshell CORRE.** No sólo el del shell de arriba: también el de //! `( … )`, el de `$( … )` y el de un componente de tubería. Es reubeno/brush#1396, y falla en //! silencio: el script termina con `rc=0` y la limpieza no se hizo. //! 2. **`exec` reemplaza el proceso.** Las cards de arje son `sh -c '…; exec '`: si el //! shell forkea, arje supervisa al shell y no al daemon, y el reinicio apunta al proceso //! equivocado. //! //! Sobre `fork` en Rust: este proceso es **de un solo hilo** a propósito (ni tokio ni threads), que //! es lo que hace legítimo el `fork` — igual que dash en C. Entre el `fork` y el `execve` el hijo //! no toma locks ajenos. use std::ffi::CString; use std::io::{Read, Write}; use std::os::unix::io::{AsRawFd, FromRawFd}; use crate::ast::*; use crate::expand; use crate::parser::parsear; use crate::shell::{Flujo, Shell, LLEGO_SENAL}; // ───────────────────────────────────────────────────────── listas y enlaces /// Corre `f` con `set -e` suspendido. Ver `Shell::errexit_suspendido`. fn suspendiendo_errexit(sh: &mut Shell, f: impl FnOnce(&mut Shell) -> T) -> T { sh.errexit_suspendido += 1; let r = f(sh); sh.errexit_suspendido -= 1; r } /// Lo que hay que hacer al ENTRAR en un subshell, en los cuatro sitios que forkean. /// /// POSIX §2.12: los traps heredados que no estén ignorados se ponen en su acción por defecto. O /// sea que un subshell **no** corre el `trap EXIT` del padre — sólo el que él mismo ponga. Las dos /// mitades son distintas y confundirlas es fácil: la otra mitad (que el trap PROPIO del subshell sí /// corre) es reubeno/brush#1396. Acá se rompió la primera versión de simi, al revés que brush, y no /// lo vio el banco diferencial sino la prueba de regresión — los dos oráculos hacen falta. fn entrar_en_subshell(sh: &mut Shell) { sh.es_subshell = true; sh.traps.clear(); } fn debe_abortar(sh: &Shell) -> bool { sh.opts.errexit && sh.errexit_suspendido == 0 && sh.estado != 0 } pub fn correr_lista(sh: &mut Shell, l: &Lista) -> Flujo { for (yo, fin) in l { atender_senales(sh); match fin { Fin::SegundoPlano => { lanzar_al_fondo(sh, yo); } Fin::Secuencial => { let f = correr_yo(sh, yo, false); if f != Flujo::Normal { return f; } } } } Flujo::Normal } /// `permite_fallo`: estamos en una condición (`if`, `while`, lado izquierdo de `&&`, `!`), donde /// un estado distinto de cero **no** dispara `set -e`. pub fn correr_yo(sh: &mut Shell, yo: &YO, permite_fallo: bool) -> Flujo { let cuerpo = |sh: &mut Shell| -> Flujo { let hay_resto = !yo.resto.is_empty(); // La primera de una cadena `&&`/`||` es una CONDICIÓN: su fallo no aborta. let f = if hay_resto { suspendiendo_errexit(sh, |sh| correr_tuberia(sh, &yo.primera)) } else { correr_tuberia(sh, &yo.primera) }; if f != Flujo::Normal { return f; } for (n, (enlace, t)) in yo.resto.iter().enumerate() { let ultima = n + 1 == yo.resto.len(); let saltar = match enlace { Enlace::Y => sh.estado != 0, Enlace::O => sh.estado == 0, }; if saltar { continue; } let f = if ultima { correr_tuberia(sh, t) } else { suspendiendo_errexit(sh, |sh| correr_tuberia(sh, t)) }; if f != Flujo::Normal { return f; } } if debe_abortar(sh) { return Flujo::Exit(sh.estado); } Flujo::Normal }; if permite_fallo { suspendiendo_errexit(sh, cuerpo) } else { cuerpo(sh) } } fn correr_tuberia(sh: &mut Shell, t: &Tuberia) -> Flujo { if t.ordenes.len() == 1 { // `! cmd` es una condición: el fallo de `cmd` no aborta. let f = if t.negada { suspendiendo_errexit(sh, |sh| correr_orden(sh, &t.ordenes[0])) } else { correr_orden(sh, &t.ordenes[0]) }; if t.negada { sh.estado = (sh.estado == 0) as i32; } if debe_abortar(sh) && f == Flujo::Normal { return Flujo::Exit(sh.estado); } return f; } let n = t.ordenes.len(); let mut entrada: Option = None; let mut pids: Vec = Vec::new(); for (i, orden) in t.ordenes.iter().enumerate() { let (lectura, escritura) = if i + 1 < n { let mut fds = [0i32; 2]; if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 { sh.error("no pude crear la tubería"); sh.estado = 1; return Flujo::Normal; } (Some(fds[0]), Some(fds[1])) } else { (None, None) }; let pid = unsafe { libc::fork() }; if pid == 0 { // hijo if let Some(e) = entrada { unsafe { libc::dup2(e, 0) }; unsafe { libc::close(e) }; } if let Some(w) = escritura { unsafe { libc::dup2(w, 1) }; unsafe { libc::close(w) }; } if let Some(r) = lectura { unsafe { libc::close(r) }; } entrar_en_subshell(sh); let f = correr_orden(sh, orden); let estado = match f { Flujo::Exit(c) => c, _ => sh.estado, }; salir_de_subshell(sh, estado); } if let Some(e) = entrada { unsafe { libc::close(e) }; } if let Some(w) = escritura { unsafe { libc::close(w) }; } entrada = lectura; pids.push(pid); } let mut ultimo = 0; for pid in pids { ultimo = esperar(pid); } sh.estado = if t.negada { (ultimo == 0) as i32 } else { ultimo }; if debe_abortar(sh) { return Flujo::Exit(sh.estado); } Flujo::Normal } fn lanzar_al_fondo(sh: &mut Shell, yo: &YO) { let pid = unsafe { libc::fork() }; if pid == 0 { entrar_en_subshell(sh); let f = correr_yo(sh, yo, true); let estado = match f { Flujo::Exit(c) => c, _ => sh.estado, }; salir_de_subshell(sh, estado); } sh.ultimo_fondo = Some(pid); sh.estado = 0; } // ───────────────────────────────────────────────────────── órdenes fn correr_orden(sh: &mut Shell, o: &Orden) -> Flujo { match o { Orden::Funcion { nombre, cuerpo } => { sh.funcs.insert(nombre.clone(), (**cuerpo).clone()); sh.estado = 0; Flujo::Normal } Orden::Simple { asignaciones, palabras, redirs } => { correr_simple(sh, asignaciones, palabras, redirs) } Orden::Grupo { cuerpo, redirs } => con_redirs(sh, redirs, |sh| correr_lista(sh, cuerpo)), Orden::Subshell { cuerpo, redirs } => { let pid = unsafe { libc::fork() }; if pid == 0 { entrar_en_subshell(sh); let f = match aplicar(sh, redirs) { Ok(_) => correr_lista(sh, cuerpo), Err(e) => { sh.error(&e); sh.estado = 1; Flujo::Normal } }; let estado = match f { Flujo::Exit(c) => c, _ => sh.estado, }; salir_de_subshell(sh, estado); } sh.estado = esperar(pid); Flujo::Normal } Orden::Si { ramas, sino, redirs } => con_redirs(sh, redirs, |sh| { for (cond, cuerpo) in ramas { let f = correr_lista_condicion(sh, cond); if let Some(f) = f { return f; } if sh.estado == 0 { return correr_lista(sh, cuerpo); } } match sino { Some(l) => correr_lista(sh, l), None => { sh.estado = 0; Flujo::Normal } } }), Orden::Bucle { hasta, cond, cuerpo, redirs } => con_redirs(sh, redirs, |sh| { sh.bucles += 1; let mut ultimo = 0; let salida = loop { if let Some(f) = correr_lista_condicion(sh, cond) { break f; } let seguir = if *hasta { sh.estado != 0 } else { sh.estado == 0 }; if !seguir { break Flujo::Normal; } match correr_lista(sh, cuerpo) { Flujo::Break(n) => { if n > 1 { break Flujo::Break(n - 1); } break Flujo::Normal; } Flujo::Continue(n) => { if n > 1 { break Flujo::Continue(n - 1); } } Flujo::Normal => {} f => break f, } ultimo = sh.estado; }; sh.bucles -= 1; if salida == Flujo::Normal { sh.estado = ultimo; } salida }), Orden::Para { var, palabras, cuerpo, redirs } => con_redirs(sh, redirs, |sh| { let lista: Vec = match palabras { None => sh.params.clone(), Some(ps) => { let mut v = Vec::new(); for p in ps { match expand::campos(sh, p) { Ok(cs) => v.extend(cs), Err(e) => { sh.error(&e); sh.estado = 1; return Flujo::Normal; } } } v } }; sh.bucles += 1; sh.estado = 0; let salida = 'bucle: { for v in lista { sh.asignar(var, &v); match correr_lista(sh, cuerpo) { Flujo::Break(n) => { if n > 1 { break 'bucle Flujo::Break(n - 1); } break 'bucle Flujo::Normal; } Flujo::Continue(n) => { if n > 1 { break 'bucle Flujo::Continue(n - 1); } } Flujo::Normal => {} f => break 'bucle f, } } Flujo::Normal }; sh.bucles -= 1; salida }), Orden::Caso { sujeto, ramas, redirs } => con_redirs(sh, redirs, |sh| { let s = match expand::una(sh, sujeto) { Ok(s) => s, Err(e) => { sh.error(&e); sh.estado = 2; return Flujo::Exit(2); } }; for (patrones, cuerpo) in ramas { for p in patrones { let pat = match expand::patron(sh, p) { Ok(p) => p, Err(e) => { sh.error(&e); sh.estado = 1; return Flujo::Normal; } }; if crate::glob::encaja(&pat, &s) { if cuerpo.is_empty() { sh.estado = 0; return Flujo::Normal; } return correr_lista(sh, cuerpo); } } } sh.estado = 0; Flujo::Normal }), } } /// Corre la lista de una condición (`if`/`while`). Devuelve `Some(flujo)` sólo si hay que abandonar /// (un `exit` o un `return` de adentro); si no, deja el estado en `sh.estado`. fn correr_lista_condicion(sh: &mut Shell, l: &Lista) -> Option { suspendiendo_errexit(sh, |sh| { for (yo, _) in l { let f = correr_yo(sh, yo, false); if f != Flujo::Normal { return Some(f); } } None }) } fn correr_simple( sh: &mut Shell, asignaciones: &[Asignacion], palabras: &[Palabra], redirs: &[Redir], ) -> Flujo { // 1. expandir las palabras let mut argv: Vec = Vec::new(); for p in palabras { match expand::campos(sh, p) { Ok(cs) => argv.extend(cs), Err(e) => { // Un error de expansión (comilla sin cerrar en una sustitución, `${x:?}`, `set -u`) // ABORTA con 2 en un shell no interactivo, no sigue con 1. Medido contra el ash. sh.error(&e); sh.estado = 2; return Flujo::Exit(2); } } } // 2. las asignaciones let mut pares: Vec<(String, String)> = Vec::new(); for a in asignaciones { match expand::una(sh, &a.valor) { Ok(v) => pares.push((a.nombre.clone(), v)), Err(e) => { // Un error de expansión (comilla sin cerrar en una sustitución, `${x:?}`, `set -u`) // ABORTA con 2 en un shell no interactivo, no sigue con 1. Medido contra el ash. sh.error(&e); sh.estado = 2; return Flujo::Exit(2); } } } if argv.is_empty() { // sólo asignaciones (y quizá redirecciones, que se abren y se cierran) match aplicar(sh, redirs) { Ok(g) => restaurar(g), Err(e) => { // Un error de expansión (comilla sin cerrar en una sustitución, `${x:?}`, `set -u`) // ABORTA con 2 en un shell no interactivo, no sigue con 1. Medido contra el ash. sh.error(&e); sh.estado = 2; return Flujo::Exit(2); } } for (k, v) in pares { sh.asignar(&k, &v); } sh.estado = 0; return Flujo::Normal; } if sh.opts.xtrace { eprintln!("+ {}", argv.join(" ")); } // 3. ¿función? if let Some(cuerpo) = sh.funcs.get(&argv[0]).cloned() { return con_redirs(sh, redirs, |sh| { let previos = std::mem::replace(&mut sh.params, argv[1..].to_vec()); sh.funcion_honda += 1; sh.locales.push(Vec::new()); let f = correr_orden(sh, &cuerpo); if let Some(marco) = sh.locales.pop() { for (nombre, previo) in marco.into_iter().rev() { match previo { Some(v) => { sh.vars.insert(nombre, v); } None => { sh.vars.remove(&nombre); } } } } sh.funcion_honda -= 1; sh.params = previos; match f { Flujo::Return => Flujo::Normal, otro => otro, } }); } // 4. ¿builtin? if crate::builtins::es_builtin(&argv[0]) { return match aplicar(sh, redirs) { Ok(guardados) => { // Las asignaciones de una orden con builtin quedan en el shell (POSIX lo deja // sin especificar para los regulares; hacerlas persistir es lo que hace ash). for (k, v) in &pares { sh.asignar(k, v); } let f = crate::builtins::correr(sh, &argv); restaurar(guardados); f } Err(e) => { sh.error(&e); sh.estado = 2; Flujo::Exit(2) } }; } // 5. orden externa lanzar_externo(sh, &argv, redirs, &pares, false) } /// Aplica redirecciones, corre `cuerpo`, restaura. Para grupos y compuestas, que NO forkean. fn con_redirs Flujo>(sh: &mut Shell, redirs: &[Redir], cuerpo: F) -> Flujo { match aplicar(sh, redirs) { Ok(guardados) => { let f = cuerpo(sh); restaurar(guardados); f } Err(e) => { sh.error(&e); sh.estado = 1; Flujo::Normal } } } // ───────────────────────────────────────────────────────── redirecciones pub struct Guardado { fd: i32, copia: i32, } pub fn aplicar(sh: &mut Shell, redirs: &[Redir]) -> Result, String> { let mut guardados = Vec::new(); for r in redirs { let (fd, nuevo) = match r { Redir::Salida { fd, destino, append } => { let ruta = expand::una(sh, destino)?; let banderas = if *append { libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND } else { libc::O_WRONLY | libc::O_CREAT | libc::O_TRUNC }; (*fd, abrir(&ruta, banderas)?) } Redir::Entrada { fd, origen } => { let ruta = expand::una(sh, origen)?; (*fd, abrir(&ruta, libc::O_RDONLY)?) } Redir::LecturaEscritura { fd, ruta } => { let ruta = expand::una(sh, ruta)?; (*fd, abrir(&ruta, libc::O_RDWR | libc::O_CREAT)?) } Redir::Duplica { fd, otro, .. } => match otro { DupDestino::Cerrar => { guardados.push(Guardado { fd: *fd, copia: unsafe { libc::dup(*fd) } }); unsafe { libc::close(*fd) }; continue; } DupDestino::Fd(p) => { let s = expand::una(sh, p)?; let n: i32 = s.parse().map_err(|_| format!("{s}: no es un descriptor"))?; let copia = unsafe { libc::dup(n) }; if copia < 0 { return Err(format!("{n}: descriptor no válido")); } (*fd, copia) } }, Redir::AquiDoc { fd, cuerpo, expande } => { let texto = if *expande { expand::una(sh, cuerpo)? } else { match &cuerpo[0] { Seg::Lit { texto, .. } => texto.clone(), _ => String::new(), } }; (*fd, fd_con_texto(&texto)?) } }; let copia = unsafe { libc::dup(fd) }; guardados.push(Guardado { fd, copia }); unsafe { libc::dup2(nuevo, fd) }; unsafe { libc::close(nuevo) }; } Ok(guardados) } pub fn restaurar(guardados: Vec) { for g in guardados.into_iter().rev() { if g.copia >= 0 { unsafe { libc::dup2(g.copia, g.fd) }; unsafe { libc::close(g.copia) }; } else { unsafe { libc::close(g.fd) }; } } } fn abrir(ruta: &str, banderas: i32) -> Result { let c = CString::new(ruta).map_err(|_| format!("{ruta}: ruta con NUL"))?; let fd = unsafe { libc::open(c.as_ptr(), banderas, 0o644 as libc::c_int) }; if fd < 0 { return Err(format!("{ruta}: no pude abrir")); } Ok(fd) } /// Un descriptor de sólo lectura con `texto` adentro, para los aquí-documentos. /// /// Se usa `memfd_create` en vez de una tubería porque una tubería se **bloquea** si el cuerpo pasa /// del buffer del kernel (64 K) y no hay nadie leyendo todavía. Si no hay memfd, cae a la tubería, /// que para los cuerpos de este árbol (2 en 21 guiones, cortos) alcanza. fn fd_con_texto(texto: &str) -> Result { let nombre = CString::new("simi-aquidoc").unwrap(); let fd = unsafe { libc::memfd_create(nombre.as_ptr(), 0) }; if fd >= 0 { let bytes = texto.as_bytes(); let mut escrito = 0; while escrito < bytes.len() { let n = unsafe { libc::write( fd, bytes[escrito..].as_ptr() as *const libc::c_void, bytes.len() - escrito, ) }; if n <= 0 { unsafe { libc::close(fd) }; return Err("aquí-documento: no pude escribir".into()); } escrito += n as usize; } unsafe { libc::lseek(fd, 0, libc::SEEK_SET) }; return Ok(fd); } let mut fds = [0i32; 2]; if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 { return Err("aquí-documento: no pude crear la tubería".into()); } let bytes = texto.as_bytes(); unsafe { libc::write(fds[1], bytes.as_ptr() as *const libc::c_void, bytes.len()) }; unsafe { libc::close(fds[1]) }; Ok(fds[0]) } // ───────────────────────────────────────────────────────── procesos /// `reemplazar` = viene del builtin `exec`: NO se forkea, se sustituye este proceso. pub fn lanzar_externo( sh: &mut Shell, argv: &[String], redirs: &[Redir], asignaciones: &[(String, String)], reemplazar: bool, ) -> Flujo { let ruta = match resolver(sh, &argv[0]) { Some(r) => r, None => { sh.error(&format!("{}: no encontrado", argv[0])); sh.estado = 127; return Flujo::Normal; } }; let mut entorno = sh.entorno(); for (k, v) in asignaciones { entorno.retain(|e| !e.starts_with(&format!("{k}="))); entorno.push(format!("{k}={v}")); } if reemplazar { if let Err(e) = aplicar(sh, redirs) { sh.error(&e); sh.estado = 1; return Flujo::Normal; } ejecutar(&ruta, argv, &entorno); // si volvemos, el execve falló sh.error(&format!("{}: no pude ejecutar", argv[0])); return Flujo::Exit(126); } let pid = unsafe { libc::fork() }; if pid < 0 { sh.error("no pude forkear"); sh.estado = 1; return Flujo::Normal; } if pid == 0 { if let Err(e) = aplicar(sh, redirs) { eprintln!("{}: {}", sh.nombre, e); unsafe { libc::_exit(1) }; } // el hijo no hereda los traps atrapados (POSIX §2.11) unsafe { libc::signal(libc::SIGINT, libc::SIG_DFL); libc::signal(libc::SIGQUIT, libc::SIG_DFL); } ejecutar(&ruta, argv, &entorno); eprintln!("{}: {}: no pude ejecutar", sh.nombre, argv[0]); unsafe { libc::_exit(126) }; } sh.estado = esperar(pid); Flujo::Normal } fn ejecutar(ruta: &str, argv: &[String], entorno: &[String]) { let c_ruta = match CString::new(ruta) { Ok(c) => c, Err(_) => return, }; let c_argv: Vec = argv.iter().filter_map(|a| CString::new(a.as_str()).ok()).collect(); let c_env: Vec = entorno.iter().filter_map(|e| CString::new(e.as_str()).ok()).collect(); let mut p_argv: Vec<*const libc::c_char> = c_argv.iter().map(|c| c.as_ptr()).collect(); p_argv.push(std::ptr::null()); let mut p_env: Vec<*const libc::c_char> = c_env.iter().map(|c| c.as_ptr()).collect(); p_env.push(std::ptr::null()); unsafe { libc::execve(c_ruta.as_ptr(), p_argv.as_ptr(), p_env.as_ptr()) }; } /// Resolución por `PATH`. Propia y no `execvp`, porque el `PATH` que manda es el del SHELL /// (`sh.vars`), que no tiene por qué ser el del `environ` heredado. fn resolver(sh: &Shell, nombre: &str) -> Option { if nombre.contains('/') { return Some(nombre.to_string()); } let path = sh.leer("PATH").unwrap_or_else(|| "/usr/bin:/bin".into()); for dir in path.split(':') { let dir = if dir.is_empty() { "." } else { dir }; let cand = format!("{dir}/{nombre}"); let c = CString::new(cand.clone()).ok()?; if unsafe { libc::access(c.as_ptr(), libc::X_OK) } == 0 { return Some(cand); } } None } fn esperar(pid: i32) -> i32 { let mut estado = 0i32; loop { let r = unsafe { libc::waitpid(pid, &mut estado, 0) }; if r < 0 { let e = std::io::Error::last_os_error(); if e.raw_os_error() == Some(libc::EINTR) { continue; } return 1; } break; } if libc::WIFEXITED(estado) { libc::WEXITSTATUS(estado) } else if libc::WIFSIGNALED(estado) { 128 + libc::WTERMSIG(estado) } else { 1 } } /// Salida de un subshell: **corre el `trap EXIT` antes de irse**. Es la invariante 1 del módulo. pub fn salir_de_subshell(sh: &mut Shell, estado: i32) -> ! { correr_trap_exit(sh, estado); let _ = std::io::stdout().flush(); let _ = std::io::stderr().flush(); unsafe { libc::_exit(estado) } } pub fn correr_trap_exit(sh: &mut Shell, estado: i32) { if sh.trap_exit_corrido { return; } sh.trap_exit_corrido = true; if let Some(accion) = sh.traps.get("EXIT").cloned() { sh.estado = estado; if let Ok(l) = parsear(&accion) { correr_lista(sh, &l); } sh.estado = estado; } } /// Entre órdenes: si llegó una señal atrapada, corre su acción. El handler sólo prende una /// bandera (es lo único async-signal-safe que se puede hacer). pub fn atender_senales(sh: &mut Shell) { for n in 1..32usize { if LLEGO_SENAL[n].swap(false, std::sync::atomic::Ordering::SeqCst) { let nombre = crate::builtins::nombre_de_senal(n as i32); if let Some(accion) = sh.traps.get(&nombre).cloned() { if let Ok(l) = parsear(&accion) { let previo = sh.estado; correr_lista(sh, &l); sh.estado = previo; } } } } } // ───────────────────────────────────────────────────────── sustituciones /// `$( … )` y `` ` … ` ``: corre el cuerpo en un subshell y devuelve su stdout sin los saltos /// finales. El subshell corre su `trap EXIT` como cualquier otro (invariante 1). pub fn sustituir_orden(sh: &mut Shell, cuerpo: &str) -> Result { let lista = parsear(cuerpo).map_err(|e| format!("sustitución: {e}"))?; let mut fds = [0i32; 2]; if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 { return Err("no pude crear la tubería".into()); } let pid = unsafe { libc::fork() }; if pid < 0 { unsafe { libc::close(fds[0]) }; unsafe { libc::close(fds[1]) }; return Err("no pude forkear".into()); } if pid == 0 { unsafe { libc::close(fds[0]) }; unsafe { libc::dup2(fds[1], 1) }; unsafe { libc::close(fds[1]) }; entrar_en_subshell(sh); let f = correr_lista(sh, &lista); let estado = match f { Flujo::Exit(c) => c, _ => sh.estado, }; salir_de_subshell(sh, estado); } unsafe { libc::close(fds[1]) }; let mut salida = String::new(); { let mut f = unsafe { std::fs::File::from_raw_fd(fds[0]) }; let mut buf = Vec::new(); let _ = f.read_to_end(&mut buf); salida = String::from_utf8_lossy(&buf).into_owned(); let _ = f.as_raw_fd(); } sh.estado = esperar(pid); while salida.ends_with('\n') { salida.pop(); } Ok(salida) } pub fn evaluar_aritmetica(sh: &mut Shell, expr: &str) -> Result { // Las variables de la expresión pueden traer expansiones: `$(( $x + 1 ))`. let expr = expandir_texto(sh, expr)?; let vars: std::cell::RefCell> = std::cell::RefCell::new(Vec::new()); let leer_sh: Vec<(String, String)> = sh .vars .iter() .map(|(k, (v, _))| (k.clone(), v.clone())) .collect(); let leer = |n: &str| { leer_sh .iter() .find(|(k, _)| k == n) .map(|(_, v)| v.clone()) .unwrap_or_default() }; let mut asignar = |n: &str, v: &str| vars.borrow_mut().push((n.to_string(), v.to_string())); let mut ctx = crate::arith::Contexto { leer: &leer, asignar: &mut asignar }; let r = crate::arith::evaluar(&expr, &mut ctx); for (k, v) in vars.into_inner() { sh.asignar(&k, &v); } r.map(|n| n.to_string()) } /// Expande las sustituciones de un texto suelto (el interior de `$(( ))`). fn expandir_texto(sh: &mut Shell, texto: &str) -> Result { if !texto.contains('$') && !texto.contains('`') { return Ok(texto.to_string()); } let escapado = texto.replace('"', "\\\""); let l = parsear(&format!(": \"{escapado}\"")).map_err(|e| format!("aritmética: {e}"))?; let Orden::Simple { palabras, .. } = &l[0].0.primera.ordenes[0] else { return Ok(texto.to_string()); }; expand::una(sh, &palabras[1]) }