jenkinsfile 0.2.1

a tools to convert Jenkinsfile to data struct
Documentation
//
// This PEG file strives to capture as much of the declarative
// pipeline syntax as possible
//
pipeline = _{ SOI ~  preceeding_junk? ~
                    pipeline_start ~
                    opening_brace ~
                    // I am pretty sure this grammar is wrong and there has to
                    // a better way to allow these declarations anywhere within
                    // the block, but still only allow one of each.
                    // For now the parser code will have to inspect this itself
                    (
                    // An agent declaration at the root level is required
                    agentDecl
                    // A stages declaration is also required
                    | stagesDecl
                    | environmentDecl
                    | optionsDecl
                    | parametersDecl
                    | postDecl
                    | toolsDecl
                    | triggersDecl
                    )*
                    ~
                    closing_brace ~
                    ending_junk? ~
                    EOI }

pipeline_start = _{ "pipeline" }
preceeding_junk = { (!pipeline_start ~ ANY)* }
ending_junk = { ANY* }

agentDecl = { "agent" ~
              ("any"
              | "none"
              | agentBlock)
            }
agentBlock = {
                opening_brace ~
                (k8sAgent
                | dockerAgent
                | dockerfileAgent
                | nodeAgent
                | ("label" ~ string)) ~
                closing_brace
             }

credentialProperty = { IDENT ~
                        "=" ~
                        "credentials" ~ opening_paren ~ string ~ closing_paren
                      }
dockerAgent = { "docker" ~
             (string
             | (
                opening_brace ~
                ("reuseNode" ~ bool)? ~
                ("image" ~ string)? ~
                ("label" ~ string)? ~
                ("args" ~ string)? ~
                ("registryUrl" ~ string)? ~
                ("registryCredentialsId" ~ string)? ~
                ("customWorkspace" ~ string)? ~
                closing_brace
              )
             )
           }

dockerfileAgent = { "dockerfile" ~
             opening_brace ~
             ("reuseNode" ~ bool)? ~
             ("filename" ~ string)? ~
             ("dir" ~ string)? ~
             ("label" ~ string)? ~
             ("additionalBuildArgs" ~ string)? ~
             ("args" ~ string)? ~
             ("customWorkspace" ~ string)? ~
             closing_brace
           }

environmentDecl = { "environment" ~
                    opening_brace ~
                    (envProperty)* ~
                    closing_brace
                  }
envProperty = {
                property
                | credentialProperty
              }

failFast = { "failFast" ~ bool }

func = { IDENT ~ opening_paren? ~ (kwargs | args | func)? ~ closing_paren? }

inputDecl = { "input" ~
              opening_brace ~
             ("message" ~ string)? ~
             ("submitter" ~ string)? ~
             ("id" ~ string)? ~
              closing_brace
            }

k8sAgent = { "kubernetes" ~
             opening_brace ~
             ("defaultContainer" ~ string)? ~
             ("yaml" ~ string)? ~
             ("yamlFile" ~ string)? ~
             closing_brace
           }

nodeAgent = { "node" ~
             opening_brace ~
             ("label" ~ string)? ~
             ("customWorkspace" ~ string)? ~
             closing_brace
           }



optionsDecl = { "options" ~
                opening_brace ~
                (func)* ~
                closing_brace
              }

parallelDecl = { "parallel" ~
                 opening_brace ~
                 (stage)+ ~
                 closing_brace
               }

parametersDecl = { "parameters" ~
                opening_brace ~
                // Not exactly a step, but the syntax looks the same to me!
                (step)* ~
                closing_brace
              }

postDecl = { "post" ~
             opening_brace ~
             (postBlock+) ~
             closing_brace
           }
postBlock = {
            ("always"
            | "changed"
            | "fixed"
            | "regression"
            | "aborted"
            | "failure"
            | "success"
            | "unstable"
            | "unsuccessful"
            | "cleanup"
            ) ~ opening_brace ~ (step)+ ~ closing_brace }

scriptStep = { "script" ~ opening_brace ~ groovy ~ closing_brace }
groovy = {
            (
            // Handle nested structures
            (opening_brace ~ groovy ~ closing_brace)
            | (!closing_brace ~ ANY)
            )*
         }

stagesDecl = { "stages" ~
                opening_brace ~
                stage+ ~
                closing_brace
              }

stage = { "stage(" ~ string ~ ")" ~
            opening_brace ~
            (whenDecl
            | failFast
            | optionsDecl
            | agentDecl
            | environmentDecl
            | inputDecl
            | toolsDecl
            | (stepsDecl | parallelDecl | stagesDecl)
            | postDecl)* ~
            closing_brace
        }

stepsDecl = { "steps" ~
            opening_brace ~
            (step | scriptStep)+ ~
            closing_brace
            }


step_args = _{ args | kwargs | array_args | map_args }
// A simple step can be invoked with parenthesis or without
simple_step = _{ IDENT ~
                ((opening_paren ~ step_args? ~ closing_paren) | step_args) ~
                // This allows for really gross abusive chaining of Groovy
                // functionality off the end of a step invocation
                ("." ~ simple_step)*
                }
// A block step is something like dir('foo') { } that accepts steps within it
// in many cases these will have keyword arguments like withCredentials
block_step = _{ IDENT ~
                (opening_paren ~ step_args? ~ closing_paren)? ~
                opening_brace ~
                (step)+ ~
                closing_brace
              }
