use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub msg: String,
pub offset: usize,
pub line: usize,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (line {})", self.msg, self.line)
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Part {
Lit(String),
Var(String),
Elem { name: String, index: Vec<Part> },
Script(Script),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Word {
pub parts: Vec<Part>,
pub expand: bool,
pub braced: bool,
pub quoted: bool,
}
impl Word {
pub fn as_literal(&self) -> Option<&str> {
match self.parts.as_slice() {
[] => Some(""),
[Part::Lit(s)] => Some(s),
_ => None,
}
}
fn literal(text: String, braced: bool, expand: bool) -> Word {
let parts = if text.is_empty() {
Vec::new()
} else {
vec![Part::Lit(text)]
};
Word {
parts,
expand,
braced,
quoted: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Command {
pub words: Vec<Word>,
pub line: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Script {
pub commands: Vec<Command>,
}
pub const MAX_NESTING_DEPTH: usize = 64_000;
pub fn parse(src: &str) -> Result<Script, ParseError> {
let mut p = Parser {
src: src.as_bytes(),
pos: 0,
line: 1,
depth: 0,
};
let script = p.parse_script(false)?;
if p.pos < p.src.len() {
return Err(p.error("extra characters after close-bracket"));
}
Ok(script)
}
pub(crate) fn substitution_at(src: &str, at: usize) -> Result<Option<(Part, usize)>, ParseError> {
let mut p = Parser {
src: src.as_bytes(),
pos: at,
line: 1,
depth: 0,
};
Ok(p.parse_dollar()?.map(|part| (part, p.pos)))
}
pub(crate) fn command_at(src: &str, at: usize) -> Result<(Script, usize), ParseError> {
let mut p = Parser {
src: src.as_bytes(),
pos: at + 1,
line: 1,
depth: 0,
};
let script = p.parse_script(true)?;
if p.peek() != Some(b']') {
return Err(p.error("missing close-bracket"));
}
p.pos += 1;
Ok((script, p.pos))
}
pub(crate) fn quoted_at(src: &str, at: usize) -> Result<(Vec<Part>, usize), ParseError> {
let mut p = Parser {
src: src.as_bytes(),
pos: at + 1,
line: 1,
depth: 0,
};
let parts = p.parse_parts(Ctx::Quoted, false)?;
p.pos += 1; Ok((parts, p.pos))
}
pub(crate) fn backslash_at(src: &str, at: usize) -> (String, usize) {
let mut p = Parser {
src: src.as_bytes(),
pos: at,
line: 1,
depth: 0,
};
let mut out = String::new();
if p.at(1) == Some(b'\n') {
p.skip_line_continuation();
out.push(' ');
} else {
p.parse_backslash(&mut out);
}
(out, p.pos)
}
pub(crate) fn braced_at(src: &str, at: usize) -> Result<(String, usize), ParseError> {
let mut p = Parser {
src: src.as_bytes(),
pos: at,
line: 1,
depth: 0,
};
let text = p.parse_braced()?;
Ok((text, p.pos))
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Ctx {
Bare,
Quoted,
Index,
}
struct Parser<'a> {
src: &'a [u8],
pos: usize,
line: usize,
depth: usize,
}
impl<'a> Parser<'a> {
fn descend(&mut self) -> Result<(), ParseError> {
self.depth += 1;
if self.depth > MAX_NESTING_DEPTH {
return Err(self.error("too many nested substitutions (infinite loop?)"));
}
Ok(())
}
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
fn at(&self, offset: usize) -> Option<u8> {
self.src.get(self.pos + offset).copied()
}
fn bump(&mut self) -> Option<u8> {
let b = self.peek()?;
self.pos += 1;
if b == b'\n' {
self.line += 1;
}
Some(b)
}
fn error(&self, msg: &str) -> ParseError {
ParseError {
msg: msg.to_string(),
offset: self.pos,
line: self.line,
}
}
fn parse_script(&mut self, nested: bool) -> Result<Script, ParseError> {
let mut commands = Vec::new();
loop {
self.skip_between_commands();
if self.pos >= self.src.len() {
break;
}
if nested && self.peek() == Some(b']') {
break;
}
let line = self.line;
let mut words = Vec::new();
loop {
words.push(self.parse_word(nested)?);
if !self.skip_word_gap() || self.at_command_end(nested) {
break;
}
}
commands.push(Command { words, line });
}
Ok(Script { commands })
}
fn skip_between_commands(&mut self) {
loop {
match self.peek() {
Some(b' ') | Some(b'\t') | Some(b'\r') | Some(b'\n') | Some(b';') => {
self.bump();
}
Some(b'\\') if self.at(1) == Some(b'\n') => {
self.skip_line_continuation();
}
Some(b'#') => {
while let Some(b) = self.peek() {
if b == b'\n' {
break;
}
if b == b'\\' && self.at(1) == Some(b'\n') {
self.skip_line_continuation();
continue;
}
self.bump();
}
}
_ => return,
}
}
}
fn skip_word_gap(&mut self) -> bool {
let start = self.pos;
loop {
match self.peek() {
Some(b' ') | Some(b'\t') | Some(b'\r') => {
self.bump();
}
Some(b'\\') if self.at(1) == Some(b'\n') => {
self.skip_line_continuation();
}
_ => break,
}
}
self.pos > start
}
fn skip_line_continuation(&mut self) {
self.bump(); self.bump(); while matches!(self.peek(), Some(b' ') | Some(b'\t')) {
self.bump();
}
}
fn at_command_end(&self, nested: bool) -> bool {
match self.peek() {
None | Some(b'\n') | Some(b';') => true,
Some(b']') => nested,
_ => false,
}
}
fn parse_word(&mut self, nested: bool) -> Result<Word, ParseError> {
let expand = self.at_expansion_prefix(nested);
if expand {
self.pos += 3;
}
match self.peek() {
Some(b'{') => {
let text = self.parse_braced()?;
self.check_word_end(nested, "close-brace")?;
Ok(Word::literal(text, true, expand))
}
Some(b'"') => {
self.bump();
let parts = self.parse_parts(Ctx::Quoted, nested)?;
self.bump(); self.check_word_end(nested, "close-quote")?;
Ok(Word {
parts,
expand,
braced: false,
quoted: true,
})
}
_ => {
let parts = self.parse_parts(Ctx::Bare, nested)?;
Ok(Word {
parts,
expand,
braced: false,
quoted: false,
})
}
}
}
fn at_expansion_prefix(&self, nested: bool) -> bool {
if self.src[self.pos..].starts_with(b"{*}") {
match self.at(3) {
None | Some(b' ') | Some(b'\t') | Some(b'\r') | Some(b'\n') | Some(b';') => false,
Some(b']') => !nested,
Some(_) => true,
}
} else {
false
}
}
fn parse_braced(&mut self) -> Result<String, ParseError> {
let open = self.pos;
self.bump(); let mut depth = 1usize;
let mut out = String::new();
loop {
let Some(b) = self.peek() else {
self.pos = open;
return Err(self.error("missing close-brace"));
};
match b {
b'\\' if self.at(1) == Some(b'\n') => {
self.skip_line_continuation();
out.push(' ');
}
b'\\' => {
self.bump();
out.push('\\');
if self.peek().is_some() {
self.copy_char(&mut out);
}
}
b'{' => {
depth += 1;
self.bump();
out.push('{');
}
b'}' => {
depth -= 1;
self.bump();
if depth == 0 {
return Ok(out);
}
out.push('}');
}
_ => self.copy_char(&mut out),
}
}
}
fn parse_parts(&mut self, ctx: Ctx, nested: bool) -> Result<Vec<Part>, ParseError> {
let mut parts: Vec<Part> = Vec::new();
let mut lit = String::new();
loop {
let Some(b) = self.peek() else {
match ctx {
Ctx::Quoted => return Err(self.error("missing \"")),
Ctx::Index => return Err(self.error("missing )")),
Ctx::Bare => break,
}
};
match b {
b'"' if ctx == Ctx::Quoted => break,
b')' if ctx == Ctx::Index => break,
b'(' if ctx == Ctx::Index => {
return Err(self.error("invalid character in array index"))
}
b' ' | b'\t' | b'\r' | b'\n' | b';' if ctx == Ctx::Bare => break,
b']' if ctx == Ctx::Bare && nested => break,
b'\\' if self.at(1) == Some(b'\n') => {
if ctx == Ctx::Bare {
break;
}
self.skip_line_continuation();
lit.push(' ');
}
b'\\' => self.parse_backslash(&mut lit),
b'[' => {
flush(&mut lit, &mut parts);
self.bump();
self.descend()?;
let inner = self.parse_script(true)?;
self.depth -= 1;
if self.peek() != Some(b']') {
return Err(self.error("missing close-bracket"));
}
self.bump();
parts.push(Part::Script(inner));
}
b'$' => {
if let Some(part) = self.parse_dollar()? {
flush(&mut lit, &mut parts);
parts.push(part);
} else {
self.bump();
lit.push('$');
}
}
_ => self.copy_char(&mut lit),
}
}
flush(&mut lit, &mut parts);
Ok(parts)
}
fn parse_dollar(&mut self) -> Result<Option<Part>, ParseError> {
if self.at(1) == Some(b'{') {
return self.parse_braced_var().map(Some);
}
let name = self.scan_var_name(self.pos + 1);
if name.is_empty() {
return Ok(None);
}
self.pos += 1 + name.len();
if self.peek() == Some(b'(') {
self.bump();
self.descend()?;
let index = self.parse_parts(Ctx::Index, false)?;
self.depth -= 1;
if self.peek() != Some(b')') {
return Err(self.error("missing )"));
}
self.bump();
return Ok(Some(Part::Elem { name, index }));
}
Ok(Some(Part::Var(name)))
}
fn scan_var_name(&self, from: usize) -> String {
let mut i = from;
while i < self.src.len() {
let b = self.src[i];
if b.is_ascii_alphanumeric() || b == b'_' {
i += 1;
} else if b == b':' {
let colons = self.src[i..].iter().take_while(|&&c| c == b':').count();
if colons < 2 {
break;
}
i += colons;
} else {
break;
}
}
String::from_utf8_lossy(&self.src[from..i]).into_owned()
}
fn parse_braced_var(&mut self) -> Result<Part, ParseError> {
let open = self.pos;
self.pos += 2; let start = self.pos;
while let Some(b) = self.peek() {
if b == b'}' {
let raw = String::from_utf8_lossy(&self.src[start..self.pos]).into_owned();
self.bump();
return Ok(braced_var_part(raw));
}
self.bump();
}
self.pos = open;
Err(self.error("missing close-brace for variable name"))
}
fn check_word_end(&mut self, nested: bool, what: &str) -> Result<(), ParseError> {
let ok = match self.peek() {
None | Some(b' ') | Some(b'\t') | Some(b'\r') | Some(b'\n') | Some(b';') => true,
Some(b']') => nested,
Some(b'\\') => self.at(1) == Some(b'\n'),
_ => false,
};
if ok {
Ok(())
} else {
Err(self.error(&format!("extra characters after {what}")))
}
}
fn parse_backslash(&mut self, out: &mut String) {
self.bump(); let Some(b) = self.peek() else {
out.push('\\');
return;
};
match b {
b'a' => {
self.bump();
out.push('\u{7}');
}
b'b' => {
self.bump();
out.push('\u{8}');
}
b'f' => {
self.bump();
out.push('\u{c}');
}
b'n' => {
self.bump();
out.push('\n');
}
b'r' => {
self.bump();
out.push('\r');
}
b't' => {
self.bump();
out.push('\t');
}
b'v' => {
self.bump();
out.push('\u{b}');
}
b'\\' => {
self.bump();
out.push('\\');
}
b'x' => {
self.bump();
match self.scan_radix(16, 2, 0xFF) {
Some(v) => push_code_point(out, v),
None => out.push('x'),
}
}
b'u' => {
self.bump();
match self.scan_radix(16, 4, 0x10FFFF) {
Some(v) => push_code_point(out, v),
None => out.push('u'),
}
}
b'U' => {
self.bump();
match self.scan_radix(16, 8, 0x10FFFF) {
Some(v) => push_code_point(out, v),
None => out.push('U'),
}
}
b'0'..=b'7' => {
match self.scan_radix(8, 3, 0xFF) {
Some(v) => push_code_point(out, v),
None => self.copy_char(out),
}
}
_ => self.copy_char(out),
}
}
fn scan_radix(&mut self, radix: u32, max_digits: usize, limit: u32) -> Option<u32> {
let mut value: u32 = 0;
let mut digits = 0;
while digits < max_digits {
let Some(b) = self.peek() else { break };
let Some(d) = (b as char).to_digit(radix) else {
break;
};
let next = value * radix + d;
if next > limit {
break;
}
value = next;
digits += 1;
self.bump();
}
(digits > 0).then_some(value)
}
fn copy_char(&mut self, out: &mut String) {
let start = self.pos;
let len = utf8_len(self.src[start]);
let end = (start + len).min(self.src.len());
match std::str::from_utf8(&self.src[start..end]) {
Ok(s) => out.push_str(s),
Err(_) => out.push(char::REPLACEMENT_CHARACTER),
}
for _ in start..end {
self.bump();
}
}
}
fn flush(lit: &mut String, parts: &mut Vec<Part>) {
if !lit.is_empty() {
parts.push(Part::Lit(std::mem::take(lit)));
}
}
fn braced_var_part(raw: String) -> Part {
if raw.ends_with(')') {
if let Some(open) = raw.find('(') {
let name = &raw[..open];
if !name.contains('(') {
let index = raw[open + 1..raw.len() - 1].to_string();
return Part::Elem {
name: name.to_string(),
index: if index.is_empty() {
Vec::new()
} else {
vec![Part::Lit(index)]
},
};
}
}
}
Part::Var(raw)
}
fn push_code_point(out: &mut String, value: u32) {
out.push(char::from_u32(value).unwrap_or(char::REPLACEMENT_CHARACTER));
}
fn utf8_len(lead: u8) -> usize {
match lead {
0x00..=0x7F => 1,
0xC0..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF7 => 4,
_ => 1,
}
}