| Crates.io | rhask |
| lib.rs | rhask |
| version | 0.3.1 |
| created_at | 2025-11-06 21:55:22.881897+00 |
| updated_at | 2025-11-23 20:10:15.247454+00 |
| description | Rhai-based Task Runner. |
| homepage | https://github.com/nakkiy/rhask |
| repository | https://github.com/nakkiy/rhask |
| max_upload_size | |
| id | 1920777 |
| size | 484,433 |
Rhask is a lightweight task runner written in Rust. Tasks and groups are authored in Rhai, can be nested arbitrarily, and are invoked through fully qualified names such as group.task (or by short names when they are unique).

# Install from crates.io
cargo install rhask
# List tasks at the project root
rhask list
# Run a task
rhask run <task>
Example rhaskfile.rhai:
task("hello", || {
actions(|| {
print("Hello from Rhask!");
});
});
rhaskfile.rhai at the repository root (copy rhaskfile_demo.rhai or rhaskfile_sample.rhai to get started).-f/--file to point Rhask at any Rhai script, e.g. rhask -f ./rhaskfile_demo.rhai list.rhask run <task> and rhask <task> behave the same; ambiguous short names print the candidates.default_task("group.task") so rhask (with no arguments) runs that entry, otherwise it falls back to listing tasks.rhaskfile.rhai.-f / --file to load any other Rhai script.task("name", || { ... }) / group("name", || { ... }) to build nested hierarchies.group.subgroup.task.rhask list deploy -F).rhask list
description() text is aligned on the right-hand side.rhask list --flat / rhask list -F
full.path description on a single line (colorized on TTYs).rhask run <task> or the shorthand rhask <task>.rhask behaves as follows:
default_task("...") when present.rhask list.<task> names print candidates and exit (Rhask will not guess). Re-run with the full path.args(#{ key: default, ... }) declares CLI parameters; () marks them as required.
key=value, --key=value, or --key value, and you can mix the styles.dir()dir("path") once per task to pin its working directory.rhaskfile.rhai; absolute paths are left unchanged.trigger()s another task, the callee’s own dir() always wins; parent settings are never inherited.dir() the task runs in the shell directory where you launched rhask.cmd / pipe / exec)cmd()cmd(["git", "status"])
pipe()cmd(["git", "branch", "-vv"])
.pipe(cmd(["grep", "gone"]))
git | grep | awk-style flows without shell syntax.build().timeout(ms), .env(#{}), .allow_exit_codes([0, 1]), and similar helpers.exec()#{ success, status, stdout, stderr, duration_ms }.exec_stream()rhask completions <shell> generates Bash/Zsh/Fish completion scripts (task names included).scripts/coverage.sh as a helper around cargo llvm-cov.import statements work as in upstream Rhai, so you can split large task files as needed.| Command | Description |
|---|---|
rhask list [group] |
Display registered tasks/groups as a tree. Passing a fully qualified name limits the output to that subtree. |
rhask list --flat / rhask list -F |
Print each task as full.path plus an aligned description (colorized on TTYs, works with group filters and tools like fzf). |
rhask run <task> [args…] |
Execute a task. Ambiguous leaves print the candidates and ask you to re-run with a full path. The shorthand rhask <task> behaves the same. |
rhask -f <file> … |
Explicitly load a Rhai script. Place -f/--file before the subcommand or task name (e.g. rhask -f ./demo.rhai list). |
rhask (no arguments) |
Run the configured default_task() or fall back to rhask list when unset. |
rhask completions <shell> |
Emit shell completion scripts (see below). |
Declaring args(#{ target: (), profile: "debug" }) assigns positional arguments in lexicographic key order (profile → target in this example). () marks required parameters. Use whichever CLI style you prefer:
rhask run build release x86_64-unknown-linux-gnukey=value: rhask run build profile=release target=x86_64-unknown-linux-gnu--key=value: rhask run build --target=x86_64-apple-darwin--key value: rhask run build --target wasm32-unknown-unknownUnknown keys raise an error, and missing required values trigger a descriptive failure.
task("build", || {
description("Build the project");
args(#{
target: (),
profile: "debug"
});
actions(|profile, target| {
print("build => profile:" + profile + ", target:" + target);
});
});
group("release_flow", || {
description("Release tasks");
task("package", || {
actions(|| {
exec(cmd(["cargo", "package"]).build());
});
});
});
| Function | Description |
|---|---|
task(name, || { ... }) |
Declare a task; call description/actions/args inside it. |
group(name, || { ... }) |
Declare a group; nest additional groups or tasks. |
description(text) |
Usable inside task()/group(); sets the label shown in listings (call once per task). |
actions(|| { ... }) |
Usable inside task(); registers the executable closure (call once). Invoke trigger() or exec(...) from here. |
args(#{ key1: default1, key2: (), ... }) |
Usable inside task(); declares CLI parameters. () = no default = required. Call once per task. |
dir(path) |
Usable inside task(); pins the working directory (call once). Relative paths resolve from the rhaskfile directory; absolute paths stay as-is. Invalid paths error at load time. |
default_task("full.path") |
Declare once at the top level (imports included) to define the fallback when rhask is run without arguments. |
trigger(name, positional?, named?) |
Usable inside actions(); runs another task. Accepts positional arrays and/or named maps. The callee’s dir() takes precedence over the caller’s. |
cmd([cmd, arg, ...]) |
Build external commands inside actions(). Chain .env() / .pipe() and finish with .build() before running exec(...) or exec_stream(...). |
exec(pipeline) / exec_stream(pipeline, stdout_cb?, stderr_cb?) |
Usable inside actions(); execute pipelines and receive #{ success, status, stdout, stderr, duration_ms }. exec_stream lets you process output live. |
dir()dir(path) is allowed once per task(). Absolute paths remain untouched; relative paths are resolved against the directory that hosts the loaded rhaskfile (the one Rhask found or the file passed via -f/--file).dir(): '...' is not a directory.dir() is present, Rhask chdirs into that location before executing actions(). Nested exec()/trigger() calls always honor the callee’s dir() rather than inheriting from parents.dir() run in the shell directory from which you launched rhask. Set dir(".") or dir("scripts") explicitly if you need predictability.rhask -f child/file.rhai, relative paths will resolve from that child file instead, so plan accordingly.task("coverage", || {
description("Run coverage helper script from scripts/");
dir("scripts");
actions(|| {
exec(cmd(["./coverage.sh", "--mode", "unit"]).build());
});
});
cmd / exec / exec_stream)cmd([program, arg, ...]), chain .pipe(cmd([...])), override the environment via .env(#{ KEY: "VALUE" }), and call .build(). After build() you can still attach .timeout(ms) or .allow_exit_codes([0, 1]).exec(cmd(...).pipe(...).build()) returns #{ success, status, stdout, stderr, duration_ms }, mirrors stdout/stderr to the terminal, and throws on disallowed exit codes.exec_stream(cmd(...).build(), stdout_cb?, stderr_cb?) suits streaming workloads. Omit the callbacks to stream directly to the console.actions()actions() so dir() semantics and nested trigger() calls stay consistent.actions(|| {
let pipeline = cmd(["git", "branch", "-vv"])
.pipe(cmd(["grep", "gone]"]))
.pipe(cmd(["awk", "{print $1}"]))
.build();
let result = exec(pipeline);
print("deleted branches:\n" + result.stdout);
});
rhask completions bash > ~/.local/share/bash-completion/rhask
echo "source ~/.local/share/bash-completion/rhask" >> ~/.bashrc
Drop the generated file into the appropriate completion directory (Bash/Zsh/Fish) and source it. Task/group names defined in Rhai are part of the completion results. When you pass -f/--file, the completion function forwards that value so suggestions always match the referenced rhaskfile.
stdout; warnings and errors go to stderr.RUST_LOG=debug rhask run … (or trace) thanks to the env_logger backend.trigger() or exec(cmd([...]).build()) must happen inside actions(); doing so elsewhere raises errors.scripts/coverage.sh wraps cargo-llvm-cov / llvm-tools-preview, installing the tools on demand (requires rustup).
./scripts/coverage.sh --mode all (default):target/coverage/html/index.html../scripts/coverage.sh --mode unit:target/coverage-unit/html/index.html../scripts/coverage.sh --mode integration:tests/*.rs only and write target/coverage-integration/html/index.html.Arguments after -- are forwarded directly to cargo llvm-cov. Use the same script from CI as well.
Dual-licensed under MIT OR Apache-2.0.