tawasuyu

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 1184Shell 70Rust 62Markdown 501503 Definitions

⎇ main crates/simi/src/builtins.rs Rust · 871 lines · 28.3 KB History Who touched what raw

simi: historial, autocompletado y edicion de linea — ya es shell de login 6dcaff428 · Sergio ·

1//! Los builtins. Son los que el producto usa de verdad: la medición de las cards dio ocho
2//! (`exit` `echo` `exec` `test` `[` `cd` `command` `continue`) y la de los `/init` empotrados suma
3//! `set`, `read`, `printf`, `export`, `trap`, `shift`, `break`, `return`, `.`, `eval`, `local`,
4//! `unset`, `pwd`, `true`, `false`, `:`, `wait`, `umask`, `type`, `hash`.
5//!
6//! `getopts` y `times` NO están todavía, y eso se dice en vez de fingirlo: ninguna card ni ningún
7//! `/init` los usa; los guiones del hub sí, y ésos corren bajo bash.
8
9use std::io::{BufRead, Write};
10
11use crate::exec;
12use crate::shell::{Flujo, Shell, LLEGO_SENAL};
13
14const BUILTINS: &[&str] = &[
15    ":", ".", "break", "cd", "command", "continue", "echo", "eval", "exec", "exit", "export",
16    "false", "hash", "local", "printf", "pwd", "read", "return", "set", "shift", "test", "[",
17    "trap", "true", "type", "umask", "unset", "wait",
18];
19
20pub fn es_builtin(n: &str) -> bool {
21    BUILTINS.contains(&n)
22}
23
24/// Para el autocompletado de `linea`: completar una ORDEN tiene que ofrecer los builtins, que no
25/// están en el `PATH` y si no se nombran acá se quedarían fuera.
26pub fn nombres() -> &'static [&'static str] {
27    BUILTINS
28}
29
30pub fn correr(sh: &mut Shell, argv: &[String]) -> Flujo {
31    let args = &argv[1..];
32    match argv[0].as_str() {
33        ":" | "true" => {
34            sh.estado = 0;
35            Flujo::Normal
36        }
37        "false" => {
38            sh.estado = 1;
39            Flujo::Normal
40        }
41        "echo" => bi_echo(sh, args),
42        "printf" => bi_printf(sh, args),
43        "pwd" => {
44            let d = sh.leer("PWD").unwrap_or_default();
45            println!("{d}");
46            sh.estado = 0;
47            Flujo::Normal
48        }
49        "cd" => bi_cd(sh, args),
50        "exit" => {
51            let c = args.first().and_then(|s| s.parse().ok()).unwrap_or(sh.estado);
52            Flujo::Exit(c)
53        }
54        "return" => {
55            let c = args.first().and_then(|s| s.parse().ok()).unwrap_or(sh.estado);
56            sh.estado = c;
57            if sh.funcion_honda == 0 {
58                // `return` fuera de una función: POSIX lo deja sin especificar; ash sale.
59                return Flujo::Exit(c);
60            }
61            Flujo::Return
62        }
63        "break" | "continue" => {
64            let n: u32 = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
65            if sh.bucles == 0 {
66                sh.estado = 0;
67                return Flujo::Normal;
68            }
69            sh.estado = 0;
70            if argv[0] == "break" {
71                Flujo::Break(n.max(1))
72            } else {
73                Flujo::Continue(n.max(1))
74            }
75        }
76        "shift" => {
77            let n: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(1);
78            // ⚠ Más allá de `$#` el estado es 1 y los posicionales NO se tocan. brush devolvía 2
79            // acá (medido en el banco POSIX); POSIX, busybox y bash devuelven 1.
80            if n > sh.params.len() {
81                sh.estado = 1;
82            } else {
83                sh.params.drain(..n);
84                sh.estado = 0;
85            }
86            Flujo::Normal
87        }
88        "export" => {
89            for a in args {
90                match a.split_once('=') {
91                    Some((k, v)) => {
92                        sh.asignar(k, v);
93                        sh.exportar(k);
94                    }
95                    None => sh.exportar(a),
96                }
97            }
98            sh.estado = 0;
99            Flujo::Normal
100        }
101        "local" => {
102            if sh.funcion_honda == 0 {
103                sh.error("local: fuera de una función");
104                sh.estado = 1;
105                return Flujo::Normal;
106            }
107            for a in args {
108                match a.split_once('=') {
109                    Some((k, v)) => {
110                        sh.declarar_local(k);
111                        sh.asignar(k, v);
112                    }
113                    None => {
114                        sh.declarar_local(a);
115                        sh.vars.insert(a.clone(), (String::new(), false));
116                    }
117                }
118            }
119            sh.estado = 0;
120            Flujo::Normal
121        }
122        "unset" => {
123            for a in args {
124                if a == "-f" {
125                    continue;
126                }
127                sh.vars.remove(a);
128                sh.funcs.remove(a);
129            }
130            sh.estado = 0;
131            Flujo::Normal
132        }
133        "set" => bi_set(sh, args),
134        "eval" => {
135            let texto = args.join(" ");
136            match crate::parser::parsear(&texto) {
137                Ok(l) => exec::correr_lista(sh, &l),
138                Err(e) => {
139                    sh.error(&format!("eval: {e}"));
140                    sh.estado = 2;
141                    Flujo::Normal
142                }
143            }
144        }
145        "." => bi_punto(sh, args),
146        "exec" => {
147            if args.is_empty() {
148                sh.estado = 0;
149                return Flujo::Normal;
150            }
151            exec::lanzar_externo(sh, args, &[], &[], true)
152        }
153        "command" => {
154            let resto: Vec<String> = args.iter().filter(|a| a.as_str() != "-p").cloned().collect();
155            if resto.first().map(|s| s.as_str()) == Some("-v") {
156                let Some(n) = resto.get(1) else {
157                    sh.estado = 2;
158                    return Flujo::Normal;
159                };
160                if es_builtin(n) || sh.funcs.contains_key(n) {
161                    println!("{n}");
162                    sh.estado = 0;
163                } else if let Some(r) = ruta_de(sh, n) {
164                    println!("{r}");
165                    sh.estado = 0;
166                } else {
167                    sh.estado = 1;
168                }
169                return Flujo::Normal;
170            }
171            if resto.is_empty() {
172                sh.estado = 0;
173                return Flujo::Normal;
174            }
175            if es_builtin(&resto[0]) {
176                return correr(sh, &resto);
177            }
178            exec::lanzar_externo(sh, &resto, &[], &[], false)
179        }
180        "type" => {
181            for a in args {
182                if es_builtin(a) {
183                    println!("{a} is a shell builtin");
184                } else if sh.funcs.contains_key(a) {
185                    println!("{a} is a function");
186                } else if let Some(r) = ruta_de(sh, a) {
187                    println!("{a} is {r}");
188                } else {
189                    println!("{a}: not found");
190                    sh.estado = 1;
191                    return Flujo::Normal;
192                }
193            }
194            sh.estado = 0;
195            Flujo::Normal
196        }
197        "hash" => {
198            sh.estado = 0;
199            Flujo::Normal
200        }
201        "umask" => {
202            match args.first() {
203                None => {
204                    let previa = unsafe { libc::umask(0o022) };
205                    unsafe { libc::umask(previa) };
206                    println!("{previa:04o}");
207                }
208                Some(v) => match u32::from_str_radix(v, 8) {
209                    Ok(m) => {
210                        unsafe { libc::umask(m as libc::mode_t) };
211                    }
212                    Err(_) => {
213                        sh.error(&format!("umask: {v}: no es octal"));
214                        sh.estado = 1;
215                        return Flujo::Normal;
216                    }
217                },
218            }
219            sh.estado = 0;
220            Flujo::Normal
221        }
222        "wait" => {
223            // sin argumentos: esperar a todos los hijos
224            loop {
225                let r = unsafe { libc::wait(std::ptr::null_mut()) };
226                if r < 0 {
227                    break;
228                }
229            }
230            sh.estado = 0;
231            Flujo::Normal
232        }
233        "trap" => bi_trap(sh, args),
234        "read" => bi_read(sh, args),
235        "test" | "[" => {
236            let mut a: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
237            if argv[0] == "[" {
238                if a.last() != Some(&"]") {
239                    sh.error("[: falta `]`");
240                    sh.estado = 2;
241                    return Flujo::Normal;
242                }
243                a.pop();
244            }
245            match evaluar_test(&a) {
246                Ok(v) => sh.estado = !v as i32,
247                Err(e) => {
248                    sh.error(&format!("test: {e}"));
249                    sh.estado = 2;
250                }
251            }
252            Flujo::Normal
253        }
254        otro => {
255            sh.error(&format!("{otro}: builtin sin implementar"));
256            sh.estado = 127;
257            Flujo::Normal
258        }
259    }
260}
261
262fn ruta_de(sh: &Shell, n: &str) -> Option<String> {
263    if n.contains('/') {
264        return Some(n.to_string());
265    }
266    let path = sh.leer("PATH").unwrap_or_else(|| "/usr/bin:/bin".into());
267    for dir in path.split(':') {
268        let cand = format!("{}/{n}", if dir.is_empty() { "." } else { dir });
269        if std::fs::metadata(&cand).is_ok() {
270            return Some(cand);
271        }
272    }
273    None
274}
275
276fn bi_echo(sh: &mut Shell, args: &[String]) -> Flujo {
277    // POSIX no da banderas a `echo`; `-n` es universal y el árbol lo usa. `-e` NO: para eso está
278    // `printf`, y fingirlo sería adoptar una divergencia.
279    let (sin_salto, resto) = match args.first().map(|s| s.as_str()) {
280        Some("-n") => (true, &args[1..]),
281        _ => (false, args),
282    };
283    let salida = resto.join(" ");
284    let mut out = std::io::stdout().lock();
285    let _ = out.write_all(salida.as_bytes());
286    if !sin_salto {
287        let _ = out.write_all(b"\n");
288    }
289    let _ = out.flush();
290    sh.estado = 0;
291    Flujo::Normal
292}
293
294fn bi_printf(sh: &mut Shell, args: &[String]) -> Flujo {
295    let Some(formato) = args.first() else {
296        sh.error("printf: falta el formato");
297        sh.estado = 2;
298        return Flujo::Normal;
299    };
300    let datos = &args[1..];
301    let mut i = 0;
302    let mut salida = String::new();
303    loop {
304        let consumidos = formatear(formato, datos, i, &mut salida);
305        match consumidos {
306            Err(e) => {
307                sh.error(&format!("printf: {e}"));
308                sh.estado = 1;
309                return Flujo::Normal;
310            }
311            Ok(0) => break,
312            Ok(n) => {
313                i += n;
314                if i >= datos.len() {
315                    break;
316                }
317            }
318        }
319    }
320    let mut out = std::io::stdout().lock();
321    let _ = out.write_all(salida.as_bytes());
322    let _ = out.flush();
323    sh.estado = 0;
324    Flujo::Normal
325}
326
327/// Una pasada del formato. Devuelve cuántos argumentos consumió (0 = el formato no tiene
328/// conversiones, o sea que no hay que repetirlo).
329fn formatear(
330    formato: &str,
331    datos: &[String],
332    desde: usize,
333    salida: &mut String,
334) -> Result<usize, String> {
335    let cs: Vec<char> = formato.chars().collect();
336    let mut i = 0;
337    let mut usados = 0;
338    while i < cs.len() {
339        match cs[i] {
340            '\\' if i + 1 < cs.len() => {
341                salida.push_str(&escape(cs[i + 1]));
342                i += 2;
343            }
344            '%' if i + 1 < cs.len() => {
345                if cs[i + 1] == '%' {
346                    salida.push('%');
347                    i += 2;
348                    continue;
349                }
350                // banderas, ancho y precisión
351                let mut j = i + 1;
352                let mut spec = String::from("%");
353                while j < cs.len() && "-+ #0".contains(cs[j]) {
354                    spec.push(cs[j]);
355                    j += 1;
356                }
357                let mut ancho = String::new();
358                while j < cs.len() && cs[j].is_ascii_digit() {
359                    ancho.push(cs[j]);
360                    j += 1;
361                }
362                let mut precision = String::new();
363                if j < cs.len() && cs[j] == '.' {
364                    j += 1;
365                    while j < cs.len() && cs[j].is_ascii_digit() {
366                        precision.push(cs[j]);
367                        j += 1;
368                    }
369                }
370                let Some(conv) = cs.get(j) else { return Err("formato incompleto".into()) };
371                let arg = datos.get(desde + usados).cloned().unwrap_or_default();
372                usados += 1;
373                let texto = match conv {
374                    's' => arg,
375                    'b' => {
376                        let mut s = String::new();
377                        let a: Vec<char> = arg.chars().collect();
378                        let mut k = 0;
379                        while k < a.len() {
380                            if a[k] == '\\' && k + 1 < a.len() {
381                                s.push_str(&escape(a[k + 1]));
382                                k += 2;
383                            } else {
384                                s.push(a[k]);
385                                k += 1;
386                            }
387                        }
388                        s
389                    }
390                    'd' | 'i' => {
391                        let n: i64 = arg.trim().parse().unwrap_or(0);
392                        n.to_string()
393                    }
394                    'u' => {
395                        let n: i64 = arg.trim().parse().unwrap_or(0);
396                        (n.max(0)).to_string()
397                    }
398                    'x' => format!("{:x}", arg.trim().parse::<i64>().unwrap_or(0)),
399                    'X' => format!("{:X}", arg.trim().parse::<i64>().unwrap_or(0)),
400                    'o' => format!("{:o}", arg.trim().parse::<i64>().unwrap_or(0)),
401                    'c' => arg.chars().next().map(|c| c.to_string()).unwrap_or_default(),
402                    c => return Err(format!("conversión `%{c}` no soportada")),
403                };
404                let texto = match precision.parse::<usize>() {
405                    Ok(p) if matches!(conv, 's' | 'b') => texto.chars().take(p).collect(),
406                    _ => texto,
407                };
408                let ancho_n: usize = ancho.parse().unwrap_or(0);
409                let izquierda = spec.contains('-');
410                let cero = spec.contains('0') && !izquierda;
411                if texto.chars().count() < ancho_n {
412                    let relleno: String = std::iter::repeat(if cero { '0' } else { ' ' })
413                        .take(ancho_n - texto.chars().count())
414                        .collect();
415                    if izquierda {
416                        salida.push_str(&texto);
417                        salida.push_str(&relleno);
418                    } else {
419                        salida.push_str(&relleno);
420                        salida.push_str(&texto);
421                    }
422                } else {
423                    salida.push_str(&texto);
424                }
425                i = j + 1;
426            }
427            c => {
428                salida.push(c);
429                i += 1;
430            }
431        }
432    }
433    Ok(usados)
434}
435
436fn escape(c: char) -> String {
437    match c {
438        'n' => "\n".into(),
439        't' => "\t".into(),
440        'r' => "\r".into(),
441        '\\' => "\\".into(),
442        '0' => "\0".into(),
443        'a' => "\x07".into(),
444        'b' => "\x08".into(),
445        'f' => "\x0c".into(),
446        'v' => "\x0b".into(),
447        otro => format!("\\{otro}"),
448    }
449}
450
451fn bi_cd(sh: &mut Shell, args: &[String]) -> Flujo {
452    let destino = match args.first() {
453        Some(d) if d == "-" => sh.leer("OLDPWD").unwrap_or_default(),
454        Some(d) => d.clone(),
455        None => sh.leer("HOME").unwrap_or_default(),
456    };
457    if destino.is_empty() {
458        sh.error("cd: HOME sin definir");
459        sh.estado = 1;
460        return Flujo::Normal;
461    }
462    let previo = sh.leer("PWD").unwrap_or_default();
463    match std::env::set_current_dir(&destino) {
464        Ok(()) => {
465            let nuevo = std::env::current_dir()
466                .map(|d| d.to_string_lossy().into_owned())
467                .unwrap_or(destino);
468            sh.asignar("OLDPWD", &previo);
469            sh.exportar("OLDPWD");
470            sh.asignar("PWD", &nuevo);
471            sh.exportar("PWD");
472            sh.estado = 0;
473        }
474        Err(e) => {
475            sh.error(&format!("cd: {destino}: {e}"));
476            sh.estado = 1;
477        }
478    }
479    Flujo::Normal
480}
481
482fn bi_set(sh: &mut Shell, args: &[String]) -> Flujo {
483    if args.is_empty() {
484        let mut nombres: Vec<&String> = sh.vars.keys().collect();
485        nombres.sort();
486        for n in nombres {
487            println!("{n}={}", sh.vars[n].0);
488        }
489        sh.estado = 0;
490        return Flujo::Normal;
491    }
492    let mut i = 0;
493    while i < args.len() {
494        let a = &args[i];
495        if a == "--" {
496            i += 1;
497            break;
498        }
499        let (prender, letras) = match a.strip_prefix('-') {
500            Some(l) => (true, l),
501            None => match a.strip_prefix('+') {
502                Some(l) => (false, l),
503                None => break,
504            },
505        };
506        for c in letras.chars() {
507            match c {
508                'e' => sh.opts.errexit = prender,
509                'u' => sh.opts.nounset = prender,
510                'x' => sh.opts.xtrace = prender,
511                'n' => sh.opts.noexec = prender,
512                'f' => sh.opts.noglob = prender,
513                otro => {
514                    sh.error(&format!("set: -{otro}: opción no soportada"));
515                    sh.estado = 2;
516                    return Flujo::Normal;
517                }
518            }
519        }
520        i += 1;
521    }
522    if i < args.len() || args.iter().any(|a| a == "--") {
523        sh.params = args[i..].to_vec();
524    }
525    sh.estado = 0;
526    Flujo::Normal
527}
528
529fn bi_punto(sh: &mut Shell, args: &[String]) -> Flujo {
530    let Some(ruta) = args.first() else {
531        sh.error(".: falta el fichero");
532        sh.estado = 2;
533        return Flujo::Normal;
534    };
535    let ruta = if ruta.contains('/') {
536        ruta.clone()
537    } else {
538        ruta_de(sh, ruta).unwrap_or_else(|| ruta.clone())
539    };
540    let texto = match std::fs::read_to_string(&ruta) {
541        Ok(t) => t,
542        Err(e) => {
543            sh.error(&format!(".: {ruta}: {e}"));
544            sh.estado = 1;
545            return Flujo::Normal;
546        }
547    };
548    match crate::parser::parsear(&texto) {
549        Ok(l) => {
550            let f = exec::correr_lista(sh, &l);
551            match f {
552                Flujo::Return => Flujo::Normal,
553                otro => otro,
554            }
555        }
556        Err(e) => {
557            sh.error(&format!(".: {ruta}: {e}"));
558            sh.estado = 2;
559            Flujo::Normal
560        }
561    }
562}
563
564fn bi_trap(sh: &mut Shell, args: &[String]) -> Flujo {
565    if args.is_empty() {
566        let mut ks: Vec<&String> = sh.traps.keys().collect();
567        ks.sort();
568        for k in ks {
569            println!("trap -- '{}' {}", sh.traps[k], k);
570        }
571        sh.estado = 0;
572        return Flujo::Normal;
573    }
574    let (accion, senales) = (&args[0], &args[1..]);
575    for s in senales {
576        let nombre = normalizar_senal(s);
577        if accion.is_empty() || accion == "-" {
578            sh.traps.remove(&nombre);
579            if let Some(n) = numero_de_senal(&nombre) {
580                unsafe { libc::signal(n, libc::SIG_DFL) };
581            }
582            continue;
583        }
584        sh.traps.insert(nombre.clone(), accion.clone());
585        if let Some(n) = numero_de_senal(&nombre) {
586            unsafe { libc::signal(n, manejar as libc::sighandler_t) };
587        }
588    }
589    sh.estado = 0;
590    Flujo::Normal
591}
592
593extern "C" fn manejar(n: libc::c_int) {
594    if (n as usize) < LLEGO_SENAL.len() {
595        LLEGO_SENAL[n as usize].store(true, std::sync::atomic::Ordering::SeqCst);
596    }
597}
598
599const SENALES: &[(&str, i32)] = &[
600    ("HUP", libc::SIGHUP),
601    ("INT", libc::SIGINT),
602    ("QUIT", libc::SIGQUIT),
603    ("TERM", libc::SIGTERM),
604    ("USR1", libc::SIGUSR1),
605    ("USR2", libc::SIGUSR2),
606    ("PIPE", libc::SIGPIPE),
607    ("ALRM", libc::SIGALRM),
608];
609
610fn normalizar_senal(s: &str) -> String {
611    let limpio = s.trim_start_matches("SIG").to_uppercase();
612    if limpio == "EXIT" || limpio == "0" {
613        return "EXIT".into();
614    }
615    if let Ok(n) = limpio.parse::<i32>() {
616        if let Some((nombre, _)) = SENALES.iter().find(|(_, v)| *v == n) {
617            return (*nombre).to_string();
618        }
619    }
620    limpio
621}
622
623fn numero_de_senal(nombre: &str) -> Option<i32> {
624    SENALES.iter().find(|(n, _)| *n == nombre).map(|(_, v)| *v)
625}
626
627pub fn nombre_de_senal(n: i32) -> String {
628    SENALES
629        .iter()
630        .find(|(_, v)| *v == n)
631        .map(|(nombre, _)| (*nombre).to_string())
632        .unwrap_or_else(|| n.to_string())
633}
634
635fn bi_read(sh: &mut Shell, args: &[String]) -> Flujo {
636    let sin_escapes = args.first().map(|s| s.as_str()) == Some("-r");
637    let nombres: Vec<String> =
638        args[if sin_escapes { 1 } else { 0 }..].iter().cloned().collect();
639    let nombres = if nombres.is_empty() { vec!["REPLY".to_string()] } else { nombres };
640    let mut linea = String::new();
641    let leidos = std::io::stdin().lock().read_line(&mut linea).unwrap_or(0);
642    if leidos == 0 {
643        sh.estado = 1;
644        return Flujo::Normal;
645    }
646    while linea.ends_with('\n') || linea.ends_with('\r') {
647        linea.pop();
648    }
649    if !sin_escapes {
650        linea = linea.replace("\\\n", "");
651    }
652    let ifs = sh.ifs();
653    let sep: Vec<char> = ifs.chars().collect();
654    let campos: Vec<&str> = linea
655        .split(|c| sep.contains(&c))
656        .filter(|s| !s.is_empty() || sep.iter().any(|c| !c.is_whitespace()))
657        .collect();
658    for (n, nombre) in nombres.iter().enumerate() {
659        let valor = if n + 1 == nombres.len() {
660            // el último se come el resto
661            campos[n.min(campos.len())..].join(" ")
662        } else {
663            campos.get(n).copied().unwrap_or("").to_string()
664        };
665        sh.asignar(nombre, &valor);
666    }
667    sh.estado = 0;
668    Flujo::Normal
669}
670
671// ───────────────────────────────────────────────────────── test
672
673/// `test` con el algoritmo por CANTIDAD de argumentos de POSIX §test, que es el que hace que
674/// `test "$x" = y` funcione con `$x` vacío o con `$x` valiendo `-f`.
675pub fn evaluar_test(a: &[&str]) -> Result<bool, String> {
676    match a.len() {
677        0 => Ok(false),
678        1 => Ok(!a[0].is_empty()),
679        2 => {
680            if a[0] == "!" {
681                return Ok(a[1].is_empty());
682            }
683            unario(a[0], a[1])
684        }
685        3 => {
686            if let Some(v) = binario(a[0], a[1], a[2])? {
687                return Ok(v);
688            }
689            if a[0] == "!" {
690                return Ok(!evaluar_test(&a[1..])?);
691            }
692            if a[0] == "(" && a[2] == ")" {
693                return evaluar_test(&a[1..2]);
694            }
695            Err(format!("expresión inválida: {a:?}"))
696        }
697        4 => {
698            if a[0] == "!" {
699                return Ok(!evaluar_test(&a[1..])?);
700            }
701            if a[0] == "(" && a[3] == ")" {
702                return evaluar_test(&a[1..3]);
703            }
704            general(a)
705        }
706        _ => general(a),
707    }
708}
709
710/// `-a` / `-o` / `!` / paréntesis, con `-a` más fuerte que `-o`.
711fn general(a: &[&str]) -> Result<bool, String> {
712    // partir por el `-o` de más afuera
713    if let Some(i) = buscar_al_nivel(a, "-o") {
714        return Ok(general(&a[..i])? || general(&a[i + 1..])?);
715    }
716    if let Some(i) = buscar_al_nivel(a, "-a") {
717        return Ok(general(&a[..i])? && general(&a[i + 1..])?);
718    }
719    if a.first() == Some(&"!") {
720        return Ok(!general(&a[1..])?);
721    }
722    if a.first() == Some(&"(") && a.last() == Some(&")") {
723        return general(&a[1..a.len() - 1]);
724    }
725    evaluar_test(a)
726}
727
728fn buscar_al_nivel(a: &[&str], op: &str) -> Option<usize> {
729    let mut prof = 0;
730    for (i, t) in a.iter().enumerate() {
731        match *t {
732            "(" => prof += 1,
733            ")" => prof -= 1,
734            _ if *t == op && prof == 0 => return Some(i),
735            _ => {}
736        }
737    }
738    None
739}
740
741fn unario(op: &str, arg: &str) -> Result<bool, String> {
742    let meta = std::fs::metadata(arg);
743    let lmeta = std::fs::symlink_metadata(arg);
744    Ok(match op {
745        "-e" => lmeta.is_ok(),
746        "-f" => meta.map(|m| m.is_file()).unwrap_or(false),
747        "-d" => meta.map(|m| m.is_dir()).unwrap_or(false),
748        "-h" | "-L" => lmeta.map(|m| m.file_type().is_symlink()).unwrap_or(false),
749        "-s" => meta.map(|m| m.len() > 0).unwrap_or(false),
750        "-r" => acceso(arg, libc::R_OK),
751        "-w" => acceso(arg, libc::W_OK),
752        "-x" => acceso(arg, libc::X_OK),
753        "-n" => !arg.is_empty(),
754        "-z" => arg.is_empty(),
755        "-t" => {
756            let fd: i32 = arg.parse().unwrap_or(-1);
757            fd >= 0 && unsafe { libc::isatty(fd) } == 1
758        }
759        "-p" | "-S" | "-b" | "-c" | "-g" | "-u" | "-k" | "-O" | "-G" => {
760            use std::os::unix::fs::FileTypeExt;
761            match lmeta {
762                Err(_) => false,
763                Ok(m) => match op {
764                    "-p" => m.file_type().is_fifo(),
765                    "-S" => m.file_type().is_socket(),
766                    "-b" => m.file_type().is_block_device(),
767                    "-c" => m.file_type().is_char_device(),
768                    _ => false,
769                },
770            }
771        }
772        otro => return Err(format!("operador unario `{otro}` no soportado")),
773    })
774}
775
776fn acceso(ruta: &str, modo: i32) -> bool {
777    match std::ffi::CString::new(ruta) {
778        Ok(c) => (unsafe { libc::access(c.as_ptr(), modo) }) == 0,
779        Err(_) => false,
780    }
781}
782
783fn binario(a: &str, op: &str, b: &str) -> Result<Option<bool>, String> {
784    let num = |s: &str| -> Result<i64, String> {
785        s.trim().parse().map_err(|_| format!("`{s}`: se esperaba un entero"))
786    };
787    Ok(Some(match op {
788        "=" | "==" => a == b,
789        "!=" => a != b,
790        "<" => a < b,
791        ">" => a > b,
792        "-eq" => num(a)? == num(b)?,
793        "-ne" => num(a)? != num(b)?,
794        "-lt" => num(a)? < num(b)?,
795        "-le" => num(a)? <= num(b)?,
796        "-gt" => num(a)? > num(b)?,
797        "-ge" => num(a)? >= num(b)?,
798        "-nt" | "-ot" => {
799            let ma = std::fs::metadata(a).and_then(|m| m.modified()).ok();
800            let mb = std::fs::metadata(b).and_then(|m| m.modified()).ok();
801            match (ma, mb) {
802                (Some(x), Some(y)) => {
803                    if op == "-nt" {
804                        x > y
805                    } else {
806                        x < y
807                    }
808                }
809                _ => false,
810            }
811        }
812        "-ef" => {
813            use std::os::unix::fs::MetadataExt;
814            match (std::fs::metadata(a), std::fs::metadata(b)) {
815                (Ok(x), Ok(y)) => x.ino() == y.ino() && x.dev() == y.dev(),
816                _ => false,
817            }
818        }
819        _ => return Ok(None),
820    }))
821}
822
823#[cfg(test)]
824mod pruebas {
825    use super::*;
826
827    #[test]
828    fn test_por_cantidad_de_argumentos() {
829        assert!(evaluar_test(&["x"]).unwrap());
830        assert!(!evaluar_test(&[""]).unwrap());
831        assert!(evaluar_test(&["-n", "x"]).unwrap());
832        assert!(evaluar_test(&["-z", ""]).unwrap());
833        assert!(evaluar_test(&["!", ""]).unwrap());
834        assert!(evaluar_test(&["a", "=", "a"]).unwrap());
835        assert!(evaluar_test(&["1", "-lt", "2"]).unwrap());
836        assert!(!evaluar_test(&["2", "-lt", "2"]).unwrap());
837        // el caso que justifica el algoritmo: `$x` vacío con `=`
838        assert!(evaluar_test(&["", "=", ""]).unwrap());
839        // `-f` como OPERANDO, no como operador
840        assert!(evaluar_test(&["-f", "=", "-f"]).unwrap());
841    }
842
843    #[test]
844    fn test_ficheros_reales() {
845        assert!(evaluar_test(&["-e", "/dev/null"]).unwrap());
846        assert!(evaluar_test(&["-d", "/"]).unwrap());
847        assert!(!evaluar_test(&["-f", "/no-existe-jamas"]).unwrap());
848        assert!(evaluar_test(&["-c", "/dev/null"]).unwrap());
849    }
850
851    #[test]
852    fn test_y_o_y_parentesis() {
853        assert!(evaluar_test(&["-d", "/", "-a", "-e", "/dev/null"]).unwrap());
854        assert!(evaluar_test(&["-f", "/nope", "-o", "-d", "/"]).unwrap());
855        assert!(!evaluar_test(&["!", "-d", "/"]).unwrap());
856        assert!(evaluar_test(&["(", "-d", "/", ")"]).unwrap());
857    }
858
859    #[test]
860    fn printf_formatea() {
861        let mut s = String::new();
862        formatear("%03d\\n", &["7".into()], 0, &mut s).unwrap();
863        assert_eq!(s, "007\n");
864        let mut s = String::new();
865        formatear("%s|%s\\n", &["a".into(), "b".into()], 0, &mut s).unwrap();
866        assert_eq!(s, "a|b\n");
867        let mut s = String::new();
868        formatear("%-4s|", &["x".into()], 0, &mut s).unwrap();
869        assert_eq!(s, "x   |");
870    }
871}