///|
/// Split a POSIX-like command line into argv (never evaluate or execute it).
/// Quotes, backslashes and line continuations are recognized. $, ``, globbing,
/// pipes and redirects stay literal. This is not PowerShell/cmd.exe syntax.
pub fn split_command(
line : String,
comments~ : Bool = false,
max_words~ : Int32 = 4096,
) -> Array[String] raise DocoptError {
guard max_words > 0 && max_words <= 32768 else {
raise Conversion("invalid word limit")
}
guard line.length() <= 1048576 else { raise Limit("command text too large") }
let out = []
let buffer = StringBuilder()
let mut mode = 0 // 0 bare, 1 single-quoted, 2 double-quoted
let mut active = false
let mut escaping = false
let mut quote_offset = 0
for offset, c in line.iter2() {
guard c != '\u0000' else { raise Conversion("NUL in command at \{offset}") }
if escaping {
if c != '\n' {
if mode == 2 && c != '"' && c != '\\' && c != '$' && c != '`' {
buffer.write_char('\\')
}
buffer.write_char(c)
active = true
}
escaping = false
continue
}
if mode == 1 {
if c == '\'' {
mode = 0
} else {
buffer.write_char(c)
}
continue
}
if c == '\\' {
escaping = true
continue
}
if mode == 2 {
if c == '"' {
mode = 0
} else {
buffer.write_char(c)
}
continue
}
if c == '\'' || c == '"' {
mode = if c == '\'' { 1 } else { 2 }
quote_offset = offset
active = true
} else if c == '#' && comments {
break
} else if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
if active {
guard out.length() < max_words else {
raise Limit("too many command words")
}
out.push(buffer.to_string())
buffer.reset()
active = false
}
} else {
buffer.write_char(c)
active = true
}
}
guard !escaping else {
raise Conversion("trailing backslash at \{line.length() - 1}")
}
guard mode == 0 else { raise Conversion("unclosed quote at \{quote_offset}") }
if active {
guard out.length() < max_words else {
raise Limit("too many command words")
}
out.push(buffer.to_string())
}
out
}
///|
/// Convenience for in-app consoles: line contains arguments, not program name.
pub fn Parser::parse_line(
self : Parser,
line : String,
options_first~ : Bool = false,
) -> Values raise DocoptError {
guard line.length() <= self.limits.argv_units else {
raise Limit("command text too large")
}
self.parse(
split_command(line, max_words=self.limits.argv_count),
options_first~,
)
}