diff --git a/build/scripts/test.toml b/build/scripts/test.toml index 9f98bfd79059cdc8bc63e9db37761e9cac6965b1..376ec85a388d43831b776151eb5dab061bae6023 100644 --- a/build/scripts/test.toml +++ b/build/scripts/test.toml @@ -6,10 +6,10 @@ dep2 = { script = "sleep 2", hide = true } # other tests hidden = { script = "echo hidden", hide = true } -debug = { info = "debug env", path="src", script = ["echo %{env.BOOL}", "echo %{env.STRING}", "echo %{arg.1}", "echo %{dir.current}", "echo %{dir.home}", "echo %{env.TYPE}", "echo %{env.ARR}", "echo %{os.platform}", "echo %{os.arch}"] } +debug = { info = "debug env", path="maid", script = ["echo %{env.BOOL}", "echo %{env.STRING}", "echo %{arg.1}", "echo %{dir.current}", "echo %{dir.home}", "echo %{env.TYPE}", "echo %{env.ARR}", "echo %{os.platform}", "echo %{os.arch}"] } broken = { info = "bad task", script = ["this_does_not_exist"] } # exit types exit = { script = "maid 'exit %{arg.1}'" } -"exit bad" = { script = ["exit_test 0", "exit_test 1", "exit_test 2"], hide = true } -"exit good" = { script = ["exit_test 2", "exit_test 1", "exit_test 0"], hide = true } \ No newline at end of file +"exit bad" = { script = ["bash -c 'exit 0'", "bash -c 'exit 1'", "bash -c 'exit 2'"], hide = true } +"exit good" = { script = ["bash -c 'exit 2'", "bash -c 'exit 1'", "bash -c 'exit 0'"], hide = true } \ No newline at end of file diff --git a/maid/client/globals.rs b/maid/client/globals.rs index ccf5ab88ffc1d1d0850e4be4a052e6121731104f..fab42cd0cf5d7f2b67f734ec69eebb8e486f0511 100644 --- a/maid/client/globals.rs +++ b/maid/client/globals.rs @@ -1,6 +1,6 @@ use global_placeholders::init; pub fn init() { - init!("maid.cache_dir", ".maid/cache/{}/target"); init!("maid.temp_dir", ".maid/temp"); + init!("maid.cache_dir", ".maid/cache/{}/target"); } diff --git a/maid/client/main.rs b/maid/client/main.rs index 4b5fc25320b157b4350eee4acc7144dcc7385c70..f7b66a263414deb65fda9bb51b2ca152f6f7183f 100644 --- a/maid/client/main.rs +++ b/maid/client/main.rs @@ -54,7 +54,7 @@ struct Cli { list: bool, /// Watch for changes in specified path - #[arg(short, long)] + #[arg(short = 'W', long)] watch: Option<String>, /// View Maid health (server health if enabled) @@ -83,14 +83,16 @@ enum System { Update, /// Return the Maidfile in json Json, - /// Hydrate json output with environment - HydrateJson, + /// Hydrate json with environment fields + JsonHydrated, } #[derive(ValueEnum, Clone)] enum Project { - /// Get Project Info + /// Retrieve project metadata Info, + /// Display current defined environment + Env, } fn main() { @@ -119,6 +121,7 @@ fn main() { if let Some(project) = cli.project { return match project { Project::Info => cli::info(&cli.path), // add more info + Project::Env => {}, // print env from maidfile }; } @@ -126,7 +129,7 @@ fn main() { return match system { System::Update => cli::butler::update(), // add real update checker System::Json => cli::tasks::json(&cli.path, &cli.task, false), - System::HydrateJson => cli::tasks::json(&cli.path, &cli.task, true), + System::JsonHydrated => cli::tasks::json(&cli.path, &cli.task, true), }; } diff --git a/maid/client/shell.rs b/maid/client/shell.rs index 02428372da37be9947a077c558b3c2b6db4a3eaa..59de9547188c3209f711b53f7e1e2d7de786b54b 100644 --- a/maid/client/shell.rs +++ b/maid/client/shell.rs @@ -1,19 +1,28 @@ -use core::mem; +use std::mem; use std::fmt::{Display, Formatter, Result as FmtResult}; #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ParseError; +pub enum ParseError { + UnterminatedQuote, + DanglingBackslash, + InvalidEscape(char), +} impl Display for ParseError { - fn fmt(&self, f: &mut Formatter) -> FmtResult { - f.write_str("missing closing quote") + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + ParseError::UnterminatedQuote => f.write_str("unterminated quote"), + ParseError::DanglingBackslash => f.write_str("dangling backslash at end of input"), + ParseError::InvalidEscape(c) => write!(f, "invalid escape sequence '\\{}' in double-quoted string", c), + } } } impl std::error::Error for ParseError {} +#[derive(Debug)] enum State { - Delimiter, + Delimiter, Backslash, Unquoted, UnquotedBackslash, @@ -22,107 +31,154 @@ enum State { DoubleQuotedBackslash, } + pub(crate) trait IntoArgs { fn try_into_args(&self) -> Result<Vec<String>, ParseError>; } impl<S: std::ops::Deref<Target = str>> IntoArgs for S { fn try_into_args(&self) -> Result<Vec<String>, ParseError> { + let mut parser = ArgumentParser::new(); + parser.parse(self) + } +} + +struct ArgumentParser { + words: Vec<String>, + current_word: String, + state: State, +} + +impl ArgumentParser { + fn new() -> Self { + Self { + words: Vec::new(), + current_word: String::new(), + state: State::Delimiter, + } + } + + fn parse(&mut self, input: &str) -> Result<Vec<String>, ParseError> { use State::*; - let mut words = Vec::new(); - let mut word = String::new(); - let mut chars = self.chars(); - let mut state = Delimiter; - - loop { - let c = chars.next(); - state = match state { - Delimiter => match c { - None => break, - Some('\'') => SingleQuoted, - Some('\"') => DoubleQuoted, - Some('\\') => Backslash, - Some('\t') | Some(' ') | Some('\n') => Delimiter, - Some(c) => { - word.push(c); - Unquoted - } - }, - Backslash => match c { - None => { - word.push('\\'); - words.push(mem::take(&mut word)); - break; - } - Some('\n') => Delimiter, - Some(c) => { - word.push(c); - Unquoted - } - }, - Unquoted => match c { - None => { - words.push(mem::take(&mut word)); - break; - } - Some('\'') => SingleQuoted, - Some('\"') => DoubleQuoted, - Some('\\') => UnquotedBackslash, - Some('\t') | Some(' ') | Some('\n') => { - words.push(mem::take(&mut word)); - Delimiter - } - Some(c) => { - word.push(c); - Unquoted - } - }, - UnquotedBackslash => match c { - None => { - word.push('\\'); - words.push(mem::take(&mut word)); - break; - } - Some('\n') => Unquoted, - Some(c) => { - word.push(c); - Unquoted - } - }, - SingleQuoted => match c { - None => return Err(ParseError), - Some('\'') => Unquoted, - Some(c) => { - word.push(c); - SingleQuoted - } - }, - DoubleQuoted => match c { - None => return Err(ParseError), - Some('\"') => Unquoted, - Some('\\') => DoubleQuotedBackslash, - Some(c) => { - word.push(c); - DoubleQuoted - } - }, - DoubleQuotedBackslash => match c { - None => return Err(ParseError), - Some('\n') => DoubleQuoted, - Some(c @ '$') | Some(c @ '`') | Some(c @ '"') | Some(c @ '\\') => { - word.push(c); - DoubleQuoted - } - Some(c) => { - word.push('\\'); - word.push(c); - DoubleQuoted - } - }, + for c in input.chars() { + self.state = match self.state { + Delimiter => self.handle_delimiter(c)?, + Backslash => self.handle_backslash(c)?, + Unquoted => self.handle_unquoted(c)?, + UnquotedBackslash => self.handle_unquoted_backslash(c)?, + SingleQuoted => self.handle_single_quoted(c)?, + DoubleQuoted => self.handle_double_quoted(c)?, + DoubleQuotedBackslash => self.handle_double_quoted_backslash(c)?, + }; + } + + self.handle_end_of_input()?; + Ok(mem::take(&mut self.words)) + } + + fn push_word(&mut self) { + if !self.current_word.is_empty() { + self.words.push(mem::take(&mut self.current_word)); + } + } + + fn handle_delimiter(&mut self, c: char) -> Result<State, ParseError> { + Ok(match c { + '\'' => State::SingleQuoted, + '"' => State::DoubleQuoted, + '\\' => State::Backslash, + '\t' | ' ' | '\n' => State::Delimiter, + c => { + self.current_word.push(c); + State::Unquoted + } + }) + } + + fn handle_backslash(&mut self, c: char) -> Result<State, ParseError> { + Ok(match c { + '\n' => State::Delimiter, + c => { + self.current_word.push(c); + State::Unquoted } + }) + } + + fn handle_unquoted(&mut self, c: char) -> Result<State, ParseError> { + Ok(match c { + '\'' => State::SingleQuoted, + '"' => State::DoubleQuoted, + '\\' => State::UnquotedBackslash, + '\t' | ' ' | '\n' => { + self.push_word(); + State::Delimiter + } + c => { + self.current_word.push(c); + State::Unquoted + } + }) + } + + fn handle_unquoted_backslash(&mut self, c: char) -> Result<State, ParseError> { + Ok(match c { + '\n' => State::Unquoted, + c => { + self.current_word.push(c); + State::Unquoted + } + }) + } + + fn handle_single_quoted(&mut self, c: char) -> Result<State, ParseError> { + Ok(match c { + '\'' => State::Unquoted, + c => { + self.current_word.push(c); + State::SingleQuoted + } + }) + } + + fn handle_double_quoted(&mut self, c: char) -> Result<State, ParseError> { + Ok(match c { + '"' => State::Unquoted, + '\\' => State::DoubleQuotedBackslash, + c => { + self.current_word.push(c); + State::DoubleQuoted + } + }) + } + + fn handle_double_quoted_backslash(&mut self, c: char) -> Result<State, ParseError> { + match c { + '\n' => Ok(State::DoubleQuoted), + '$' | '`' | '"' | '\\' => { + self.current_word.push(c); + Ok(State::DoubleQuoted) + } + c => Err(ParseError::InvalidEscape(c)) } + } - Ok(words) + fn handle_end_of_input(&mut self) -> Result<(), ParseError> { + match self.state { + State::SingleQuoted | State::DoubleQuoted => + Err(ParseError::UnterminatedQuote), + State::DoubleQuotedBackslash => + Err(ParseError::DanglingBackslash), + State::Backslash | State::UnquotedBackslash => { + self.current_word.push('\\'); + self.push_word(); + Ok(()) + } + _ => { + self.push_word(); + Ok(()) + } + } } }