aboutsummaryrefslogtreecommitdiffstats
path: root/src/parse
diff options
context:
space:
mode:
authorJonas Maier <jonas@x77.dev>2026-03-18 13:13:11 +0100
committerJonas Maier <jonas@x77.dev>2026-03-18 13:13:11 +0100
commit37db397e58105fcc9f5fe0c356cd03f966715bff (patch)
tree8c407f3cfabd6b27c74a55c766c1994e39a9c3b2 /src/parse
parent6d5d57d9dd4a558b8e1d6501f6e4ffc0f340c283 (diff)
downloadpish-37db397e58105fcc9f5fe0c356cd03f966715bff.tar.gz
parsing works again
Diffstat (limited to 'src/parse')
-rw-r--r--src/parse/mod.rs193
-rw-r--r--src/parse/test.rs25
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"?")])])])
+ )
+}