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");
}
}