only is a cross-platform task runner driven by an Onlyfile. This guide starts small, then adds parameters, dependencies, guards, shells, namespaces, and troubleshooting.
Create a file named Onlyfile in your project root:
# Minimal task.
hello():
echo "hello from only"
Run it:
only helloRun only with no task to list what the file exposes:
onlyonly discovers Onlyfile or onlyfile from the current directory upward. To see which file was found:
only -pTo use a specific file:
only -f ./examples/Onlyfile helloUse # for ordinary comments. Lines starting with % document the next task or namespace, and show up in only and only --help output.
# Ordinary comments are ignored.
% Format the codebase.
fmt():
cargo fmt --all
% Run tests.
test():
cargo test
Now only gives a readable task list instead of just names.
Tasks use function-style signatures. A parameter without a default is required; a parameter with a default is optional.
% Serve the dev site.
serve(port="3000", host="127.0.0.1"):
echo "Serving on {{host}}:{{port}}"
Run it with defaults:
only serve
# Serving on 127.0.0.1:3000Pass positional arguments in declaration order:
only serve 8080
# Serving on 127.0.0.1:8080
only serve 8080 0.0.0.0
# Serving on 0.0.0.0:8080Override by name with --set:
only --set host=0.0.0.0 serve
# Serving on 0.0.0.0:3000
only --set host=0.0.0.0 serve 8080
# Serving on 0.0.0.0:8080--set is a global option, so put it before the task path. It can be repeated:
only --set host=0.0.0.0 --set port=8080 serveParameter binding precedence is:
--set NAME=VALUE- positional arguments
- defaults from the task signature
greet(name):
echo "Hello, {{name}}"
only greet Ada
# Hello, AdaRunning only greet fails before any command runs because name has no default.
Use \{{ and \}} when you need literal {{ and }} in a command.
show(template="value"):
echo "{{template}} and literal \{{template\}}"
only show
# value and literal {{template}}Use & to run dependencies before the task body.
fmt():
cargo fmt --all --check
check():
cargo check
ci() & fmt & check:
echo "CI complete"
only ci runs fmt, then check, then ci.
Use parentheses to run a stage in parallel.
build():
cargo build --release
package():
echo "package"
publish():
echo "publish"
release() & build & (package, publish):
echo "Release complete"
build runs first. After it succeeds, package and publish run together. The release body runs after both finish.
A task whose name starts with _ is a helper task. It can be used as a dependency, but it is hidden from normal listings and cannot be invoked directly.
_prepare():
cargo fmt --all --check
ci() & _prepare:
cargo test
Use this for internal steps that are useful inside workflows but noisy in only output.
A guard selects a task variant only when the current machine matches the probe.
test() ? @has("cargo-nextest"):
cargo nextest run
test():
cargo test
only test uses cargo nextest run when cargo-nextest exists on PATH; otherwise it falls back to cargo test.
Supported probes:
| Probe | Matches when |
|---|---|
@os("macos") / @os("linux") / @os("windows") |
std::env::consts::OS equals the argument |
@arch("x86_64") / @arch("aarch64") |
std::env::consts::ARCH equals the argument |
@env("CI") |
the environment variable is set |
@has("cargo") |
the command exists on PATH |
Selection rules:
- The first matching guarded variant wins.
- If no guarded variant matches, the unguarded variant is used as fallback.
- If there is no fallback, the task is unavailable on this machine.
By default, commands run through the built-in cross-platform deno shell. That is what gives only consistent behavior across platforms.
Use a task-level shell only when a command really needs a host shell:
install() ? @os("windows") shell?=pwsh:
Write-Output "Installing on Windows"
install():
cargo install --path crates/cli --force
shell= requires that exact shell. shell?= prefers that shell, but allows a known compatible fallback:
shell?=pwshcan fall back topowershellshell?=bashcan fall back tosh
Accepted shell names:
| Shell | Behavior |
|---|---|
deno |
built-in cross-platform shell |
bash |
runs bash -c |
sh |
runs sh -c |
pwsh |
runs PowerShell 7+ |
powershell |
runs Windows PowerShell |
You can also set a file-level default shell:
!shell bash
hello():
echo "hello from bash"
Resolution order is:
- task-level
shell=orshell?= - file-level
!shell - built-in default
deno
Directives start with ! and apply to the whole file.
!echo true
!preview false
!shell deno
| Directive | Values | Default | Effect |
|---|---|---|---|
!echo |
true / false |
true |
Controls command output on success. Failures still surface output. |
!preview |
true / false |
false |
Prints the selected task variant and rendered commands before execution. |
!label |
true / false |
true |
Controls the [task] prefix on task output. |
!shell |
shell name | deno |
Sets the default shell for tasks without an explicit shell. |
Use !preview true when you want to see exactly which task variant and commands will run after parameters are bound.
Use !label false when you want raw command output but still want task progress lines.
Namespaces group related tasks.
% Development workflow.
[dev]
% Build in development mode.
build():
cargo build
% Run in development mode.
run():
cargo run
% Release workflow.
[rel]
build():
cargo build --release
Run namespaced tasks by putting the namespace before the task:
only dev build
only dev run
only rel buildRun only dev to see help for just that namespace.
!echo true
!preview false
% Serve the dev site.
serve(port="3000", host="127.0.0.1"):
echo "Serving on {{host}}:{{port}}"
% Format and check the workspace.
check():
cargo fmt --all --check
cargo check
% Prefer nextest when available.
test() ? @has("cargo-nextest"):
cargo nextest run
test():
cargo test
_package():
echo "Packaging release"
publish():
echo "Publishing release"
% Run the local CI workflow.
ci() & check & test:
echo "CI complete"
% Build first, then package and publish together.
release() & check & (_package, publish):
echo "Release complete"
% Development commands.
[dev]
build():
cargo build
run():
cargo run
Useful calls:
only
only serve
only --set host=0.0.0.0 serve 8080
only ci
only release
only dev buildRun:
only -pIf the path is not what you expect, pass one explicitly:
only -f ./Onlyfile ciCommon causes:
- the task starts with
_, so it is a helper - the task is inside a namespace, so call it as
only <namespace> <task> - the namespace only contains helper tasks, so the namespace is hidden from the top-level list
- the file has parse or semantic errors, so the task was never registered
Check three things:
- The task signature declares the parameter.
- The command body uses
{{name}}, not$name. --setis written before the task path:
only --set port=8080 servePositionals bind in signature order.
serve(port="3000", host="127.0.0.1"):
echo "{{host}}:{{port}}"
only serve 8080 sets port, not host. Use --set host=... when you want to skip earlier parameters.
All guarded variants failed to match and there was no unguarded fallback. Add a fallback:
test() ? @has("cargo-nextest"):
cargo nextest run
test():
cargo test
Use the built-in deno shell when possible. If you need a host shell, install it or pick one that exists on PATH.
Use shell?= when the fallback is acceptable; use shell= when the exact shell is required.
build() shell?=bash:
./scripts/build.sh
Accepted names are deno, bash, sh, pwsh, and powershell.
!echo false is active. Remove it or set:
!echo true
!preview true is active. Remove it or set:
!preview false
{{name}}must match a declared parameter.- Every unescaped
{{needs a matching}}. - Use
\{{and\}}for literal braces.
only validates a file before running commands. Diagnostic codes are stable enough to search for.
| Code | Meaning | Fix |
|---|---|---|
parse.unexpected-token |
grammar did not expect the current token | check punctuation, indentation, and header shape |
parse.malformed-directive |
broken ! directive line |
use !echo true, !preview false, or !shell <name> |
parse.malformed-namespace-header |
broken [namespace] line |
keep the namespace header on its own line |
parse.malformed-task-header |
broken task signature | use name(): or name(param="default"): |
| Code | Meaning | Fix |
|---|---|---|
lower.invalid-directive |
unknown directive or invalid directive value | use one of !echo, !preview, !shell with valid values |
lower.invalid-task |
task header could not become a typed task | check parameters, guard, shell, and trailing colon |
lower.invalid-namespace |
namespace header could not become a typed namespace | use plain [name] |
lower.invalid-guard |
guard shape is invalid | use @os(...), @arch(...), @env(...), or @has(...) |
| Code | Meaning | Fix |
|---|---|---|
semantic.duplicate-directive |
same directive appears twice | keep one copy |
semantic.duplicate-task |
duplicate task signature | rename it or make the variant distinct |
semantic.duplicate-parameter |
same parameter appears twice | rename one parameter |
semantic.ambiguous-guard |
two variants use the same guard | remove one or change the guard |
semantic.namespace-conflict |
namespace and global task share a name | rename one |
semantic.undefined-dependency |
dependency task is not defined | define it or fix the spelling |
semantic.undefined-variable |
{{name}} is not a parameter |
declare the parameter or fix the interpolation |
| Message shape | Meaning |
|---|---|
task '<name>' is not defined |
no such task in the selected file |
helper task '<name>' cannot be invoked directly |
helpers are dependency-only |
task '<name>' is not available for this environment |
guards did not match and no fallback exists |
missing required parameter '{{name}}' |
required parameter was not supplied |
unknown parameter '<name>' for task '<task>' |
--set targeted a non-existent parameter |
duplicate parameter override '<name>' |
same --set name appeared twice |
cyclic dependency detected: ... |
dependencies form a cycle |
unterminated interpolation expression |
{{ has no closing }} |
unsupported shell '<name>' |
shell name is not accepted |
<shell> not found... |
selected shell is not available on PATH |
| Command | Purpose |
|---|---|
only |
list available tasks |
only <task> |
run a global task |
only <namespace> <task> |
run a namespaced task |
only --help |
show help |
only <task> --help |
show task help, including parameters |
only -p / only --path |
print discovered Onlyfile path |
only -f <path> / only --file <path> |
use a specific file |
only --set name=value <task> |
override a task parameter |
| Syntax | Meaning |
|---|---|
% text |
document the next task or namespace |
!echo true |
configure command output |
!preview true |
preview selected task and rendered commands |
!shell bash |
set file-level default shell |
task(): |
define a task |
task(name="value"): |
define a parameter with default |
task(name): |
define a required parameter |
{{name}} |
interpolate a parameter |
_task(): |
helper task |
task() & a & b: |
serial dependencies |
task() & a & (b, c): |
parallel dependency stage |
task() ? @has("cmd"): |
guarded task variant |
task() shell=bash: |
require an exact task shell |
task() shell?=bash: |
prefer a task shell with fallback |
[namespace] |
namespace following tasks |