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: el /bin/sh propio del producto — 4448 líneas, sólo libc, y CERO divergencias contra el ash
1//! Expansiones, **en el orden que manda POSIX §2.6** — y el orden es el contrato, no un detalle:
2//!
3//! 1. tilde · 2. parámetro, orden y aritmética (de izquierda a derecha) · 3. división en campos
4//! por `IFS` · 4. expansión de rutas · 5. quita de comillas.
5//!
6//! Un shell que divide antes de expandir, o que globea lo que vino entrecomillado, pasa igual
7//! cualquier banco de casos sencillos y rompe el primer `for f in $LISTA` de verdad.
8
9use crate::ast::{Palabra, ParamOp, Seg};
10use crate::glob;
11use crate::shell::Shell;
12
13/// Un trozo ya expandido. `comillado` decide si se divide en campos y si se globea;
14/// `corte` fuerza un campo nuevo (lo necesita `"$@"`).
15struct Trozo {
16 texto: String,
17 comillado: bool,
18 corte: bool,
19}
20
21/// Campos finales de una palabra: expansión + división + rutas + quita de comillas.
22pub fn campos(sh: &mut Shell, p: &Palabra) -> Result<Vec<String>, String> {
23 let trozos = expandir(sh, p)?;
24 let crudos = armar(trozos, &sh.ifs());
25 let mut out = Vec::new();
26 for (campo, globeable) in crudos {
27 let texto: String = campo.iter().map(|(c, _)| *c).collect();
28 if !sh.opts.noglob && globeable && hay_meta_sin_proteger(&campo) {
29 let hits = glob::expandir(&como_patron(&campo));
30 if hits.is_empty() {
31 out.push(texto);
32 } else {
33 out.extend(hits);
34 }
35 } else {
36 out.push(texto);
37 }
38 }
39 Ok(out)
40}
41
42/// Una palabra donde la división en campos NO aplica: asignaciones, destinos de redirección,
43/// cuerpos de aquí-documento, sujeto de `case`.
44pub fn una(sh: &mut Shell, p: &Palabra) -> Result<String, String> {
45 let trozos = expandir(sh, p)?;
46 Ok(trozos.into_iter().map(|t| t.texto).collect())
47}
48
49/// Como `una`, pero devolviendo un PATRÓN: lo que vino entrecomillado queda escapado, así
50/// `case "$x" in \*)` compara un asterisco literal. Lo usan `case` y `${x#pat}`.
51pub fn patron(sh: &mut Shell, p: &Palabra) -> Result<String, String> {
52 let trozos = expandir(sh, p)?;
53 let mut campo: Vec<(char, bool)> = Vec::new();
54 for t in trozos {
55 for c in t.texto.chars() {
56 campo.push((c, t.comillado));
57 }
58 }
59 Ok(como_patron(&campo))
60}
61
62/// ¿Hay un metacarácter de ruta SIN proteger? Los protegidos vinieron de comillas y son literales.
63fn hay_meta_sin_proteger(campo: &[(char, bool)]) -> bool {
64 campo.iter().any(|(c, prot)| !prot && matches!(c, '*' | '?' | '['))
65}
66
67/// Texto de patrón para `glob`: los caracteres protegidos se escapan con `\`, y una barra
68/// invertida NO protegida —la que salió de una expansión— se escapa para que siga siendo una
69/// barra literal.
70///
71/// ⚠ Acá estaba el bug que el banco diferencial destapó: la primera versión usaba `\` como marca
72/// interna y después la quitaba con un `quitar_escapes` final, así que `$(echo \\$V)` perdía la
73/// barra que la sustitución había producido (`ash` daba `\hola`, simi daba `hola`). **La quita de
74/// comillas es sobre las comillas de la PALABRA, nunca sobre lo que produjo una expansión** — y eso
75/// no se puede representar con un escape dentro del texto: hace falta la máscara.
76fn como_patron(campo: &[(char, bool)]) -> String {
77 let mut out = String::new();
78 for (c, prot) in campo {
79 if *prot {
80 if matches!(c, '*' | '?' | '[' | ']' | '\\') {
81 out.push('\\');
82 }
83 } else if *c == '\\' {
84 out.push('\\');
85 }
86 out.push(*c);
87 }
88 out
89}
90
91fn expandir(sh: &mut Shell, p: &Palabra) -> Result<Vec<Trozo>, String> {
92 let mut out: Vec<Trozo> = Vec::new();
93 for seg in p {
94 match seg {
95 Seg::Lit { texto, comillado } => {
96 out.push(Trozo { texto: texto.clone(), comillado: *comillado, corte: false })
97 }
98 Seg::Tilde => {
99 let casa = sh.leer("HOME").unwrap_or_default();
100 out.push(Trozo { texto: casa, comillado: true, corte: false });
101 }
102 Seg::Arith { expr, comillado } => {
103 let texto = crate::exec::evaluar_aritmetica(sh, expr)?;
104 out.push(Trozo { texto, comillado: *comillado, corte: false });
105 }
106 Seg::CmdSub { cuerpo, comillado } => {
107 let texto = crate::exec::sustituir_orden(sh, cuerpo)?;
108 out.push(Trozo { texto, comillado: *comillado, corte: false });
109 }
110 Seg::Param { nombre, op, arg, comillado } => {
111 expandir_param(sh, nombre, *op, arg, *comillado, &mut out)?;
112 }
113 }
114 }
115 Ok(out)
116}
117
118fn expandir_param(
119 sh: &mut Shell,
120 nombre: &str,
121 op: ParamOp,
122 arg: &Palabra,
123 comillado: bool,
124 out: &mut Vec<Trozo>,
125) -> Result<(), String> {
126 // `$@` y `$*`: los únicos que producen varios campos.
127 if nombre == "@" || nombre == "*" {
128 let params = sh.params.clone();
129 if op == ParamOp::Largo {
130 out.push(Trozo { texto: params.len().to_string(), comillado, corte: false });
131 return Ok(());
132 }
133 if nombre == "*" && comillado {
134 let sep = sh.ifs().chars().next().map(|c| c.to_string()).unwrap_or_default();
135 out.push(Trozo { texto: params.join(&sep), comillado: true, corte: false });
136 return Ok(());
137 }
138 for (n, v) in params.iter().enumerate() {
139 out.push(Trozo { texto: v.clone(), comillado, corte: n > 0 });
140 }
141 return Ok(());
142 }
143
144 let valor = sh.leer(nombre);
145 let definida_no_vacia = valor.as_deref().is_some_and(|v| !v.is_empty());
146 let definida = valor.is_some();
147
148 let texto = match op {
149 ParamOp::Valor => {
150 if valor.is_none() && sh.opts.nounset {
151 return Err(format!("{nombre}: variable sin definir"));
152 }
153 valor.unwrap_or_default()
154 }
155 ParamOp::Largo => valor.unwrap_or_default().chars().count().to_string(),
156 ParamOp::Defecto { dos_puntos } => {
157 let usar = if dos_puntos { definida_no_vacia } else { definida };
158 if usar {
159 valor.unwrap_or_default()
160 } else {
161 una(sh, arg)?
162 }
163 }
164 ParamOp::Asigna { dos_puntos } => {
165 let usar = if dos_puntos { definida_no_vacia } else { definida };
166 if usar {
167 valor.unwrap_or_default()
168 } else {
169 let v = una(sh, arg)?;
170 sh.asignar(nombre, &v);
171 v
172 }
173 }
174 ParamOp::Error { dos_puntos } => {
175 let usar = if dos_puntos { definida_no_vacia } else { definida };
176 if usar {
177 valor.unwrap_or_default()
178 } else {
179 let m = una(sh, arg)?;
180 let m = if m.is_empty() { "parámetro sin definir".to_string() } else { m };
181 return Err(format!("{nombre}: {m}"));
182 }
183 }
184 ParamOp::Alterno { dos_puntos } => {
185 let usar = if dos_puntos { definida_no_vacia } else { definida };
186 if usar {
187 una(sh, arg)?
188 } else {
189 String::new()
190 }
191 }
192 ParamOp::QuitaPrefijo { largo } => {
193 let v = valor.unwrap_or_default();
194 let pat = patron(sh, arg)?;
195 quitar(&v, &pat, true, largo)
196 }
197 ParamOp::QuitaSufijo { largo } => {
198 let v = valor.unwrap_or_default();
199 let pat = patron(sh, arg)?;
200 quitar(&v, &pat, false, largo)
201 }
202 };
203 out.push(Trozo { texto, comillado, corte: false });
204 Ok(())
205}
206
207/// `${x#pat}` / `${x%pat}`. `prefijo` elige el lado; `largo` elige la coincidencia más larga.
208fn quitar(valor: &str, pat: &str, prefijo: bool, largo: bool) -> String {
209 let cs: Vec<char> = valor.chars().collect();
210 let mut mejor: Option<usize> = None;
211 let rango: Vec<usize> = (0..=cs.len()).collect();
212 for n in rango {
213 let trozo: String = if prefijo {
214 cs[..n].iter().collect()
215 } else {
216 cs[cs.len() - n..].iter().collect()
217 };
218 if glob::encaja(pat, &trozo) {
219 match mejor {
220 None => mejor = Some(n),
221 Some(m) => {
222 if (largo && n > m) || (!largo && n < m) {
223 mejor = Some(n);
224 }
225 }
226 }
227 if !largo {
228 break; // la más corta: la primera que encaja
229 }
230 }
231 }
232 match mejor {
233 None => valor.to_string(),
234 Some(n) => {
235 if prefijo {
236 cs[n..].iter().collect()
237 } else {
238 cs[..cs.len() - n].iter().collect()
239 }
240 }
241 }
242}
243
244/// División en campos por `IFS`. Devuelve `(campo con máscara, se_puede_globear)`.
245fn armar(trozos: Vec<Trozo>, ifs: &str) -> Vec<(Vec<(char, bool)>, bool)> {
246 let mut campos: Vec<(Vec<(char, bool)>, bool)> = Vec::new();
247 let mut actual: Vec<(char, bool)> = Vec::new();
248 let mut globeable = false;
249 let mut hay_algo = false;
250
251 for t in trozos {
252 if t.corte {
253 campos.push((std::mem::take(&mut actual), globeable));
254 globeable = false;
255 hay_algo = true;
256 }
257 if t.comillado {
258 actual.extend(t.texto.chars().map(|c| (c, true)));
259 hay_algo = true;
260 continue;
261 }
262 globeable = true;
263 hay_algo = true;
264 let mut primero = true;
265 for parte in dividir(&t.texto, ifs) {
266 if !primero {
267 campos.push((std::mem::take(&mut actual), globeable));
268 globeable = true;
269 }
270 actual.extend(parte.chars().map(|c| (c, false)));
271 primero = false;
272 }
273 if !t.texto.is_empty() && t.texto.chars().next_back().is_some_and(|c| ifs.contains(c)) {
274 campos.push((std::mem::take(&mut actual), globeable));
275 globeable = true;
276 }
277 }
278 if hay_algo {
279 campos.push((actual, globeable));
280 }
281 // POSIX: los campos vacíos que salen de la división se descartan; los que vienen de comillas
282 // se quedan. Un campo vacío no-globeable venía de comillas.
283 campos.retain(|(t, g)| !t.is_empty() || !*g);
284 campos
285}
286
287/// Divide por `IFS`: los blancos de `IFS` colapsan, los no-blancos delimitan de a uno.
288fn dividir(s: &str, ifs: &str) -> Vec<String> {
289 if ifs.is_empty() {
290 return vec![s.to_string()];
291 }
292 let blancos: Vec<char> = ifs.chars().filter(|c| matches!(c, ' ' | '\t' | '\n')).collect();
293 let otros: Vec<char> = ifs.chars().filter(|c| !matches!(c, ' ' | '\t' | '\n')).collect();
294 let mut out: Vec<String> = Vec::new();
295 let mut act = String::new();
296 let mut i = 0;
297 let cs: Vec<char> = s.chars().collect();
298 while i < cs.len() {
299 let c = cs[i];
300 if blancos.contains(&c) {
301 out.push(std::mem::take(&mut act));
302 while i < cs.len() && blancos.contains(&cs[i]) {
303 i += 1;
304 }
305 continue;
306 }
307 if otros.contains(&c) {
308 out.push(std::mem::take(&mut act));
309 i += 1;
310 continue;
311 }
312 act.push(c);
313 i += 1;
314 }
315 out.push(act);
316 // los blancos del principio y del final no crean campos vacíos
317 while out.first().is_some_and(|f| f.is_empty()) && out.len() > 1 {
318 out.remove(0);
319 }
320 out
321}
322
323#[cfg(test)]
324mod pruebas {
325 use super::*;
326 use crate::parser::parsear;
327
328 fn campos_de(sh: &mut Shell, entrada: &str) -> Vec<String> {
329 let l = parsear(entrada).unwrap();
330 let orden = &l[0].0.primera.ordenes[0];
331 let crate::ast::Orden::Simple { palabras, .. } = orden else { panic!() };
332 let mut out = Vec::new();
333 for p in &palabras[1..] {
334 out.extend(campos(sh, p).unwrap());
335 }
336 out
337 }
338
339 #[test]
340 fn comillas_no_dividen_y_desnudo_si() {
341 let mut sh = Shell::nuevo();
342 sh.asignar("x", "a b");
343 assert_eq!(campos_de(&mut sh, "cmd \"$x\""), vec!["a b"]);
344 assert_eq!(campos_de(&mut sh, "cmd $x"), vec!["a", "b"]);
345 }
346
347 #[test]
348 fn expansiones_de_parametro() {
349 let mut sh = Shell::nuevo();
350 sh.asignar("v", "arch.tar.gz");
351 assert_eq!(campos_de(&mut sh, "cmd ${v%%.*}"), vec!["arch"]);
352 assert_eq!(campos_de(&mut sh, "cmd ${v##*.}"), vec!["gz"]);
353 assert_eq!(campos_de(&mut sh, "cmd ${v%.gz}"), vec!["arch.tar"]);
354 assert_eq!(campos_de(&mut sh, "cmd ${v#arch.}"), vec!["tar.gz"]);
355 assert_eq!(campos_de(&mut sh, "cmd ${#v}"), vec!["11"]);
356 assert_eq!(campos_de(&mut sh, "cmd ${nada:-def}"), vec!["def"]);
357 assert_eq!(campos_de(&mut sh, "cmd ${v:+si}"), vec!["si"]);
358 }
359
360 #[test]
361 fn posicionales() {
362 let mut sh = Shell::nuevo();
363 sh.params = vec!["uno".into(), "dos tres".into()];
364 assert_eq!(campos_de(&mut sh, "cmd \"$@\""), vec!["uno", "dos tres"]);
365 assert_eq!(campos_de(&mut sh, "cmd $@"), vec!["uno", "dos", "tres"]);
366 assert_eq!(campos_de(&mut sh, "cmd \"$*\""), vec!["uno dos tres"]);
367 assert_eq!(campos_de(&mut sh, "cmd $#"), vec!["2"]);
368 }
369
370 #[test]
371 fn ifs_con_separador_no_blanco() {
372 let mut sh = Shell::nuevo();
373 sh.asignar("IFS", ":");
374 sh.asignar("x", "a:b:c");
375 assert_eq!(campos_de(&mut sh, "cmd $x"), vec!["a", "b", "c"]);
376 }
377
378 #[test]
379 fn lo_entrecomillado_no_se_globea() {
380 let mut sh = Shell::nuevo();
381 // `"/dev/nul*"` es literal; `/dev/nul*` expande
382 assert_eq!(campos_de(&mut sh, "cmd \"/dev/nul*\""), vec!["/dev/nul*"]);
383 assert_eq!(campos_de(&mut sh, "cmd /dev/nul*"), vec!["/dev/null"]);
384 }
385
386 #[test]
387 fn glob_sin_coincidencia_queda_tal_cual() {
388 let mut sh = Shell::nuevo();
389 assert_eq!(campos_de(&mut sh, "cmd /no-existe-jamas*"), vec!["/no-existe-jamas*"]);
390 }
391
392 #[test]
393 fn campo_vacio_entrecomillado_sobrevive() {
394 let mut sh = Shell::nuevo();
395 sh.asignar("vacia", "");
396 assert_eq!(campos_de(&mut sh, "cmd \"$vacia\""), vec![""]);
397 assert!(campos_de(&mut sh, "cmd $vacia").is_empty());
398 }
399}