Taskfile Format
Filesystem
A CJTaskrunner taskfile is a plain-text file named cjtasks.
The CLI does not search parent or child directories.
CJTaskrunner uses the taskfile’s directory as the project root. Relative paths inside the taskfile are resolved from that directory.
File syntax
The simplest taskfile defines a task name and a command:
hello:
echo "hello world"
Run the task from the same directory:
cj hello
Running cj without arguments displays the available tasks:
> cj
Tasks in /Users/me/my-project/cjtasks:
hello
Adding @desc to a task displays a short description in the summary:
In the cjtasks file:
hello:
@desc prints "hello world" and exits
echo "hello world"
goodbye:
@desc prints "goodbye" and exits
echo "goodbye"
Command Lines
Tasks are bash-like in that they resemble bash commands, and most of the time
you should be able to just use any bash command as a task. However, CJTaskrunner is not a shell, nor does it run commands through the shell. Insteads, commands are split
into arguments before invoking them on the named executable. Variables are
substituted before the command is run, so the command will only see the resolved
value.
Glob arguments will not work because they are not expanded by CJTaskrunner and
they will not be seen by a shell. So you cannot have a task for example ls *
because the * is a bash token that expands to a list of file names before
being sent to ls.
However, if you do need to use shell-specific features, you can use the @shell directive. This should be avoided whenever possible as it couples your taskfiles to your shell. Instead we recommend either putting your shell-specific commands into a shell script, or if your needs are simple enough, use CJTaskrunner directives.
CJTaskrunner uses a small domain-specific language, so taskfiles require no
language runtime unless you use @shell in which case it depends on the operating system shell.
The language is line-based and indentation-sensitive. The official format uses two spaces per indentation level. The parser also accepts one leading tab per level when the whole file uses tabs consistently. Indented lines beneath a header form a block. Task definitions may nest one level.
Top-level entries are task definitions, @help:, @env:, or @version
headers.
Blank lines are ignored. Comment-only lines start with optional indentation
followed by # and are ignored.
Top-level entries are either:
@version <name> <value>@help:@env:<task-name>:<task-name> (<argument>, ...):
Indented entries must use full indentation levels. A file may not mix leading
spaces and leading tabs for indentation. Run cj --format to normalize
indentation to spaces.
Inline comments are not stripped. echo # hi passes # and hi as command arguments.
Taskfile Discovery
When discovering inside a directory, CJTaskrunner loads cjtasks as the base
file when it exists. It then loads existing overlays from lowest to highest
precedence:
cjtasksproduction.cjtasksstaging.cjtasksdevelopment.cjtaskslocal.cjtasks
Before validation and execution, CJTaskrunner flattens the selected files into one effective taskfile.
Task definitions from a higher precedence replace lower ones, but must have the same number of arguments. Tasks not replaced by a higher layer remain available, and cross-layer task references are valid.
The bottom file must be named cjtasks and is the base layer if it exists. This
is the only layer that can hold @version directives, and any version related
directives use the @version from the base layer.
You can overwrite a parent task alone and it will still inherit subtasks. You can overwrite subtasks as well, but due to the way they are defined, you will also need to overwrite the parent task. A syntax to extend subtasks may be added in the future.
Environment entries replace lower entries by name. Optionality (: or ?:) is overwritten as well.
The highest-precedence @help: block wins.
Tasks
Task keys are top-level lines, or nested task headings inside another task:
build:
cargo build
ext:
@desc extension tasks
build:
@desc build extension assets
cargo build
The nested build: task above is addressed as ext:build. Task definitions are
limited to one nested level.
Tasks may declare required named positional arguments:
say (NAME, PUNCTUATION):
echo hello $NAME$PUNCTUATION
Argument values become runtime variables only for the called task. Previous values with the same names are restored when the task returns. Missing, extra, or undeclared arguments are errors before task execution begins.
Task names must match:
^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$
env and help are valid task names. The directives are distinguished by
their @ prefix.
Task names must not match a directory in the selected taskfile’s directory.
Task lines are non-empty indented lines under a task. They run in order.
Execution stops on first non-zero status unless a same-level @or handles it.
Execution stops with an error after 100,000 task steps as possible infinite-loop protection.
Semicolons split multiple same-level expressions on one physical line. Semicolons inside single or double quotes are preserved.
Semicolons cannot replace a newline where an indentation increase follows immediately.
run:
@set MODE prod; @if $MODE == prod
cargo build --release
A taskfile line ending in \ immediately before a line break continues on the
next physical line. The next line’s leading spaces and tabs are removed before
the lines are joined. Continuations can span multiple lines:
announce:
@echo This message is continued \
onto the next line
This produces one task expression: @echo This message is continued onto the next line.
@if accepts a truthy value, ==, and != string comparisons. Use @if-in
or @if-not-in for exact case-sensitive list membership:
compile (TARGET):
@if-in $TARGET linux macos windows
@echo supported target
Membership requires at least one candidate. Quoted values remain one word, so
@if-in $TARGET linux "mac os" windows can match mac os.
Ordinary Commands
Ordinary task lines execute directly as argv commands through the platform process API.
build:
cargo build --release
This executes as:
["cargo", "build", "--release"]
CJTaskrunner is not a shell. Ordinary commands do not interpret shell syntax:
- pipes
- redirects
- globbing
- command chaining
- shell builtins
~home-directory expansionNAME=value commandenvironment prefixes
Use @shell for other shell behavior. Backslash line continuation is handled
by the taskfile parser before ordinary commands are executed.
For command-specific environment variables, prefer exported task variables:
electron-install:
rm -rf node_modules
rm -rf $HOME/.electron-gyp/9.4.4
@export npm_config_runtime electron
@export npm_config_target 9.4.4
@export npm_config_disturl https://electronjs.org/headers
@export npm_config_arch x64
@export npm_config_force_process_config true
npm install
Or keep the shell syntax on a single @shell line:
electron-install:
rm -rf node_modules
rm -rf $HOME/.electron-gyp/9.4.4
@shell npm_config_runtime=electron npm_config_target=9.4.4 npm_config_disturl=https://electronjs.org/headers npm_config_arch=x64 npm_config_force_process_config=true npm install
Child stdin, stdout, and stderr are inherited unless a command is being captured
by @set NAME:.
Word Splitting
CJTaskrunner splits command and directive arguments with shell-like quote handling:
- Whitespace separates words outside quotes.
- Single and double quotes group text.
- Quotes are removed.
- Backslash can escape the next character.
- Unterminated quotes are errors.
This is not a full shell parser.
Interpolation
Interpolation applies to ordinary command argv tokens, @shell text, and directive arguments.
Supported forms:
$NAME
${NAME}
${NAME?}
${NAME?fallback}
${NAME?"fallback value"}
Rules:
$NAMEreads the current CJTaskrunner variable value and errors when absent.${NAME}reads the current variable value and errors when absent.${NAME?}reads the current variable value or an empty string when absent.${NAME?fallback}and${NAME?"fallback value"}use the fallback whenNAMEis absent.\$NAMEand\${NAME}escape interpolation.- Variable references are invalid in
@desctext and every line of an@help:block. Escape the dollar sign to include literal variable-shaped text there; the escape is not displayed. - Directive operands are interpolated as strings before validation, including operands that name variables.
- In ordinary commands, an interpolated value remains one argv value.
- In
@shell, interpolated values are shell-quoted before/bin/sh -c.
CJTaskrunner does not support shell-style command substitution, arithmetic expansion, pattern replacement, or nested expansion.