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

⎇ main crates/simi/src/main.rs Rust · 232 siq'ikuna · 9.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! simi — el `/bin/sh` del producto.
2//!
3//! «simi» es *boca, lengua* en quechua: es cómo se le habla a la máquina.
4//!
5//! **Por qué existe.** El frente C del ADR 0008 pone a busybox en la raíz de confianza del arranque
6//! (`STAGE1_COMPONENTS`, `ATTEST_PATHS`) y en el `exec` de cada card de arje. Botarlo pedía un `sh`,
7//! y el candidato que se midió —brush 0.4.0— pesa 6,9 MB, arranca 3,7× más lento que el ash de
8//! busybox y trae dos divergencias medidas contra POSIX (reubeno/brush#1394 y #1396).
9//!
10//! **Alcance, y está MEDIDO, no supuesto** (`docs/plan-botar-busybox.md`):
11//!
12//! | consumidor | qué usa |
13//! |---|---|
14//! | las 23 cards de arje con shell | 8 builtins, 13 construcciones, ninguna función, ningún `if` |
15//! | los 21 `/init` empotrados de las imágenes | + `if`, funciones, `case`, aquí-documentos, `${…}`, posicionales |
16//! | `takana-live-install.sh` (corre en la imagen viva) | POSIX entero |
17//!
18//! Lo que NO está y se dice en vez de fingirlo: control de trabajos (`jobs`/`fg`/`bg`), edición de
19//! línea, historial, `alias`, `getopts`, `[[ ]]`, arrays. El shell interactivo de `getty` es otro
20//! consumidor y no está cubierto todavía.
21//!
22//! **Extensiones NO-POSIX que sí están** (2026-10-22, a pedido explícito del usuario: «si hay
23//! mejoras que no rompan posix, hazles»). POSIX deja esta sintaxis **sin definir**, así que un
24//! script conforme nunca la escribe y soportarla no puede cambiarle el comportamiento a nadie —
25//! verificado: el banco diferencial sigue en **DIVERGE=0** sobre 1339 casos, y el banco POSIX da
26//! los MISMOS 2 fallos preexistentes (`while-read`, `getopts`) antes y después.
27//!
28//! | forma | qué hace |
29//! |---|---|
30//! | `${x/pat/rep}` `${x//pat/rep}` | sustituir la primera / todas. Patrón glob, coincidencia más larga y más a la izquierda |
31//! | `${x/#pat/rep}` `${x/%pat/rep}` | anclado al principio / al final |
32//! | `${x:desde}` `${x:desde:largo}` | subcadena; los dos índices son expresiones ARITMÉTICAS y un negativo cuenta desde el final |
33//! | `${x^}` `${x^^}` `${x,}` `${x,,}` | caja: primera letra o todas |
34//!
35//! ⚠ **`${!x}` (indirección) NO se añadió**, y es deliberado: choca con el `$!` que el lexer ya
36//! trata como nombre especial, y era la menos usada de las cuatro. Se dice en vez de fingirlo.
37//!
38//! Por qué se añadieron: el corpus las usa. `${t/unknown-/}` en `wasi-libcxx` era la ÚNICA
39//! divergencia de simi en 1339 casos, y rechazarla obligaba a reescribir recetas por una razón
40//! ajena a lo que la receta hace.
41//!
42//! Uso: `simi [-c ORDEN | FICHERO] [-e] [-u] [-x] [-n] [ARGS…]`, y por stdin si no hay ninguno.
43
44mod arith;
45mod ast;
46mod builtins;
47mod exec;
48mod expand;
49mod glob;
50mod lexer;
51mod linea;
52mod parser;
53mod shell;
54
55use std::io::Read;
56
57use shell::{Flujo, Shell};
58
59fn main() -> std::process::ExitCode {
60    let args: Vec<String> = std::env::args().collect();
61    let mut sh = Shell::nuevo();
62    // argv[0] con guión delante = shell de login; hoy sólo cambia el nombre en los mensajes.
63    let argv0 = args.first().cloned().unwrap_or_else(|| "simi".into());
64    sh.nombre = argv0.trim_start_matches('-').rsplit('/').next().unwrap_or("simi").to_string();
65
66    let mut i = 1;
67    let mut orden: Option<String> = None;
68    let mut fichero: Option<String> = None;
69    while i < args.len() {
70        let a = &args[i];
71        match a.as_str() {
72            "-c" => {
73                i += 1;
74                match args.get(i) {
75                    Some(c) => orden = Some(c.clone()),
76                    None => {
77                        eprintln!("{}: -c necesita una orden", sh.nombre);
78                        return std::process::ExitCode::from(2);
79                    }
80                }
81            }
82            "-s" => {}
83            "--" => {
84                i += 1;
85                break;
86            }
87            _ if a.starts_with('-') && a.len() > 1 && !a.starts_with("--") => {
88                for c in a[1..].chars() {
89                    match c {
90                        'e' => sh.opts.errexit = true,
91                        'u' => sh.opts.nounset = true,
92                        'x' => sh.opts.xtrace = true,
93                        'n' => sh.opts.noexec = true,
94                        'f' => sh.opts.noglob = true,
95                        'i' | 'l' => {}
96                        otro => {
97                            eprintln!("{}: -{otro}: opción no soportada", sh.nombre);
98                            return std::process::ExitCode::from(2);
99                        }
100                    }
101                }
102            }
103            _ => {
104                if orden.is_none() {
105                    fichero = Some(a.clone());
106                }
107                break;
108            }
109        }
110        i += 1;
111    }
112
113    // Los argumentos que quedan son los posicionales. Con `-c`, el primero es `$0`.
114    let resto: Vec<String> = args[i..].to_vec();
115    let texto = match (&orden, &fichero) {
116        (Some(c), _) => {
117            if !resto.is_empty() {
118                sh.nombre = resto[0].clone();
119                sh.params = resto[1..].to_vec();
120            }
121            c.clone()
122        }
123        (None, Some(f)) => {
124            sh.nombre = f.clone();
125            sh.params = resto[1..].to_vec();
126            match std::fs::read_to_string(f) {
127                Ok(t) => t,
128                Err(e) => {
129                    eprintln!("{}: {}: {}", argv0, f, e);
130                    return std::process::ExitCode::from(127);
131                }
132            }
133        }
134        (None, None) => {
135            // ⚠ **Sin fichero ni `-c` hay DOS caminos, y antes había uno.** Si la entrada es una
136            // TERMINAL, esto es un shell interactivo y entra al REPL con editor de línea,
137            // historial y Tab (`linea.rs`). Si no lo es —una tubería, `simi < guion`— se traga la
138            // entrada y se ejecuta como script, que es el comportamiento de siempre y el que usan
139            // las cards y los `/init`.
140            //
141            // La distinción es `isatty(0)`, no una bandera: un shell que decide esto por una
142            // opción se equivoca el día que alguien lo invoque de la otra forma.
143            if unsafe { libc::isatty(libc::STDIN_FILENO) } == 1 {
144                return repl(&mut sh);
145            }
146            let mut t = String::new();
147            if std::io::stdin().read_to_string(&mut t).is_err() {
148                eprintln!("{}: no pude leer la entrada", sh.nombre);
149                return std::process::ExitCode::from(2);
150            }
151            t
152        }
153    };
154
155    let lista = match parser::parsear(&texto) {
156        Ok(l) => l,
157        Err(e) => {
158            eprintln!("{}: error de sintaxis: {}", sh.nombre, e);
159            return std::process::ExitCode::from(2);
160        }
161    };
162    // `-n`: parsear y no ejecutar. Es lo que usa el banco diferencial, y lo que hace que un guion
163    // roto se detecte sin correrlo.
164    if sh.opts.noexec {
165        return std::process::ExitCode::SUCCESS;
166    }
167
168    let flujo = exec::correr_lista(&mut sh, &lista);
169    let estado = match flujo {
170        Flujo::Exit(c) => c,
171        _ => sh.estado,
172    };
173    exec::correr_trap_exit(&mut sh, estado);
174    let final_ = if sh.trap_exit_corrido { estado } else { sh.estado };
175    std::process::ExitCode::from((final_ & 0xff) as u8)
176}
177
178/// El bucle interactivo. Separado de la ruta de script a propósito: lee UNA orden, la ejecuta y
179/// vuelve, de modo que un error de sintaxis o un fallo de la orden no termina la sesión — que es
180/// justo lo contrario de lo que debe hacer un script.
181fn repl(sh: &mut Shell) -> std::process::ExitCode {
182    let mut ed = linea::Editor::nuevo();
183    // `PS1` por defecto si nadie la puso. Se ASIGNA (no sólo se usa) para que `echo $PS1` la vea,
184    // que es lo que alguien espera al entrar.
185    if sh.leer("PS1").is_none() {
186        sh.asignar("PS1", "\\u@\\h:\\w\\$ ");
187    }
188    let mut ultimo = 0i32;
189    loop {
190        let p = linea::prompt(sh);
191        match ed.leer(&p) {
192            linea::Linea::Fin => break,
193            linea::Linea::Interrumpida => {
194                // Ctrl-C: `$?` pasa a 130 como en bash (128 + SIGINT), y se sigue.
195                ultimo = 130;
196                sh.estado = 130;
197                continue;
198            }
199            linea::Linea::Texto(t) => {
200                if t.trim().is_empty() {
201                    continue;
202                }
203                match parser::parsear(&t) {
204                    Ok(lista) => {
205                        // `correr_lista` devuelve `Flujo`, no un código: un `exit` escrito en el
206                        // prompt tiene que TERMINAR la sesión con su código, y el resto de los
207                        // flujos (`break`/`continue` sueltos) no significan nada acá y se ignoran.
208                        match exec::correr_lista(sh, &lista) {
209                            Flujo::Exit(c) => {
210                                ultimo = c;
211                                break;
212                            }
213                            _ => ultimo = sh.estado,
214                        }
215                    }
216                    Err(e) => {
217                        // Un error de sintaxis en interactivo NO mata la sesión: se avisa y se
218                        // sigue. `2` es el código que POSIX pide para un error de sintaxis.
219                        eprintln!("{}: {e}", sh.nombre);
220                        ultimo = 2;
221                        sh.estado = 2;
222                    }
223                }
224            }
225        }
226    }
227    // El historial se guarda ANTES del trap de salida: si el trap aborta, lo tecleado no se pierde.
228    ed.guardar();
229    exec::correr_trap_exit(sh, ultimo);
230    let final_ = if sh.trap_exit_corrido { ultimo } else { sh.estado };
231    std::process::ExitCode::from((final_ & 0xff) as u8)
232}