Skip to content

Commit

Permalink
feat: supports ; as separator (#2)
Browse files Browse the repository at this point in the history
* feat: better execution summary

* fix: skip all subcommands which are q

* feat: add ; separator

* ci: add release workflows

* release: 0.1.2
  • Loading branch information
guuzaa authored Jan 19, 2025
1 parent 4473088 commit 17878e6
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 70 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Release

on:
release:
types: [created]

env:
CARGO_TERM_COLOR: always

jobs:
publish:
name: Publish to crates.io
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cargo cache
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Cargo login
run: cargo login ${{ secrets.CRATES_IO_TOKEN }}

- name: Cargo publish
run: cargo publish
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "cargo-q"
version = "0.1.1"
version = "0.1.2"
edition = "2021"
description = "A cargo subcommand for running multiple cargo commands in a time"
keywords = ["cargo"]
keywords = ["cargo", "subcommand", "plugin"]
categories = ["command-line-utilities", "development-tools::cargo-plugins"]
license = "Apache-2.0"
readme = "README.md"
repository = "https://github.com/guuzaa/cargo-q"
exclude = [".github", ".vscode", ".gitignore"]

[dependencies]
clap = { version = "4.5", features = ["derive"] }
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Cargo subcommand to run multiple Cargo commands in a time.
<summary>TODO</summary>

- ✅ Add sequential execution
- Add ; as command separator
- Add ; as command separator
- ❌ Add & as command separator
- ❌ Add > as command separator
- ❌ Add parallel execution
Expand Down
11 changes: 6 additions & 5 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ pub struct Cli {

impl Cli {
pub fn parse() -> Self {
// Skip the first argument which is "q" for cargo subcommands
let mut args = std::env::args().collect::<Vec<_>>();
if args.len() >= 2 && args[1] == "q" {
args.remove(1);
}
// Skip the all arguments which are "q" for cargo subcommands
let args = std::env::args()
.collect::<Vec<_>>()
.into_iter()
.filter(|arg| arg != "q")
.collect::<Vec<_>>();

Self::parse_from(args)
}
Expand Down
72 changes: 39 additions & 33 deletions src/executor.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::parser::Strategy;
use crate::process::{ColorExt, ExecutionSummary};
use crate::routine::Routine;
use std::io::{self, Error, ErrorKind};
use std::time::Instant;

pub(crate) struct Executor {
pub(super) parallel: bool,
Expand All @@ -28,47 +28,53 @@ impl Executor {
));
}

let start_time = Instant::now();
let mut success_count = 0;
let total_commands = self.routines.len();
let mut summary = ExecutionSummary::new(total_commands);

for cmd in &self.routines {
let success = cmd.run(self.verbose)?;
for (idx, cmd) in self.routines.iter().enumerate() {
let cmd_str = if cmd.args.is_empty() {
cmd.name.clone()
} else {
format!("{} {}", cmd.name, cmd.args.join(" "))
};
println!(
"\n {} {}",
format!("[{}/{}]", idx + 1, total_commands).bold(),
cmd_str
);

match self.strategy {
Strategy::Independent => {
// Continue to next command regardless of success
if success {
success_count += 1;
match cmd.run(self.verbose) {
Ok((success, output)) => {
match self.strategy {
Strategy::Independent => {
if success {
summary.increment_success();
} else if !output.stderr.is_empty() {
eprintln!("error: Command failed but continuing due to Independent strategy");
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
}
Strategy::Dependent | Strategy::Pipe => {
if !success {
if !output.stderr.is_empty() {
eprintln!("error: {}", String::from_utf8_lossy(&output.stderr));
}
return Err(Error::new(
ErrorKind::Other,
format!("Command failed: cargo {}", cmd_str),
));
}
summary.increment_success();
}
}
}
Strategy::Dependent | Strategy::Pipe => {
// Stop if command failed
if !success {
let elapsed = start_time.elapsed();
eprintln!(
"Summary: {}/{} commands succeeded ({:.2}s)",
success_count,
total_commands,
elapsed.as_secs_f32()
);
return Err(Error::new(
ErrorKind::Other,
format!("Command 'cargo {} {}' failed", cmd.name, cmd.args.join(" ")),
));
}
success_count += 1;
Err(e) => {
eprintln!("error: Failed to execute command: {}", e);
return Err(e);
}
}
}

let elapsed = start_time.elapsed();
println!(
"Summary: {}/{} commands succeeded ({:.2}s)",
success_count,
total_commands,
elapsed.as_secs_f32()
);
Ok(())
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod cli;
mod executor;
mod parser;
mod process;
mod routine;

use cli::Cli;
Expand Down
83 changes: 75 additions & 8 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,17 @@ impl Parser {
}

pub fn parse(&self, input: &str, parallel: bool, verbose: bool) -> Executor {
// For now, only implement space separator (Independent strategy)
let routines = input
let routines = if input.contains(';') {
self.parse_semicolon_separated(input)
} else {
self.parse_space_separated(input)
};

Executor::new(parallel, verbose, routines, Strategy::Independent)
}

fn parse_space_separated(&self, input: &str) -> Vec<Routine> {
input
.split_whitespace()
.map(|cmd| {
let parts: Vec<&str> = cmd.split_whitespace().collect();
Expand All @@ -30,9 +39,29 @@ impl Parser {
args: parts[1..].iter().map(|s| s.to_string()).collect(),
}
})
.collect();
.collect()
}

Executor::new(parallel, verbose, routines, Strategy::Independent)
fn parse_semicolon_separated(&self, input: &str) -> Vec<Routine> {
input
.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())
.collect()
}
}

Expand All @@ -41,7 +70,7 @@ mod tests {
use super::*;

#[test]
fn test_parse_simple_commands() {
fn test_parse_space_separated() {
let parser = Parser::new();
let input = "check test";
let executor = parser.parse(input, false, false);
Expand All @@ -57,15 +86,53 @@ mod tests {
}

#[test]
fn test_parse_simple_one_command() {
fn test_parse_semicolon_separated() {
let parser = Parser::new();
let input = "check";
let input = "test --features feature1 ; run";
let executor = parser.parse(input, false, false);

assert_eq!(executor.strategy, Strategy::Independent);
assert_eq!(executor.routines.len(), 1);
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!(executor.routines[1].args.is_empty());
}

#[test]
fn test_parse_semicolon_with_empty() {
let parser = Parser::new();
let input = "check ; ; test";
let executor = parser.parse(input, false, false);

assert_eq!(executor.strategy, Strategy::Independent);
assert_eq!(executor.routines.len(), 2);

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());
}

#[test]
fn test_parse_semicolon_with_spaces() {
let parser = Parser::new();
let input = " check ; test ; run ";
let executor = parser.parse(input, false, false);

assert_eq!(executor.strategy, Strategy::Independent);
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());
}
}
75 changes: 75 additions & 0 deletions src/process.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use std::fmt;
use std::time::Instant;

// Terminal colors support
pub trait ColorExt {
fn red(self) -> ColoredString;
fn green(self) -> ColoredString;
fn bold(self) -> ColoredString;
}

impl<T: fmt::Display> ColorExt for T {
fn red(self) -> ColoredString {
ColoredString(format!("\x1b[31m{}\x1b[0m", self))
}
fn green(self) -> ColoredString {
ColoredString(format!("\x1b[32m{}\x1b[0m", self))
}
fn bold(self) -> ColoredString {
ColoredString(format!("\x1b[1m{}\x1b[0m", self))
}
}

pub struct ColoredString(String);

impl fmt::Display for ColoredString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}

pub struct ExecutionSummary {
success_count: usize,
total_commands: usize,
start_time: Instant,
}

impl ExecutionSummary {
pub fn new(total_commands: usize) -> Self {
Self {
success_count: 0,
total_commands,
start_time: Instant::now(),
}
}

pub fn increment_success(&mut self) {
self.success_count += 1;
}

fn print_summary(&mut self) {
let elapsed = self.start_time.elapsed().as_secs_f32();
let status = if self.success_count == self.total_commands {
"Finished".green()
} else {
"Failed".red()
};
println!(
"\n{} {} command(s) in {:.2}s",
status, self.total_commands, elapsed
);
if self.success_count != self.total_commands {
println!(
" {} succeeded, {} failed",
self.success_count,
self.total_commands - self.success_count
);
}
}
}

impl Drop for ExecutionSummary {
fn drop(&mut self) {
self.print_summary();
}
}
Loading

0 comments on commit 17878e6

Please sign in to comment.