///|
fn valid_name_char(c : Char) -> Bool {
c is ('a'..<='z' | 'A'..<='Z' | '0'..<='9' | '_' | '-')
}
///|
fn valid_command_char(c : Char) -> Bool {
valid_name_char(c) ||
c is ('.' | '+') ||
(!c.is_ascii() && c.is_printable() && !c.is_whitespace())
}
///|
fn check_name(
name : String,
description : String,
command~ : Bool = false,
) -> Unit raise CliError {
let valid_char = if command { valid_command_char } else { valid_name_char }
if name.is_empty() || name.has_prefix("-") || !name.iter().all(valid_char) {
raise CliError::InvalidDefinition("\{description}: invalid name '\{name}'")
}
}
///|
fn validate_definition(
cmd : CommandDef,
ancestors : ArrayView[String],
globals : ArrayView[OptionDef],
) -> Unit raise CliError {
let path = ancestors.to_owned()
path.push(cmd.name)
let scope = path.join(" ")
check_name(cmd.name, "command", command=true)
let names : Map[String, Bool] = Map([])
let shorts : Map[Char, Bool] = Map([])
let next_globals = globals.to_owned()
for option in globals {
names[option.name] = true
if option.short is Some(short) {
shorts[short] = true
}
}
for option in cmd.options {
check_name(option.name, scope)
if names.contains(option.name) {
raise CliError::InvalidDefinition(
"\{scope}: duplicate or inherited global argument '\{option.name}'",
)
}
names[option.name] = true
if option.short is Some(short) {
if short == '-' || !valid_name_char(short) {
raise CliError::InvalidDefinition("\{scope}: invalid short option")
}
if shorts.contains(short) {
raise CliError::InvalidDefinition(
"\{scope}: duplicate or inherited global short option '-\{short}'",
)
}
shorts[short] = true
}
if option.global {
next_globals.push(option)
}
}
let mut optional_seen = false
for positional in cmd.positionals {
check_name(positional.name, scope)
if names.contains(positional.name) {
raise CliError::InvalidDefinition(
"\{scope}: duplicate or inherited global argument '\{positional.name}'",
)
}
names[positional.name] = true
if positional.required && optional_seen {
raise CliError::InvalidDefinition(
"\{scope}: required positionals must precede optional positionals",
)
}
optional_seen = optional_seen || !positional.required
}
if !cmd.subcommands.is_empty() && cmd.positionals.any(pos => pos.required) {
raise CliError::InvalidDefinition(
"\{scope}: required positionals cannot be combined with subcommands",
)
}
let commands : Map[String, Bool] = Map([])
for sub in cmd.subcommands {
if sub.name == "help" || commands.contains(sub.name) {
raise CliError::InvalidDefinition(
"\{scope}: duplicate or reserved subcommand '\{sub.name}'",
)
}
commands[sub.name] = true
validate_definition(sub, path, next_globals)
}
}