Skip to content
mhs003Public

About

A deterministic command orchestration tool

Resources

Stars

3 stars

Watchers

1 watching

Forks

Repository files navigation

Runner

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.

Install

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/run

The Makefile provides the same build as make. The repository's example install task copies the binary to ~/.local/bin/run:

./build/run install

Ensure ~/.local/bin is on your PATH if you want to invoke the installed binary as run.

Quick Start

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 test

With 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.

CLI

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.

File Format

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.

Variables

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.

Tasks and Commands

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/

Dependencies

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.

Annotations

[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.

Comments

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"

Interpolation

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.

Execution

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.

VS Code Extension

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 build

Open that directory in VS Code and press F5 to launch an Extension Development Host. npm run package creates a VSIX package.

Examples

  • sample.runner is an annotated configuration demonstrating variables, lazy shell substitutions, dependencies, annotations, and argument interpolation.
  • .runner is the configuration used by this repository itself.

License

MIT. See LICENSE.

About

A deterministic command orchestration tool

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages