Skip to content

Commit

Permalink
Pre- & Post- commands (#14)
Browse files Browse the repository at this point in the history
Closes #5
  • Loading branch information
danreeves authored Nov 16, 2018
1 parent 52f4b24 commit e5f6a8d
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 28 deletions.
16 changes: 15 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cargo-cmd"
description = "Like npm scripts, but for cargo"
description = "Alias any shell command in your Cargo.toml. It's like npm scripts, but for cargo."
version = "0.2.1"
authors = ["Dan Reeves <[email protected]>"]
repository = "https://github.com/danreeves/cargo-cmd"
Expand All @@ -13,16 +13,30 @@ keywords = ["cargo", "cmd", "scripts", "commands", "npm"]
echo = "echo"
greet = "cargo cmd echo 'Hello, planet!'"
dev = "cargo watch -s 'clear; cargo build && cargo cmd greet'"

pass = "exit 0"
fail = "exit 42"

prechain = "echo 1"
chain = "echo 2"
postchain = "echo 3"

prefailchain = "exit 42"
failchain = "echo 2"

pretest = "echo pre"
test = "cargo test"
posttest = "echo post"

[dependencies]
toml = "0.4.8"
serde_derive = "1.0.79"
serde = "1.0.79"
subprocess = "0.1.16"
structopt = "0.2.12"
clap = "2.32.0"

[dev-dependencies]
assert_cli = "0.6.3"

[target.'cfg(windows)'.dependencies]
Expand Down
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![docs.rs docs][5]][6]
[![license][7]][8]

Like `npm` scripts, but for `cargo`.
Alias any shell command in your `Cargo.toml`. It's like `npm` scripts, but for `cargo`.

## Installation

Expand All @@ -30,6 +30,50 @@ $ cargo cmd greet
Hello, planet!
```

### Advanced use

#### Passing arguments

It's possible to pass arguments into your command by passing them directly to `cargo cmd`.

```toml
[package.metadata.commands]
echo = "echo"
```

```sh
$ cargo cmd echo 'Hello, planet!'
> echo 'Hello, planet!'
Hello, planet!
```

#### Pre and Post commands

You are able to set up commands to run before and after your command by prefixing the name with `pre` or `post` respectively.

```toml
[package.metadata.commands]
pretest = "./setup.sh"
test = "cargo test"
posttest = "./teardown.sh"
```

```sh
$ cargo cmd test

[pretest]
> ./setup.sh
Setting up DB...

[test]
> cargo test
...

