import data; import cli; import completion; import syntax; enum ScriptArgKind { Flag, Option, Positional } struct ScriptRule { name: Span, kind: ScriptArgKind, long: Span = Span { 0, 0 }, short: Span = Span { 0, 0 }, description: Span = Span { 0, 0 }, choices: Values = Values { 0, 0 }, defaults: Values = Values { 0, 0 }, repeat: RepeatMode = RepeatMode.Once, required: Requirement = Requirement.Optional, value_type: ValueType = ValueType.Text, } enum TailMode { Strict, Forward } struct ScriptInfo { program: Span, about: Span = Span { 0, 0 }, version: Span = Span { 0, 0 }, tail: TailMode = TailMode.Strict } const SUBCOMMAND_VAR = "subcommand"; const UNPARSED_VAR = "unparsed"; struct ScriptCommand { info: ScriptInfo, name: Span, body: i64, parent: i64, start: i64, len: i64 } struct ParsedBinding { name: Span, value: Values } enum DeclarationState { Run, Stop, Error } enum HelpScope { Arguments, Commands } struct DeclarationResult { state: DeclarationState, error: Span = Span { 0, 0 } } fn variable_name(s: u8[:]) -> bool { if s.len == 0 || (!ascii::is_alpha(s[0]) && s[0] != '_') || s == "status" { return false; } for c in s { if !ascii::is_alnum(c) && c != '_' { return false; } } return true; } fn entry_error(entry: i64, error: Span) -> Span { let site = nodes[entry]; var out: u8[>..] = []; out.append(if site.path.len > 0 { view(site.path) } else { "" }); out.push(':'); format(out, site.line); out.append(": "); out.append(view(error)); let source = view(site.source); var start = site.offset; var end = site.offset; while start > 0 && source[start - 1] != '\n' { start -= 1; } while end < source.len && source[end] != '\n' { end += 1; } out.push('\n'); out.append(source[start..end]); out.push('\n'); // Preserve tabs so that the caret follows the source indentation. for i in start..site.offset { let c = source[i]; if c == '\t' { out.push('\t'); } else if (c & UTF8_CONT_MASK) != UTF8_CONT_TAG { out.push(' '); } } out.push('^'); return save(out[..]); } recursive fn static_values(n: i64) -> Values, Span { let start = values.len; var word = n; while word >= 0 { let t = nodes[word]; if t.kind == Kind.Literal { values.push(plain(t.text)); } else if t.kind == Kind.List { let nested, error = static_values(t.a); if error.len > 0 { return Values { 0, 0 }, error; } // Nested results already occupy this contiguous value range. } else { return Values { 0, 0 }, save("argument declarations require literal words"); } word = t.next; } return value_end(start), Span { 0, 0 }; } fn read_rule(spec: i64[:], rules: ScriptRule[>..]&, site: i64&) -> Span { let tag = view(nodes[spec[0]].text); var kind = ScriptArgKind.Flag; if tag == "option" { kind = ScriptArgKind.Option; } else if tag == "positional" { kind = ScriptArgKind.Positional; } else if tag != "flag" { return save("unknown argument declaration"); } if spec.len < 2 || nodes[spec[1]].kind != Kind.Literal { return save("argument declaration requires a variable name"); } site = spec[1]; let name = nodes[spec[1]].text; if !variable_name(view(name)) || view(name) == SUBCOMMAND_VAR { return save("invalid argument variable name"); } var rule = ScriptRule { name, kind }; var i = 2; while i < spec.len { site = spec[i]; let word = nodes[spec[i]]; i += 1; if word.kind == Kind.List { if rule.choices.len > 0 || kind == ScriptArgKind.Flag { return save("invalid argument choices"); } let choices, error = static_values(word.a); if error.len > 0 { return error; } if choices.len == 0 { return save("argument choices cannot be empty"); } rule.choices = choices; continue; } if word.kind != Kind.Literal { return save("argument declarations require literal words"); } let s = view(word.text); if word.a == QUOTED_LITERAL { if rule.description.len > 0 { return save("duplicate argument description"); } rule.description = word.text; } else if starts_with(s, "--") { if kind == ScriptArgKind.Positional || rule.long.len > 0 || !portable_name(s[2..]) { return save("invalid long option name"); } rule.long = Span { word.text.start + 2, word.text.len - 2 }; } else if starts_with(s, "-") { if kind == ScriptArgKind.Positional || rule.short.len > 0 || s.len != 2 || !ascii::is_alnum(s[1]) { return save("invalid short option name"); } rule.short = Span { word.text.start + 1, 1 }; } else if s == "required" { rule.required = Requirement.Required; } else if s == "repeat" && kind != ScriptArgKind.Positional { rule.repeat = RepeatMode.Multiple; } else if s == "multiple" && kind == ScriptArgKind.Positional { rule.repeat = RepeatMode.Multiple; } else if s == "int" && kind != ScriptArgKind.Flag { rule.value_type = ValueType.Integer; } else if s == "default" && kind != ScriptArgKind.Flag { if i == spec.len || rule.defaults.len > 0 { return save("default requires one literal value or list"); } site = spec[i]; let d = nodes[spec[i]]; i += 1; if d.kind == Kind.Literal { rule.defaults = singleton(d.text); } else if d.kind == Kind.List { let defaults, error = static_values(d.a); if error.len > 0 { return error; } rule.defaults = defaults; } else { return save("default requires literal values"); } } else { return save("unknown argument modifier"); } } site = spec[1]; if kind != ScriptArgKind.Positional && rule.long.len == 0 && rule.short.len == 0 { return save("option requires an alias"); } if view(rule.long) == "help" || view(rule.long) == "version" || view(rule.long) == "completions" || view(rule.short) == "h" { return save("reserved argument alias"); } if rule.repeat == RepeatMode.Once && rule.defaults.len > 1 { return save("one argument cannot have multiple defaults"); } for prior in rules { if view(prior.name) == view(name) { return save("duplicate argument variable"); } if rule.long.len > 0 && view(prior.long) == view(rule.long) { return save("duplicate long option"); } if rule.short.len > 0 && view(prior.short) == view(rule.short) { return save("duplicate short option"); } if kind != ScriptArgKind.Positional || prior.kind != ScriptArgKind.Positional { continue; } if prior.repeat == RepeatMode.Multiple { return save("multiple positional must be last"); } if prior.required == Requirement.Optional && rule.required == Requirement.Required { return save("required positional cannot follow an optional positional"); } } for j in rule.defaults.len { let failure = script_value(rule, bytes(rule.defaults, j)); if failure.len > 0 { return failure; } } rules.push(rule); return Span { 0, 0 }; } fn read_entry(n: i64, info: ScriptInfo&, rules: ScriptRule[>..]&, site: i64&) -> Span { let command = nodes[n]; if command.kind == Kind.ArgCommand { return Span { 0, 0 }; } if command.kind != Kind.Simple { return save("argument block accepts declarations only"); } var spec: i64[>..<] = []; var word = command.a; while word >= 0 { spec.push(word); word = nodes[word].next; } if spec.len == 0 || nodes[spec[0]].kind != Kind.Literal { return save("invalid argument declaration"); } site = spec[0]; let tag = view(nodes[spec[0]].text); if tag == "allow-unknown" { if spec.len != 1 || info.tail == TailMode.Forward { return save("allow-unknown requires one declaration without arguments"); } info.tail = TailMode.Forward; return Span { 0, 0 }; } if tag != "name" && tag != "about" && tag != "version" { return read_rule(spec, rules, site); } if spec.len > 1 { site = spec[1]; } if spec.len > 2 { site = spec[2]; } if spec.len != 2 || nodes[spec[1]].kind != Kind.Literal { return save("metadata requires one literal word"); } let value = nodes[spec[1]].text; if tag == "name" { if !portable_name(view(value)) { return save("invalid program name"); } info.program = value; } else if tag == "about" { info.about = value; } else { info.version = value; } return Span { 0, 0 }; } fn read_schema(body: i64, program: Span, rules: ScriptRule[>..]&) -> ScriptInfo, Span { var info = ScriptInfo { program }; var cur = body; while cur >= 0 { let n = nodes[cur]; if n.kind == Kind.Empty { break; } let entry = if n.kind == Kind.Sequence { n.a } else { cur }; var site = entry; let error = read_entry(entry, info, rules, site); if error.len > 0 { return info, entry_error(site, error); } cur = if n.kind == Kind.Sequence { n.b } else { -1 }; } if !portable_name(view(info.program)) { return info, save("set a portable program name in the argument block"); } if info.tail == TailMode.Forward { var count = 0; for rule in rules { if view(rule.name) == UNPARSED_VAR { return info, save("unparsed is reserved with allow-unknown"); } if rule.kind != ScriptArgKind.Positional { continue; } if rule.repeat != RepeatMode.Once || rule.required != Requirement.Required { return info, save("allow-unknown requires fixed required positional arguments"); } count += 1; } if count == 0 { return info, save("allow-unknown requires a positional prefix"); } } return info, Span { 0, 0 }; } fn prefix_count(info: ScriptInfo, rules: ScriptRule[:]) -> i64 { if info.tail == TailMode.Strict { return NO_PREFIX; } var count = 0; for rule in rules { if rule.kind == ScriptArgKind.Positional { count += 1; } } return count; } recursive fn read_commands(body: i64, program: Span, parent: i64, rules: ScriptRule[>..]&, commands: ScriptCommand[>..]&) -> Span { let id = commands.len; var info = ScriptInfo { program }; { var local: ScriptRule[>..] = []; let parsed, error = read_schema(body, program, local); if error.len > 0 { return error; } info = parsed; let name = program; if parent >= 0 { info.program = concat_span(concat_span(commands[parent].info.program, save(" ")), name); } commands.push(ScriptCommand { info, name, body, parent, rules.len, local.len }); for rule in local { var ancestor = id; while ancestor >= 0 { let scope = commands[ancestor]; if ancestor != id { for i in scope.start..scope.start + scope.len { if view(rules[i].name) == view(rule.name) { return save("argument variable conflicts with parent declaration"); } } } ancestor = scope.parent; } rules.push(rule); } } var cur = body; while cur >= 0 { let n = nodes[cur]; let entry = if n.kind == Kind.Sequence { n.a } else { cur }; if entry >= 0 && nodes[entry].kind == Kind.ArgCommand { if info.tail == TailMode.Forward { return entry_error(entry, save("allow-unknown requires a leaf command")); } let child = nodes[entry]; if child.a < 0 || nodes[child.a].kind != Kind.Literal || nodes[child.a].next >= 0 || !portable_name(view(nodes[child.a].text)) { return entry_error(entry, save("command requires one literal portable name")); } let child_name = nodes[child.a].text; for prior in commands { if prior.parent == id && view(prior.name) == view(child_name) { return entry_error(child.a, save("duplicate subcommand")); } } let failure = read_commands(child.b, child_name, id, rules, commands); if failure.len > 0 { return failure; } } cur = if n.kind == Kind.Sequence { n.b } else { -1 }; } if parent < 0 { var forwarding = false; for scope in commands { if scope.info.tail == TailMode.Forward { forwarding = true; } } if forwarding { for rule in rules { if view(rule.name) == UNPARSED_VAR { return save("unparsed conflicts with a pass-through command"); } } } } return Span { 0, 0 }; } fn command_help(id: i64, rules: ScriptRule[:], commands: ScriptCommand[:], out: u8[>..]&) { let scope = commands[id]; var mode = HelpScope.Arguments; for child in commands { if child.parent == id { mode = HelpScope.Commands; break; } } script_help(scope.info, rules[scope.start..scope.start + scope.len], out, mode); var heading = false; for child in commands { if child.parent != id { continue; } if !heading { out.append("\nCommands:\n"); heading = true; } out.append(" "); out.append(view(child.name)); if child.info.about.len > 0 { out.append(" "); out.append(view(child.info.about)); } out.push('\n'); } } fn make_cli(rules: ScriptRule[:], info: ScriptInfo, choices: (const u8[:])[>..]&, out: CliRule[>..]&) { for rule in rules { if rule.kind == ScriptArgKind.Positional { continue; } let start = choices.len; for i in rule.choices.len { choices.push(bytes(rule.choices, i)); } let option = CliOption { view(rule.long), view(rule.short), view(rule.description), if rule.kind == ScriptArgKind.Flag { ArgumentMode.Flag } else { ArgumentMode.Value }, choices[start..] }; out.push(CliRule { option: option, repeat: rule.repeat, value_type: rule.value_type }); } out.push(CliRule { option: CliOption { "help", "h", "Show help", ArgumentMode.Flag, NO_CHOICES } }); if info.version.len > 0 { out.push(CliRule { option: CliOption { "version", "", "Show the version", ArgumentMode.Flag, NO_CHOICES } }); } out.push(CliRule { option: CliOption { "completions", "", "Print a completion script", ArgumentMode.Value, SHELL_CHOICES } }); } fn script_help(info: ScriptInfo, rules: ScriptRule[:], out: u8[>..]&, scope: HelpScope = HelpScope.Arguments) { if info.about.len > 0 { out.append(view(info.about)); out.push('\n'); } out.append("Usage: "); out.append(view(info.program)); out.append(" [options]"); for rule in rules { if rule.kind != ScriptArgKind.Positional { continue; } out.append(if rule.required == Requirement.Required { " <" } else { " [" }); out.append(view(rule.name)); if rule.repeat == RepeatMode.Multiple { out.append("..."); } out.append(if rule.required == Requirement.Required { ">" } else { "]" }); } if scope == HelpScope.Commands { out.append(" [arguments...]"); } if info.tail == TailMode.Forward { out.append(" [--] [unparsed...]"); } out.append("\n\nArguments:\n"); for rule in rules { out.append(" "); if rule.kind == ScriptArgKind.Positional { out.append(view(rule.name)); } else { if rule.short.len > 0 { out.push('-'); out.append(view(rule.short)); } if rule.long.len > 0 { if rule.short.len > 0 { out.append(", "); } out.append("--"); out.append(view(rule.long)); } if rule.kind == ScriptArgKind.Option { out.append(" <"); out.append(view(rule.name)); out.push('>'); } } if rule.description.len > 0 { out.append(" "); out.append(view(rule.description)); } if rule.choices.len > 0 { out.append(" [choices: "); out.append(view(join_values(rule.choices, ", "))); out.push(']'); } if rule.defaults.len > 0 { out.append(" [default: "); out.append(view(join_values(rule.defaults, " "))); out.push(']'); } if rule.required == Requirement.Required { out.append(" [required]"); } out.push('\n'); } out.append(" -h, --help Show help\n"); if info.version.len > 0 { out.append(" --version Show the version\n"); } out.append(" --completions Print a completion script\n"); if info.tail == TailMode.Forward { out.append("\nArguments after the positional prefix belong to $unparsed, including options.\nOne optional -- after the prefix separates the forwarded arguments.\n"); } } fn script_value(rule: ScriptRule, value: u8[:]) -> Span { if rule.value_type == ValueType.Integer { let number, ok = parse_int(value); if !ok { return save("expected an integer"); } } if rule.choices.len == 0 { return Span { 0, 0 }; } for i in rule.choices.len { if bytes(rule.choices, i) == value { return Span { 0, 0 }; } } return save("invalid option value"); } fn completion_target(name: u8[:]) -> CompletionShell, bool { if name == "fish" { return CompletionShell.Fish, true; } if name == "bash" { return CompletionShell.Bash, true; } if name == "zsh" { return CompletionShell.Zsh, true; } return CompletionShell.Rc, name == "rc"; } fn declared_completions(info: ScriptInfo, cli: CliRule[:], rules: ScriptRule[:], target: u8[:], out: u8[>..]&) -> Span { let shell, valid = completion_target(target); if !valid { return save("expected fish, bash, zsh, or rc"); } var options: CliOption[>..] = []; for rule in cli { options.push(rule.option); } var choices: (const u8[:])[>..] = []; var positional: CliPositional[>..] = []; for rule in rules { if rule.kind != ScriptArgKind.Positional { continue; } let start = choices.len; for i in rule.choices.len { choices.push(bytes(rule.choices, i)); } positional.push(CliPositional { choices[start..], if rule.repeat == RepeatMode.Multiple { PositionalMode.Multiple } else { PositionalMode.Once } }); } let script = completion_script(shell, view(info.program), options, positional, prefix_count(info, rules)); out.append(script); return Span { 0, 0 }; } fn parse_flat(body: i64, program: Span, args: Values, result: ParsedBinding[>..]&, out: u8[>..]&) -> DeclarationResult { var rules: ScriptRule[>..] = []; let info, schema_error = read_schema(body, program, rules); if schema_error.len > 0 { return DeclarationResult { DeclarationState.Error, schema_error }; } var choices: (const u8[:])[>..] = []; var cli: CliRule[>..] = []; make_cli(rules, info, choices, cli); var arguments: (const u8[:])[>..] = []; for i in args.len { arguments.push(bytes(args, i)); } let matches, error, enabled = scan_args(cli, arguments, NO_CHOICES, prefix_count(info, rules)); if error.len > 0 { return DeclarationResult { DeclarationState.Error, save(error) }; } var named = 0; for rule in rules { if rule.kind != ScriptArgKind.Positional { named += 1; } } for found in matches { if found.option == named { script_help(info, rules, out); return DeclarationResult { DeclarationState.Stop }; } if info.version.len > 0 && found.option == named + 1 { out.append(view(info.program)); out.push(' '); out.append(view(info.version)); out.push('\n'); return DeclarationResult { DeclarationState.Stop }; } if found.option == cli.len - 1 { let failure = declared_completions(info, cli, rules, found.value, out); return DeclarationResult { if failure.len > 0 { DeclarationState.Error } else { DeclarationState.Stop }, failure }; } } let positional_start = values.len; for found in matches { if found.option == CLI_POSITIONAL { values.push(plain(save(found.value))); } } let positional = value_end(positional_start); var consumed = 0; named = 0; for rule in rules { let start = values.len; if rule.kind == ScriptArgKind.Positional { let count = if rule.repeat == RepeatMode.Multiple { positional.len - consumed } else { min(1, positional.len - consumed) }; for i in count { values.push(item(positional, consumed + i)); } consumed += count; } else { for found in matches { if found.option != named { continue; } values.push(plain(save(if rule.kind == ScriptArgKind.Flag { "1" } else { found.value }))); } named += 1; } if values.len == start { for i in rule.defaults.len { values.push(item(rule.defaults, i)); } } let value = value_end(start); if value.len == 0 && rule.required == Requirement.Required { return DeclarationResult { DeclarationState.Error, save("missing required argument") }; } for i in value.len { let failure = script_value(rule, bytes(value, i)); if failure.len > 0 { return DeclarationResult { DeclarationState.Error, failure }; } } result.push(ParsedBinding { rule.name, value }); } if consumed != positional.len { return DeclarationResult { DeclarationState.Error, save("unexpected positional argument") }; } if info.tail == TailMode.Forward { let start = values.len; for found in matches { if found.option == CLI_UNPARSED { values.push(plain(save(found.value))); } } result.push(ParsedBinding { save(UNPARSED_VAR), value_end(start) }); } return DeclarationResult { DeclarationState.Run }; } fn parse_declaration(body: i64, program: Span, args: Values, result: ParsedBinding[>..]&, out: u8[>..]&) -> DeclarationResult { var rules: ScriptRule[>..] = []; var commands: ScriptCommand[>..] = []; let failure = read_commands(body, program, -1, rules, commands); if failure.len > 0 { return DeclarationResult { DeclarationState.Error, failure }; } result.push(ParsedBinding { save(SUBCOMMAND_VAR), Values { 0, 0 } }); if commands.len == 1 { return parse_flat(body, program, args, result, out); } for scope in commands { if scope.info.tail == TailMode.Forward { result.push(ParsedBinding { save(UNPARSED_VAR), Values { 0, 0 } }); break; } } // Clear inactive branches so repeated declarations cannot expose stale values. for rule in rules { result.push(ParsedBinding { rule.name, Values { 0, 0 } }); } var id = 0; var offset = 0; var selected: i64[>..] = []; var segments: Values[>..] = []; loop { let scope = commands[id]; selected.push(id); var choices: (const u8[:])[>..] = []; var cli: CliRule[>..] = []; make_cli(rules[scope.start..scope.start + scope.len], scope.info, choices, cli); var children: (const u8[:])[>..] = []; for child in commands { if child.parent == id { children.push(view(child.name)); } } var arguments: (const u8[:])[>..] = []; for i in offset..args.len { arguments.push(bytes(args, i)); } let matches, error, enabled = scan_args(cli, arguments, children, prefix_count(scope.info, rules[scope.start..scope.start + scope.len])); if error.len > 0 { return DeclarationResult { DeclarationState.Error, save(error) }; } var next = -1; var boundary = arguments.len; for found in matches { if found.option == cli.len - 1 { let failure = command_completions(commands, rules, found.value, out); return DeclarationResult { if failure.len > 0 { DeclarationState.Error } else { DeclarationState.Stop }, failure }; } if found.option >= 0 && cli[found.option].option.long == "help" { command_help(id, rules, commands, out); return DeclarationResult { DeclarationState.Stop }; } if found.option >= 0 && cli[found.option].option.long == "version" { out.append(view(scope.info.program)); out.push(' '); out.append(view(scope.info.version)); out.push('\n'); return DeclarationResult { DeclarationState.Stop }; } if found.option != CLI_COMMAND { continue; } boundary = found.index; for child, index in commands { if child.parent == id && view(child.name) == found.value { next = index; break; } } } if next < 0 && children.len > 0 { var positional = 0; var capacity = 0; for found in matches { if found.option < 0 { positional += 1; } } for i in scope.start..scope.start + scope.len { if rules[i].kind != ScriptArgKind.Positional { continue; } capacity += if rules[i].repeat == RepeatMode.Multiple { positional } else { 1 }; } if positional > capacity { return DeclarationResult { DeclarationState.Error, save("unknown subcommand") }; } command_help(id, rules, commands, out); return DeclarationResult { DeclarationState.Stop }; } let segment = Values { args.start + offset, boundary }; segments.push(segment); if next < 0 { break; } offset += boundary + 1; id = next; } for index, depth in selected { let parsed = parse_flat(commands[index].body, commands[index].name, segments[depth], result, out); if parsed.state != DeclarationState.Run { return parsed; } } // The selected scopes already contain the path. Exclude the root program. let start = values.len; for i in 1..selected.len { values.push(plain(commands[selected[i]].name)); } result.push(ParsedBinding { save(SUBCOMMAND_VAR), value_end(start) }); return DeclarationResult { DeclarationState.Run }; } fn script_program(path: u8[:]) -> Span { var begin = 0; for c, i in path { if c == '/' { begin = i + 1; } } return save(path[begin..]); } fn source_declaration(source: u8[:], path: Span = Span { 0, 0 }) -> i64, Span { let root = parse(source, 1, path); if parse_error.len > 0 { return -1, parse_error; } var found = -1; var cur = root; while cur >= 0 { let n = nodes[cur]; let entry = if n.kind == Kind.Sequence { n.a } else { cur }; if entry >= 0 && nodes[entry].kind == Kind.Args { if found >= 0 { return -1, save("script contains multiple argument blocks"); } found = nodes[entry].a; } cur = if n.kind == Kind.Sequence { n.b } else { -1 }; } return found, Span { 0, 0 }; } fn metadata_requested(args: Values) -> bool { for i in args.len { let word = bytes(args, i); if word == "--" { return false; } if word == "--help" || word == "--version" || word == "--completions" { return true; } if starts_with(word, "--help=") || starts_with(word, "--version=") || starts_with(word, "--completions=") { return true; } if starts_with(word, "-") && !starts_with(word, "--") && contains(word, 'h') { return true; } } return false; } fn file_metadata(source: u8[:], program: Span, args: Values, out: u8[>..]&, path: Span = Span { 0, 0 }) -> DeclarationResult { // Flat scripts keep their execution path unless metadata is necessary. if !metadata_requested(args) && find(source, "command") < 0 { return DeclarationResult { DeclarationState.Run }; } let body, failure = source_declaration(source, path); if failure.len > 0 { return DeclarationResult { DeclarationState.Error, failure }; } if body < 0 { for i in args.len { let word = bytes(args, i); if word == "--" { break; } if word == "--completions" || starts_with(word, "--completions=") { return DeclarationResult { DeclarationState.Error, save("script has no top-level argument block") }; } } return DeclarationResult { DeclarationState.Run }; } var parsed: ParsedBinding[>..] = []; return parse_declaration(body, program, args, parsed, out); } fn completions_from_source(source: u8[:], program: Span, target: u8[:], out: u8[>..]&, path: Span = Span { 0, 0 }) -> Span { let body, failure = source_declaration(source, path); if failure.len > 0 { return failure; } if body < 0 { return save("script has no top-level argument block"); } var rules: ScriptRule[>..] = []; var commands: ScriptCommand[>..] = []; let schema_error = read_commands(body, program, -1, rules, commands); if schema_error.len > 0 { return schema_error; } if commands.len > 1 { return command_completions(commands, rules, target, out); } let info = commands[0].info; var choices: (const u8[:])[>..] = []; var cli: CliRule[>..] = []; make_cli(rules, info, choices, cli); return declared_completions(info, cli, rules, target, out); } fn command_completions(commands: ScriptCommand[:], rules: ScriptRule[:], target: u8[:], out: u8[>..]&) -> Span { let shell, valid = completion_target(target); if !valid { return save("expected fish, bash, zsh, or rc"); } var choices: (const u8[:])[>..] = []; var cli: CliRule[>..] = []; var options: CliOption[>..] = []; var positional: CliPositional[>..] = []; var tree: CliCommand[>..] = []; for scope, id in commands { let option_start = options.len; let position_start = positional.len; cli.clear(); make_cli(rules[scope.start..scope.start + scope.len], scope.info, choices, cli); for rule in cli { options.push(rule.option); } for i in scope.start..scope.start + scope.len { let rule = rules[i]; if rule.kind != ScriptArgKind.Positional { continue; } let start = choices.len; for j in rule.choices.len { choices.push(bytes(rule.choices, j)); } for child in commands { if child.parent == id { choices.push(view(child.name)); } } positional.push(CliPositional { choices[start..], if rule.repeat == RepeatMode.Multiple { PositionalMode.Multiple } else { PositionalMode.Once } }); } let start = choices.len; for child in commands { if child.parent == id { choices.push(view(child.name)); } } if choices.len > start { positional.push(CliPositional { choices[start..] }); } tree.push(CliCommand { view(scope.name), scope.parent, options[option_start..], positional[position_start..], prefix_count(scope.info, rules[scope.start..scope.start + scope.len]) }); } out.append(completion_tree(shell, view(commands[0].info.program), tree)); if shell == CompletionShell.Rc { out.append("fn "); out.append(completion_name(view(commands[0].info.program))); out.append("_tree {args {\n"); write_command(0, commands, rules, out); out.append("}}\n"); } return Span { 0, 0 }; } recursive fn write_command(id: i64, commands: ScriptCommand[:], rules: ScriptRule[:], out: u8[>..]&) { let scope = commands[id]; if scope.parent < 0 { out.append("name "); shell_quote(out, view(scope.info.program), CompletionShell.Rc); out.push('\n'); } if scope.info.version.len > 0 { out.append("version "); shell_quote(out, view(scope.info.version), CompletionShell.Rc); out.push('\n'); } if scope.info.tail == TailMode.Forward { out.append("allow-unknown\n"); } for i in scope.start..scope.start + scope.len { let rule = rules[i]; out.append(if rule.kind == ScriptArgKind.Flag { "flag " } else if rule.kind == ScriptArgKind.Option { "option " } else { "positional " }); out.append(view(rule.name)); if rule.long.len > 0 { out.append(" --"); out.append(view(rule.long)); } if rule.short.len > 0 { out.append(" -"); out.append(view(rule.short)); } if rule.repeat == RepeatMode.Multiple { out.append(if rule.kind == ScriptArgKind.Positional { " multiple" } else { " repeat" }); } if rule.value_type == ValueType.Integer { out.append(" int"); } if rule.required == Requirement.Required { out.append(" required"); } if rule.choices.len > 0 { out.append(" ("); for j in rule.choices.len { shell_quote(out, bytes(rule.choices, j), CompletionShell.Rc); out.push(' '); } out.push(')'); } out.push('\n'); } for child, index in commands { if child.parent != id { continue; } out.append("command "); shell_quote(out, view(child.name), CompletionShell.Rc); out.append(" {\n"); write_command(index, commands, rules, out); out.append("}\n"); } }