## Command Reference
| [`WORKDIR`](#workdir) | `WORKDIR <path>` |
| [`WORKSPACE`](#workspace) | `WORKSPACE SNAPSHOT\|LOCAL` |
| [`ENV`](#env) | `ENV KEY=value` |
| [`INHERIT_ENV`](#inherit_env) | `INHERIT_ENV <key>...` |
| [`ECHO`](#echo) | `ECHO <message>` |
| [`RUN`](#run) | `RUN <command...> \| RUN ["exe", "arg", ...]` |
| [`COPY`](#copy) | `COPY [--from-current-workspace] <from> <to>` |
| [`COPY_GIT`](#copy_git) | `COPY_GIT [--include-dirty] <rev> <src> <dst>` |
| [`SYMLINK`](#symlink) | `SYMLINK <from> <to>` |
| [`MKDIR`](#mkdir) | `MKDIR <path>` |
| [`LS`](#ls) | `LS [<path>]` |
| [`CWD`](#cwd) | `CWD` |
| [`READ`](#read) | `READ [<path>]` |
| [`READ_LINE`](#read_line) | `READ_LINE $var` |
| [`WRITE`](#write) | `WRITE <path> [<contents>]` |
| [`APPEND`](#append) | `APPEND <path> [<contents>]` |
| [`EXPAND`](#expand) | `EXPAND [<path>] [<KEY=val> ...]` |
| [`ASSERT_FILE`](#assert_file) | `ASSERT_FILE [--hash <sha256>] <path> [<expected>]` |
| [`ASSERT_DIR`](#assert_dir) | `ASSERT_DIR <path>` |
| [`ASSERT_ABSENT`](#assert_absent) | `ASSERT_ABSENT <path>` |
| [`ASSERT_STDOUT`](#assert_stdout) | `ASSERT_STDOUT <substring>` |
| [`HASH_SHA256`](#hash_sha256) | `HASH_SHA256 <path>` |
| [`EXIT`](#exit) | `EXIT <code>` |
| [`SLEEP`](#sleep) | `SLEEP <duration>` |
| [`WITH_IO`](#with_io) | `WITH_IO [<stream>[=pipe:<name>\|=$var], ...] <command> \| WITH_IO [bindings] { <commands> }` |
| [`FOR`](#for) | `FOR $item: TYPE IN <expr> { <commands> } \| FOR $key: STRING, $value: TYPE IN <expr> { <commands> }` |
| [`IF`](#if) | `IF <expr> { <commands> } [ELSE IF <expr> { <commands> }] [ELSE { <commands> }]` |
| [`LET`](#let) | `LET $var: TYPE = <expr> \| LET $var: TYPE = ASYNC { <commands> } \| LET $var: TYPE = <command> \| LET $var: TYPE = AWAIT $task` |
| [`MUTATION`](#mutation) | `$var = <expr>` |
| [`ASYNC`](#async) | `ASYNC <command...> \| ASYNC { <commands> } \| LET $var: HANDLE = ASYNC { <commands> }` |
| [`AWAIT`](#await) | `AWAIT $var \| LET $out: STRING = AWAIT $var` |
| [`CANCEL`](#cancel) | `CANCEL $var` |
| [`TIMEOUT`](#timeout) | `TIMEOUT <duration> <command...> \| TIMEOUT <duration> { <commands> } \| TIMEOUT <duration> AWAIT $var` |
| [`FUNC`](#func) | `FUNC NAME($param: TYPE, ...) { <commands> }` |
| [`CALL`](#call) | `CALL NAME(<expr>, ...) \| LET $var: TYPE = CALL NAME(<expr>, ...)` |
| [`RETURN`](#return) | `RETURN <expr>` |
| [`WHILE`](#while) | `WHILE <bool-expr> { <commands> }` |
| [`BREAK`](#break) | `BREAK` |
| [`CONTINUE`](#continue) | `CONTINUE` |
### WITH_IO
Reroute standard streams.
**Syntax:** `WITH_IO [<stream>[=pipe:<name>|=$var], ...] <command> | WITH_IO [bindings] { <commands> }`
Reroutes the standard streams of the next command or, in block form,
of every enclosed command.
Bindings map streams (`stdin`, `stdout`, `stderr`) to named script
pipes (`stdout=pipe:name`, `stderr=pipe:name`) or to a PIPE-typed
variable (`stdin=$p`, resolved against the live pipe registry when
the step runs). Both stdout and stderr pipes capture output the same way.
Pipes hold bytes in memory and spill to a temp file above 8 MiB, so a
producer can finish before the consumer starts.
If WITH_IO wraps an ASYNC block whose body is a single RUN, guarded or
not, the pipe is a zero copy OS kernel pipe instead: pair it with a
consumer that runs while the producer is alive, since output past the
64 KiB kernel buffer stalls until drained. That promotion never crosses
a CALL boundary: pipes created, bound, or passed by variable inside FUNC
bodies are always script pipes, even when the surrounding task would
otherwise promote.
A second producer or consumer on a live name is an explicit error. A name
bound as output can later feed another command's `stdin`, connecting
commands without touching the terminal. Binding `stdout` and `stderr` to
the same live pipe name fails deterministically. Merge streams in shell
via `2>&1` instead.
Nested blocks stack defaults; inline bindings override inherited ones for
their command only; closing a block restores previous wiring.
**Examples:**
**Example: with_io block**
```oxdock
WITH_IO [stdout=pipe:log] {
ECHO first
ECHO second
}
WITH_IO [stdin=pipe:log] WRITE captured.txt
```
**Example: variable pipe binding**
```oxdock
# Declare the pipe first with the explicit handle operator
# (like `env:KEY`): `pipe:log` names a pipe without touching
# a stream. A plain string here would be a TypeMismatch.
# `$p` (not `pipe:$p`) is the variable form; literals stay
# `pipe:name`.
LET $p: PIPE = pipe:log
WITH_IO [stdout=$p] ECHO hello
WITH_IO [stdin=$p] READ_LINE $line
WRITE line.txt "{{ $line }}"
ASSERT_FILE line.txt "hello"
```
### FOR
Iterate over a list or map.
**Syntax:** `FOR $item: TYPE IN <expr> { <commands> } | FOR $key: STRING, $value: TYPE IN <expr> { <commands> }`
The loop variable receives each element (lists) or value (maps); with
two variables, the first receives the key.
Loop variables are declared with explicit types and scoped per iteration
via declare_var; they do not leak outward. The body may be a braced block
or a single-line `{ ... }` command.
`GLOB("...")` patterns must be quoted (`*` is not a bare word, so
`GLOB(*)` is a parse error); GLOB returns a root-relative sorted list,
empty when nothing matches, and rejects `..` escapes.
**Examples:**
**Example: for loop**
```oxdock
LET $items: LIST = ["a", "b"]
FOR $item: STRING IN $items {
ECHO $item
}
LET $map: MAP = {"x": 1}
FOR $k: STRING, $v: INT IN $map {
ECHO "$k=$v"
}
```
**Example: expand every match**
```oxdock
# single-line body; $x is a template path, WHO an override
WRITE a.txt "hi \{{ env:WHO }}!"
FOR $x: STRING IN GLOB("*.txt") { EXPAND $x WHO=World }
ASSERT_STDOUT "hi World!"
```
### IF
Conditional execution.
**Syntax:** `IF <expr> { <commands> } [ELSE IF <expr> { <commands> }] [ELSE { <commands> }]`
The condition is evaluated as a boolean expression.
Prefix `!` negates (`IF !false`); only Bool values are accepted as
conditions.
**Examples:**
**Example: if else**
```oxdock
IF true {
ECHO yes
} ELSE {
ECHO no
}
IF false {
ECHO skipped
} ELSE IF true {
ECHO fallback
}
IF !false {
ECHO inverted
}
```
### LET
Bind script-local variables.
**Syntax:** `LET $var: TYPE = <expr> | LET $var: TYPE = ASYNC { <commands> } | LET $var: TYPE = <command> | LET $var: TYPE = AWAIT $task`
Declares a script-local variable with an explicit type (STRING, INT,
FLOAT, BOOL, PIPE, LIST, MAP, HANDLE, DURATION, PATH). Duplicate LET
in the same scope frame is a redeclaration error; mutate with
`$var = <expr>`.
Variables are usable in templates (`{{ $var }}`), guards, and
expressions. With `ASYNC`, spawns a background task and stores its
handle (see ASYNC). The `$` sigil on the name is mandatory.
The right-hand side is always an expression — literals, lists, maps,
comparisons, `env:KEY` reads, `pipe:NAME` handles, `INSPECT($var)`
snapshots, `GLOB("*.md")` — never a `{{ ... }}` template;
interpolation happens in string values, not here.
Bare words need no quotes: `LET $d: STRING = 30s` binds the same string
as quoted.
When the right-hand side is a synchronous command
(`LET $out: STRING = ECHO hi`), the command runs to completion and its
exact stdout bytes are captured into the variable as a string (no newline
stripping; commands with no stdout capture as `""`; non-UTF8 stdout is
an error). Combining capture with an explicit
`WITH_IO [stdout=pipe:...]` is a parse error.
`LET $out: STRING = AWAIT $var` captures a background task's stdout the
same way; bare `AWAIT $var` forwards it to the parent stdout instead.
`LET $e: STRING = env:FOO` reads the script environment into a plain
string.
**Examples:**
**Example: let**
```oxdock
LET $name: STRING = "world"
ECHO "hello, {{ $name }}"
LET $items: LIST = ["a", "b"]
LET $count: INT = 42
```
**Example: glob binding**
```oxdock
# the RHS is an expression: GLOB(...) runs and binds a list
WRITE a.txt "x"
LET $files: LIST = GLOB("*.txt")
FOR $f: STRING IN $files { ECHO $f }
ASSERT_STDOUT "a.txt"
```
**Example: scoped variable reverts**
```oxdock
# LET inside a braced block reverts when the block exits
LET $a: STRING = "outer"
[bool:true] {
LET $a: STRING = "inner"
WRITE inner.txt "{{ $a }}"
}
WRITE outer.txt "{{ $a }}"
ASSERT_FILE inner.txt "inner"
ASSERT_FILE outer.txt "outer"
```
**Example: capture command output**
```oxdock
LET $out: STRING = ECHO hi
WRITE captured.txt "{{ $out }}"
ASSERT_FILE captured.txt "hi\n"
```
**Example: inspect a variable**
```oxdock
# INSPECT($var) snapshots a variable into a MAP: declared
# type plus live details (pipe backend stats here), so
# scripts can branch on engine state.
LET $p: PIPE = pipe:log
WITH_IO [stdout=$p] ECHO hello
LET $info: MAP = INSPECT($p)
IF $info.is_os_pipe {
WRITE unexpected.txt "should be a script pipe"
}
WRITE kind.txt "{{ $info.type }}"
ASSERT_FILE kind.txt "PIPE"
```
### MUTATION
Mutate a declared variable.
**Syntax:** `$var = <expr>`
Reassigns an existing variable, validating the new value against the
TypeKind bound at LET time via coerce_value with ExecState context.
The leading `$` distinguishes mutation from `KEY=value` command
assignments. Assigning an undeclared variable or a mismatched type is
an error.
**Examples:**
**Example: mutate**
```oxdock
LET $count: INT = 1
$count = 2
```
### ASYNC
Run steps in a background thread.
**Syntax:** `ASYNC <command...> | ASYNC { <commands> } | LET $var: HANDLE = ASYNC { <commands> }`
Runs a command or block of commands in a background thread with
subshell isolation.
Mutations (ENV, WORKDIR) stay within the block. With `LET`, stores a
task handle for `AWAIT`.
**Examples:**
**Example: async**
```oxdock
ASYNC ECHO "first"
ASYNC {
ECHO "first"
ECHO "second"
}
```
**Example: async task handle**
```oxdock
LET $task: HANDLE = ASYNC {
ECHO "built"
}
AWAIT $task
```
### AWAIT
Join a background task.
**Syntax:** `AWAIT $var | LET $out: STRING = AWAIT $var`
Blocks until the named task completes. Propagates errors if the task failed.
Bare `AWAIT $var` forwards the task's stdout to the parent stdout;
`LET $out: STRING = AWAIT $var` captures it into `$out` instead (same
UTF-8 and spilling rules as `LET $var: STRING = <command>`).
**Examples:**
**Example: await**
```oxdock
LET $task: HANDLE = ASYNC ECHO "done"
AWAIT $task
```
**Example: await capture**
```oxdock
LET $task: HANDLE = ASYNC ECHO "done"
LET $out: STRING = AWAIT $task
WRITE captured.txt "{{ $out }}"
ASSERT_FILE captured.txt "done\n"
```
### CANCEL
Synchronously cancel a background task.
**Syntax:** `CANCEL $var`
Kills the named background task spawned via LET $var: HANDLE = ASYNC ....
Blocking: returns only after the task thread has been joined and its OS
process reaped, so no residual filesystem or stream mutation follows. A
later AWAIT $var reports cancellation. Only named tasks can be cancelled.
**Examples:**
**Example: cancel**
```oxdock
LET $task: HANDLE = ASYNC SLEEP 30s
CANCEL $task
```
### TIMEOUT
Enforce an execution deadline.
**Syntax:** `TIMEOUT <duration> <command...> | TIMEOUT <duration> { <commands> } | TIMEOUT <duration> AWAIT $var`
Aborts the wrapped step or block with a deadline error if it exceeds the
duration (e.g. 500ms, 10s, 2m; a bare number means seconds).
A blocking foreground process is killed.
**Examples:**
**Example: timeout**
```oxdock
TIMEOUT 30s WRITE heartbeat.txt alive
```
**Example: timeout block**
```oxdock
TIMEOUT 30s {
WRITE a.txt one
WRITE b.txt two
}
```
**Example: timeout variable duration**
```oxdock
# durations resolve at runtime, so variables work too
LET $budget: DURATION = "30s"
TIMEOUT $budget WRITE heartbeat.txt alive
ASSERT_FILE heartbeat.txt alive
```
### FUNC
Define a user function.
**Syntax:** `FUNC NAME($param: TYPE, ...) { <commands> }`
Defines a user function with UPPERCASE name and explicitly typed
parameters.
Params bind by position with declare_var coercion before the body runs.
Bodies run in a fresh variable scope; LETs inside do not leak. A nested
FUNC definition is scoped to its block and reverts on exit. Names share
one namespace with host-registered functions.
**Examples:**
**Example: func def call**
```oxdock
FUNC GREET($name: STRING) {
RETURN $name
}
LET $res: STRING = CALL GREET("ada")
WRITE greeting.txt "{{ $res }}"
ASSERT_FILE greeting.txt "ada"
```
### CALL
Invoke a user or host function.
**Syntax:** `CALL NAME(<expr>, ...) | LET $var: TYPE = CALL NAME(<expr>, ...)`
Invokes a FUNC-defined or host-registered function by UPPERCASE name.
Bare CALL discards the return value and keeps stdout side effects.
LET $var: TYPE = CALL captures the RETURN value (fallthrough without
RETURN captures as ""), coerced to the declared type; stdout inside the
callee stays observable via ASSERT_STDOUT and pipes.
Combining LET-capture with WITH_IO [stdout=pipe:...] is a parse error.
**Examples:**
**Example: call**
```oxdock
FUNC SHOUT($name: STRING) {
ECHO "{{ $name }}"
RETURN $name
}
CALL SHOUT("ada")
ASSERT_STDOUT "ada"
```
**Example: call with pipes**
```oxdock
# A pipe handle travels into a function as a typed argument
# and is usable as a binding target in both directions.
# `pipe:ch` constructs the handle; `$p` passes it on.
FUNC DRAIN($q: PIPE) {
WITH_IO [stdin=$q] READ_LINE $line
RETURN $line
}
LET $p: PIPE = pipe:ch
WITH_IO [stdout=$p] ECHO "payload"
LET $got: STRING = CALL DRAIN($p)
WRITE got.txt "{{ $got }}"
ASSERT_FILE got.txt "payload"
```
### RETURN
Return a value from a function.
**Syntax:** `RETURN <expr>`
Ends the nearest enclosing function call with a value.
Falling off the end without RETURN yields "". RETURN outside a function
(including at top level or across an ASYNC boundary) is an error.
**Examples:**
**Example: return**
```oxdock
FUNC PICK($flag: BOOL) {
IF $flag {
RETURN "yes"
}
RETURN "no"
}
LET $res: STRING = CALL PICK(true)
WRITE picked.txt "{{ $res }}"
ASSERT_FILE picked.txt "yes"
```
### WHILE
Loop while a condition holds.
**Syntax:** `WHILE <bool-expr> { <commands> }`
Re-evaluates a Bool condition each iteration (same is_truthy rule as IF;
non-Bool is a type error).
Each iteration runs in a fresh scope; mutate outer state with $var = ...
so the next check observes it. BREAK exits the loop; CONTINUE skips to
the next check.
**Examples:**
**Example: while loop**
```oxdock
LET $done: BOOL = false
WHILE !$done {
WRITE tick.txt "once"
$done = true
}
ASSERT_FILE tick.txt "once"
```
### BREAK
Exit the innermost loop.
**Syntax:** `BREAK`
Exits the innermost enclosing FOR or WHILE loop.
BREAK outside a loop, or across a FUNC or ASYNC boundary, is an error.
**Examples:**
**Example: break**
```oxdock
FOR $x: STRING IN ["a", "b"] {
BREAK
}
```
### CONTINUE
Skip to the next loop iteration.
**Syntax:** `CONTINUE`
Skips the rest of the innermost enclosing FOR or WHILE body and starts
the next iteration.
CONTINUE outside a loop, or across a FUNC or ASYNC boundary, is an error.
**Examples:**
**Example: continue**
```oxdock
FOR $x: STRING IN ["a", "b"] {
CONTINUE
}
```
### WORKDIR
Change the working directory.
**Syntax:** `WORKDIR <path>`
Sets the current working directory.
Relative paths resolve against the current directory; `/` resets to
the workspace root. Paths cannot escape the workspace.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | Directory to change to |
**Examples:**
**Example: change working directory**
```oxdock
WORKDIR project/src
WRITE generated.txt generated-under-workdir
ASSERT_FILE generated.txt generated-under-workdir
```
### WORKSPACE
Switch workspace roots.
**Syntax:** `WORKSPACE SNAPSHOT|LOCAL`
SNAPSHOT or LOCAL root.
**Arguments:**
| `target` | `SNAPSHOT\|LOCAL` | yes | Target root |
**Examples:**
**Example: switch roots**
```oxdock
WORKSPACE LOCAL
```
### ENV
Set an environment variable.
**Syntax:** `ENV KEY=value`
Inserts or updates an env var.
The value uses the unified string-value rules shared by every command:
`"..."` or `'...'` quotes keep exact bytes (spaces, tabs), a lone `$var`
evaluates that variable, `{{ ... }}` placeholders interpolate, unquoted
words join with single spaces, and the first `=` splits key from value
(`KEY=a=b` stores `a=b`).
A `$var` inside larger text stays literal — write `{{ $var }}` to
interpolate there.
**Arguments:**
| `assignment` | `STRING` | yes | KEY=value pair; the value resolves as STRING |
**Examples:**
**Example: set env**
```oxdock
ENV APP_MODE=production
```
**Example: quoted value with spaces**
```oxdock
# quotes keep the space: SET_FORTH stores `outer scope`
ENV SET_FORTH="outer scope"
WRITE out.txt "{{ env:SET_FORTH }}"
ASSERT_FILE out.txt "outer scope"
```
**Example: variable value**
```oxdock
# a lone $var evaluates, like ECHO $var
LET $who: STRING = "Alice"
ENV GREETING=$who
WRITE out.txt "{{ env:GREETING }}"
ASSERT_FILE out.txt "Alice"
```
**Example: all value forms agree**
```oxdock
# a bare variable, a quoted literal, and a template all
# store plain strings through the same value rules
LET $x: STRING = "Ada"
ENV A=$x
ENV B="hello world"
ENV C="{{ $x }} concatenated"
```
**Example: scoped env reverts**
```oxdock
# ENV inside a braced block reverts when the block exits
ENV MODE=production
[bool:true] {
ENV MODE=staging
WRITE inner.txt "{{ env:MODE }}"
}
WRITE outer.txt "{{ env:MODE }}"
ASSERT_FILE inner.txt "staging"
ASSERT_FILE outer.txt "production"
```
### INHERIT_ENV
Inherit env vars from host.
**Syntax:** `INHERIT_ENV <key>...`
Declares which host environment variables to inherit into the script.
Must appear before any other commands and at most once. Without this
directive, the script starts with an empty environment.
**Arguments:**
| `keys` | [`STRING...`](#value-type-string) | no | Host variables to inherit |
**Examples:**
**Example: inherit env**
```oxdock
INHERIT_ENV [PATH, HOME]
```
### ECHO
Print to stdout.
**Syntax:** `ECHO <message>`
Outputs message to stdout.
**Arguments:**
| `message` | [`STRING...`](#value-type-string) | yes | Text |
**Output:** Stdout
**Examples:**
**Example: echo**
```oxdock
ECHO build-complete
```
**Example: variables**
```oxdock
# a lone $x evaluates; {{ }} interpolates inside text
LET $x: STRING = "World"
ECHO {{ $x }}
ECHO $x
ASSERT_STDOUT "World"
```
### RUN
Execute shell command or direct executable.
**Syntax:** `RUN <command...> | RUN ["exe", "arg", ...]`
Shell form (`RUN <command...>`) runs the joined command string in the
system shell (`$SHELL -c` / `COMSPEC /C`).
Exec form (`RUN ["exe", "arg", ...]`) spawns the executable directly
with no shell, so there is no shell expansion, globbing, redirection,
or pipes; use it for portable commands.
Guards and wrappers (`ASYNC`, `TIMEOUT`, `WITH_IO`, `LET`) apply to
both forms.
**Arguments:**
| `command` | [`STRING...`](#value-type-string) | yes | Command |
**Examples:**
**Example: run**
```oxdock
RUN echo hello
```
**Example: run exec form**
```oxdock
RUN ["cargo", "--version"]
```
### COPY
Copy file into workspace.
**Syntax:** `COPY [--from-current-workspace] <from> <to>`
Copies from host.
**Arguments:**
| `from` | [`PATH`](#value-type-path) | yes | Source |
| `to` | [`PATH`](#value-type-path) | yes | Dest |
**Flags:**
| `--from-current-workspace` | `BOOL` | Copy from workspace instead of build context |
**Examples:**
**Example: copy**
```oxdock roots:unified
WRITE src.txt content
COPY src.txt dst.txt
ASSERT_FILE dst.txt content
```
**Example: copy from workspace**
```oxdock roots:unified
WRITE ws-src.txt ws-content
COPY --from-current-workspace ws-src.txt ws-copy.txt
ASSERT_FILE ws-copy.txt ws-content
```
### COPY_GIT
Copy from git revision.
**Syntax:** `COPY_GIT [--include-dirty] <rev> <src> <dst>`
Checkout and copy.
**Arguments:**
| `rev` | [`STRING`](#value-type-string) | yes | Rev |
| `src` | [`PATH`](#value-type-path) | yes | Src |
| `dst` | [`PATH`](#value-type-path) | yes | Dst |
**Flags:**
| `--include-dirty` | `BOOL` | Include dirty |
**Examples:**
**Example: git copy**
```oxdock expect_error:"COPY source missing"
COPY_GIT HEAD src.txt dst.txt
```
### SYMLINK
Create symlink.
**Syntax:** `SYMLINK <from> <to>`
Creates symlink.
**Arguments:**
| `from` | [`PATH`](#value-type-path) | yes | Target |
| `to` | [`PATH`](#value-type-path) | yes | Link |
**Examples:**
**Example: symlink**
```oxdock roots:unified
WRITE original.txt content
SYMLINK original.txt link.txt
ASSERT_FILE link.txt content
```
### MKDIR
Create directory.
**Syntax:** `MKDIR <path>`
Creates dir with parents.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | Dir path |
**Examples:**
**Example: mkdir**
```oxdock
MKDIR deeply/nested/tree
```
### LS
List directory.
**Syntax:** `LS [<path>]`
Lists entries.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | no | Dir |
**Output:** Stdout
**Examples:**
**Example: ls**
```oxdock
MKDIR inventory
WRITE inventory/a.txt a
LS inventory
```
### CWD
Print working directory.
**Syntax:** `CWD`
Outputs cwd.
**Output:** Stdout
**Examples:**
**Example: cwd**
```oxdock
CWD
```
### READ
Read file to stdout.
**Syntax:** `READ [<path>]`
Outputs file contents.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | no | File |
**Output:** Stdout
**Examples:**
**Example: read**
```oxdock
WRITE note.txt "hello"
READ note.txt
```
### READ_LINE
Read one line from stdin into a variable.
**Syntax:** `READ_LINE $var`
Reads bytes until newline without waiting for EOF, leaving the pipe open.
Trailing newline is stripped (shell-read parity). On premature EOF
assigns accumulated bytes and returns.
**Arguments:**
| `var` | `STRING` | yes | Target variable (`$name`); the line binds as STRING |
**Examples:**
**Example: read line**
```oxdock
WITH_IO [stdout=pipe:lines] ECHO "first"
WITH_IO [stdin=pipe:lines] READ_LINE $reply
```
### WRITE
Write to file.
**Syntax:** `WRITE <path> [<contents>]`
Writes contents.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | File |
| `contents` | [`STRING...`](#value-type-string) | no | Content |
**Examples:**
**Example: write**
```oxdock
WRITE output.txt hello-world
```
### APPEND
Append to file.
**Syntax:** `APPEND <path> [<contents>]`
Appends contents.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | File |
| `contents` | [`STRING...`](#value-type-string) | no | Content |
**Examples:**
**Example: append**
```oxdock
WRITE log.txt line1
APPEND log.txt line2
ASSERT_FILE log.txt line1line2
```
### EXPAND
Expand a template file (or stdin) to stdout.
**Syntax:** `EXPAND [<path>] [<KEY=val> ...]`
A template is any text file — or piped stdin when no path is given —
containing `{{ ... }}` placeholders. EXPAND replaces each placeholder
and prints the result to stdout.
Placeholders: `{{ NAME }}` reads a `KEY=val` override passed on this
command; `{{ env:NAME }}` reads an override, falling back to the
environment; `{{ $var }}` reads a script variable (dotted paths allowed).
A missing key is an error, never a silent empty.
Substitution runs in a single pass. EXPAND is not recursive and does not
expand nested placeholders: a value that itself contains `{{ ... }}` is
inserted verbatim and never expanded again.
A bare `$var` argument is a template path; `KEY=val` arguments are
overrides whose values follow the unified string-value rules (same as
`ENV`: quotes keep exact bytes, a lone `$var` evaluates,
`{{ ... }}` interpolates).
NOTE: `WRITE` interpolates `{{ ... }}` while writing, so escape it
(`\{{ ... }}`) when writing a template file for a later `EXPAND`.
With no path, the template arrives on stdin through a pipe. When piping
from a shell, single-quote the template (`echo '{{ $x }}'`): double
quotes let the shell swallow `$x`, so oxdock receives an empty `{{ }}`
placeholder and errors.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | no | Template file to expand; omit to expand stdin |
| `overrides` | `STRING...` | no | Template overrides shadowing that key (unified string values) |
**Output:** Stdout
**Examples:**
**Example: expand**
```oxdock
ENV NAME="Alice"
WRITE template.md "Hello {{ env:NAME }}!"
EXPAND template.md
ASSERT_STDOUT "Hello Alice!"
```
**Example: override with spaces**
```oxdock
# WRITE would interpolate {{ }} right away, so escape it:
# the file must literally contain {{ env:NAME }} for EXPAND
WRITE template.md "Hello \{{ env:NAME }}!"
EXPAND template.md NAME="Alice Smith"
ASSERT_STDOUT "Hello Alice Smith!"
```
**Example: variable override**
```oxdock
# same escaping: keep the placeholder literal until EXPAND;
# a lone $who evaluates, like ECHO $who
LET $who: STRING = "Bob"
WRITE template.md "Hi \{{ env:WHO }}!"
EXPAND template.md WHO=$who
ASSERT_STDOUT "Hi Bob!"
```
**Example: override forms agree**
```oxdock
# a bare variable and a template-with-tail expand identically
LET $x: STRING = "Ada"
WRITE template.md "Hi \{{ env:NAME }} and \{{ env:NAME2 }}!"
EXPAND template.md NAME=$x NAME2="{{ $x }} concatenated"
ASSERT_STDOUT "Hi Ada and Ada concatenated!"
```
**Example: expand stdin**
```oxdock
# no path: the template arrives on stdin through a pipe
WITH_IO [stdout=pipe:tpl] ECHO "Hello \{{ env:NAME }}!"
WITH_IO [stdin=pipe:tpl] EXPAND NAME=Alice
ASSERT_STDOUT "Hello Alice!"
```
**Example: override does not leak**
```oxdock
# KEY=val overrides shadow env for that EXPAND only —
# they never update the environment itself
ENV NAME="Alice"
WRITE template.md "Hi \{{ env:NAME }}!"
EXPAND template.md NAME="Bob"
ASSERT_STDOUT "Hi Bob!"
EXPAND template.md
ASSERT_STDOUT "Hi Alice!"
```
### ASSERT_FILE
Assert file exists.
**Syntax:** `ASSERT_FILE [--hash <sha256>] <path> [<expected>]`
Checks the path is a file, then optionally compares its bytes (or
`--hash` SHA-256 digest) against the expectation.
Any mismatch aborts the pipeline with a step-numbered error showing
expected vs actual.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | File |
| `expected` | [`STRING...`](#value-type-string) | no | Expected |
**Flags:**
| `--hash` | `STRING` | SHA-256 |
**Examples:**
**Example: assert file**
```oxdock
WRITE payload.bin stable-content
ASSERT_FILE payload.bin stable-content
```
**Example: assert file hash**
```oxdock
# --hash compares the SHA-256 digest instead of raw bytes
WRITE payload.bin stable-content
ASSERT_FILE --hash 08135c1b6349b0e4f894c36221952f0de00e6b4d82f80895abf359755e77103c payload.bin
```
### ASSERT_DIR
Assert dir exists.
**Syntax:** `ASSERT_DIR <path>`
Checks the path is a directory, aborting the pipeline with a
step-numbered error otherwise.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | Dir |
**Examples:**
**Example: assert dir**
```oxdock
MKDIR dist/assets
ASSERT_DIR dist/assets
```
### ASSERT_ABSENT
Assert path absent.
**Syntax:** `ASSERT_ABSENT <path>`
Checks nothing exists at the path, aborting the pipeline with a
step-numbered error if it does.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | Path |
**Examples:**
**Example: assert absent**
```oxdock
ASSERT_ABSENT missing.txt
```
### ASSERT_STDOUT
Assert stdout contains.
**Syntax:** `ASSERT_STDOUT <substring>`
Checks the preceding step's stdout contains the substring, aborting the
pipeline with a step-numbered error otherwise.
**Arguments:**
| `substring` | [`STRING...`](#value-type-string) | yes | Substring |
**Examples:**
**Example: assert stdout**
```oxdock
ECHO build-complete
ASSERT_STDOUT build-complete
```
### HASH_SHA256
Print SHA-256.
**Syntax:** `HASH_SHA256 <path>`
Computes digest.
**Arguments:**
| `path` | [`PATH`](#value-type-path) | yes | File |
**Output:** Stdout
**Examples:**
**Example: hash**
```oxdock
WRITE payload.txt hello
HASH_SHA256 payload.txt
```
### EXIT
Exit pipeline.
**Syntax:** `EXIT <code>`
Stops the pipeline immediately with an `EXIT requested with code <code>`
error; steps after it never run, at any nesting depth.
Enclosing blocks still unwind their LET/ENV/WORKDIR/WORKSPACE state,
anonymous background tasks are killed synchronously, and files written
before the EXIT persist.
**Arguments:**
| `code` | [`INT`](#value-type-int) | yes | Code |
**Examples:**
**Example: exit**
```oxdock expect_error:"EXIT requested with code 0"
EXIT 0
```
### SLEEP
Pause execution for a duration.
**Syntax:** `SLEEP <duration>`
Parks the step for the duration (e.g. 500ms, 10s, 2m).
Cooperative: checks for cancellation so an enclosing TIMEOUT or task
teardown interrupts the sleep. Cross-platform alternative to shell sleep
for testing time boundaries.
**Arguments:**
| `duration` | [`DURATION`](#value-type-duration) | yes | How long to sleep |
**Examples:**
**Example: sleep**
```oxdock
SLEEP 100ms
```
**Example: sleep variable duration**
```oxdock
# durations resolve at runtime, so variables work too —
# quoted or bare, both bind the same string
LET $pause: STRING = "100ms"
SLEEP $pause
LET $bare: STRING = 100ms
SLEEP $bare
```
## Value types
### Value type: STRING
Arbitrary text. Quotes keep exact bytes, lone `$var` evaluates, `{{ ... }}` interpolates.
### Value type: INT
64-bit signed integer, e.g. an exit code.
### Value type: FLOAT
64-bit float, e.g. a ratio.
### Value type: BOOL
Boolean `true` or `false`.
### Value type: PIPE
Named script pipe. Validity is checked against the pipe registry at coercion time.
### Value type: LIST
Ordered list of values.
### Value type: MAP
String-keyed map of values.
### Value type: HANDLE
Background ASYNC task handle for AWAIT/CANCEL.
### Value type: DURATION
Positive time span: `500ms`, `10s`, `2m`, `1h`; bare number means seconds.
### Value type: PATH
Workspace path, resolved against cwd and guarded against escape.