4 stable releases
2.0.15 | Dec 2, 2022 |
---|---|
2.0.14 | Nov 30, 2022 |
2.0.12 | Jul 29, 2022 |
2.0.10 |
|
0.1.14 |
|
#106 in Cargo plugins
1.5MB
963 lines
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.
# 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] [<ARGUMENTS>...]
ARGS:
COMMAND Name of the command to run
URL Downloads a script, compiles then runs it
FILE Compiles a file then runs it
<ARGUMENTS>... 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.
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.
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.
command = { cmd = ["echo first", "echo second", "echo third"], parallel = true }
working_dir
String. The path where the command is supposed to execute in.
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.
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.
# 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.
# 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.
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.
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.
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
.
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.
# 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!
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.
# 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
.
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.
# 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:
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.
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.
Dependencies
~6–48MB
~727K SLoC