use crate::BString; #[derive(Debug)] pub enum Ast { AssignVar(AssignVar), Pipes(Pipes), } #[derive(Debug)] pub struct AssignVar { pub to: String, // TODO: body } #[derive(Debug)] pub struct Pipes { pub cmds: Vec, } pub enum StringPart { Boring(BString), Var(VarName), Cmd(Ast), } /// `"hi ${var} $(cmd) "` gets mapped to `[Boring("hi "), Var("var"), String(" "), Cmd(...), Boring(" ")]` pub struct ShellString { parts: Vec, } fn is_symbol(x: u8) -> bool { match x { 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'_' } 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 ShellString { fn parse(b: &mut Cursor<'_>) -> Result { b.spaces(); if b.is_empty() { return Err(ParseError::Eof); } let mut delim = b.peek(); if delim != b'\'' && delim != b'"' { delim = b' '; } else { b.adv(); } 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 { if delim != b' ' { b.adv(); } 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'(' { todo!() } else { // doesn't seem to be a variable or expansion, just add $ back into the string add_char(p, b'$'); continue; } continue; } if delim == b' ' && is_symbol(x) { return Ok(Self { parts }); } add_char(p, x); } if b.is_completion() || delim == b' ' { Ok(Self { parts }) } else { Err(ParseError::Eof) } } } #[derive(Debug)] pub struct Command { pub cmd: BString, pub args: Vec, } #[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), } type Result = std::result::Result; pub fn do_parse(x: &[u8]) -> Result { Ast::parse(&mut Cursor::new(x, ParseMode::Command)) } pub enum CompletionKind { Command, Argument, None, } pub struct CompletionContext { pub kind: CompletionKind, pub partial: BString, } 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() { CompletionContext { kind: CompletionKind::Command, partial: cmd.cmd.clone(), } } else { CompletionContext { kind: CompletionKind::Argument, partial: cmd.args[cmd.args.len() - 1].clone(), } } } else { CompletionContext { kind: CompletionKind::None, partial: Vec::new(), } } } _ => CompletionContext { kind: CompletionKind::None, partial: Vec::new(), }, } } 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, } impl<'a> Cursor<'a> { fn new(buf: &'a [u8], mode: ParseMode) -> Self { Self { buf, mode, spaced: false, } } // non empty fn has(&self) -> bool { !self.buf.is_empty() } fn is_empty(&self) -> bool { self.buf.is_empty() } fn peek(&self) -> u8 { self.buf[0] } fn adv(&mut self) -> u8 { let out = self.buf[0]; self.buf = &self.buf[1..]; self.spaced = false; out } fn spaces(&mut self) { while let Some(b' ' | b'\t') = self.buf.first() { 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) } } fn parse_quoted_string(b: &mut Cursor<'_>, delim: u8) -> Result> { // TODO: escape sequence stuff let mut s = Vec::new(); while b.has() { if delim == b' ' && b.peek() == b'|' { return if s.len() == 0 { Err(ParseError::UnexpectedPipe) } else { Ok(s) }; } if b.peek() == delim { b.adv(); if delim == b' ' { b.spaced = true; } return Ok(s); } s.push(b.adv()); } if delim == b' ' || b.is_completion() { Ok(s) } else { Err(ParseError::Incomplete) } } impl Parse for Vec { fn parse(b: &mut Cursor<'_>) -> Result { b.spaces(); if b.is_empty() { return Err(ParseError::Eof); } let c = b.peek(); if c == b'|' { Err(ParseError::UnexpectedPipe) } else if c == b'\'' || c == b'"' { b.adv(); parse_quoted_string(b, c) } else if c.is_ascii_graphic() { parse_quoted_string(b, b' ') } else { Err(ParseError::Unknown(c)) } } } 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: Vec = b.parse()?; let mut args = Vec::new(); loop { let arg: Result> = b.parse(); match arg { Ok(arg) => args.push(arg), Err(ParseError::Eof | ParseError::UnexpectedPipe) => break, Err(e) => Err(e)?, } } Ok(Self { cmd: path, args }) } } 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 { Err(ParseError::Unknown(c))?; } } } }