only reads tasks from an Onlyfile and runs them on macOS, Linux, and Windows.
This guide shows you the language step by step. You can start with one task, then add parameters, dependencies, parallel work, guards, shells, and groups as your project grows.
For a complete workflow, see the example Onlyfile.
- Language version
- 1. Create your first Onlyfile
- 2. Document your tasks
- 3. Add parameters
- 4. Add dependencies
- 5. Run independent tasks in parallel
- 6. Hide internal helper tasks
- 7. Select task versions with guards
- 8. Choose a shell when needed
- 9. Organize larger projects with groups
- 10. Put the pieces together
- Troubleshooting
- Quick reference
Use this line to tell only the oldest language version your file needs:
!version MAJOR.MINOR
For example, !version 0.1 has a major number and a minor number.
This line is optional. Without it, only reads the file without checking its language version first.
When you use !version, place it before all other declarations. Blank lines and comments may come before it.
Create a file named Onlyfile in your project root:
hello():
echo "hello from only"
Run the task:
only helloRun only without a task to see the available tasks:
onlyonly looks for Onlyfile or onlyfile in the current directory and then walks upward.
To see which file was found:
only --whereYou can also select a file yourself:
only ci -p ./examples/Onlyfile --dry-runUse # for normal comments:
# This is only a source comment.
check():
cargo check
Use [help] to give the next task or group a short description:
[help] Check the project
[desc] Check all build targets.
[pass] Check passed.
[fail] Check failed.
check():
cargo check
[help] appears in the task list and help page. Add one or more [desc] lines when you need more detail. [desc] can be used on its own.
[pass] is printed after the task succeeds. [fail] is printed after it fails. You can use either one without [help].
Keep these lines directly above the task. A blank line separates them from the task.
Add parameters inside the parentheses after a task name:
build(profile="dev"):
cargo build --profile {{profile}}
Run it with the default:
only buildOr pass another value:
only build release{{profile}} inserts the chosen value into the command.
A parameter with a default is optional.
Leave out the default to make a parameter required:
greet(name):
echo "Hello, {{name}}"
Pass the value positionally:
only greet AdaIf you run:
only greetonly reports the missing parameter before running any command.
Use .. to collect all remaining values:
run(args..):
cargo run {{args}}
Then:
only run --release --bin demobinds args.. to:
--release --bin demo
A slice can follow fixed parameters:
tool(name, args..):
{{name}} {{args}}
A slice parameter:
- accepts zero or more values;
- must be the final parameter;
- cannot have a default value.
Use -s or --set when you want to set a parameter by name:
serve(port="3000", host="127.0.0.1"):
echo "{{host}}:{{port}}"
Override only the host:
only serve -s host=0.0.0.0Override more than one value:
only serve -s host=0.0.0.0 -s port=8080Place -s after the task arguments.
Parameter values are chosen in this order:
-s NAME=VALUE/--set NAME=VALUE- positional arguments
- defaults from the task signature
Use \{{ and \}} when you need literal braces:
show(value="demo"):
echo "{{value}} \{{value\}}"
This prints:
demo {{value}}
Use & when one task must run before another.
check():
cargo check
test():
cargo test
ci() & check & test:
echo "CI complete"
Run:
only ciThe order is:
check
↓
test
↓
ci
Each & adds another step.
Dependencies are tasks, not copies of their commands. If several tasks need the same dependency, only runs it once.
You can pass values to a dependency:
package() & build("release"):
echo "Package complete"
A dependency without values uses its defaults. If it has a required parameter, pass the value as shown above.
Put independent dependencies inside parentheses:
ci() & (back.test, front.test):
echo "CI complete"
Here, back.test and front.test may run at the same time.
Compare these two forms:
ci() & check & test:
check → test → ci
and:
ci() & (check, test):
check ─┐
├→ ci
test ──┘
Use sequential stages when tasks depend on each other.
Use a parallel group when the tasks are independent.
You do not need to guess how a workflow will run.
Use dry-run:
only ci --dry-runTo include rendered commands:
only ci --dry-run --fullFor larger workflows, dry-run shows the final order after only chooses the matching task variants.
Prefix a task name with _ to make it a helper:
_prepare():
cargo check
ci() & _prepare:
cargo test
Helpers:
- can be used as dependencies;
- do not appear in normal task listings;
- cannot be run directly.
Use helpers for setup steps that users do not need to run by hand.
A task can have more than one way to run.
Use ? with a guard to choose the first version that matches your system:
test() ? @has("cargo-nextest"):
cargo nextest run
test():
cargo test
If cargo-nextest is available as a command, the first variant is used.
Otherwise, the unguarded task is the fallback.
You can also check the operating system:
package() ? @os("windows"):
echo "Package for Windows"
package():
echo "Package for Unix"
Available checks include:
| Probe | Matches when |
|---|---|
@os("macos") |
the current OS is macOS |
@os("linux") |
the current OS is Linux |
@os("windows") |
the current OS is Windows |
@arch("x86_64") |
the current architecture is x86-64 |
@arch("aarch64") |
the current architecture is AArch64 |
@env("CI") |
the environment variable exists |
@has("cargo") |
cargo is available as a command |
Variant selection follows three rules:
- the first matching guarded variant wins;
- if no guard matches, the unguarded variant is used;
- if nothing matches and there is no fallback, the task is unavailable.
The first variant provides the default [help], [desc], [pass], and [fail] text. Later variants keep any fields they leave out. When a later variant writes a field, it replaces the whole field.
Guards let you choose a task version without putting system checks in shell scripts.
By default, commands use the built-in cross-platform deno shell.
For common tasks, you normally do not need to choose a shell:
check():
cargo check
Choose a specific shell only when your command needs one.
Require Bash:
build() shell=bash:
./scripts/build.sh
Use shell~= when only may use a backup shell:
build() shell~=bash:
./scripts/build.sh
Known fallbacks are:
pwsh → powershell
bash → sh
You can combine shells with guards:
show-user() ? @os("windows") shell~=pwsh:
Write-Output $env:USERNAME
Supported shells are:
| Shell | Behavior |
|---|---|
deno |
built-in cross-platform shell |
bash |
runs bash -c |
sh |
runs sh -c |
pwsh |
runs PowerShell 7+ |
powershell |
runs Windows PowerShell |
Each normal command line starts a new shell.
Use | when several lines need to share the same shell process:
version() shell=bash:
| version=$(git describe --tags --always)
| echo "$version"
Both lines run in the same shell, so the second line can use the variable created by the first.
You can mix command blocks with normal commands:
paths() shell=bash:
| cd crates/cli
| pwd
pwd
The first pwd prints a path ending in crates/cli. The final pwd starts a new shell and prints the project root.
Use a bare | when you need a blank line inside a command block.
Use !shell if most tasks need the same shell:
!shell bash
hello():
echo "hello"
Shell selection follows this order:
- task-level
shell=orshell~= - file-level
!shell - built-in
deno
Available file-level directives are:
| Directive | Purpose |
|---|---|
!version MAJOR.MINOR |
tell only the oldest language version your file needs |
!shell NAME |
set the default shell |
Use groups when several parts of your project have similar tasks.
group front {
check():
pnpm lint
test():
pnpm test
}
group back {
check():
cargo check
test():
cargo test
}
Run a grouped task by placing the group before the task:
only front check
only front test
only back check
only back testRun only the group name to see its tasks:
only frontYou can also refer to grouped tasks from dependencies:
ci() & (front.test, back.test):
echo "CI complete"
This lets you keep short task names while still showing which part of the project they belong to.
You can combine tasks, parameters, guards, groups, and parallel dependencies without turning the file into a large script.
Here is a small frontend + Rust example:
build(profile="dev"):
cargo build --profile {{profile}}
ci() & (back.test, front.test):
echo "CI complete"
group back {
test() ? @has("cargo-nextest"):
cargo nextest run
test():
cargo test
}
group front {
test():
pnpm test
}
Now you can run:
only build
only build release
only back test
only front test
only ciInspect the workflow before running it:
only ci --dry-run --fullThe important idea is simple:
You say which tasks must run first.
onlyworks out the order.
Check the discovered file:
only --whereOr select one explicitly:
only ci -p ./OnlyfileCheck whether:
- the task starts with
_; - the task belongs to a group;
- the group contains only hidden helper tasks;
- the
Onlyfilecontains an error.
For a grouped task, use:
only <group> <task>Make sure:
- the parameter exists;
- the task uses the expected parameter;
-sappears before the task path.
For example:
only serve -s port=8080Positional values follow declaration order.
Given:
serve(port="3000", host="127.0.0.1"):
echo "{{host}}:{{port}}"
this:
only serve 8080sets port.
To change only host, use:
only serve -s host=0.0.0.0This means no guarded variant matched and no fallback exists.
Add a fallback when appropriate:
test() ? @has("cargo-nextest"):
cargo nextest run
test():
cargo test
Prefer the built-in shell when possible.
If you need a specific shell, you can:
- install the shell;
- choose another supported shell;
- use
shell~=when a compatible fallback is acceptable.
Example:
build() shell~=bash:
./scripts/build.sh
Supported names are:
deno
bash
sh
pwsh
powershell
Check that:
{{name}}matches a declared parameter;- every unescaped
{{has a closing}}; - literal braces use
\{{and\}}.
| Command | Purpose |
|---|---|
only |
list available tasks |
only <task> |
run a root task |
only <group> <task> |
run a grouped task |
only --help |
show help |
only <task> --help |
show task help and parameters |
only --where |
print the discovered Onlyfile path |
only <task> -p <path> / only <task> --path <path> |
use a specific file |
only <task> -s name=value |
override a parameter |
only <task> --dry-run |
show the execution plan |
only <task> --dry-run --full |
show the plan and rendered commands |
only <task> -q / only <task> --quiet |
hide progress lines but keep command output |
only --fmt |
format the Onlyfile |
only --check |
check Onlyfile formatting |
only --upgrade |
update only |
| Syntax | Meaning |
|---|---|
# text |
ordinary comment |
!version MAJOR.MINOR |
tell only the oldest language version your file needs |
!shell bash |
set the file-level shell |
task(): |
define a task |
task(name): |
required parameter |
task(name="value"): |
parameter with a default |
task(args..): |
slice parameter |
{{name}} |
interpolate a parameter |
_task(): |
define a helper task |
task() & a & b: |
ordered dependency stages |
task() & (a, b): |
parallel dependency stage |
task() ? @has("cmd"): |
guarded task variant |
task() shell=bash: |
require an exact shell |
task() shell~=bash: |
prefer a shell with fallback |
| command |
continue a command block in one shell process |
group name { ... } |
group related tasks |