///|
pub suberror ParseError {
Invalid(String)
StageCycle(Int)
} derive(Debug)
///|
pub struct Instruction {
line : Int
end_line : Int
name : String
arguments : String
heredocs : Array[HereDoc]
} derive(Debug, Eq, ToJson)
///|
fn words(s : String) -> Array[String] {
let result = []
let mut word = ""
for c in s.iter() {
if c == ' ' || c == '\t' {
if word != "" {
result.push(word)
word = ""
}
} else {
word += c.to_string()
}
}
if word != "" {
result.push(word)
}
result
}
///|
/// Source-preserving instruction arguments, physical line ranges, escape directives.
/// RUN/COPY heredoc bodies are captured without interpreting their contents.
pub fn parse(source : String) -> Array[Instruction] raise ParseError {
if !valid_unicode(source) {
raise Invalid("ill-formed UTF-16 input")
}
if source.length() > 1000000 {
raise Invalid("Dockerfile too large")
}
let out = []
let mut logical = ""
let mut start = 1
let mut number = 0
let mut escape = "\\"
let mut directive_region = true
let mut continuing = false
let seen_directives : Map[String, Bool] = Map([])
let lines = source.split("\n").collect()
let mut position = 0
while position < lines.length() {
let raw = lines[position]
position += 1
number = position
let s = raw.trim_end(chars="\r").trim_start().to_owned()
if s.has_prefix("#") {
if directive_region {
let pair = s[1:].trim().to_owned().split("=").collect()
let key = pair[0].trim().to_owned().to_lower()
if pair.length() >= 2 && ["syntax", "escape", "check"].contains(key) {
if seen_directives.contains(key) {
raise Invalid("duplicate parser directive " + key)
}
seen_directives[key] = true
if key == "escape" {
let value = pair[1:]
.map(x => x.to_owned())
.join("=")
.trim()
.to_owned()
if value != "\\" && value != "`" {
raise Invalid("invalid escape directive")
}
escape = value
}
} else {
directive_region = false
}
}
continue
}
if s == "" {
directive_region = false
continue
}
directive_region = false
if !continuing {
start = number
}
let mut suffix = 0
let clean = s.trim_end().to_owned()
for i = clean.length() - 1; i >= 0; i = i - 1 {
if clean[i:i + 1] == escape {
suffix += 1
} else {
break
}
}
continuing = suffix % 2 == 1
logical += if continuing {
clean[:clean.length() - 1].to_owned()
} else {
clean
}
if continuing {
continue
}
let parts = words(logical)
if parts.is_empty() {
continue
}
let name = parts[0].to_upper()
let args = logical[parts[0].length():].trim_start().to_owned()
if args == "" {
raise Invalid("missing arguments at line " + start.to_string())
}
let headers = if name == "RUN" || name == "COPY" {
heredoc_headers(args)
} else {
[]
}
let heredocs = []
for (delimiter, strip_tabs, expand) in headers {
let body = StringBuilder()
let mut closed = false
while position < lines.length() {
let line = lines[position].trim_end(chars="\r").to_owned()
position += 1
let content = if strip_tabs {
line.trim_start(chars="\t").to_owned()
} else {
line
}
if content == delimiter {
closed = true
break
}
body.write_string(content)
body.write_char('\n')
}
if !closed {
raise Invalid(
"unclosed heredoc " + delimiter + " at line " + start.to_string(),
)
}
heredocs.push({
name: delimiter,
content: body.to_string(),
strip_tabs,
expand,
})
}
number = position
let instruction : Instruction = {
line: start,
end_line: number,
name,
arguments: args,
heredocs,
}
if ["COPY", "ADD", "SHELL"].contains(name) || args.has_prefix("[") {
ignore(instruction.split_arguments())
}
out.push(instruction)
logical = ""
}
if continuing {
raise Invalid("dangling line continuation")
}
out
}