Grammar and lexical structure¶
Language Grammar¶
File-value statements¶
file-value-statement = file-value-get | file-value-check | file-value-update ;
file-value-get = "get", file-value-format, string, "from", string, "as", variable ;
file-value-check = "check", file-value-format, string, "in", string,
("equals" | "differs", "from"), string ;
file-value-update = "update", file-value-format, string, "in", string, "to", string,
"or", ("fail" | "add", ["as", scalar-type]) ;
project-version-check = "check", "project", "version",
("equals" | "differs", "from"), string ;
project-version-update = "update", "project", "version", "to", string ;
file-value-format = "property" | "json" | "yaml" | "toml" | "match" ;
scalar-type = "string" | "number" | "boolean" ;
The first string is the format-specific selector and the second is the file
path. match updates cannot use or add; JSON, YAML, and TOML additions require
an explicit scalar type. See Structured file values
for selector rules and runtime guarantees.
check project version and update project version are selector-free forms
that operate on the project declaration in the currently executing Drun file.
They honor custom --file paths and use the same dry-run and atomic-write
guarantees as other structured file-value statements.
Changelog statements¶
changelog-promote = "promote", "changelog", string, "to", "version", string,
["on", string] ;
The first string is the changelog file path, the second is the release
version, and the optional on string is a YYYY-MM-DD release date. See
Changelog promotion for the exact
transformation and failure modes.
Composite Git version guard¶
git-version-ensure = "git", "ensure", value,
"is", "newer", "than", "latest", "version", "from", source,
[ "using", access-method ],
[ inline-tag-contract ],
[ "as", variable ] ;
access-method = "https" | "ssh" | "cli" | "remote" | "filesystem" ;
inline-tag-contract = "matching", "tags",
( tag-preset | string | "pattern", string ) ;
tag-preset = "semver" | "semver_optional_v" ;
source = identifier ;
value = variable | string ;
The modifiers have a fixed order and may occur at most once. The candidate is
validated after interpolation as stable MAJOR.MINOR.PATCH. in series,
matching version, ordered by, allow fetch, and latest tag belong to the
primitive Git query grammar and are not part of this guard. See
Ensuring a release version is newer
for atomic outcomes and dry-run behavior.
Lexical Structure¶
Tokens¶
The language consists of the following token types:
- Keywords: Reserved words with special meaning
- Identifiers: User-defined names for variables, tasks, etc.
- Literals: String, number, boolean, array, and object literals
- Operators: Arithmetic, comparison, and logical operators
- Punctuation: Colons, commas, parentheses, brackets, braces
- Comments: Single-line and multi-line comments
Keywords¶
# Control flow
if, else, when, for, each, in, try, catch, finally, break, continue
# Declarations
task, project, let, set, capture, define, given, requires, accepts
# Dependencies and lifecycle
depends, on, before, after, running, then, parallel, drun, setup, teardown
# Types and constraints
from, matching, pattern, format, as, list, of, between, and, defaults, to
# Logical operators
and, or, not, is
# Built-in actions
build, deploy, push, run, stop, remove, scale, rollback, wait, commit
copy, move, create, backup, step, info, warn, warning, error, success, fail
get, check, update, ensure
# File-value formats and comparisons
property, json, yaml, toml, match, equals, differs
# Smart detection
docker, kubernetes, git, image, container, replicas, branch, tag
directory, folder, dir, file, exists, running, healthy, available
# Special values
true, false, now, current, secret, env
# Built-in functions
current git commit, current git branch, now.format, pwd, hostname, env, available tasks
Comments¶
drun v2 supports both single-line and multiline comments for documenting your automation workflows.
Single-line Comments¶
Single-line comments start with # and continue to the end of the line:
# This is a single-line comment
task "example": # End-of-line comment
info "Hello"
Multiline Comments¶
Multiline comments use C-style /* */ syntax and can span multiple lines:
/*
This is a multiline comment
that can span several lines
and is useful for detailed documentation
*/
version: 2.0
/*
Project configuration and setup
Author: Development Team
Last updated: 2025-09-22
*/
project "my-app" version "1.0":
info "Starting application setup"
Key Features:
- Multiline comments preserve formatting and indentation
- They can appear anywhere in the file where whitespace is allowed
- Unterminated multiline comments are handled gracefully (consume to end of file)
- Comments are completely ignored during parsing and execution
- Useful for file headers, detailed explanations, and temporary code disabling
Best Practices:
- Use single-line comments for brief explanations
- Use multiline comments for file headers, detailed documentation, and block commenting
- Consider using multiline comments to temporarily disable sections of code during development
Indentation¶
drun v2 uses Python-style indentation to define code blocks and supports both tabs and spaces:
Supported Indentation Styles¶
# Spaces (2 or 4 spaces per level)
task "spaces-example":
info "Level 1 with spaces"
if true:
step "Level 2 with spaces"
for each item in ["a", "b"]:
info "Level 3: {item}"
# Tabs
task "tabs-example":
info "Level 1 with tabs"
if true:
step "Level 2 with tabs"
for each item in ["a", "b"]:
info "Level 3: {item}"
Indentation Rules¶
- Tab Equivalence: Each tab character equals 4 spaces for indentation level calculation
- Consistency: Maintain consistent indentation style within each file
- Block Structure: Indentation defines code blocks (similar to Python)
- Nesting: Deeper indentation creates nested blocks
- Dedentation: Returning to a previous indentation level closes blocks
- Non-code lines: Blank lines and comment-only lines do not establish or change indentation, regardless of their leading spaces or tabs
Mixed Indentation¶
While both tabs and spaces are supported, mixing them is discouraged but technically allowed:
task "mixed-example":
info "4 spaces"
info "1 tab (equivalent to 4 spaces)"
info "8 spaces"
info "2 tabs (equivalent to 8 spaces)"
Error Handling¶
Invalid indentation patterns will result in parse errors:
task "invalid":
info "Level 1"
info "Invalid: 3 spaces doesn't match any previous level"
Best Practices¶
- Choose one style: Use either tabs or spaces consistently throughout your project
- Editor configuration: Configure your editor to show whitespace characters
- Team standards: Establish indentation standards for your team
- Generated files:
xdrun --inituses tabs by default
xdrun --init Template Manifests¶
xdrun --init and xdrun --init-minimal can bootstrap a spec from a template catalog instead of the built-in starter config:
xdrun --list-templates --templates-repo ../drun-templates
xdrun --init --template go-cli --templates-repo ../drun-templates
xdrun --init --from-template ../drun-templates --template go-cli
# Or target a specific manifest:
xdrun --init \
--from-template github:owner/repo/templates/drun-init.yaml@main \
--template go-cli
CLI Rules¶
--templates-repo <path>points at a local template repository root that containstemplates.yaml--list-templates --templates-repo <path>lists templates from that local repository--list-templates --from-template <manifest>lists templates from a specific manifest--template <name> --templates-repo <path>resolves through that local repository--from-templateacceptsgithub:...,drunhub:...,https://..., a local manifest path, or a local directory root containingtemplates.yaml--templateis required when--from-templateis used for initialization- If neither
--templates-reponor--from-templateis provided,xdrunfalls back to configured catalog sources such asDRUN_TEMPLATES_MANIFEST,DRUN_TEMPLATES_REPO, or the remote official catalog --fileand--save-as-defaultbehave the same as regularxdrun --init
For local template development, a directory target lets you test without publishing a remote manifest. If --from-template points at a directory, xdrun reads templates.yaml from that directory root automatically.
Manifest Format¶
The manifest referenced by --from-template, or by the official catalog, is not the final .drun spec. The manifest can define templates as a map or a sequence:
version: "1"
templates:
go-cli:
kind: go-cli
description: "Drun starter for Go CLI projects"
source: "templates/go-cli.drun"
Equivalent sequence form:
version: "1"
templates:
- name: go-cli
kind: go-cli
description: "Drun starter for Go CLI projects"
source: "templates/go-cli.drun"
Each template entry supports:
name: the template selector passed to--templatesource: the.drunfile to fetch; it can be remote, local, or relative to the manifestkind: optional specialization such asgo-clidescription: optional human-facing description
Placeholder Contract¶
Template-authored .drun files can use a small placeholder vocabulary that xdrun --init rewrites safely:
{{project_name}}: inferred from the current working directory{{binary_name}}: defaults to the inferred project name{{cmd_path}}: defaults to./cmd/{{binary_name}}{{module_name}}: inferred from localgo.modwhen present, otherwise falls back to the project name
Go CLI Template Example¶
For kind: go-cli, xdrun --init also applies narrow Go-specific rewrites for common command shapes such as go build -o ./bin/<name> and ./cmd/<name>.
Example remote template:
# drun (do-run) CLI is a fast, semantic task runner with
# its own powerful automation language. Effortless tasks, serious speed.
# Learn more at https://github.com/phillarmonic/drun
version: 2.0
project "{{project_name}}" version "1.0":
task "default" means "Welcome":
info "{{project_name}} Drun Spec"
task "build" means "Build {{binary_name}}":
step "Building {{binary_name}}..."
run "go build -ldflags=\"-X 'main.version=v0.0.1 (dev build)'\" -o ./bin/{{binary_name}} {{cmd_path}}"
success "Build completed for {{binary_name}}"
task "install" means "Install {{binary_name}}":
step "Installing {{binary_name}}..."
run "go install {{cmd_path}}"
success "Install completed for {{module_name}}"
This keeps the rewrite contract explicit: init only rewrites known placeholders and a few well-known Go command patterns. It does not attempt free-form semantic rewriting of arbitrary shell commands.
String Interpolation¶
Strings support variable interpolation using {$variable} syntax for declared variables and {variable} for loop variables:
let $name = "world"
info "Hello, {$name}!" # Outputs: Hello, world!
# Loop variables use $ prefix
for each $item in $items:
info "Processing {$item}" # Loop variable with $
# Complex expressions in interpolation
info "Current time: {now.format('HH:mm:ss')}"
Multi-line Strings¶
drun v2 supports multi-line strings enclosed in quotes, allowing you to write commands that span multiple lines while maintaining readability and supporting all string features like interpolation and escape sequences.
Basic Multi-line Strings¶
Strings can span multiple lines, preserving line breaks:
task "example":
run "echo Line 1
echo Line 2
echo Line 3"
This will execute as three separate echo commands, preserving the newlines.
Line Continuation¶
Use a backslash \ before a newline to continue a command on the next line without inserting a line break:
task "docker build":
run "docker run --rm \
-v $(pwd):/workspace \
-e ENV=production \
-e DEBUG=false \
myimage:latest"
The backslash-newline combination is removed, joining the lines together.
Escaped Quotes¶
Use \" to include literal quotes within strings:
task "with quotes":
run "echo \"Hello, World!\"
echo \"Status: \\\"Running\\\"\"
echo Done"
Multi-line Strings with Interpolation¶
Interpolation works seamlessly within multi-line strings:
task "deploy":
let $environment = "production"
let $version = "1.2.3"
run "echo Deploying to: {$environment}
echo Version: {$version}
echo Status: Ready
./deploy.sh {$environment} {$version}"
Complex Real-World Example¶
Combining all features for readable, maintainable scripts:
task "test coverage" means "Run tests and generate coverage report":
let $app_env = "test"
let $coverage_file = "coverage.xml"
step "Running test suite with coverage"
run "rm -f {$coverage_file}
docker compose exec \
-e APP_ENV={$app_env} \
-e XDEBUG_MODE=coverage \
-u=www-data \
php vendor/bin/phpunit --coverage-clover ./{$coverage_file}
docker compose exec \
-e APP_ENV={$app_env} \
-u=www-data \
php bin/console tests:probe-coverage {$coverage_file}"
success "Coverage report generated: {$coverage_file}"
Key Features¶
- Preserve Line Breaks: Newlines within strings are preserved in the output
- Line Continuation: Use
\before newline to join lines without a break - Escape Sequences: Support for
\",\\,\n,\t,\r - Interpolation: Full support for variable and expression interpolation
- Readability: Write complex shell scripts in a clean, maintainable format
Best Practices¶
- Use line continuation for long command lines to improve readability
- Preserve natural line breaks for multi-command scripts
- Combine with interpolation for dynamic, reusable command templates
- Escape quotes when passing quoted strings to shell commands
EBNF Grammar¶
(* Top-level constructs *)
program = { version_statement | project_declaration | snippet_definition | template_task_definition | task_definition | service_definition | orchestration_definition } ;
version_statement = "version" ":" number_literal ;
annotation = "@" identifier "(" [ string_literal { "," string_literal } ] ")" ;
(* Project declaration *)
project_declaration = "project" string_literal [ "version" string_literal ] ":"
{ project_setting } ;
project_setting = "set" identifier "to" expression
| "set" identifier "as" "list" "to" array_literal
| "include" string_literal
| "before" "any" "task" ":" statement_block
| "after" "any" "task" ":" statement_block
| "requires" "tools" ":" { tool_requirement | tool_task_source }
| shell_config ;
(* Reusable declarations *)
snippet_definition = { annotation } "snippet" string_literal ":" statement_block ;
template_task_definition = { annotation } "template" "task" string_literal ":"
{ task_property }
statement_block ;
(* Task definition *)
task_definition = { annotation } "task" task_name [ "mode" string_literal ] [ "means" string_literal ] ":"
{ task_property }
statement_block ;
task_name = string_literal | identifier_like ;
task_property = parameter_declaration
| dependency_declaration
| lifecycle_hook
| variable_declaration ;
(* Parameters *)
parameter_declaration = "requires" "tools" ":" { tool_requirement | tool_task_source }
| "requires" parameter_spec
| "given" parameter_spec
| "accepts" parameter_spec ;
parameter_spec = identifier [ parameter_constraint ] [ parameter_default ] ;
tool_requirement = tool_name { comparison_operator ( string_literal | number ) } ;
tool_task_source = "from" "tasks" ":" { task_name } ;
parameter_constraint = "from" array_literal
| "matching" "pattern" string_literal
| "matching" "email" "format"
| "as" type_name [ range_constraint ]
| "as" "list" [ "of" type_name ] ;
parameter_default = "defaults" "to" expression ;
range_constraint = "between" number "and" number ;
(* Dependencies *)
dependency_declaration = "depends" "on" dependency_list ;
dependency_list = dependency_item { ( "," | "and" ) dependency_item } [ "then" dependency_item ] ;
dependency_item = identifier [ "in" "parallel" ] ;
(* Lifecycle hooks *)
lifecycle_hook = "before" "any" "task" ":" statement_block
| "after" "any" "task" ":" statement_block
| "on" "drun" "setup" ":" statement_block
| "on" "drun" "teardown" ":" statement_block ;
(* Statements *)
statement_block = { statement } ;
statement = expression_statement
| control_statement
| declaration_statement
| action_statement ;
expression_statement = expression ;
control_statement = if_statement
| conditional_when_statement
| for_statement
| try_statement ;
declaration_statement = variable_declaration
| constant_declaration ;
action_statement = built_in_action
| shell_command
| detection_statement
| task_call_statement ;
(* Control flow *)
if_statement = "if" condition ":" statement_block
{ "else" "if" condition ":" statement_block }
[ "else" ":" statement_block ] ;
conditional_when_statement = "when" condition ":" statement_block
[ "otherwise" ":" statement_block ] ;
for_statement = "for" "each" variable "in" ( expression | array_literal ) [ "in" "parallel" ] ":"
statement_block ;
try_statement = "try" ":" statement_block
{ "catch" identifier ":" statement_block }
[ "finally" ":" statement_block ] ;
(* Task calls *)
task_call_statement = "call" "task" task_name [ "with" parameter_list ] ;
parameter_list = parameter_assignment { parameter_assignment } ;
parameter_assignment = identifier "=" ( string_literal | number ) ;
(* Conditions *)
condition = logical_expression ;
logical_expression = comparison_expression
{ ( "and" | "or" ) comparison_expression } ;
comparison_expression = additive_expression
[ comparison_operator additive_expression
| "is" version_order "than" "version" additive_expression ]
| file_comparison_expression
| filesystem_exists_expression ;
file_comparison_expression = "file" file_path [ "not" ] "matches" "file" file_path ;
filesystem_exists_expression = ( "file" | directory_noun ) file_path
( "exists" | "not" "exists" | "does" "not" "exist" ) ;
directory_noun = "folder" | "directory" | "dir" ;
file_path = string_literal | interpolated_string | identifier ;
version_order = "older" | "newer" ;
comparison_operator = "is" | "is" "not" | "==" | "!=" | "<" | ">" | "<=" | ">="
| "contains" | "matches" | "exists" ;
(* Expressions *)
expression = logical_expression ;
additive_expression = multiplicative_expression
{ ( "+" | "-" ) multiplicative_expression } ;
multiplicative_expression = unary_expression
{ ( "*" | "/" | "%" ) unary_expression } ;
unary_expression = [ "not" ] primary_expression ;
primary_expression = identifier
| literal
| function_call
| member_access
| interpolated_string
| "(" expression ")" ;
function_call = identifier "(" [ argument_list ] ")" ;
member_access = primary_expression "." identifier ;
argument_list = expression { "," expression } ;
(* Variables *)
variable_declaration = "let" identifier "be" expression
| "set" identifier "to" expression
| capture_expression_statement
| capture_shell_statement ;
capture_expression_statement = "capture" identifier "from" expression ;
capture_shell_statement = "capture" "from" "shell" string_literal "as" variable
| "capture" "from" "shell" "as" variable ":" statement_block ;
constant_declaration = "define" identifier "as" expression ;
(* Built-in actions *)
built_in_action = docker_action
| kubernetes_action
| git_action
| file_action
| network_action
| status_action
| deployment_action ;
docker_action = "build" "docker" "image" [ string_literal ]
| "push" "image" string_literal [ "to" string_literal ]
| "run" "container" string_literal [ container_options ]
| "stop" "container" string_literal
| "remove" "container" string_literal ;
kubernetes_action = "deploy" string_literal "to" "kubernetes" [ kubernetes_options ]
| "scale" string_literal "to" number "replicas"
| "rollback" string_literal
| "wait" "for" "rollout" [ "of" string_literal ] ;
git_action = "commit" "changes" [ "with" "message" string_literal ]
| "push" "to" "branch" string_literal
| "create" "tag" string_literal
| "checkout" "branch" string_literal ;
file_action = "copy" string_literal "to" string_literal
| "move" string_literal "to" string_literal
| "remove" string_literal
| "create" ( "file" | directory_noun ) string_literal
| "delete" ( "file" | directory_noun ) string_literal
| "backup" string_literal [ "as" string_literal ] ;
status_action = "step" string_literal
| "info" string_literal
| "warn" string_literal
| "warning" string_literal
| "error" string_literal
| "success" string_literal
| "fail" [ "with" string_literal ] ;
(* Shell commands *)
shell_command = shell_action ( string_literal | ":" statement_block )
| "capture" ( string_literal | ":" statement_block ) "as" identifier ;
shell_action = "run" | "exec" | "shell" ;
(* Detection statements *)
detection_statement = "detect" detection_target
| "detect" "available" tool_alternatives [ "as" variable_name ]
| "if" tool_list availability_verb "available" [ "and" "version" comparison_operator version_value ] ":" statement_block [ "else" ":" statement_block ]
| "if" tool_list availability_verb "not" "available" [ "and" "version" comparison_operator version_value ] ":" statement_block [ "else" ":" statement_block ]
| "if" ( tool_name | string_literal ) "version" comparison_operator version_value ":" statement_block [ "else" ":" statement_block ]
| "when" "in" environment_name "environment" ":" statement_block [ "else" ":" statement_block ] ;
tool_list = ( tool_name | string_literal ) { "," ( tool_name | string_literal ) } ;
availability_verb = "is" | "are" ;
detection_target = "project" "type"
| tool_name [ "version" ] ;
tool_alternatives = ( tool_name | string_literal ) { "or" ( tool_name | string_literal ) } ;
tool_name = identifier ;
environment_name = "ci" | "local" | "production" | "staging" | "development" | identifier ;
variable_name = "$" identifier ;
version_value = string_literal | number_literal ;
(* Shell configuration *)
shell_config = "shell" "config" ":" { platform_config } ;
platform_config = identifier ":" platform_settings ;
platform_settings = { platform_setting } ;
platform_setting = "executable" ":" string_literal
| "args" ":" string_array
| "environment" ":" key_value_pairs ;
string_array = { "-" string_literal } ;
key_value_pairs = { identifier ":" string_literal } ;
(* Literals *)
literal = string_literal
| number_literal
| boolean_literal
| array_literal
| object_literal ;
string_literal = '"' { string_character } '"' ;
interpolated_string = '"' { string_character | interpolation } '"' ;
interpolation = "{" expression [ pipe_operations ] "}" ;
pipe_operations = { "|" pipe_operation } ;
pipe_operation = "replace" string_literal ( "by" | "with" ) string_literal
| "without" ( "prefix" | "suffix" ) string_literal
| "uppercase" | "lowercase" | "trim" ;
(* Variable syntax: declared variables use $prefix, loop variables are bare identifiers *)
variable = "$" identifier ; (* Declared variables: $name, $environment *)
loop_variable = identifier ; (* Loop variables: item, i, file *)
identifier_like = identifier { "-" identifier } ;
number_literal = integer_literal | float_literal ;
integer_literal = digit { digit } ;
float_literal = integer_literal "." integer_literal ;
boolean_literal = "true" | "false" ;
array_literal = "[" [ expression { "," expression } ] "]" ;
object_literal = "{" [ object_member { "," object_member } ] "}" ;
object_member = ( identifier | string_literal ) ":" expression ;
(* Identifiers and keywords *)
identifier = letter { letter | digit | "_" } ;
letter = "a" | "b" | ... | "z" | "A" | "B" | ... | "Z" ;
digit = "0" | "1" | ... | "9" ;
(* Service and orchestration declarations *)
service_definition = "service" string_literal "in" string_literal ":" statement_block ;
orchestration_definition = "orchestrate" string_literal ":" statement_block ;
orchestration_action_statement = "orchestrate" string_literal orchestration_action
[ service_filter ]
[ orchestration_option_block ]
[ "starting" "from" ( string_literal | variable | interpolation ) ] ;
orchestration_action = "start"
| "up"
| "stop"
| "restart"
| "recreate"
| "status"
| "show" "endpoints"
| "endpoints"
| "health"
| "health_check"
| "build"
| "pull"
| "down"
| "logs"
| "clone" "repositories"
| "update" "repositories"
| "list" "branches" [ string_literal ]
| "switch" "branch" "to" "default"
| "set" "all" "branches" "to" "default" ;
service_filter = "services" array_literal
| "service" ( string_literal | variable | interpolation ) ;
orchestration_option_block = "with" orchestration_option { [ "," ] orchestration_option } ;
orchestration_option = "cache" string_literal
| "branch" string_literal
| "timeout" string_literal ;
(* Comments *)
single_line_comment = "#" { any_character_except_newline } ;
multiline_comment = "/*" { any_character } "*/" ;