diff --git a/Cargo.lock b/Cargo.lock index 58f2d8f..cbc9ff3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,7 +54,7 @@ dependencies = [ [[package]] name = "cargo-q" -version = "0.1.2" +version = "0.1.3" dependencies = [ "clap", ] diff --git a/Cargo.toml b/Cargo.toml index 82a9a17..d1d984d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-q" -version = "0.1.2" +version = "0.1.3" edition = "2021" description = "A cargo subcommand for running multiple cargo commands in a time" keywords = ["cargo", "subcommand", "plugin"] diff --git a/README.md b/README.md index e145058..b4a58ee 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Cargo subcommand to run multiple Cargo commands in a time. - ✅ Add sequential execution - ✅ Add ; as command separator -- ❌ Add & as command separator +- ✅ Add & as command separator - ❌ Add > as command separator - ❌ Add parallel execution diff --git a/src/cli.rs b/src/cli.rs index 9db72db..b30e105 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,15 +2,30 @@ use clap::Parser; #[derive(Parser, Debug)] #[command(name = "cargo-q")] +#[command(version)] +#[command(about = "A cargo subcommand for running multiple cargo commands in a time")] +#[command(author)] pub struct Cli { /// Commands to execute + /// + /// Supports multiple separators: + /// + /// space: Independent commands (e.g., "check test") + /// + /// ; : Independent commands with args (e.g., "test --features f1 ; run") + /// + /// & : Dependent commands (e.g., "check & test & run") pub command_string: String, - /// Run in verbose mode + /// Run commands in verbose mode + /// + /// Shows the output of each command as it runs #[arg(short, long)] pub verbose: bool, /// Run commands in parallel + /// + /// Only works with independent commands (space or ; separator) #[arg(short, long)] pub parallel: bool, } diff --git a/src/parser.rs b/src/parser.rs index 6d6ab3b..1b38b80 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -20,25 +20,23 @@ impl Parser { } pub fn parse(&self, input: &str, parallel: bool, verbose: bool) -> Executor { - let routines = if input.contains(';') { - self.parse_semicolon_separated(input) + // Check for & first as it's the most restrictive + if input.contains('&') { + let routines = self.parse_ampersand_separated(input); + Executor::new(parallel, verbose, routines, Strategy::Dependent) + } else if input.contains(';') { + let routines = self.parse_semicolon_separated(input); + Executor::new(parallel, verbose, routines, Strategy::Independent) } else { - self.parse_space_separated(input) - }; - - Executor::new(parallel, verbose, routines, Strategy::Independent) + let routines = self.parse_space_separated(input); + Executor::new(parallel, verbose, routines, Strategy::Independent) + } } fn parse_space_separated(&self, input: &str) -> Vec { input .split_whitespace() - .map(|cmd| { - let parts: Vec<&str> = cmd.split_whitespace().collect(); - Routine { - name: parts[0].to_string(), - args: parts[1..].iter().map(|s| s.to_string()).collect(), - } - }) + .map(|cmd| self.create_routine(cmd)) .collect() } @@ -47,22 +45,31 @@ impl Parser { .split(';') .map(str::trim) .filter(|s| !s.is_empty()) - .map(|cmd| { - let parts: Vec<&str> = cmd.split_whitespace().collect(); - if parts.is_empty() { - return Routine { - name: String::new(), - args: Vec::new(), - }; - } - Routine { - name: parts[0].to_string(), - args: parts[1..].iter().map(|s| s.to_string()).collect(), - } - }) - .filter(|routine| !routine.name.is_empty()) + .map(|cmd| self.create_routine(cmd)) + .filter(|routine| !routine.is_empty()) + .collect() + } + + fn parse_ampersand_separated(&self, input: &str) -> Vec { + input + .split('&') + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(|cmd| self.create_routine(cmd)) + .filter(|routine| !routine.is_empty()) .collect() } + + fn create_routine(&self, cmd: &str) -> Routine { + let parts: Vec<&str> = cmd.split_whitespace().collect(); + if parts.is_empty() { + return Routine::default(); + } + Routine { + name: parts[0].to_string(), + args: parts[1..].iter().map(|s| s.to_string()).collect(), + } + } } #[cfg(test)] @@ -101,6 +108,60 @@ mod tests { assert!(executor.routines[1].args.is_empty()); } + #[test] + fn test_parse_ampersand_separated() { + let parser = Parser::new(); + let input = "check & test & run"; + let executor = parser.parse(input, false, false); + + assert_eq!(executor.strategy, Strategy::Dependent); + assert_eq!(executor.routines.len(), 3); + + assert_eq!(executor.routines[0].name, "check"); + assert!(executor.routines[0].args.is_empty()); + + assert_eq!(executor.routines[1].name, "test"); + assert!(executor.routines[1].args.is_empty()); + + assert_eq!(executor.routines[2].name, "run"); + assert!(executor.routines[2].args.is_empty()); + } + + #[test] + fn test_parse_ampersand_with_args() { + let parser = Parser::new(); + let input = "test --features feature1 & run --release"; + let executor = parser.parse(input, false, false); + + assert_eq!(executor.strategy, Strategy::Dependent); + assert_eq!(executor.routines.len(), 2); + + assert_eq!(executor.routines[0].name, "test"); + assert_eq!(executor.routines[0].args, vec!["--features", "feature1"]); + + assert_eq!(executor.routines[1].name, "run"); + assert_eq!(executor.routines[1].args, vec!["--release"]); + } + + #[test] + fn test_parse_ampersand_without_spaces() { + let parser = Parser::new(); + let input = "check&test&run"; + let executor = parser.parse(input, false, false); + + assert_eq!(executor.strategy, Strategy::Dependent); + assert_eq!(executor.routines.len(), 3); + + assert_eq!(executor.routines[0].name, "check"); + assert!(executor.routines[0].args.is_empty()); + + assert_eq!(executor.routines[1].name, "test"); + assert!(executor.routines[1].args.is_empty()); + + assert_eq!(executor.routines[2].name, "run"); + assert!(executor.routines[2].args.is_empty()); + } + #[test] fn test_parse_semicolon_with_empty() { let parser = Parser::new(); diff --git a/src/routine.rs b/src/routine.rs index d58af38..1534725 100644 --- a/src/routine.rs +++ b/src/routine.rs @@ -1,7 +1,7 @@ use std::io; use std::process::{Command, Output, Stdio}; -#[derive(Debug)] +#[derive(Debug, Default)] pub(crate) struct Routine { pub(crate) name: String, pub(crate) args: Vec, @@ -28,4 +28,8 @@ impl Routine { Ok((output.status.success(), output)) } } + + pub fn is_empty(&self) -> bool { + self.name.is_empty() + } }