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//! 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}