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…
⎇ 6809ef4be28f606aae856890a74667187ff75b05
crates/simi/src/lexer.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
simi: el /bin/sh propio del producto — 4448 líneas, sólo libc, y CERO divergencias contra el ash
1//! Tokenizador. Primero se tokeniza TODO, después se parsea: así los cuerpos de los aquí-documentos
2//! —que viven en las líneas siguientes al operador— ya están resueltos cuando el parser los necesita.
3//!
4//! El sitio delicado de todo el shell está acá: **la barra invertida dentro de `` ` ` ``**
5//! (POSIX.1-2024 §2.6.3). Es lo que rompió a brush (reubeno/brush#1394) y se lleva 25 líneas del
6//! `libtool` que genera cualquier `configure`. La regla, que NO es la de `$( )`:
7//!
8//! > dentro de `` ` ` `` la barra invertida conserva su significado literal, **salvo** cuando
9//! > precede a `$`, `` ` `` o `\`, donde se ELIMINA antes de parsear el texto de la orden.
10//!
11//! Ver `pruebas::comillas_invertidas_posix_2_6_3`, que es el caso de brush calcado.
12
13use crate::ast::{Palabra, ParamOp, Seg};
14
15#[derive(Debug, Clone, PartialEq)]
16pub enum Tok {
17 Pal(Palabra),
18 Op(String),
19 IoNum(i32),
20 /// Referencia al cuerpo del aquí-documento, en la tabla que devuelve `tokenizar`.
21 AquiDoc(usize),
22 Nueva,
23}
24
25pub struct Lexer {
26 src: Vec<char>,
27 i: usize,
28 toks: Vec<Tok>,
29 /// `(cuerpo, expande)`
30 cuerpos: Vec<(String, bool)>,
31 /// aquí-documentos anunciados en esta línea y pendientes de leer su cuerpo
32 pendientes: Vec<(usize, String, bool, bool)>, // (id, delim, expande, quitar_tabs)
33}
34
35/// `(tokens, cuerpos de aquí-documento)`
36pub fn tokenizar(entrada: &str) -> Result<(Vec<Tok>, Vec<(String, bool)>), String> {
37 let mut lx = Lexer {
38 src: entrada.chars().collect(),
39 i: 0,
40 toks: Vec::new(),
41 cuerpos: Vec::new(),
42 pendientes: Vec::new(),
43 };
44 lx.correr()?;
45 Ok((lx.toks, lx.cuerpos))
46}
47
48const OPERADORES: &[&str] = &[
49 "<<-", ";;", "&&", "||", ">>", "<<", ">&", "<&", "<>", ";", "&", "|", "(", ")", "<", ">",
50];
51
52fn es_meta(c: char) -> bool {
53 matches!(c, ' ' | '\t' | '\n' | ';' | '&' | '|' | '(' | ')' | '<' | '>')
54}
55
56impl Lexer {
57 fn ver(&self, n: usize) -> Option<char> {
58 self.src.get(self.i + n).copied()
59 }
60
61 fn correr(&mut self) -> Result<(), String> {
62 loop {
63 // espacios (sin newline) y continuación de línea
64 while let Some(c) = self.ver(0) {
65 if c == ' ' || c == '\t' {
66 self.i += 1;
67 } else if c == '\\' && self.ver(1) == Some('\n') {
68 self.i += 2;
69 } else if c == '#' && self.al_principio_de_palabra() {
70 while self.ver(0).is_some() && self.ver(0) != Some('\n') {
71 self.i += 1;
72 }
73 } else {
74 break;
75 }
76 }
77 let Some(c) = self.ver(0) else { break };
78
79 if c == '\n' {
80 self.i += 1;
81 self.leer_cuerpos_pendientes()?;
82 self.toks.push(Tok::Nueva);
83 continue;
84 }
85 if let Some(op) = self.operador_en_curso() {
86 self.i += op.chars().count();
87 if op == "<<" || op == "<<-" {
88 self.anunciar_aqui_doc(&op)?;
89 } else {
90 self.toks.push(Tok::Op(op));
91 }
92 continue;
93 }
94 let pal = self.leer_palabra()?;
95 // `2>` y `2<`: un número pegado a una redirección es el descriptor, no una palabra.
96 if let Some(n) = numero_de_fd(&pal) {
97 if matches!(self.ver(0), Some('>') | Some('<')) {
98 self.toks.push(Tok::IoNum(n));
99 continue;
100 }
101 }
102 self.toks.push(Tok::Pal(pal));
103 }
104 self.leer_cuerpos_pendientes()?;
105 Ok(())
106 }
107
108 fn al_principio_de_palabra(&self) -> bool {
109 // El carácter INMEDIATAMENTE anterior, sin saltarse los blancos: `a#b` no es comentario
110 // (el `#` va pegado a `a`) y `a #b` sí. Saltarse los blancos invertía las dos.
111 match self.i.checked_sub(1).map(|n| self.src[n]) {
112 None => true,
113 Some(c) => es_meta(c),
114 }
115 }
116
117 fn operador_en_curso(&self) -> Option<String> {
118 let resto: String = self.src[self.i..].iter().take(3).collect();
119 OPERADORES.iter().find(|op| resto.starts_with(**op)).map(|op| op.to_string())
120 }
121
122 fn anunciar_aqui_doc(&mut self, op: &str) -> Result<(), String> {
123 while matches!(self.ver(0), Some(' ') | Some('\t')) {
124 self.i += 1;
125 }
126 // El delimitador: si trae comillas, el cuerpo NO se expande.
127 let mut delim = String::new();
128 let mut comillado = false;
129 while let Some(c) = self.ver(0) {
130 if es_meta(c) {
131 break;
132 }
133 match c {
134 '\'' | '"' => {
135 comillado = true;
136 let cierre = c;
137 self.i += 1;
138 while let Some(d) = self.ver(0) {
139 if d == cierre {
140 break;
141 }
142 delim.push(d);
143 self.i += 1;
144 }
145 self.i += 1;
146 }
147 '\\' => {
148 comillado = true;
149 self.i += 1;
150 if let Some(d) = self.ver(0) {
151 delim.push(d);
152 self.i += 1;
153 }
154 }
155 _ => {
156 delim.push(c);
157 self.i += 1;
158 }
159 }
160 }
161 if delim.is_empty() {
162 return Err("aquí-documento sin delimitador".into());
163 }
164 let id = self.cuerpos.len();
165 self.cuerpos.push((String::new(), !comillado));
166 self.pendientes.push((id, delim, !comillado, op == "<<-"));
167 self.toks.push(Tok::Op("<<".into()));
168 self.toks.push(Tok::AquiDoc(id));
169 Ok(())
170 }
171
172 fn leer_cuerpos_pendientes(&mut self) -> Result<(), String> {
173 let pendientes: Vec<_> = self.pendientes.drain(..).collect();
174 for (id, delim, _expande, quitar_tabs) in pendientes {
175 let mut cuerpo = String::new();
176 loop {
177 if self.i >= self.src.len() {
178 break; // EOF sin delimitador: POSIX avisa; acá se acepta lo leído
179 }
180 let mut linea = String::new();
181 while let Some(c) = self.ver(0) {
182 self.i += 1;
183 if c == '\n' {
184 break;
185 }
186 linea.push(c);
187 }
188 let comparar = if quitar_tabs { linea.trim_start_matches('\t') } else { &linea[..] };
189 if comparar == delim {
190 break;
191 }
192 cuerpo.push_str(comparar);
193 cuerpo.push('\n');
194 }
195 self.cuerpos[id].0 = cuerpo;
196 }
197 Ok(())
198 }
199
200 // ───────────────────────────────────────────── palabras
201
202 fn leer_palabra(&mut self) -> Result<Palabra, String> {
203 let mut segs: Vec<Seg> = Vec::new();
204 // El literal en curso y SU estado de comillado. Cuando el estado cambia hay que cerrar el
205 // seg: si no, `lt_y=\"hola\"` queda como UN seg entrecomillado y el parser ya no ve la
206 // asignación —el prefijo antes del `=` tiene que ser desnudo— así que la ejecuta como una
207 // orden. Es lo que rompía el bucle de `config.status` de libtool, medido con el banco.
208 let mut lit = String::new();
209 let mut lit_q: Option<bool> = None;
210 let mut vacio_comillado = false;
211
212 macro_rules! volcar {
213 () => {
214 if !lit.is_empty() {
215 segs.push(Seg::Lit {
216 texto: std::mem::take(&mut lit),
217 comillado: lit_q.unwrap_or(false),
218 });
219 }
220 lit_q = None;
221 };
222 }
223 macro_rules! empujar {
224 ($c:expr, $q:expr) => {{
225 let q: bool = $q;
226 if lit_q.is_some() && lit_q != Some(q) {
227 volcar!();
228 }
229 lit_q = Some(q);
230 lit.push($c);
231 }};
232 }
233
234 if self.ver(0) == Some('~') {
235 segs.push(Seg::Tilde);
236 self.i += 1;
237 }
238
239 while let Some(c) = self.ver(0) {
240 if es_meta(c) {
241 break;
242 }
243 match c {
244 '\\' => {
245 self.i += 1;
246 match self.ver(0) {
247 Some('\n') => self.i += 1, // continuación de línea: desaparece
248 Some(d) => {
249 empujar!(d, true);
250 self.i += 1;
251 }
252 None => empujar!('\\', true),
253 }
254 }
255 '\'' => {
256 self.i += 1;
257 let mut vacia = true;
258 let mut cerrada = false;
259 while let Some(d) = self.ver(0) {
260 self.i += 1;
261 if d == '\'' {
262 cerrada = true;
263 break;
264 }
265 empujar!(d, true);
266 vacia = false;
267 }
268 if !cerrada {
269 return Err("comilla simple sin cerrar".into());
270 }
271 if vacia && lit.is_empty() && segs.is_empty() {
272 vacio_comillado = true;
273 }
274 }
275 '"' => {
276 self.i += 1;
277 let mut vacia = true;
278 let mut cerrada = false;
279 loop {
280 let Some(d) = self.ver(0) else { break };
281 if d == '"' {
282 self.i += 1;
283 cerrada = true;
284 break;
285 }
286 vacia = false;
287 match d {
288 '\\' => {
289 self.i += 1;
290 match self.ver(0) {
291 // Dentro de comillas dobles la barra sólo escapa a estos cuatro.
292 Some(e @ ('$' | '`' | '"' | '\\')) => {
293 empujar!(e, true);
294 self.i += 1;
295 }
296 Some('\n') => self.i += 1,
297 Some(e) => {
298 empujar!('\\', true);
299 empujar!(e, true);
300 self.i += 1;
301 }
302 None => empujar!('\\', true),
303 }
304 }
305 '$' => {
306 volcar!();
307 let s = self.leer_dolar(true)?;
308 segs.push(s);
309 }
310 '`' => {
311 volcar!();
312 let cuerpo = self.leer_backtick(true)?;
313 segs.push(Seg::CmdSub { cuerpo, comillado: true });
314 }
315 _ => {
316 empujar!(d, true);
317 self.i += 1;
318 }
319 }
320 }
321 if !cerrada {
322 return Err("comilla doble sin cerrar".into());
323 }
324 if vacia && lit.is_empty() && segs.is_empty() {
325 vacio_comillado = true;
326 }
327 }
328 '$' => {
329 volcar!();
330 let s = self.leer_dolar(false)?;
331 segs.push(s);
332 }
333 '`' => {
334 volcar!();
335 let cuerpo = self.leer_backtick(false)?;
336 segs.push(Seg::CmdSub { cuerpo, comillado: false });
337 }
338 _ => {
339 empujar!(c, false);
340 self.i += 1;
341 }
342 }
343 }
344 volcar!();
345 if segs.is_empty() && vacio_comillado {
346 segs.push(Seg::Lit { texto: String::new(), comillado: true });
347 }
348 if segs.is_empty() {
349 return Err("palabra vacía".into());
350 }
351 Ok(segs)
352 }
353
354 /// `$x`, `${…}`, `$(( … ))`, `$( … )`, `$?`, `$1`, `$@`…
355 fn leer_dolar(&mut self, comillado: bool) -> Result<Seg, String> {
356 self.i += 1; // $
357 match self.ver(0) {
358 Some('(') if self.ver(1) == Some('(') => {
359 self.i += 2;
360 let expr = self.leer_hasta_parentesis_doble()?;
361 Ok(Seg::Arith { expr, comillado })
362 }
363 Some('(') => {
364 self.i += 1;
365 let cuerpo = self.leer_hasta_parentesis()?;
366 Ok(Seg::CmdSub { cuerpo, comillado })
367 }
368 Some('{') => {
369 self.i += 1;
370 self.leer_llaves(comillado)
371 }
372 Some(c) if c.is_ascii_digit() => {
373 self.i += 1;
374 Ok(Seg::Param {
375 nombre: c.to_string(),
376 op: ParamOp::Valor,
377 arg: vec![],
378 comillado,
379 })
380 }
381 Some(c @ ('@' | '*' | '#' | '?' | '$' | '!' | '-' | '0')) => {
382 self.i += 1;
383 Ok(Seg::Param { nombre: c.to_string(), op: ParamOp::Valor, arg: vec![], comillado })
384 }
385 Some(c) if c == '_' || c.is_ascii_alphabetic() => {
386 let mut n = String::new();
387 while let Some(d) = self.ver(0) {
388 if d == '_' || d.is_ascii_alphanumeric() {
389 n.push(d);
390 self.i += 1;
391 } else {
392 break;
393 }
394 }
395 Ok(Seg::Param { nombre: n, op: ParamOp::Valor, arg: vec![], comillado })
396 }
397 // `$` solo: literal
398 _ => Ok(Seg::Lit { texto: "$".into(), comillado }),
399 }
400 }
401
402 fn leer_llaves(&mut self, comillado: bool) -> Result<Seg, String> {
403 let largo = if self.ver(0) == Some('#') {
404 // `${#x}`; ojo con `${#}` que es el número de posicionales
405 if matches!(self.ver(1), Some('}')) {
406 self.i += 2;
407 return Ok(Seg::Param {
408 nombre: "#".into(),
409 op: ParamOp::Valor,
410 arg: vec![],
411 comillado,
412 });
413 }
414 self.i += 1;
415 true
416 } else {
417 false
418 };
419 let mut nombre = String::new();
420 if let Some(c @ ('@' | '*' | '#' | '?' | '$' | '!' | '-')) = self.ver(0) {
421 nombre.push(c);
422 self.i += 1;
423 } else {
424 while let Some(d) = self.ver(0) {
425 if d == '_' || d.is_ascii_alphanumeric() {
426 nombre.push(d);
427 self.i += 1;
428 } else {
429 break;
430 }
431 }
432 }
433 if nombre.is_empty() {
434 return Err("${} sin nombre".into());
435 }
436 if largo {
437 if self.ver(0) != Some('}') {
438 return Err("${#x} mal cerrado".into());
439 }
440 self.i += 1;
441 return Ok(Seg::Param { nombre, op: ParamOp::Largo, arg: vec![], comillado });
442 }
443 let op = match self.ver(0) {
444 Some('}') => {
445 self.i += 1;
446 return Ok(Seg::Param { nombre, op: ParamOp::Valor, arg: vec![], comillado });
447 }
448 Some(':') => {
449 self.i += 1;
450 match self.ver(0) {
451 Some('-') => ParamOp::Defecto { dos_puntos: true },
452 Some('=') => ParamOp::Asigna { dos_puntos: true },
453 Some('?') => ParamOp::Error { dos_puntos: true },
454 Some('+') => ParamOp::Alterno { dos_puntos: true },
455 _ => return Err("operador ${x:…} no soportado".into()),
456 }
457 }
458 Some('-') => ParamOp::Defecto { dos_puntos: false },
459 Some('=') => ParamOp::Asigna { dos_puntos: false },
460 Some('?') => ParamOp::Error { dos_puntos: false },
461 Some('+') => ParamOp::Alterno { dos_puntos: false },
462 Some('#') => {
463 if self.ver(1) == Some('#') {
464 self.i += 1;
465 ParamOp::QuitaPrefijo { largo: true }
466 } else {
467 ParamOp::QuitaPrefijo { largo: false }
468 }
469 }
470 Some('%') => {
471 if self.ver(1) == Some('%') {
472 self.i += 1;
473 ParamOp::QuitaSufijo { largo: true }
474 } else {
475 ParamOp::QuitaSufijo { largo: false }
476 }
477 }
478 _ => return Err("operador ${…} no soportado".into()),
479 };
480 self.i += 1; // el carácter del operador
481 // el argumento, hasta la `}` que cierra (contando anidamiento y comillas)
482 let arg_txt = self.leer_hasta_llave()?;
483 let arg = if arg_txt.is_empty() {
484 vec![]
485 } else {
486 let (ts, _) = tokenizar(&arg_txt)?;
487 match ts.into_iter().find_map(|t| match t {
488 Tok::Pal(p) => Some(p),
489 _ => None,
490 }) {
491 Some(p) => p,
492 None => vec![Seg::Lit { texto: String::new(), comillado: true }],
493 }
494 };
495 Ok(Seg::Param { nombre, op, arg, comillado })
496 }
497
498 fn leer_hasta_llave(&mut self) -> Result<String, String> {
499 let mut prof = 1;
500 let mut out = String::new();
501 while let Some(c) = self.ver(0) {
502 if c == '\\' {
503 // barra invertida: se copia con lo que escapa, sin interpretarlo
504 out.push(c);
505 self.i += 1;
506 if let Some(d) = self.ver(0) {
507 out.push(d);
508 self.i += 1;
509 }
510 continue;
511 }
512 match c {
513 '{' => prof += 1,
514 '}' => {
515 prof -= 1;
516 if prof == 0 {
517 self.i += 1;
518 return Ok(out);
519 }
520 }
521 '\'' | '"' => {
522 let cierre = c;
523 out.push(c);
524 self.i += 1;
525 while let Some(d) = self.ver(0) {
526 out.push(d);
527 self.i += 1;
528 if d == cierre {
529 break;
530 }
531 if d == '\\' {
532 if let Some(e) = self.ver(0) {
533 out.push(e);
534 self.i += 1;
535 }
536 }
537 }
538 continue;
539 }
540 _ => {}
541 }
542 out.push(c);
543 self.i += 1;
544 }
545 Err("${…} sin cerrar".into())
546 }
547
548 fn leer_hasta_parentesis(&mut self) -> Result<String, String> {
549 let mut prof = 1;
550 let mut out = String::new();
551 while let Some(c) = self.ver(0) {
552 if c == '\\' {
553 // barra invertida: se copia con lo que escapa, sin interpretarlo
554 out.push(c);
555 self.i += 1;
556 if let Some(d) = self.ver(0) {
557 out.push(d);
558 self.i += 1;
559 }
560 continue;
561 }
562 match c {
563 '(' => prof += 1,
564 ')' => {
565 prof -= 1;
566 if prof == 0 {
567 self.i += 1;
568 return Ok(out);
569 }
570 }
571 '\'' | '"' => {
572 let cierre = c;
573 out.push(c);
574 self.i += 1;
575 while let Some(d) = self.ver(0) {
576 out.push(d);
577 self.i += 1;
578 if d == cierre {
579 break;
580 }
581 if d == '\\' && cierre == '"' {
582 if let Some(e) = self.ver(0) {
583 out.push(e);
584 self.i += 1;
585 }
586 }
587 }
588 continue;
589 }
590 _ => {}
591 }
592 out.push(c);
593 self.i += 1;
594 }
595 Err("$( sin cerrar".into())
596 }
597
598 fn leer_hasta_parentesis_doble(&mut self) -> Result<String, String> {
599 let mut prof = 0;
600 let mut out = String::new();
601 while let Some(c) = self.ver(0) {
602 if c == '\\' {
603 out.push(c);
604 self.i += 1;
605 if let Some(d) = self.ver(0) {
606 out.push(d);
607 self.i += 1;
608 }
609 continue;
610 }
611 if c == '(' {
612 prof += 1;
613 } else if c == ')' {
614 if prof == 0 {
615 if self.ver(1) == Some(')') {
616 self.i += 2;
617 return Ok(out);
618 }
619 return Err("$(( sin cerrar".into());
620 }
621 prof -= 1;
622 }
623 out.push(c);
624 self.i += 1;
625 }
626 Err("$(( sin cerrar".into())
627 }
628
629 /// El cuerpo de `` ` … ` ``, aplicando POSIX §2.6.3 a la barra invertida.
630 ///
631 /// Dentro de las comillas invertidas la barra es LITERAL salvo ante `$`, `` ` `` y `\`, donde
632 /// se elimina. brush aplicaba acá las reglas de `$( )` y por eso el `config.status` de libtool
633 /// generaba 25 líneas con un nivel de escapado de menos.
634 fn leer_backtick(&mut self, en_comillas_dobles: bool) -> Result<String, String> {
635 self.i += 1; // `
636 let mut out = String::new();
637 while let Some(c) = self.ver(0) {
638 match c {
639 '`' => {
640 self.i += 1;
641 return Ok(out);
642 }
643 '\\' => {
644 // Fuera de comillas la barra se elimina ante `$`, `` ` `` y `\`. DENTRO de
645 // comillas dobles el cuerpo del backtick sigue estando entre comillas, así que
646 // el `"` se suma al conjunto: `"`echo \"`"` deja el cuerpo `echo "`, que es un
647 // error de sintaxis — y es lo que hace el ash. Medido con el banco.
648 let quitar = matches!(
649 self.ver(1),
650 Some('$') | Some('`') | Some('\\')
651 ) || (en_comillas_dobles && self.ver(1) == Some('"'));
652 if quitar {
653 out.push(self.ver(1).unwrap());
654 self.i += 2;
655 } else {
656 out.push('\\');
657 self.i += 1;
658 }
659 }
660 _ => {
661 out.push(c);
662 self.i += 1;
663 }
664 }
665 }
666 Err("` sin cerrar".into())
667 }
668}
669
670fn numero_de_fd(p: &Palabra) -> Option<i32> {
671 if p.len() != 1 {
672 return None;
673 }
674 match &p[0] {
675 Seg::Lit { texto, comillado: false } if !texto.is_empty() => {
676 if texto.chars().all(|c| c.is_ascii_digit()) {
677 texto.parse().ok()
678 } else {
679 None
680 }
681 }
682 _ => None,
683 }
684}
685
686/// Si la palabra es exactamente un literal sin comillas, devuelve su texto. Lo usa el parser para
687/// reconocer palabras reservadas: `"if"` entrecomillado NO es la reservada `if`.
688pub fn literal_desnudo(p: &Palabra) -> Option<&str> {
689 match p.as_slice() {
690 [Seg::Lit { texto, comillado: false }] => Some(texto),
691 _ => None,
692 }
693}
694
695#[cfg(test)]
696mod pruebas {
697 use super::*;
698
699 fn cmdsub(entrada: &str) -> String {
700 let (ts, _) = tokenizar(entrada).unwrap();
701 for t in ts {
702 if let Tok::Pal(p) = t {
703 for s in p {
704 if let Seg::CmdSub { cuerpo, .. } = s {
705 return cuerpo;
706 }
707 }
708 }
709 }
710 panic!("no había sustitución en {entrada:?}");
711 }
712
713 /// POSIX.1-2024 §2.6.3 — el bug de brush (reubeno/brush#1394), calcado del reproductor.
714 #[test]
715 fn comillas_invertidas_posix_2_6_3() {
716 // \$ ⇒ se elimina la barra: el cuerpo pide `echo $V`… NO: la barra ante `$` se ELIMINA,
717 // así que el cuerpo es `echo $V` y la expansión la hace la orden de adentro.
718 assert_eq!(cmdsub(r#"echo "a: [`echo \$V`]""#), "echo $V");
719 // \\ ⇒ una sola barra
720 assert_eq!(cmdsub(r#"echo "b: [`echo \\`]""#), r"echo \");
721 // \x ⇒ la barra NO se toca (x no es $ ` ni \)
722 assert_eq!(cmdsub(r#"echo "[`echo \x`]""#), r"echo \x");
723 // en $( ) no hay regla especial: el cuerpo va tal cual
724 let (ts, _) = tokenizar(r#"echo "[$(echo \$V)]""#).unwrap();
725 let cuerpo = ts
726 .into_iter()
727 .find_map(|t| match t {
728 Tok::Pal(p) => p.into_iter().find_map(|s| match s {
729 Seg::CmdSub { cuerpo, .. } => Some(cuerpo),
730 _ => None,
731 }),
732 _ => None,
733 })
734 .unwrap();
735 assert_eq!(cuerpo, r"echo \$V");
736 }
737
738 #[test]
739 fn aqui_doc_con_y_sin_comillas() {
740 let (_, cuerpos) = tokenizar("cat <<EOF\nuno $V\nEOF\ncat <<'FIN'\ndos $V\nFIN\n").unwrap();
741 assert_eq!(cuerpos.len(), 2);
742 assert_eq!(cuerpos[0], ("uno $V\n".to_string(), true));
743 assert_eq!(cuerpos[1], ("dos $V\n".to_string(), false));
744 }
745
746 #[test]
747 fn aqui_doc_guion_come_tabulaciones() {
748 let (_, cuerpos) = tokenizar("cat <<-EOF\n\t\tsin-tab\n\tEOF\n").unwrap();
749 assert_eq!(cuerpos[0].0, "sin-tab\n");
750 }
751
752 #[test]
753 fn descriptor_pegado_a_redireccion() {
754 let (ts, _) = tokenizar("cmd 2>/dev/null").unwrap();
755 assert!(ts.contains(&Tok::IoNum(2)));
756 assert!(ts.contains(&Tok::Op(">".into())));
757 }
758
759 #[test]
760 fn comentario_solo_al_principio_de_palabra() {
761 let (ts, _) = tokenizar("echo a#b # comentario").unwrap();
762 let pals: Vec<_> = ts
763 .iter()
764 .filter_map(|t| match t {
765 Tok::Pal(p) => literal_desnudo(p),
766 _ => None,
767 })
768 .collect();
769 assert_eq!(pals, vec!["echo", "a#b"]);
770 }
771
772 #[test]
773 fn comillas_vacias_son_un_campo() {
774 let (ts, _) = tokenizar("echo ''").unwrap();
775 match &ts[1] {
776 Tok::Pal(p) => assert_eq!(p, &vec![Seg::Lit { texto: String::new(), comillado: true }]),
777 t => panic!("{t:?}"),
778 }
779 }
780}