///|
const MaxConfigDepth = 5
///|
suberror OptionConflict {
OptionConflict(String)
}
///|
suberror ConfigLineError {
ConfigLineError(String, String)
}
///|
suberror ConfigOptionError {
ConfigOptionError(String, String)
}
///|
const MaxConfigLineBytes = 10485760
///|
fn config_arguments(text : String) -> Array[String] raise {
let args : Array[String] = []
for raw in text.split("\n") {
if @utf8.encode(raw.to_owned()).length() >= MaxConfigLineBytes {
raise UsageError("config line exceeds 10485760 bytes")
}
let line = raw.trim_start(chars=" \t").to_owned()
if line == "" || line.has_prefix("#") {
continue
}
let offset = args.length()
let dashed = line.has_prefix("-")
let mut end = 0
while end < line.length() {
let character = line.code_unit_at(end)
if character == ' ' ||
character == '\t' ||
(!dashed && (character == '=' || character == ':')) {
break
}
end += 1
}
let name = line[:end].to_owned()
args.push(if dashed { name } else { "--" + name })
let mut start = end
while start < line.length() {
let character = line.code_unit_at(start)
if character == ' ' ||
character == '\t' ||
(!dashed && (character == '=' || character == ':')) {
start += 1
} else {
break
}
}
if start < line.length() && line.code_unit_at(start) == '"' {
let value = StringBuilder()
let mut escaped = false
for character in line[start + 1:] {
if escaped {
value.write_char(
match character {
'n' => '\n'
'r' => '\r'
't' => '\t'
'v' => '\u000b'
other => other
},
)
escaped = false
} else if character == '\\' {
escaped = true
} else if character == '"' {
break
} else {
value.write_char(character)
}
}
args.push(value.to_string())
} else {
let mut end = start
while end < line.length() &&
![' ', '\t', '\r', '\n', '\u000b', '\u000c'].contains(
line.code_unit_at(end).to_int().to_char().unwrap(),
) {
end += 1
}
if end > start {
args.push(line[start:end].to_owned())
}
}
let specs = option_specs()
let flag = specs.any(spec => {
!spec.takes_value &&
(
args[offset] == "--" + spec.name ||
spec.short.map(short => "-" + short.to_string()) == Some(args[offset])
)
})
if flag && args.length() == offset + 2 && args[offset + 1] == "" {
ignore(args.pop())
}
let parsed = @cli.parse(args[offset:], specs) catch {
@cli.CliError(kind~, ..) =>
raise ConfigLineError(
name,
match kind {
UnknownOption => "is unknown"
MissingOptionValue => "requires parameter"
_ => "is badly used here"
},
)
}
if !parsed.operands.is_empty() {
raise ConfigLineError(name, "had unsupported trailing garbage")
}
if parsed.order.any(name => {
specs.any(spec => spec.name == name && spec.takes_value)
}) &&
!parsed
.last_value("header")
.map(value => value.has_prefix("@"))
.unwrap_or(false) {
ignore(parse_options(args[offset:])) catch {
UsageError(_) =>
raise ConfigLineError(
name,
if parsed.order.any(name => {
[
"retry", "retry-delay", "retry-max-time", "max-redirs", "connect-timeout",
"max-time", "idle-timeout",
].contains(name)
}) {
"expected a proper numerical parameter"
} else {
"is badly used here"
},
)
}
}
}
args
}
///|
async fn config_text(path : String) -> String {
if path == "-" {
@stdio.stdin.read_all().text()
} else {
@fs.read_file(path).text()
}
}
///|
async fn config_file_into(
output : Array[String],
text : String,
path : String,
read : async (String) -> String,
depth : Int32,
file_headers~ : Set[Int32],
) -> Unit {
for index, line in text.split("\n").to_array() {
let args = config_arguments(line.to_owned()) catch {
ConfigLineError(option, reason) => {
let parsed = try @cli.parse(output, option_specs()) catch {
_ => None
} noraise {
parsed => Some(parsed)
}
let silent = parsed
.map(parsed => enabled(parsed, "silent"))
.unwrap_or(false)
if !silent ||
parsed.map(parsed => enabled(parsed, "show-error")).unwrap_or(false) {
config_diagnostic(
(if path == "-" { "" } else { path }) +
":" +
(index + 1).to_string() +
" config file option '" +
option +
"' " +
reason,
)
}
raise ConfigOptionError(
"--config",
if reason == "is unknown" {
"found an unknown config option"
} else {
reason
},
)
}
error => raise error
}
expand_config_into(output, args, read, depth, file_headers~) catch {
ConfigOptionError(option, reason) as error => {
config_report(
output,
(if path == "-" { "" } else { path }) +
":" +
(index + 1).to_string() +
" config file option '" +
option.trim_start(chars="-").to_owned() +
"' " +
reason,
)
if reason == "error encountered when reading a file" {
config_report(output, "cannot read config from '" + path + "'")
}
raise error
}
error => raise error
}
}
}
///|
async fn config_report(output : Array[String], message : String) -> Unit {
let parsed = try @cli.parse(output, option_specs()) catch {
_ => None
} noraise {
parsed => Some(parsed)
}
if !parsed.map(parsed => enabled(parsed, "silent")).unwrap_or(false) ||
parsed.map(parsed => enabled(parsed, "show-error")).unwrap_or(false) {
config_diagnostic(message)
}
}
///|
async fn config_diagnostic(message : String) -> Unit {
// Curl's non-terminal diagnostic width is 79 bytes including its prefix.
let bytes = @utf8.encode(message)
let width = 73
let mut offset = 0
while offset < bytes.length() {
let mut count = (bytes.length() - offset).min(width)
if bytes.length() - offset > width {
let mut cut = width - 1
while cut > 0 &&
bytes[offset + cut] != b' ' &&
bytes[offset + cut] != b'\t' {
cut -= 1
}
if cut > 0 {
count = cut + 1
}
}
@stdio.stderr.write(
b"curl: " + bytes[offset:offset + count].to_owned() + b"\n",
)
offset += count
}
}
///|
async fn expand_config_into(
output : Array[String],
args : ArrayView[String],
read : async (String) -> String,
depth : Int32,
file_headers~ : Set[Int32] = Set([]),
) -> Unit {
let specs = option_specs()
let mut index = 0
while index < args.length() {
let arg = args[index]
index += 1
if arg == "--" {
for value in args[index:] {
output.append(["--url", value])
}
break
}
let tokens : Array[(String, String?)] = []
if arg.has_prefix("--") {
match arg.split_once("=") {
Some((name, value)) =>
tokens.push((name.to_owned(), Some(value.to_owned())))
None => tokens.push((arg, None))
}
} else if arg.has_prefix("-") && arg != "-" {
let chars = arg.iter().to_array()
for position = 1; position < chars.length(); position = position + 1 {
let short = chars[position]
let name = "-" + short.to_string()
if specs.any(spec => spec.short == Some(short) && spec.takes_value) {
tokens.push(
(
name,
if position + 1 < chars.length() {
Some(String::from_iter(chars[position + 1:].iter()))
} else {
None
},
),
)
break
}
tokens.push((name, None))
}
} else {
// Keeping URLs as options preserves their order across included files.
output.append(["--url", arg])
continue
}
for (name, attached) in tokens {
let spec = specs
.iter()
.find_first(spec => {
name == "--" + spec.name ||
spec.short.map(char => "-" + char.to_string()) == Some(name)
})
guard spec is Some(spec) else {
raise UsageError("unrecognized option: '" + name + "'")
}
let takes_value = spec.takes_value
let value = if takes_value && attached is None {
guard args.get(index) is Some(value) else {
raise UsageError(name + " requires an argument")
}
index += 1
Some(value)
} else {
attached
}
let current = if !takes_value && attached is Some(value) {
[name + "=" + value]
} else if value is Some(value) {
[name, value]
} else {
[name]
}
ignore(@cli.parse(current, specs)) catch {
@cli.CliError(message~, option~, ..) =>
raise UsageError("\{message}: '\{option}'")
}
if spec.name == "head" || spec.name == "no-head" {
let previous = @cli.parse(output, specs)
let opposite = if spec.name == "head" { "no-head" } else { "head" }
if previous.contains(opposite) {
raise OptionConflict("--" + spec.name)
}
}
// Validate each value before a later option can read a file or stdin.
// Cross-option constraints still belong to the final option assembly.
if takes_value &&
spec.name != "config" &&
!(spec.name == "header" && value.unwrap().has_prefix("@")) {
ignore(parse_options(current))
}
if (name == "--header" || name == "-H") &&
value is Some(value) &&
value.has_prefix("@") {
let path = value[1:].to_owned()
let text = read(path) catch {
_ => {
config_report(output, "Failed to open " + path)
raise ConfigOptionError(
name, "error encountered when reading a file",
)
}
}
for raw in text.split("\n") {
let line = raw.trim_start(chars=" \t").to_owned()
if line == "" || line.has_prefix("#") {
continue
}
if @utf8.encode(raw.to_owned()).length() >= 102400 {
raise ConfigOptionError(
name, "error encountered when reading a file",
)
}
let parsed = @cli.parse(output, option_specs())
file_headers.add(parsed.values("header").length())
output.append(["--header", line])
}
} else if name == "--config" || name == "-K" {
guard depth < MaxConfigDepth else {
config_report(
output,
"Max config file recursion level reached (" +
MaxConfigDepth.to_string() +
")",
)
raise ConfigOptionError(name, "is badly used here")
}
let path = value.unwrap()
let text = read(path) catch {
_ => {
config_report(output, "cannot read config from '" + path + "'")
raise ConfigOptionError(
name, "error encountered when reading a file",
)
}
}
config_file_into(output, text, path, read, depth + 1, file_headers~) catch {
ConfigOptionError(_, reason) => raise ConfigOptionError(name, reason)
error => raise error
}
} else if !takes_value && attached is Some(value) {
output.push(name + "=" + value)
} else {
output.push(name)
if value is Some(value) {
output.push(value)
}
}
}
}
}
///|
async fn configured_arguments(
args : ArrayView[String],
file_headers~ : Set[Int32],
) -> Array[String] {
let output : Array[String] = []
let disabled = args.get(0) == Some("--disable") ||
args.get(0).map(arg => arg.has_prefix("-q")).unwrap_or(false)
if !disabled {
let candidates : Array[String] = []
let locations = [
("CURL_HOME", "/.curlrc"),
("XDG_CONFIG_HOME", "/curlrc"),
("HOME", "/.curlrc"),
]
if @env.get_env_var("XDG_CONFIG_HOME").unwrap_or("") == "" {
locations.append([
("CURL_HOME", "/.config/curlrc"),
("HOME", "/.config/curlrc"),
])
}
for (variable, suffix) in locations {
if @env.get_env_var(variable) is Some(directory) && directory != "" {
candidates.push(directory + suffix)
}
}
for path in candidates {
let text = config_text(path) catch { _ => continue }
config_file_into(output, text, path, config_text, 0, file_headers~) catch {
ConfigOptionError(_, _) => ()
error => raise error
}
break
}
}
expand_config_into(output, args, config_text, 0, file_headers~)
output
}