sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
simi: las cuatro expansiones de bash que no rompen POSIX
1//! El árbol. Nada de estado ni de ejecución acá: sólo la forma del lenguaje.
2
3/// Un trozo de palabra. La palabra se arma en el lexer y se expande en `expand`, en ese orden:
4/// tilde → parámetro/orden/aritmética → división en campos → rutas → quita de comillas.
5#[derive(Debug, Clone, PartialEq)]
6pub enum Seg {
7 /// Texto literal. `comillado` = vino de comillas ⇒ **no** se divide en campos ni se globea.
8 Lit { texto: String, comillado: bool },
9 /// `~` al principio de una palabra (o de un campo tras `:` en una asignación).
10 Tilde,
11 /// `$x`, `${x}`, `${x:-def}`, `${#x}`, `${x%%pat}`… `comillado` va aparte porque
12 /// `"$x"` no se divide y `$x` sí.
13 Param { nombre: String, op: ParamOp, arg: Vec<Seg>, comillado: bool },
14 /// `$( … )` y `` ` … ` ``. El cuerpo es shell otra vez y se parsea al expandir.
15 CmdSub { cuerpo: String, comillado: bool },
16 /// `$(( … ))`.
17 Arith { expr: String, comillado: bool },
18}
19
20/// ⚠ **Sin `Copy` desde 2026-10-22**: las tres extensiones de abajo llevan datos (una palabra de
21/// reemplazo, dos expresiones aritméticas), y `Vec`/`String` no son `Copy`. El coste fue cambiar
22/// dos sitios en `expand.rs` a pasar `&ParamOp`. También se cae `Eq`, porque `Seg` sólo tiene
23/// `PartialEq`.
24#[derive(Debug, Clone, PartialEq)]
25pub enum ParamOp {
26 /// `$x` / `${x}`
27 Valor,
28 /// `${#x}`
29 Largo,
30 /// `${x:-p}` · `${x-p}` (sin dos puntos: sólo si NO está definida)
31 Defecto { dos_puntos: bool },
32 /// `${x:=p}` · `${x=p}`
33 Asigna { dos_puntos: bool },
34 /// `${x:?p}` · `${x?p}`
35 Error { dos_puntos: bool },
36 /// `${x:+p}` · `${x+p}`
37 Alterno { dos_puntos: bool },
38 /// `${x#p}` · `${x##p}`
39 QuitaPrefijo { largo: bool },
40 /// `${x%p}` · `${x%%p}`
41 QuitaSufijo { largo: bool },
42
43 // ══ EXTENSIONES QUE NO ROMPEN POSIX (2026-10-22) ══════════════════════════════════════════
44 // POSIX deja esta sintaxis **sin definir**: un script conforme nunca la escribe, así que
45 // soportarla no puede cambiarle el comportamiento a nadie. Se añaden porque el corpus las usa
46 // —`${t/unknown-/}` en `wasi-libcxx` era la ÚNICA divergencia de simi en 1339 casos— y porque
47 // rechazarlas obligaba a reescribir recetas ajenas al shell. No se añade `${!x}`: choca con el
48 // `$!` que el lexer ya trata como nombre especial, y es la menos usada de las cuatro.
49 /// `${x/pat/rep}` · `${x//pat/rep}` — sustituir. `pat` va en `arg`, el reemplazo acá.
50 /// `ancla` guarda el `#`/`%` de `${x/#pat/rep}`: tiene que separarse en el LEXER, porque un
51 /// `#` al empezar una palabra abre un COMENTARIO y el patrón llegaría vacío a la expansión
52 /// (medido: `${v/#a/X}` devolvía la cadena intacta, un no-op silencioso).
53 Sustituye { todas: bool, ancla: Option<char>, rep: Palabra },
54 /// `${x:desde}` · `${x:desde:largo}` — subcadena. Las dos son expresiones ARITMÉTICAS (se
55 /// evalúan con `arith`), y un `desde` negativo cuenta desde el final, como en bash.
56 Subcadena { desde: String, largo: Option<String> },
57 /// `${x^}` `${x^^}` `${x,}` `${x,,}` — cambiar caja (primera letra o todas).
58 Caso { mayus: bool, todas: bool },
59}
60
61pub type Palabra = Vec<Seg>;
62
63#[derive(Debug, Clone, PartialEq)]
64pub enum Redir {
65 /// `n> pal` (`trunca` falso ⇒ `>>`)
66 Salida { fd: i32, destino: Palabra, append: bool },
67 /// `n< pal`
68 Entrada { fd: i32, origen: Palabra },
69 /// `n<> pal`
70 LecturaEscritura { fd: i32, ruta: Palabra },
71 /// `n>&m`, `n<&m`, y los cierres `n>&-` / `n<&-`
72 Duplica { fd: i32, otro: DupDestino, escritura: bool },
73 /// `n<< DELIM` ya resuelto: el cuerpo viene del lexer. `expande` = el delimitador NO estaba
74 /// entrecomillado.
75 AquiDoc { fd: i32, cuerpo: Palabra, expande: bool },
76}
77
78#[derive(Debug, Clone, PartialEq)]
79pub enum DupDestino {
80 Fd(Palabra),
81 Cerrar,
82}
83
84#[derive(Debug, Clone, PartialEq)]
85pub struct Asignacion {
86 pub nombre: String,
87 pub valor: Palabra,
88}
89
90#[derive(Debug, Clone, PartialEq)]
91pub enum Orden {
92 Simple {
93 asignaciones: Vec<Asignacion>,
94 palabras: Vec<Palabra>,
95 redirs: Vec<Redir>,
96 },
97 /// `{ …; }`
98 Grupo { cuerpo: Lista, redirs: Vec<Redir> },
99 /// `( … )`
100 Subshell { cuerpo: Lista, redirs: Vec<Redir> },
101 Si {
102 ramas: Vec<(Lista, Lista)>, // (condición, cuerpo)
103 sino: Option<Lista>,
104 redirs: Vec<Redir>,
105 },
106 /// `while`/`until`
107 Bucle { hasta: bool, cond: Lista, cuerpo: Lista, redirs: Vec<Redir> },
108 Para {
109 var: String,
110 /// `None` = `for x; do` ⇒ itera sobre `"$@"`
111 palabras: Option<Vec<Palabra>>,
112 cuerpo: Lista,
113 redirs: Vec<Redir>,
114 },
115 Caso {
116 sujeto: Palabra,
117 ramas: Vec<(Vec<Palabra>, Lista)>,
118 redirs: Vec<Redir>,
119 },
120 Funcion { nombre: String, cuerpo: Box<Orden> },
121}
122
123#[derive(Debug, Clone, PartialEq)]
124pub struct Tuberia {
125 /// `! cmd | cmd`
126 pub negada: bool,
127 pub ordenes: Vec<Orden>,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum Enlace {
132 Y, // &&
133 O, // ||
134}
135
136#[derive(Debug, Clone, PartialEq)]
137pub struct YO {
138 pub primera: Tuberia,
139 pub resto: Vec<(Enlace, Tuberia)>,
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum Fin {
144 /// `;` o fin de línea
145 Secuencial,
146 /// `&`
147 SegundoPlano,
148}
149
150pub type Lista = Vec<(YO, Fin)>;