//! akllay — el awk del producto. //! //! *akllay* es «escoger, seleccionar» en quechua —lo que se hace al separar los granos buenos de //! los malos—, y es exactamente lo que hace awk: elige los registros que casan un patrón y escoge //! campos de cada uno. El nombre sigue el criterio del proyecto (los nombres propios van en //! quechua) y el precedente de `simi`: **lo registra este comentario y lo DECIDE un ADR**. //! //! ⚠ **Se llamó `qillqa` («escritura») durante un commit, y ese nombre ESTABA TOMADO**: en //! `tawasuyu/02_ruway/qillqa` vive un producto entero —registro público a prueba de manipulación, //! 16 crates, SDD propio— y su `qillqa-cli` instala un binario llamado exactamente `qillqa`. Dos //! binarios homónimos en el mismo `/usr/bin` es una colisión de la que no avisa nada: gana el //! último que hidrate. Lo peor es que la práctica de comprobarlo ya estaba escrita en el otro repo //! (`01_yachay/qhali/SDD.md`: «Nombre: `qhali`. Verificado sin colisión»), y no la seguí. //! Verificado ahora, en los dos repos y contra los 2193 binarios del store: `akllay` está libre, y //! `simi` también lo estaba —sus apariciones en tawasuyu son «runa simi», el nombre del idioma. //! //! Lo que no está sujeto a decisión es el nombre del comando instalado: la receta enlaza `awk`, //! porque eso es contrato y lo llaman los `configure` de medio corpus. //! //! Uso: akllay [-F sepc] [-v var=valor]… 'programa' [fichero | var=valor]… //! akllay [-F sepc] [-v var=valor]… -f fuente.awk [-f más.awk]… [fichero | var=valor]… mod ast; mod formato; mod interp; mod lexer; mod parser; mod regex; mod valor; use interp::{Flujo, Interprete}; use std::io::Write; use valor::Valor; fn uso() -> ! { eprintln!("uso: akllay [-F sepc] [-v var=valor]... 'programa' [fichero...]"); eprintln!(" akllay [-F sepc] [-v var=valor]... -f fuente.awk [fichero...]"); std::process::exit(2); } fn main() { let argv: Vec = std::env::args().collect(); let mut fuentes: Vec = Vec::new(); let mut asignaciones: Vec = Vec::new(); let mut fs: Option = None; let mut texto_programa: Option = None; let mut i = 1; while i < argv.len() { let a = &argv[i]; if a == "--" { i += 1; break; } if a == "-" || !a.starts_with('-') { break; } // Las opciones se pueden pegar al valor (`-F:`) o separar (`-F :`), como en todo POSIX. let (op, pegado) = { let cs: Vec = a.chars().collect(); (cs[1], cs[2..].iter().collect::()) }; let mut toma = |pegado: &str, i: &mut usize| -> String { if !pegado.is_empty() { return pegado.to_string(); } *i += 1; argv.get(*i).cloned().unwrap_or_else(|| uso()) }; match op { 'F' => { let v = toma(&pegado, &mut i); // `-F '\t'` llega con la barra literal: hay que desescaparlo como una cadena. // Y `-F t` es el tabulador por compatibilidad histórica con el awk original. fs = Some(if v == "t" { "\t".to_string() } else { interp::desescapar(&v) }); } 'v' => asignaciones.push(toma(&pegado, &mut i)), 'f' => { let v = toma(&pegado, &mut i); match std::fs::read_to_string(&v) { Ok(s) => fuentes.push(s), Err(e) => { eprintln!("akllay: no puedo leer `{v}`: {e}"); std::process::exit(2); } } } _ => uso(), } i += 1; } if fuentes.is_empty() { match argv.get(i) { None => uso(), Some(p) => { texto_programa = Some(p.clone()); i += 1; } } } let fuente = match texto_programa { Some(t) => t, None => fuentes.join("\n") }; let prog = match parser::Parser::nuevo(&fuente).and_then(|mut p| p.programa()) { Ok(p) => p, Err(e) => { eprintln!("akllay: {e}"); std::process::exit(2); } }; let mut it = Interprete::nuevo(prog); // ENVIRON, antes que nada: un BEGIN puede leerlo. { let env = it.array_publico("ENVIRON"); for (k, v) in std::env::vars() { env.borrow_mut().insert(k, Valor::de_entrada(&v)); } } if let Some(f) = fs { it.poner_var("FS", Valor::Cad(f)); } for a in &asignaciones { match interp::posicion_asignacion(a) { Some(p) => it.poner_var(&a[..p], Valor::de_entrada(&interp::desescapar(&a[p + 1..]))), None => { eprintln!("akllay: `-v {a}` no tiene forma var=valor"); std::process::exit(2); } } } // ARGV/ARGC: ARGV[0] es el nombre del programa y los operandos van detrás. { let operandos: Vec = argv[i..].to_vec(); let arr = it.array_publico("ARGV"); arr.borrow_mut().insert("0".into(), Valor::Cad("akllay".into())); for (n, o) in operandos.iter().enumerate() { arr.borrow_mut().insert((n + 1).to_string(), Valor::de_entrada(o)); } it.poner_var("ARGC", Valor::Num((operandos.len() + 1) as f64)); } let mut codigo = 0; let mut salir_ya = false; match it.ejecutar_begin() { Err(e) => { fin_con_error(&mut it, &e); } Ok(Flujo::Sale(c)) => { codigo = c; salir_ya = true; } Ok(_) => {} } // El bucle principal sólo corre si hay reglas que no sean BEGIN. Si el programa es *sólo* un // BEGIN, awk NO lee la entrada — y eso importa: `awk 'BEGIN{print 1}'` no se cuelga esperando. if !salir_ya && (it.hay_main() || it.hay_end()) { 'registros: loop { let r = match it.siguiente_registro() { None => break, Some(r) => r }; it.poner_registro(r); match it.ejecutar_reglas() { Err(e) => { fin_con_error(&mut it, &e); } Ok(Flujo::Sale(c)) => { codigo = c; salir_ya = true; break 'registros; } Ok(Flujo::SiguienteFichero) => { it.cerrar_fuente_actual(); } Ok(_) => {} } } } if !salir_ya || it.hay_end() { // POSIX: un `exit` en BEGIN o en el cuerpo SÍ ejecuta los END, salvo que el propio END // llame a `exit`. Por eso se entra acá incluso con `salir_ya`. match it.ejecutar_end() { Err(e) => { fin_con_error(&mut it, &e); } Ok(Flujo::Sale(c)) => { codigo = c; } Ok(_) => {} } } it.cerrar_todo(); if it.codigo_salida != 0 && codigo == 0 { codigo = it.codigo_salida; } std::process::exit(codigo); } fn fin_con_error(it: &mut Interprete, e: &str) -> ! { // `@salir:N` es cómo viaja un `exit` desde dentro de una función: no es un error de verdad. if let Some(n) = e.strip_prefix("@salir:") { let c = n.parse().unwrap_or(0); let _ = it.ejecutar_end(); it.cerrar_todo(); std::process::exit(c); } it.cerrar_todo(); let _ = std::io::stderr().flush(); eprintln!("akllay: {e}"); std::process::exit(2); }