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 815Rust 56Shell 37Markdown 291229 Definiciones

⎇ main crates/simi/src/builtins.rs Rust · 865 líneas · 28.0 KB Historia Quién tocó qué crudo

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