gioser

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…

TOML 1182Shell 87Rust 63Markdown 541592 Definitions

⎇ 13769aa7b41bf597cf8d0ff840d0ecfae76fdb5c crates/simi/src/expand.rs Rust · 399 lines · 14.0 KB History Who touched what raw

simi: el /bin/sh propio del producto — 4448 líneas, sólo libc, y CERO divergencias contra el ash 6809ef4be · Sergio ·

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}