WHITESPACE = _{ " " | "\t" }
linebreak = _{ "\r\n" | "\n" }
COMMENT = _{ line_comment | block_comment }
line_comment = _{ "//" ~ (!linebreak ~ ANY)* }
block_comment = _{ "/*" ~ (block_comment | !"*/" ~ ANY)* ~ "*/" }
// Hash comments are only valid at the start of a line (possibly indented)
hash_comment = _{ WHITESPACE* ~ "#" ~ (!linebreak ~ ANY)* }
script = { SOI ~ element* ~ EOI }
element = _{ blank | hash_comment | semicolon | guard_line | block_start | block_end | command }
blank = _{ linebreak+ }
semicolon = _{ ";" }
block_start = { "{" }
block_end = { "}" }
guard_line = { "[" ~ ws? ~ guard_expr ~ ws? ~ "]" }
guard_expr = { guard_seq }
guard_seq = { guard_factor ~ ( ws? ~ "," ~ ws? ~ guard_factor )* }
guard_factor = { guard_not }
guard_not = { (invert ~ ws?)* ~ guard_primary }
guard_primary = _{ guard_group | guard_or_call | guard_term }
guard_group = { "(" ~ ws? ~ guard_expr ~ ws? ~ ")" }
guard_or_call = { or_kw ~ "(" ~ ws? ~ guard_expr_list ~ ws? ~ ")" }
or_kw = _{ "or" }
guard_expr_list = { guard_expr ~ ( ws? ~ "," ~ ws? ~ guard_expr )* }
guard_term = { ws? ~ guard_predicate ~ ws? }
invert = { "!" }
guard_predicate = _{ env_guard | bare_platform }
env_guard = { env_prefix ~ ws? ~ env_key ~ env_comparison? }
env_prefix = { "env" ~ ":" }
env_key = @{ guard_key_char+ }
guard_key_char = _{ !( ":" | "=" | "!" | "," | "|" | ")" | "]" | linebreak | "//" | "/*" ) ~ ANY }
env_comparison = { not_equals_env | equals_env }
equals_env = { eq_op ~ env_value }
not_equals_env = { neq_op ~ env_value }
env_value = @{ guard_value_char+ }
guard_value_char = _{ !( "," | "|" | ")" | "]" | linebreak | "//" | "/*" ) ~ ANY }
eq_op = { "==" }
neq_op = { "!=" }
bare_platform = { platform_tag }
platform_tag = @{ ASCII_ALPHANUMERIC+ }
command = _{
inherit_env_command |
workdir_command |
workspace_command |
env_command |
echo_command |
run_bg_command | // Must come before run_command to avoid prefix match
run_command |
copy_git_command | // Must come before copy_command
copy_command |
with_io_command |
hash_sha256_command |
symlink_command |
mkdir_command |
ls_command |
cwd_command |
read_command |
write_command |
append_command |
assert_file_command |
assert_dir_command |
assert_absent_command |
assert_stdout_command |
exit_command
}
// Commands
workdir_command = ${ "WORKDIR" ~ sep ~ argument }
workspace_command = ${ "WORKSPACE" ~ sep ~ workspace_target }
env_command = ${ "ENV" ~ sep ~ env_pair }
echo_command = ${ "ECHO" ~ sep ~ message }
run_command = ${ "RUN" ~ sep ~ run_args }
run_bg_command = ${ "RUN_BG" ~ sep ~ run_args }
from_current_workspace_flag = { "--from-current-workspace" }
copy_command = ${ "COPY" ~ sep ~ (from_current_workspace_flag ~ sep)? ~ argument ~ sep ~ argument }
with_io_command = ${ "WITH_IO" ~ sep ~ io_flags ~ (sep ~ command)? }
copy_git_command = ${ "COPY_GIT" ~ sep ~ (include_dirty_flag ~ sep)? ~ argument ~ sep ~ argument ~ sep ~ argument }
hash_sha256_command = ${ "HASH_SHA256" ~ sep ~ argument }
symlink_command = ${ "SYMLINK" ~ sep ~ argument ~ sep ~ argument }
mkdir_command = ${ "MKDIR" ~ sep ~ argument }
ls_command = ${ "LS" ~ (sep ~ argument)? }
cwd_command = ${ "CWD" }
read_command = ${ "READ" ~ (sep ~ argument)? }
write_command = ${ "WRITE" ~ sep ~ argument ~ (sep ~ message)? }
append_command = ${ "APPEND" ~ sep ~ argument ~ (sep ~ message)? }
// Verification commands. ASSERT_FILE alternates on explicit forms so the
// --hash digest can never be captured into the path slot, and a missing or
// malformed digest after --hash is a loud parse error rather than a silent skip.
assert_file_command = _{ assert_file_hash_command | assert_file_content_command }
assert_file_hash_command = ${ "ASSERT_FILE" ~ sep ~ "--hash" ~ sep ~ hash_digest ~ sep ~ argument }
assert_file_content_command = ${ "ASSERT_FILE" ~ sep ~ !hash_flag ~ argument ~ (sep ~ message)? }
assert_dir_command = ${ "ASSERT_DIR" ~ sep ~ argument }
assert_absent_command = ${ "ASSERT_ABSENT" ~ sep ~ argument }
assert_stdout_command = ${ "ASSERT_STDOUT" ~ sep ~ message }
hash_flag = { "--hash" }
hash_digest = @{ ASCII_HEX_DIGIT{64} }
exit_command = ${ "EXIT" ~ sep ~ exit_code }
// Inherit environment variables (selective list only). Placed here so it's available
// as a top-level command in the prelude. NOTE: `ALL` form is intentionally omitted.
inherit_env_command = ${ "INHERIT_ENV" ~ sep ~ inherit_list }
inherit_list = { "[" ~ ws? ~ env_key ~ (ws? ~ "," ~ ws? ~ env_key)* ~ ws? ~ "]" }
// Arguments
sep = _{ WHITESPACE+ }
argument = { quoted_string | templated_arg | unquoted_arg }
templated_arg = @{ "{{" ~ (!"}}" ~ ANY)* ~ "}}" }
unquoted_arg = @{ (!WHITESPACE ~ !linebreak ~ !";" ~ ANY)+ }
include_dirty_flag = { "--include-dirty" }
io_flags = { "[" ~ ws? ~ io_binding ~ (ws? ~ "," ~ ws? ~ io_binding)* ~ ws? ~ "]" }
io_binding = { io_stream ~ (ws? ~ "=" ~ ws? ~ pipe_binding)? }
io_stream = { "stdin" | "stdout" | "stderr" }
pipe_binding = { "pipe" ~ ":" ~ pipe_name }
pipe_name = @{ (ASCII_ALPHANUMERIC | "_" | "-")+ }
workspace_target = { "SNAPSHOT" | "LOCAL" | "snapshot" | "local" }
env_pair = { env_key_part ~ ws? ~ "=" ~ ws? ~ env_value_part }
env_key_part = @{ (!"=" ~ !WHITESPACE ~ !linebreak ~ ANY)+ }
env_value_part = { quoted_string | unquoted_env_value }
unquoted_env_value = @{ (!WHITESPACE ~ !linebreak ~ !";" ~ ANY)+ }
message = { (quoted_string | unquoted_msg_content)+ }
unquoted_msg_content = { !(linebreak | ";") ~ ANY }
run_args = { (quoted_string | unquoted_run_content)+ }
unquoted_run_content = { !(linebreak | "//" | "/*" | ";") ~ ANY }
exit_code = @{ ASCII_DIGIT+ }
quoted_string = @{
"\"" ~ ( "\\\"" | (!"\"" ~ ANY) )* ~ "\"" |
"'" ~ ( "\\'" | (!"'" ~ ANY) )* ~ "'"
}
ws = _{ (WHITESPACE | linebreak)* }