Grammar and lexical structure¶
Language Grammar¶
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, error, success, fail
# Smart detection
docker, kubernetes, git, image, container, replicas, branch, tag
directory, 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
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
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 }
| 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 }
| "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 ) } ;
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 ] ;
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" "directory" string_literal
| "backup" string_literal [ "as" string_literal ] ;
status_action = "step" string_literal
| "info" string_literal
| "warn" 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 } "*/" ;