# Cargo Commander
The simple way of running commands
[![Crates.io](https://img.shields.io/crates/v/cargo-commander)](https://crates.io/crates/cargo-commander)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/simonhyll)](https://www.patreon.com/simonhyll)
[![GitHub last commit (branch)](https://img.shields.io/github/last-commit/simonhyll/cargo-commander/main)](https://github.com/simonhyll/cargo-commander/commit/main)
[![Build and test](https://github.com/simonhyll/cargo-commander/actions/workflows/build.yml/badge.svg)](https://github.com/simonhyll/cargo-commander/actions/workflows/build.yml)
[![Website](https://img.shields.io/website?down_color=red&down_message=offline&up_color=green&up_message=online&url=https%3A%2F%2Fsimonhyll.github.io%2Fcargo-commander%2F)](https://simonhyll.github.io/cargo-commander/)
## Introduction
Cargo Commander serves to fill the gap in the `cargo` commands capabilities, namely not being able to run commands in a
similar fashion the way `npm` does with scripts. But while I was at it I decided to add some extra functionality to it.
New: In addition to running commands specified in either `Commands.toml`, `Cargo.toml` or `package.json`, functionality to execute scripts similar to how `cargo-script` does is being worked on. You can try it by either running a local script, `cargo cmd script.rs`, or running a remote script, `cargo cmd https://url.to.script`. This is currently in the early beta stages and functions by running `rustc input -o output`, then executing the output, so it's currently limited to using the standard library and the script has to be contained within that singular file. More features to come!
## Getting started
Either create your commands under a `[commands]` or `[package.metadata.commands]` section in `Cargo.toml`, or create a
new `Commands.toml` file. They all use the same syntax. Cargo commander also parses the `scripts` section
inside `package.json` if it's found. Normally scripts inside package.json are only allowed to be strings, but Cargo
Commander parses `package.json` by converting from json to toml, meaning you can add all the same options in json as you
can in toml.
```bash
# Install cargo-commander
cargo install cargo-commander
# Run your command
cargo cmd COMMAND
# Output of 'cargo cmd --help'
cargo-commander 2.0.15
A powerful tool for managing project commands
USAGE:
cargo cmd [OPTIONS] [COMMAND/URL/FILE] [...]
ARGS:
COMMAND Name of the command to run
URL Downloads a script, compiles then runs it
FILE Compiles a file then runs it
... Arguments to the command
OPTIONS:
-h, --help Print help information
-f, --file PATH Custom path to command file to parse
-p, --parallel Forces all commands to run in parallel
```
## Command
A command can either be a string or a command object using the below fields to customize its behavior.
```text
cmd = String or Array, where an array can either contain string commands or other command objects
parallel = true/false, only makes a difference if the command object contains an array, makes all commands run in parallel
shell = String, the syntax is simply "program arg arg arg"
env = Array, an array of strings in the format "VAR=SOMETHING"
args = Array, an array of strings in the format "ARG=Default value", if no default is given an empty string is used
working_dir = String, path to the directory to use as working directory either relative to the command file or the current directory
```
### cmd
This can be either a string, a command object or an array of command objects.
If `cmd` is a multiline string the contents of the command is saved to a temporary file that gets safely deleted after
the program finishes. The arguments are then used to replace content within the string, and the only argument sent to
the shell is the path to the temporary file. We can use this behavior together with the `shell` option to create a file
whose absolute path gets passed as an argument to whatever program you specify as a shell. See the examples for how this
might look.
```toml
command = "echo Basic usage"
command = ["echo As an array"]
command = { cmd = "echo Hello" }
command = { cmd = ["echo Hello", "echo World"] }
command = { cmd = [{ cmd = "echo And hello again" }] }
command = { cmd = { cmd = "echo Hello again" } }
```
### parallel
Boolean, defaults to false. If the `cmd` of the command object is an array, all sub commands will be run at the same
time.
```toml
command = { cmd = ["echo first", "echo second", "echo third"], parallel = true }
```
### working_dir
String. The path where the command is supposed to execute in.
```toml
command = { cmd = "ls", working_dir = "src" }
command = { cmd = "ls", working_dir = "path/to/folder" }
```
### args
Array of strings in the format `args=["arg","argument=Default"]`. If an argument is a string without a default value set
it'll simply be replaced with an empty string.
```toml
command = { cmd = "echo $name", args = ["name=World"] }
```
### env
Array of strings in the format `env=["variable=Value"]`. Sets environment variables in the command. This is similar to
how `args` works, but the difference is
that `env` changes environment variables. This option is generally speaking not super useful, you probably want to
use `load_dotenv` instead.
```toml
# Unix
command = { cmd = "echo $HELLO", env = ["HELLO=World"] }
# Windows
command = { cmd = "echo %HELLO%", env = ["HELLO=World"] }
```
### load_dotenv
Boolean, defaults to false. Allows you to load environment variables from a .env file. The .env file should be located
in the same folder as the file that contains the command being run. This option is unaffected by the `working_dir`
option.
```toml
# Create a .env file with the contents "HELLO=World"
# Unix
command = { cmd = "echo $HELLO", load_dotenv = true }
# Windows
command = { cmd = "echo %HELLO%", load_dotenv = true }
```
### until
Integer. Which status code counts as a successful run. Normally we don't check the status code of the command, but with
this option we can tell the command to keep repeating until it reaches a specific exit code. If you set this
to `until=0` it would mean that you keep running the command until you reach a status 0 exit code. With `until=404` it
would keep running until you reach code 404. If you want to avoid infinite looping you should set `max_repeat` as well.
```toml
command = { cmd = "echo Hello", until = 0 }
```
### repeat
Integer. Minimum number of times the command is meant to run. If you run this together with `until` you'll always be
running the command at least this number of times.
```toml
command = { cmd = "echo Hello", repeat = 2 }
```
### delay
Integer or float. Amount of time to sleep before running the command. If you use this together with any of the
repetition based options this delay will be added before every run of the command.
```toml
command = { cmd = "echo Hello", delay = 2 }
command = { cmd = "echo Hello", delay = 3.7 }
```
### max_repeat
Integer. Sets the maximum number of times the command is allowed to retry. This is mostly useful when running together
with `until`.
```toml
command = { cmd = "echo Hello", repeat = 5, max_repeat = 1 }
command = { cmd = "echo Hello", until = 0, max_repeat = 1000 }
```
## Examples
### Opening documentation
I have a tendency to create multiple `mdbook` books for documenting my projects. It's really neat, but it can be a bit
of a bother to open them all one by one. So what I do is put the command to open each document under a `docs` section,
then run the section rather than each individual page, using the `-p` flag to make the section run in parallel.
```toml
# Commands.toml
[docs]
crate_one = { cmd = "mdbook serve --open --port 9001", working_dir = "crates/one/docs" }
crate_two = { cmd = "mdbook serve --open --port 9002", working_dir = "crates/two/docs" }
crate_three = { cmd = "mdbook serve --open --port 9003", working_dir = "crates/three/docs" }
crate_four = { cmd = "mdbook serve --open --port 9004", working_dir = "crates/four/docs" }
```
Now we can open all documents using a single command!
```bash
cargo cmd -p docs
```
### Passing a custom argument
Let's say you want to get a running shell inside a Kubernetes pod where you don't know the pod name beforehand, probably
because the pod was created by e.g. a deployment or a cronjob. There is a `kubectl` command you know of that can get you
a running shell inside the pod, the problem is that the command is pretty long and annoying to write every time, and
copy pasting the command from somewhere else every time gets repetitive really fast.
```toml
# Commands.toml
shell = { cmd = "kubectl exec --stdin --tty $pod -- /bin/bash", args = ["pod"] }
```
Now we can always get a shell to our pod by simple running the below simplified syntax. Now instead of having to both
find the name of your pod and copy it into the longer `kubectl` command, you can now easily remember that you have
a `shell` command that takes the argument `pod`.
```bash
cargo cmd shell pod=my-pod-123-654
```
### Running a script
With a mix of the `shell` option and the behavior we've set for when a command is a multiline string we can achieve
running scripts written directly in your command.
```toml
# Using python -c
hello_py_c = { cmd = "print('Hello')", shell = "python -c" }
# Using python and multiline string and an argument
hello_py = { cmd = """import os
print("Hello")
print("$name")
""", args = ["name=World"], shell = "python" }
```
You can then run it as follows:
```bash
cargo cmd hello_py_c
Hello
# Or multiline
cargo cmd hello_py
Hello
World
# ... With argument
cargo cmd hello_py name=Commander
Hello
Commander
```
### Keep retrying until command succeeds
Sometimes you run programs or write scripts that can fail. It's ok, it happens to everyone. Maybe it's a networked
resource it's trying to reach, or maybe a file on your computer. No matter what the reason, the program will sometimes
exit with a successful code `0`, other times it exits with code `404` because the page it tried to reach wasn't found.
We can easily create a simple retry loop using `until`, combined with `delay` so that the program isn't ran too often,
and `max_repeat` so that we don't try forever.
```toml
command = { cmd = "python script.py", until = 0, delay = 3, max_repeat = 1000 }
```
Running that command makes it keep retrying with a 3 seconds delay between retries. It will retry until it gets a 0
status returned, or a maximum of 1000 times.
## Notes
### Environment variables don't persist
I've tried to get this to work as intended but for now I've kind of given up on this since it appears to be anywhere
between impossible and really, really annoying to get to work right. So each each command will have a "fresh" set of
environment variables, if one command changes environment variables another command won't pick up on those changes, they
are run in different shells. You can either use the `env` option, or you can run a script in every command that sets up
environment variables, or you can use `load_dotenv` to load variables from a `.env` file. I consider these options to be
sufficient, if you really want variables to persist across commands you'll have to make a pull request with your
changes, or wait until I feel like delving deeper into the issue.