use crate::BString; pub trait Stage { type Str: std::fmt::Debug + Clone; } #[derive(Debug, Clone)] pub struct PreExpansion; #[derive(Debug, Clone)] pub struct PostExpansion; impl Stage for PreExpansion { type Str = ExpString; } impl Stage for PostExpansion { type Str = BString; } type Res = std::result::Result; pub trait Expander { type Error; fn expand_var(&mut self, v: BString) -> Res; fn expand_cmd(&mut self, c: Ast) -> Res; } #[derive(Debug, Clone)] pub enum Ast { AssignVar(AssignVar), Pipes(Pipes), } impl Ast { pub fn expand(self, e: &mut E) -> Res, E::Error> { match self { Ast::AssignVar(assign_var) => todo!(), Ast::Pipes(pipes) => Ok(Ast::Pipes(pipes.expand(e)?)), } } } #[derive(Debug, Clone)] pub struct AssignVar { pub to: String, // TODO: body } #[derive(Debug, Clone)] pub struct Pipes { pub cmds: Vec>, } impl Pipes { fn expand(self, e: &mut E) -> Res, E::Error> { let mut cmds = Vec::with_capacity(self.cmds.len()); for cmd in self.cmds.into_iter() { cmds.push(cmd.expand(e)?); } Ok(Pipes { cmds }) } } #[derive(Debug, Clone)] pub enum StringPart { Boring(BString), Var(VarName), Cmd(Ast), } impl StringPart { pub fn is_boring(&self) -> bool { matches!(self, StringPart::Boring(..)) } pub fn unwrap_boring(self) -> BString { match self { StringPart::Boring(items) => items, _ => panic!("unwrap on non-boring value"), } } } #[derive(Debug, Clone)] /// `"hi ${var} $(cmd) "` gets mapped to `[Boring("hi "), Var("var"), String(" "), Cmd(...), Boring(" ")]` pub struct ExpString { parts: Vec, } impl ExpString { fn expand(self, e: &mut E) -> Res { let mut out = BString::new(); for part in self.parts.into_iter() { let mut x = match part { StringPart::Boring(items) => items, StringPart::Var(v) => e.expand_var(v.name)?, StringPart::Cmd(ast) => { let exp = ast.expand(e)?; e.expand_cmd(exp)? } }; out.append(&mut x); } Ok(out) } } fn is_symbol(x: u8) -> bool { match x { b'|' | b'{' | b'}' | b'$' | b'(' | b')' | b'\'' | b'"' => true, _ => false, } } fn is_var_begin(x: u8) -> bool { x.is_ascii_alphabetic() } fn is_var_name(x: u8) -> bool { x.is_ascii_alphanumeric() || x == b'_' } #[derive(Debug, Clone)] struct VarName { name: BString, } impl Parse for VarName { fn parse(b: &mut Cursor<'_>) -> Result { if b.is_empty() { return Err(ParseError::Eof); } if !is_var_begin(b.peek()) { return Err(ParseError::ExpectedAlphabetic); } let mut name = BString::new(); while b.has() { let x = b.peek(); if is_var_name(x) { b.adv(); name.push(x) } else { break; } } Ok(Self { name }) } } impl Parse for ExpString { fn parse(b: &mut Cursor<'_>) -> Result { b.spaces(); if b.is_empty() { 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, x: u8| match p.last_mut() { Some(StringPart::Boring(v)) => v.push(x), _ => p.push(StringPart::Boring(vec![x])), }; while b.has() { let x = b.peek(); if escaping { add_char(p, x); escaping = false; b.adv(); continue; } if x == delim || (b.peek_space() && delim == b' ') { if delim != b' ' { b.adv(); } return Ok(Self { parts }); } if delim == b' ' && is_symbol(x) && x != b'$' { return Ok(Self { parts }); } b.adv(); if delim == b'\'' { // no fancy stuff here add_char(p, x); continue; } if x == b'\\' { escaping = true; continue; } if x == b'$' { if !b.has() { add_char(p, x); continue; } let x = b.peek(); 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')' { p.push(StringPart::Cmd(cmd)); } else { return Err(ParseError::Expected(')')); } } else { // doesn't seem to be a variable or expansion, just add $ back into the string add_char(p, b'$'); continue; } continue; } add_char(p, x); } if b.is_completion() || delim == b' ' { Ok(Self { parts }) } else { Err(ParseError::Eof) } } } #[derive(Debug, Clone)] pub struct Command { pub cmd: T::Str, pub args: Vec, } impl Command { fn expand(self, e: &mut E) -> Res, E::Error> { let cmd = self.cmd.expand(e)?; let mut args = Vec::with_capacity(self.args.len()); for arg in self.args.into_iter() { args.push(arg.expand(e)?); } Ok(Command { cmd, args }) } } #[derive(Debug)] pub enum ParseError { /// "clean" EOF, i.e. not in the middle of something Eof, /// "unclean" EOF, i.e. EOF after beginning a quoted string Incomplete, UnexpectedPipe, ExpectedAlphabetic, Unknown(u8), Expected(char), NotAString, } type Result = std::result::Result; pub fn do_parse(x: &[u8]) -> Res, (ParseError, &[u8])> { let mut c = Cursor::new(x, ParseMode::Command); match Ast::parse(&mut c) { Ok(ast) => Ok(ast), Err(e) => Err((e, c.buf)), } } pub enum CompletionKind { Command, Argument, None, } pub struct CompletionContext { pub kind: CompletionKind, pub partial: BString, } impl CompletionContext { pub fn none() -> Self { Self { kind: CompletionKind::None, partial: BString::new(), } } } fn expstr_cc(s: &ExpString, kind: CompletionKind) -> CompletionContext { if s.parts.len() > 1 || !s.parts[0].is_boring() { CompletionContext::none() } else { CompletionContext { kind, partial: s.parts[0].clone().unwrap_boring().clone(), } } } pub fn completion_context<'a>(x: &'a [u8]) -> CompletionContext { let mut cursor = Cursor::new(x, ParseMode::Completion); let ast = Ast::parse(&mut cursor); match ast { Ok(Ast::Pipes(pipes)) if cursor.spaced == false => { if let Some(cmd) = pipes.cmds.last() { if cmd.args.is_empty() { expstr_cc(&cmd.cmd, CompletionKind::Command) } else { expstr_cc(&cmd.args[cmd.args.len() - 1], CompletionKind::Argument) } } else { CompletionContext::none() } } _ => CompletionContext::none(), } } trait Parse: Sized { fn parse(b: &mut Cursor<'_>) -> Result; } enum ParseMode { Command, Completion, } struct Cursor<'a> { buf: &'a [u8], mode: ParseMode, /// if the last byte that was consumed was whitespace or part of a word spaced: bool, backtrace: bool, } impl<'a> Cursor<'a> { fn new(buf: &'a [u8], mode: ParseMode) -> Self { Self { buf, mode, spaced: false, backtrace: false, } } // non empty fn has(&self) -> bool { !self.buf.is_empty() } fn is_empty(&self) -> bool { self.buf.is_empty() } fn bt(&self, word: &str) { if self.backtrace { let bt = std::backtrace::Backtrace::capture(); let bt = format!("{bt}"); println!("{word} {}\r", self.buf[0] as char); for l in bt.lines().skip(4).take(2) { println!("{l}\r"); } println!("\r"); } } fn peek(&self) -> u8 { self.bt("peek"); self.buf[0] } fn adv(&mut self) -> u8 { self.bt("adv"); let out = self.buf[0]; self.buf = &self.buf[1..]; self.spaced = false; out } fn peek_space(&self) -> bool { if self.buf.is_empty() { return false; } matches!(self.buf[0], b' ' | b'\t' | b'\n' | b'\r') } fn spaces(&mut self) { while self.peek_space() { self.adv(); self.spaced = true; } } fn is_completion(&self) -> bool { match self.mode { ParseMode::Completion => true, _ => false, } } fn parse(&mut self) -> Result { T::parse(self) } } impl Parse for Ast { fn parse(b: &mut Cursor<'_>) -> Result { Ok(Self::Pipes(b.parse()?)) } } impl Parse for Command { fn parse(b: &mut Cursor<'_>) -> Result { let path: ExpString = b.parse()?; let mut args = Vec::new(); loop { match ExpString::parse(b) { Ok(arg) => args.push(arg), Err(ParseError::NotAString) => break, Err(e) => Err(e)?, } } let x = Ok(Self { cmd: path, args }); x } } impl Parse for Pipes { fn parse(b: &mut Cursor<'_>) -> Result { let mut cmds: Vec> = vec![b.parse()?]; loop { b.spaces(); if b.is_empty() { return Ok(Pipes { cmds }); } let c = b.peek(); if c == b'|' { b.adv(); cmds.push(b.parse()?); } else if is_symbol(c) { return Ok(Pipes { cmds }); } else { Err(ParseError::Unknown(c))?; } } } }