aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJonas Maier <>2026-03-07 13:04:22 +0100
committerJonas Maier <>2026-03-07 13:04:22 +0100
commit4bfeec396b4f9e727587064de6c12942f8c6afff (patch)
tree40116c5ef277360017dfd83268dda5d92e87ae67 /src
parent6504d8c244af90fad76d101406a47a1ac4b22e22 (diff)
downloadpish-4bfeec396b4f9e727587064de6c12942f8c6afff.tar.gz
can parse concatenated quotes
Diffstat (limited to 'src')
-rw-r--r--src/parse/mod.rs163
-rw-r--r--src/parse/test.rs8
2 files changed, 97 insertions, 74 deletions
diff --git a/src/parse/mod.rs b/src/parse/mod.rs
index f7aa364..7a514e5 100644
--- a/src/parse/mod.rs
+++ b/src/parse/mod.rs
@@ -3,7 +3,7 @@ use crate::BString;
#[cfg(test)]
mod test;
-pub trait Stage : PartialEq {
+pub trait Stage: PartialEq {
type Str: std::fmt::Debug + Clone + PartialEq;
}
@@ -154,7 +154,7 @@ impl CmdDisplay for StringPart {
write!(w, "var(")?;
var_name.name.as_slice().cdisplay(w)?;
write!(w, ")")
- },
+ }
StringPart::Cmd(ast) => {
write!(w, "cmdp(")?;
ast.cdisplay(w)?;
@@ -402,115 +402,130 @@ impl Parse for ExpString {
return Err(ParseError::NotAString);
}
- let mut delim = b.peek();
- if delim == b'\'' || delim == b'"' {
- b.adv();
- } else if is_symbol(delim) && delim != b'$' {
- return Err(ParseError::NotAString);
- } else {
- delim = b' ';
- }
-
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;
- while b.has() {
- let x = b.peek();
-
- if escaping {
- add_char(p, x);
- escaping = false;
+ 'cont: while b.has() {
+ let mut delim = b.peek();
+ if delim == b'\'' || delim == b'"' {
b.adv();
- continue;
+ } else if is_symbol(delim) && delim != b'$' {
+ return if already_parsed {
+ Ok(Self { parts })
+ } else {
+ Err(ParseError::NotAString)
+ };
+ } else {
+ delim = b' ';
}
- if x == delim || (b.peek_space() && delim == b' ') {
- if delim != b' ' {
+ while b.has() {
+ let x = b.peek();
+
+ if escaping {
+ add_char(p, x);
+ escaping = false;
b.adv();
+ continue;
}
- return Ok(Self { parts });
- }
-
- if delim == b' ' && is_symbol(x) && x != b'$' {
- return Ok(Self { parts });
- }
- b.adv();
+ if delim == b' ' && (x.is_ascii_whitespace() || (is_symbol(x) && x != b'$')) {
+ if x == b'\'' || x == b'"' {
+ break;
+ } else {
+ return Ok(Self { parts });
+ }
+ }
- if delim == b'\'' {
- // no fancy stuff here
- add_char(p, x);
- continue;
- }
+ if x == delim {
+ b.adv();
+ already_parsed = true;
+ continue 'cont;
+ }
- if x == b'\\' {
- escaping = true;
- continue;
- }
+ b.adv();
- if x == b'$' {
- if !b.has() {
+ if delim == b'\'' {
+ // no fancy stuff here
add_char(p, x);
continue;
}
- let x = b.peek();
-
- if x == b'?' || x == b'!' {
- b.adv();
- p.push(StringPart::Var(VarName { name: vec![x] }))
- } else if is_var_begin(x) {
- let v = VarName::parse(b)?;
- p.push(StringPart::Var(v));
- } else if x == b'{' {
- b.adv();
- let v = VarName::parse(b)?;
+ if x == b'\\' {
+ escaping = true;
+ continue;
+ }
+ if x == b'$' {
if !b.has() {
- return Err(ParseError::Eof);
- } else if b.peek() == b':' {
- todo!(": in var expansion")
+ add_char(p, x);
+ continue;
}
- if !b.has() {
- return Err(ParseError::Eof);
- } else if b.peek() != b'}' {
- return Err(ParseError::Incomplete);
- }
+ let x = b.peek();
+
+ if x == b'?' || x == b'!' {
+ b.adv();
+ p.push(StringPart::Var(VarName { name: vec![x] }))
+ } else if is_var_begin(x) {
+ let v = VarName::parse(b)?;
+ p.push(StringPart::Var(v));
+ } else if x == b'{' {
+ b.adv();
+ let v = VarName::parse(b)?;
+
+ if !b.has() {
+ return Err(ParseError::Eof);
+ } else if b.peek() == b':' {
+ todo!(": in var expansion")
+ }
+
+ if !b.has() {
+ return Err(ParseError::Eof);
+ } else if b.peek() != b'}' {
+ return Err(ParseError::Incomplete);
+ }
- b.adv();
- p.push(StringPart::Var(v));
- } else if x == b'(' {
- b.adv();
- let cmd = Ast::parse(b)?;
- b.spaces();
- if b.is_empty() {
- return Err(ParseError::Eof);
- } else if b.peek() == b')' {
b.adv();
- p.push(StringPart::Cmd(cmd));
+ p.push(StringPart::Var(v));
+ } else if x == b'(' {
+ b.adv();
+ let cmd = Ast::parse(b)?;
+ b.spaces();
+ if b.is_empty() {
+ return Err(ParseError::Eof);
+ } else if b.peek() == b')' {
+ b.adv();
+ p.push(StringPart::Cmd(cmd));
+ } else {
+ return Err(ParseError::Expected(')'));
+ }
} else {
- return Err(ParseError::Expected(')'));
+ // doesn't seem to be a variable or expansion, just add $ back into the string
+ add_char(p, b'$');
+ continue;
}
- } else {
- // doesn't seem to be a variable or expansion, just add $ back into the string
- add_char(p, b'$');
+
continue;
}
+ add_char(p, x);
+ }
+
+ if b.has() && b"\"'".contains(&b.peek()) {
continue;
}
- add_char(p, x);
+ break;
}
- if b.is_completion() || delim == b' ' {
+ if b.is_completion() || already_parsed {
Ok(Self { parts })
} else {
Err(ParseError::Eof)
diff --git a/src/parse/test.rs b/src/parse/test.rs
index 9352cfd..9a7e359 100644
--- a/src/parse/test.rs
+++ b/src/parse/test.rs
@@ -26,3 +26,11 @@ fn command_interp() {
])]))])])]),
)
}
+
+#[test]
+fn string_concat() {
+ parse_test(
+ parse(br#" foo'bar'"baz" "#),
+ pipes([cmd([estr(b"foobarbaz")])]),
+ );
+}