[posttest]
> ./teardown.sh
Tearing down DB...
```

## License
[MIT © Dan Reeves](./LICENSE)

Expand Down
60 changes: 39 additions & 21 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,33 +44,40 @@ fn main() {
let (command, rest) = match cli {
Cli::Cmd { command, rest } => (command, rest),
};
let cmds = exit_if_error(get_cmds(&command));
let shell_command = cmds.get(&command);

if let Some(command) = shell_command {
// This is naughty but Exec::shell doesn't let us do it with .args because
// it ends up as an argument to sh/cmd.exe instead of our user command
// or escaping things weirdly.
let command = format!("{} {}", command, rest.join(" "));
println!("> {}", command);
let sh = Exec::shell(command);
let exit = sh.join().unwrap();
let commands = unwrap_or_exit(get_commands(&command));
let is_multiple_commands = commands.len() > 1;

for (index, command) in commands.iter().enumerate() {
if is_multiple_commands {
println!("\n[{}]", &command.0);
}
let command = &command.1;
let exit = execute_command(command, &rest);

if exit.success() {
process::exit(0);
if index == commands.len() {
process::exit(0);
}
} else {
match exit {
ExitStatus::Exited(exit_code) => process::exit(exit_code as i32),
_ => process::exit(1),
}
}
} else {
eprintln!("Command \"{}\" not found in Cargo.toml", &command);
process::exit(1);
}
}

fn exit_if_error<T>(result: Result<T, String>) -> T {
fn execute_command(command: &str, rest: &Vec<String>) -> ExitStatus {
// This is naughty but Exec::shell doesn't let us do it with .args because
// it ends up as an argument to sh/cmd.exe instead of our user command
// or escaping things weirdly.
let command = format!("{} {}", command, rest.join(" "));
println!("> {}", command);
let sh = Exec::shell(command);
sh.join().unwrap_or(ExitStatus::Exited(0))
}

fn unwrap_or_exit<T>(result: Result<T, String>) -> T {
match result {
Err(error_msg) => {
clap::Error::with_description(&error_msg[..], clap::ErrorKind::InvalidValue).exit();
Expand All @@ -79,11 +86,17 @@ fn exit_if_error<T>(result: Result<T, String>) -> T {
}
}

fn get_cmds(command: &str) -> Result<HashMap<String, String>, String> {
fn get_commands(command: &str) -> Result<Vec<(String, String)>, String> {
let mut cargo_toml = File::open("Cargo.toml").or(Err(
"Could not find or open Cargo.toml in the current directory",
))?;
let mut cargo_str = String::new();
let mut commands = vec![];
let names = vec![
format!("pre{}", command),
command.to_string(),
format!("post{}", command),
];

cargo_toml
.read_to_string(&mut cargo_str)
Expand All @@ -92,13 +105,18 @@ fn get_cmds(command: &str) -> Result<HashMap<String, String>, String> {
let cargo_toml: Cargotoml =
toml::from_str(&cargo_str[..]).or(Err("Could not find commands in Cargo.toml"))?;

let commands = cargo_toml.package.metadata.commands;
let cargo_commands = cargo_toml.package.metadata.commands;

for name in names {
let command_to_run = &cargo_commands.get(&name);

{
let command_to_run = &commands.get(command);
if command_to_run.is_none() {
if name == command && command_to_run.is_none() {
return Err(format!("Command \"{}\" not found in Cargo.toml", &command));
}

if command_to_run.is_some() {
commands.push((name, command_to_run.unwrap().to_string()));
}
}

Ok(commands)
Expand Down
52 changes: 47 additions & 5 deletions tests/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ fn it_shows_help_for_no_args() {
#[test]
fn it_errors_if_cmd_not_found() {
assert_cli::Assert::main_binary()
.with_args(&["cmd", "test"])
.with_args(&["cmd", "notfound"])
.fails()
.and()
.stderr()
.contains("Command \"test\" not found in Cargo.toml")
.contains("Command \"notfound\" not found in Cargo.toml")
.unwrap();
}

Expand Down Expand Up @@ -50,8 +50,50 @@ fn it_passes_extra_arguments_to_the_command() {
.succeeds()
.and()
.stdout()
// This is both the printout of the command being executed
// and the output itself
.contains("> echo hello planet\nhello planet")
.contains("> echo hello planet")
.unwrap();
}

#[test]
fn it_runs_the_pre_command() {
assert_cli::Assert::main_binary()
.with_args(&["cmd", "chain"])
.succeeds()
.and()
.stdout()
.contains("[prechain]")
.unwrap();
}

#[test]
fn it_runs_the_post_command() {
assert_cli::Assert::main_binary()
.with_args(&["cmd", "chain"])
.succeeds()
.and()
.stdout()
.contains("[postchain]")
.unwrap();
}

#[test]
fn it_labels_the_command_if_running_multiple() {
assert_cli::Assert::main_binary()
.with_args(&["cmd", "chain"])
.succeeds()
.and()
.stdout()
.contains("[chain]")
.unwrap();
}

#[test]
fn it_stops_the_chain_if_a_command_fails() {
assert_cli::Assert::main_binary()
.with_args(&["cmd", "failchain"])
.fails()
.and()
.stdout()
.doesnt_contain("[chain]")
.unwrap();
}

0 comments on commit e5f6a8d

Please sign in to comment.