Runner is a zero-dependency Go task runner with a small, shell-oriented configuration language. It reads a .runner file, resolves task dependencies, interpolates variables, and executes commands through /bin/sh -c.
Build the binary from a clone of this repository:
git clone https://github.com/mhs003/runner.git
cd runner
go build -o build/run ./cmd/runThe Makefile provides the same build as make. The repository's example install task copies the binary to ~/.local/bin/run:
./build/run installEnsure ~/.local/bin is on your PATH if you want to invoke the installed binary as run.
Create .runner in a project directory:
@vars:
BIN = app
build:
go build -o {{BIN}} .
test:
go test ./...
Run a task from that directory:
run build
run testWith no task name, Runner runs main:
main:
echo "hello"
Use run --init to create a starter .runner file. It fails rather than overwriting an existing file.
run [flags] [task] [task arguments...]
| Flag | Description |
|---|---|
--list |
List all tasks, body lines, header dependencies, and file variables. |
--json |
Print --list output as JSON. |
--dry |
Print interpolated commands without executing them. |
--file path |
Use an explicit configuration file instead of the default .runner. |
--init |
Create .runner in the current directory and exit. |
-h, --help |
Show help generated by Go's flag package. |
When no explicit --file is supplied, Runner loads .runner from the current directory and ~/.runner.global when present. Local variables and tasks override entries with the same name; unique entries from both files remain available. An explicit --file is standalone and does not merge the global file. If neither default file exists, Runner reports No config found.
Arguments after the task name are parsed as positional arguments, named arguments, or boolean flags:
run test Hello World
run test --entry ./cmd/app
run test --entry=./cmd/app
run test -v
run test -abc # -a, -b, and -c are boolean flags--key value and -k value are named arguments. A key followed by another option is treated as a boolean flag. Combined short options such as -abc are always boolean flags and cannot contain a value.
Runner's format is indentation-based. Two spaces are recommended. Leading spaces determine whether a line belongs to a block; tabs are counted as one character and are not equivalent to two-space indentation.
The optional @vars: block contains KEY = value assignments. Values are split at the first = and trimmed. Quotes are stored literally:
@vars:
BIN = "app"
Here {{BIN}} expands to "app", including the quote characters.
Variables can reference other variables in any order:
@vars:
NAME = world
GREETING = hello {{NAME}}
References in @vars are resolved before execution. Missing references remain unresolved until command interpolation, where missing values become empty strings. Circular references are errors.
A task header starts at indentation 0 and ends with :. Its body contains indented shell command lines:
build:
mkdir -p build
go build -o build/app .
All commands for a task are combined into one /bin/sh -c invocation. Shell variables therefore persist across command lines.
Prefix a command with ! to print it before running it:
install:
!cp build/app ~/.local/bin/
Header dependencies run before their task and are resolved with a topological sort:
deploy: build test:
echo "deploying"
Inline dependencies are written as an indented @ line. They are recursively inlined into the caller's shell script, so shell state persists across the call:
main:
MSG="starting"
@ build --target x86_64
echo "$MSG"
The first word after @ is the dependency name. Remaining words are arguments for that dependency. For example, @ build --target x86_64 supplies --target x86_64 to build; it does not call two dependencies.
Inline dependency arguments are parsed like CLI arguments and temporarily added to the dependency's variable scope. They overwrite existing keys during that call; newly introduced keys are removed afterward.
Header dependencies run once in resolved order. Inline dependencies are collected at each call site and can detect recursive cycles. Missing dependencies are errors.
[exit-on-error] applies only to the immediately following task:
[exit-on-error]
test:
false
echo "this never runs"
The annotation prepends set -e to that task's combined shell script. Without it, normal /bin/sh behavior applies and a failing command does not necessarily stop subsequent commands.
Lines whose trimmed content starts with # are ignored, including indented comment lines. A # on a top-level line starts an inline comment. A # in an indented command is passed to the shell:
# ignored
build: # top-level comment is removed
echo "# is part of this shell command"
Use {{NAME}} anywhere in a command. Values are interpolated immediately before the command is collected for execution.
| Form | Meaning |
|---|---|
{{KEY}} |
File, built-in, or CLI variable. |
{{CWD}} |
Current working directory. |
{{OS}} |
Runtime operating system. |
{{ARCH}} |
Runtime architecture. |
{{ARGS}} |
Positional CLI arguments joined with spaces. |
{{1}}, {{2}}, ... |
Individual positional CLI arguments. |
{{--key}}, {{-k}} |
Named CLI argument. |
{{--flag}}, {{-f}} |
Boolean CLI flag, usually true when supplied. |
{{@}} |
All original arguments after the task name, including options and flags. |
{{@N}} |
First N positional arguments. |
{{N@}} |
Last N positional arguments. |
{{M@N}} |
Positional arguments from M through N, inclusive. |
The range forms clamp to the available positional arguments. Invalid or reversed ranges produce an empty value. {{@}} with no arguments is empty.
Use || for a fallback. The fallback is first looked up as a variable name and, if not found or empty, is used literally:
build:
go build -o {{BIN}} {{--entry||ENTRY}}
Shell substitutions in variable values use $(...):
@vars:
HOST = $(hostname)
TOOL = go
WHERE = $(which $TOOL)
They run lazily when used, with @vars exposed as environment variables, and the result is cached for the duration of one Runner invocation. A failed substitution stops execution with an error, including during --dry because dry-run still needs to interpolate values.
Variable precedence, from lowest to highest, is file variables, built-ins, positional arguments, named arguments, and flags. Missing variables interpolate to an empty string unless a fallback is supplied.
The execution pipeline is:
Load -> Lex -> Parse -> ResolveVars -> inject variables -> resolve header dependencies -> execute
Commands inherit the terminal's standard input, output, and error streams. Runner executes tasks sequentially and does not provide a keep-going option. A task's inline dependencies are flattened into its shell script. --dry prints that flattened command list without starting the task shell, but lazy $(...) substitutions may still run during interpolation.
vscode-runner/ contains a companion extension for .runner and .runner.global files. It provides syntax highlighting, CodeLens buttons to run or dry-run each task, and go-to-definition for task and variable references in the current document.
To develop the extension:
cd vscode-runner
npm install
npm run buildOpen that directory in VS Code and press F5 to launch an Extension Development Host. npm run package creates a VSIX package.
sample.runneris an annotated configuration demonstrating variables, lazy shell substitutions, dependencies, annotations, and argument interpolation..runneris the configuration used by this repository itself.
MIT. See LICENSE.