diff options
| author | Jonas Maier <jonas@x77.dev> | 2026-03-18 13:13:11 +0100 |
|---|---|---|
| committer | Jonas Maier <jonas@x77.dev> | 2026-03-18 13:13:11 +0100 |
| commit | 37db397e58105fcc9f5fe0c356cd03f966715bff (patch) | |
| tree | 8c407f3cfabd6b27c74a55c766c1994e39a9c3b2 /src/parse | |
| parent | 6d5d57d9dd4a558b8e1d6501f6e4ffc0f340c283 (diff) | |
| download | pish-37db397e58105fcc9f5fe0c356cd03f966715bff.tar.gz | |
parsing works again
Diffstat (limited to 'src/parse')
| -rw-r--r-- | src/parse/mod.rs | 193 | ||||
| -rw-r--r-- | src/parse/test.rs | 25 |
2 files changed, 105 insertions, 113 deletions
diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 172974c..1e88337 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -522,7 +522,7 @@ impl Parse for VarName { } } -#[derive(Clone)] +#[derive(Clone, Debug)] enum StringDelimiter { /// no delimiter, i.e. when parsing a simple command like `echo foo` None, @@ -584,10 +584,16 @@ impl StringDelimiter { let ident = peek_ident(&b.buf); if b.buf[ident.len()..].starts_with(b"\"\"\"") { b.advance(ident.len() + 3); + if b.has() && b.peek() == b'\n' { + b.adv(); + } return Some(Self::InterpCustom(ident.to_vec())); } if b.buf[ident.len()..].starts_with(b"'''") { b.advance(ident.len() + 3); + if b.has() && b.peek() == b'\n' { + b.adv(); + } return Some(Self::StrictCustom(ident.to_vec())); } @@ -616,17 +622,14 @@ impl StringDelimiter { /// otherwise, consumes no tokens and returns false fn try_end(&self, b: &mut Cursor<'_>) -> bool { if !b.has() { - return false; + return matches!(self, Self::None); } let x = b.peek(); let buf = &mut b.buf; match self { - StringDelimiter::None if x.is_ascii_whitespace() || is_symbol(x) && x != b'$' => { - b.adv(); - true - } + StringDelimiter::None if x.is_ascii_whitespace() || is_symbol(x) && x != b'$' => true, StringDelimiter::Interp if x == b'"' => { b.adv(); true @@ -654,6 +657,45 @@ impl StringDelimiter { fn is_strict(&self) -> bool { matches!(self, Self::Strict | Self::StrictCustom(_)) } + + fn is_none(&self) -> bool { + matches!(self, Self::None) + } +} + +fn parse_escape_code(b: &mut Cursor<'_>) -> Result<u8> { + let x = b.peek(); + + let y = match x { + b'n' => b'\n', + b'r' => b'\r', + b't' => b'\t', + b'e' => 0x1b, // escape + b'x' => { + // parse two hex digits + b.adv(); + if b.buf.len() < 2 { + Err(ParseError::Eof)?; + } + let x1 = b.peek(); + b.adv(); + let x2 = b.peek(); + + if !x1.is_ascii_hexdigit() || !x2.is_ascii_hexdigit() { + Err(ParseError::NotHexDigit)?; + } + + let x1 = (x1 as char).to_digit(16).unwrap_or(0); + let x2 = (x2 as char).to_digit(16).unwrap_or(0); + + ((x1 << 4) | x2) as u8 + } + _ => x, + }; + + b.adv(); + + Ok(y) } impl Parse for ExpString { @@ -665,96 +707,39 @@ impl Parse for ExpString { let mut parts = Vec::new(); let p = &mut parts; - let mut escaping = false; let add_char = |p: &mut Vec<StringPart>, x: u8| match p.last_mut() { Some(StringPart::Boring(v)) => v.push(x), _ => p.push(StringPart::Boring(vec![x])), }; - let mut already_parsed = false; - - 'cont: while b.has() { - let mut delim = b.peek(); - if delim == b'\'' || delim == b'"' { - b.adv(); - } else if is_symbol(delim) && delim != b'$' && delim != b'\\' { - return if already_parsed { - Ok(Self { parts, delim }) - } else { - Err(ParseError::NotAString) - }; - } else { - delim = b' '; - } - - already_parsed = false; - - while b.has() { - let x = b.peek(); - - if escaping { - let x = match x { - b'n' => b'\n', - b'r' => b'\r', - b't' => b'\t', - b'e' => 0x1b, // escape - b'x' => { - // parse two hex digits - b.adv(); - if b.buf.len() < 2 { - Err(ParseError::Eof)?; - } - let x1 = b.peek(); - b.adv(); - let x2 = b.peek(); - if !x1.is_ascii_hexdigit() || !x2.is_ascii_hexdigit() { - Err(ParseError::NotHexDigit)?; - } + let mut already_parsed = false; - let x1 = (x1 as char).to_digit(16).unwrap_or(0); - let x2 = (x2 as char).to_digit(16).unwrap_or(0); + 'outer: loop { + let Some(delim) = StringDelimiter::try_begin(b) else { + break; + }; - ((x1 << 4) | x2) as u8 - } - _ => x, - }; - add_char(p, x); - escaping = false; - already_parsed = true; - b.adv(); - continue; - } + already_parsed = true; - if delim == b' ' && (x.is_ascii_whitespace() || (is_symbol(x) && x != b'$')) { - if x == b'\'' || x == b'"' { - break; + while !delim.try_end(b) { + if !b.has() { + if b.is_completion() { + break 'outer; } else { - return Ok(Self { parts, delim }); + return Err(ParseError::Eof); } } - if x == delim { - b.adv(); - already_parsed = true; - continue 'cont; - } - - b.adv(); - - if delim == b'\'' { - // no fancy stuff here - add_char(p, x); - continue; - } + let x = b.peek(); if x == b'\\' { - escaping = true; - continue; - } + b.adv(); + add_char(p, parse_escape_code(b)?); + } else if x == b'$' && !delim.is_strict() { + b.adv(); - if x == b'$' { if !b.has() { - add_char(p, x); + add_char(p, b'$'); continue; } @@ -833,47 +818,33 @@ impl Parse for ExpString { } else { // doesn't seem to be a variable or expansion, just add $ back into the string add_char(p, b'$'); - continue; - } - - if delim == b' ' { - already_parsed = true; } - continue; - } - - if delim == b' ' - && x == b'~' - && p.is_empty() - && (!b.has() || b" /".contains(&b.peek())) - { - p.push(StringPart::Var(Var { - name: VarName { - name: b"HOME".to_vec(), - }, - default: None, - already_complete: true, - })); } else { - add_char(p, x); - } + b.adv(); - if delim == b' ' { - already_parsed = true; + if delim.is_none() + && x == b'~' + && p.is_empty() + && (!b.has() || b.peek().is_ascii_whitespace() || b.peek() == b'/') + { + p.push(StringPart::Var(Var { + name: VarName { + name: b"HOME".to_vec(), + }, + default: None, + already_complete: true, + })); + } else { + add_char(p, x); + } } } - - if b.has() && b"\"'".contains(&b.peek()) { - continue; - } - - break; } if b.is_completion() || already_parsed { Ok(Self { parts, delim: b' ' }) } else { - Err(ParseError::Eof) + Err(ParseError::NotAString) } } } diff --git a/src/parse/test.rs b/src/parse/test.rs index d221341..3513c3f 100644 --- a/src/parse/test.rs +++ b/src/parse/test.rs @@ -1,7 +1,9 @@ use super::*; fn parse(x: &[u8]) -> Ast<PreExpansion> { - do_parse(x).unwrap() + do_parse(x) + .map_err(|(err, rest)| (err, String::from_utf8_lossy(&rest))) + .unwrap() } const TIMEOUT_MS: u64 = 100; @@ -268,7 +270,10 @@ $var line 3 """"# ), - pipes([cmd([estr(b"echo"), str([plain(b"line 1\n"),var(b"var"),plain(b"\nline 3\n")])]),]) + pipes([cmd([ + estr(b"echo"), + str([plain(b"line 1\n"), var(b"var"), plain(b"\nline 3\n")]) + ]),]) ); } @@ -300,3 +305,19 @@ more text pipes([cmd([estr(b"echo"), estr(b"text\n\"\"\"\nmore text\n")]),]) ); } + +#[test] +fn exit_code() { + parse_test!( + parse(b"echo $?"), + pipes([cmd([estr(b"echo"), str([var(b"?")])])]) + ) +} + +#[test] +fn exit_code_2() { + parse_test!( + parse(b"echo \"$?\""), + pipes([cmd([estr(b"echo"), str([var(b"?")])])]) + ) +} |
