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: historial, autocompletado y edicion de linea — ya es shell de login
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}