// Hard-coding this until the parser has richer symbol support
checkout_step = { "checkout" ~ "scm" }
cmd_step = { ( "sh" | "bat" | "stash" | "unstash" | "echo" ) ~ string}
sleep_step = { "sleep"  ~ number }
step = { checkout_step | cmd_step | sleep_step | block_step | simple_step }

value = _{ string | bool | number | magic_vars }
arg = _{ value | array_args | map_args | step }
args = { (arg ~ comma?)+ }

kwarg = { IDENT ~ ":" ~ arg }
kwargs = _{ (kwarg ~ comma?)+ }

map_args = _{ opening_brack ~ kwargs ~ closing_brack }
// This syntax is some Groovy invocation magic that I'm not sure I fully
// understand at the moment
array_args = _{ opening_brack ~ args ~ closing_brack }
property = { IDENT ~ "=" ~ string }

// These are magic variables that are legitimate to use in "declarative"
// pipeline. Supports concatenation
magic_vars = _{ (envRef | scmRef | paramsRef | hudsonRef | "WORKSPACE") ~ (plus ~ (string | magic_vars))? }
// This is required for some step invocations where ABI objects must be
// referenced
hudsonRef = { "hudson." ~ nestedRef }
envRef = { "env." ~ nestedRef }
scmRef = { "scm." ~ nestedRef }
paramsRef = { "params." ~ nestedRef }
// Just parse anything until the whitespace I guess
nestedRef = _{ (IDENT ~ (opening_brack ~ (number) ~ closing_brack)? ~ "."?)+ }


toolsDecl = { "tools" ~
              opening_brace ~
              (IDENT ~ string)* ~
              closing_brace
            }
triggersDecl = { "triggers" ~
                opening_brace ~
                (func)* ~
                closing_brace
              }

// when {} directives are quite complex
///////////////////////////////////////
whenAll = { "allOf" ~ opening_brace ~ whenCondition+ ~ closing_brace }
whenAny = { "anyOf" ~ opening_brace ~ whenCondition+ ~ closing_brace }
whenBeforeInput = { "beforeInput" ~ bool }
whenBeforeAgent = { "beforeAgent" ~ bool }
whenBeforeOptions = { "beforeOptions" ~ bool }
whenBranch = { "branch" ~ string }
whenCondition = { whenExpression | whenBranch | whenEnvironment | whenTag | whenTriggered }
whenDecl = { "when" ~
                opening_brace ~
                (whenQualifiers | whenPredicate | whenCondition)+ ~
                closing_brace
              }
whenEnvironment = { "environment" ~ kwargs }
whenExpression = { "expression" ~ opening_brace ~ (!closing_brace ~ ANY)* ~ closing_brace }
whenNot = { "not" ~ opening_brace ~ whenCondition+ ~ closing_brace }
whenPredicate = { whenNot | whenAll | whenAny }
whenQualifiers = { whenBeforeInput | whenBeforeOptions | whenBeforeAgent }
whenTag = { "tag" ~ string }
whenTriggered = { "triggeredBy" ~ string }
///////////////////////////////////////

IDENT = @{ (ASCII_ALPHA | "$") ~ (ASCII_ALPHANUMERIC | "_")* }

string        = { (triple_single_quoted | single_quoted | triple_double_quoted | double_quoted) ~ (plus ~ (magic_vars | string))* }
single_quoted = ${ single_quote ~ inner_single_str ~ single_quote }
triple_single_quoted = ${ triple_single_quote ~ inner_triple_single_str ~ triple_single_quote }

double_quoted = ${ (quote ~ inner_double_str ~ quote) }
triple_double_quoted = ${ triple_quote ~ inner_triple_double_str ~ triple_quote }

triple_single_quote = ${ single_quote ~ single_quote ~ single_quote }
triple_quote = ${ quote ~ quote ~ quote }

inner_single_str = @{ (!("'" | "\\") ~ ANY)* ~ (escape ~ inner_single_str)? }
inner_triple_single_str = @{ (!("'''" | "\\") ~ ANY)* ~ (escape ~ inner_triple_single_str)? }
inner_double_str = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner_double_str)? }
inner_triple_double_str = @{ (!("\"\"\"" | "\\") ~ ANY)* ~ (escape ~ inner_triple_double_str)? }
inner_chr = @{ escape | ANY }
escape    = @{ "\\" ~ ("\"" | "\\" | "r" | "n" | "t" | "0" | "'" | code | unicode) }
code      = @{ "x" ~ hex_digit{2} }
unicode   = @{ "u" ~ opening_brace ~ hex_digit{2, 6} ~ closing_brace }
hex_digit = @{ '0'..'9' | 'a'..'f' | 'A'..'F' }

quote          = { "\"" }
single_quote   = { "'" }

assignment_operator = { "=" }
opening_brace       = { "{" }
closing_brace       = { "}" }
opening_paren       = { "(" }
closing_paren       = { ")" }
opening_brack       = { "[" }
closing_brack       = { "]" }
plus = @{ "+" }

number = @{ '0'..'9'+ }
integer = @{ number | "-" ~ "0"* ~ '1'..'9' ~ number? }

comma = @{ "," }
bool = @{ (TRUE | FALSE) }
TRUE = { "true" }
FALSE = { "false" }

WHITESPACE = _{ " " | "\t" | NEWLINE }
block_comment = _{ "/*" ~ (block_comment | !"*/" ~ ANY)* ~ "*/" }
COMMENT    = _{ block_comment | ("//" ~ (!NEWLINE~ ANY)*) }