123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 | // This file defines how to compile CLI argument parsing
#include <gc.h>
#include <string.h>
#include "../environment.h"
#include "../naming.h"
#include "../stdlib/datatypes.h"
#include "../stdlib/text.h"
#include "../types.h"
#include "../util.h"
#include "compilation.h"
static Text_t get_flag_options(type_t *t, Text_t separator) {
if (t->tag == BoolType) {
return Text("yes|no");
} else if (t->tag == PathType) {
return Text("path");
} else if (t->tag == EnumType) {
Text_t options = EMPTY_TEXT;
for (tag_t *tag = Match(t, EnumType)->tags; tag; tag = tag->next) {
if (Match(tag->type, StructType)->fields)
options = Texts(options, tag->name, " ", get_flag_options(tag->type, separator));
else options = Texts(options, tag->name);
if (tag->next) options = Texts(options, separator);
}
return options;
} else if (t->tag == StructType) {
Text_t options = EMPTY_TEXT;
for (arg_t *field = Match(t, StructType)->fields; field; field = field->next) {
options = Texts(options, get_flag_options(field->type, separator));
if (field->next) options = Texts(options, " ");
}
return options;
} else if (is_numeric_type(t)) {
return Text("N");
} else if (t->tag == TextType || t->tag == CStringType) {
return Text("text");
} else if (t->tag == ListType) {
Text_t item_option = get_flag_options(Match(t, ListType)->item_type, separator);
return Texts(item_option, "1 ", item_option, "2...");
} else if (t->tag == TableType && Match(t, TableType)->value_type == PRESENT_TYPE) {
Text_t item_option = get_flag_options(Match(t, TableType)->key_type, separator);
return Texts(item_option, "1 ", item_option, "2...");
} else if (t->tag == TableType) {
Text_t key_option = get_flag_options(Match(t, TableType)->key_type, separator);
Text_t value_option = get_flag_options(Match(t, TableType)->value_type, separator);
return Texts(key_option, "1:", value_option, "1 ", key_option, "2:", value_option, "2...");
} else {
return Text("value");
}
}
// An argument's flag as the runtime actually accepts it. The dashes follow the
// role, not the length: `arg->name` is registered as `.name` (always a long
// `--flag`, even when it is one letter) and `arg->alias` as `.short_flag`
// (always a single-letter `-f`). Deriving them from the length instead
// documented `-x` for `func main.add(x:Int)`, which the parser rejects.
static OptionalText_t flagify(const char *name, bool is_short) {
if (!name) return NONE_TEXT;
Text_t flag = Text$from_str(name);
flag = Text$replace(flag, Text("_"), Text("-"));
return is_short ? Texts("-", flag) : Texts("--", flag);
}
// Emit a compile-time Text_t value for `text`. The `Text(...)` macro is
// ASCII-only (it stores the literal's *byte* count as a grapheme count and
// tags it TEXT_ASCII), so anything that can contain non-ASCII, whether Unicode
// command names, doc comments, or user-supplied USAGE/HELP metadata, has to go
// through Text$from_str(), which decodes the UTF-8, or text operations on it
// crash.
static Text_t text_literal(Text_t text) {
return Texts("Text$from_str(", compile_text_literal(text), ")");
}
// A subcommand's CLI word: the identifier with underscores converted to dashes
static const char *cli_word(const char *part, size_t len) {
char *word = GC_MALLOC_ATOMIC(len + 1);
for (size_t i = 0; i < len; i++)
word[i] = part[i] == '_' ? '-' : part[i];
word[len] = '\0';
return word;
}
static cli_command_def_t *get_child(cli_command_def_t **children, const char *word) {
cli_command_def_t **p = children;
while (*p && !streq((*p)->word, word))
p = &(*p)->next;
if (!*p) *p = new (cli_command_def_t, .word = word);
return *p;
}
// Collect a program's `func main.foo.baz(...)` definitions into a subcommand
// tree (or NULL if it defines no subcommands). A `use`d file's subcommands are
// part of the program's interface, since `use` merges its bindings into this
// scope (unless the import is given a name like `foo := use ./foo.tm).
public
cli_command_def_t *get_cli_subcommands(env_t *env) {
cli_command_def_t *commands = NULL;
// `use` statements are bound before the file's own definitions, so this is
// the order `--help` lists the commands in:
List_t entries = env->locals->entries;
for (int64_t i = 0; i < (int64_t)entries.length; i++) {
struct {
const char *name;
binding_t *binding;
} *entry = entries.data + entries.stride * i;
if (strncmp(entry->name, "main.", strlen("main.")) != 0) continue;
binding_t *binding = entry->binding;
if (binding->type->tag != FunctionType || !binding->fn_def) continue;
// Walk the dotted path, creating the nodes along the way:
cli_command_def_t **level = &commands, *cmd = NULL;
for (const char *p = entry->name + strlen("main."); p;) {
const char *dot = strchr(p, '.');
cmd = get_child(level, cli_word(p, dot ? (size_t)(dot - p) : strlen(p)));
level = &cmd->children;
p = dot ? dot + 1 : NULL;
}
assert(!cmd->binding);
cmd->binding = binding;
}
return commands;
}
// Emit the storage, argument spec, and handler function for one command, plus
// (recursively) its children, and return the name of its cli_command_t. The
// runtime (src/stdlib/cli.c) does all the dispatching and help rendering from
// these, the same code path the `tomo` CLI itself goes through.
static Text_t compile_command_spec(env_t *env, cli_command_def_t *cmd, Text_t c_path, Text_t *defs, Text_t *inits) {
// Children first: a parent's child list refers to their cli_command_t's.
Text_t child_names = EMPTY_TEXT;
int num_children = 0;
for (cli_command_def_t *child = cmd->children; child; child = child->next) {
Text_t child_path = Texts(c_path, "$", Text$replace(Text$from_str(child->word), Text("-"), Text("_")));
Text_t child_name = compile_command_spec(env, child, child_path, defs, inits);
child_names = Texts(child_names, num_children > 0 ? Text(", ") : EMPTY_TEXT, "&", child_name);
num_children += 1;
}
if (num_children > 0)
*defs = Texts(*defs, "static cli_command_t *cli_children$", c_path, "[] = {", child_names, "};\n");
arg_t *args = cmd->binding ? Match(cmd->binding->type, FunctionType)->args : NULL;
int num_args = 0;
if (cmd->binding) {
// One static per argument for the parser to fill in, reset to the
// type's empty value before parsing (statics start zeroed, which isn't
// every type's empty value):
for (arg_t *arg = args; arg; arg = arg->next) {
Text_t storage = Texts("cli_arg$", c_path, "$", Text$from_str(arg->name));
*defs = Texts(*defs, "static ", compile_declaration(arg->type, storage), ";\n");
*inits = Texts(*inits, storage, " = ", compile_empty(arg->type), ";\n");
}
*defs = Texts(*defs, "static cli_arg_t cli_spec$", c_path, "[] = {\n");
for (arg_t *arg = args; arg; arg = arg->next) {
*defs = Texts(
*defs, "{", quoted_text(Text$replace(Text$from_str(arg->name), Text("_"), Text("-"))), ", &cli_arg$",
c_path, "$", Text$from_str(arg->name), ", ", compile_type_info(arg->type),
arg->default_val ? EMPTY_TEXT : Text(", .required=true"),
arg->alias ? Texts(", .short_flag=", quoted_text(Text$from_str(arg->alias)),
"[0]") // TODO: escape char properly
: EMPTY_TEXT,
arg->comment.length > 0 ? Texts(", .description=", compile_text_literal(arg->comment)) : EMPTY_TEXT,
arg->default_val
? Texts(", .default_text=",
compile_text_literal(Text$from_strn(
arg->default_val->start, (size_t)(arg->default_val->end - arg->default_val->start))))
: EMPTY_TEXT,
"},\n");
num_args += 1;
}
*defs = Texts(*defs, "};\n");
// The handler applies default values (lazily, so their side effects
// only happen when the argument wasn't given), calls the function, then
// prints what it returned (if anything):
*defs = Texts(*defs, "static int cli_handler$", c_path,
"(cli_command_t *self, List_t extra_args) {\n"
"(void)self, (void)extra_args;\n");
int64_t i = 0;
for (arg_t *arg = args; arg; arg = arg->next) {
if (arg->default_val) {
Text_t default_val =
arg->type ? compile_to_type(env, arg->default_val, arg->type) : compile(env, arg->default_val);
*defs = Texts(*defs, "if (!cli_spec$", c_path, "[", i, "].populated) cli_arg$", c_path, "$",
Text$from_str(arg->name), " = ", default_val, ";\n");
}
i += 1;
}
Text_t call = Texts(cmd->binding->code, "(");
for (arg_t *arg = args; arg; arg = arg->next)
call = Texts(call, "cli_arg$", c_path, "$", Text$from_str(arg->name), arg->next ? Text(", ") : EMPTY_TEXT);
call = Texts(call, ")");
// If a value is returned, print it (optionally with syntax highlighting):
type_t *ret = Match(cmd->binding->type, FunctionType)->ret;
if (ret->tag == VoidType || ret->tag == AbortType) *defs = Texts(*defs, call, ";\n");
else *defs = Texts(*defs, compile_value_print(ret, call));
*defs = Texts(*defs, "return 0;\n}\n");
}
Text_t summary = cmd->binding ? Match(cmd->binding->fn_def, FunctionDef)->comment : EMPTY_TEXT;
Text_t command_name = Texts("cli_command$", c_path);
*defs = Texts(*defs, "static cli_command_t ", command_name, " = {",
cmd->word ? Texts(".name=", quoted_text(Text$from_str(cmd->word)), ", ") : EMPTY_TEXT,
summary.length > 0 ? Texts(".summary=", compile_text_literal(summary), ", ") : EMPTY_TEXT,
cmd->binding ? Texts(".spec_len=", num_args, ", .spec=cli_spec$", c_path, ", .handler=cli_handler$",
c_path, ", ")
: EMPTY_TEXT,
num_children > 0 ? Texts(".num_children=", (int64_t)num_children, ", .children=cli_children$", c_path)
: EMPTY_TEXT,
"};\n");
return command_name;
}
// Compile a program's whole command-line interface to a static cli_spec_t that
// `tomo_dispatch_command()` (src/stdlib/cli.c) drives, the same as the `tomo`
// CLI's own. If the file defines a plain `main()`, it becomes the root
// command's handler: it runs when the first argument doesn't name a
// subcommand, exactly like `main.stash` runs when the word after it isn't
// `pop`.
public
Text_t compile_cli_dispatch(env_t *env, ast_t *file_ast, cli_command_def_t *commands, Text_t entry) {
cli_command_def_t root = {.children = commands};
binding_t *main_binding = get_binding(env, "main");
if (main_binding && main_binding->type->tag == FunctionType && main_binding->fn_def) root.binding = main_binding;
Text_t defs = EMPTY_TEXT, inits = EMPTY_TEXT;
Text_t root_name = compile_command_spec(env, &root, Text("main"), &defs, &inits);
OptionalText_t version = ast_metadata(file_ast, "VERSION");
if (version.tag == TEXT_NONE) version = Text("0.0.1");
// -v is the long-standing short alias for --version in generated programs
// (the `tomo` CLI itself is long-only, since -v there means --verbose):
defs = Texts(defs, "static cli_spec_t cli_spec$program = {.version=", Text$quoted(version, false, Text("\"")),
", .version_short='v'};\n");
// USAGE/HELP metadata overrides the autogenerated text for the program as
// a whole (Text_t values can't be static initializers, so they're assigned
// at startup along with the rest of the spec):
OptionalText_t usage = ast_metadata(file_ast, "USAGE");
OptionalText_t help = ast_metadata(file_ast, "HELP");
inits = Texts(inits, "cli_spec$program.root = ", root_name, ";\n");
if (usage.tag != TEXT_NONE)
// Built at startup so it picks up the palette (which depends on whether
// the output is colored), same as the autogenerated usage text:
inits = Texts(inits,
"cli_spec$program.root.usage = Texts(tomo_cli_style().usage, \"Usage:\", "
"tomo_cli_style().reset, \" \", Text$from_str(argv[0]), Text(\" \"), ",
text_literal(usage), ");\n");
if (help.tag != TEXT_NONE) inits = Texts(inits, "cli_spec$program.root.help = ", text_literal(help), ";\n");
return Texts(defs, "int parse_and_run$$", entry,
"(int argc, char *argv[]) {\n"
"tomo_init();\n",
namespace_name(env, env->namespace, Text("$initialize")), "();\n\n", inits,
"return tomo_dispatch_command(argc, argv, &cli_spec$program);\n"
"}\n");
}
// The .TP option entries for one argument spec:
static Text_t manpage_options(arg_t *args) {
Text_t man = EMPTY_TEXT;
for (arg_t *arg = args; arg; arg = arg->next) {
OptionalText_t flag = flagify(arg->name, /*is_short=*/false);
assert(flag.tag != TEXT_NONE);
Text_t flags = Texts("\\f[B]", flag, "\\f[R]");
if (arg->alias) flags = Texts(flags, ", \\f[B]", flagify(arg->alias, /*is_short=*/true), "\\f[R]");
if (non_optional(arg->type)->tag == BoolType)
flags = Texts(flags, " | \\f[B]--no-", Text$without_prefix(flag, Text("--")), "\\f[R]");
man = Texts(man, "\n.TP\n", flags);
if (non_optional(arg->type)->tag != BoolType) {
Text_t options = Texts("\\f[I]", get_flag_options(arg->type, Text("\\f[R] | \\f[I]")), "\\f[R]");
man = Texts(man, " ", options);
}
if (arg->comment.length > 0) {
man = Texts(man, "\n", arg->comment);
}
}
return man;
}
// The .SS sections documenting each subcommand (and sub-subcommand):
static Text_t manpage_commands(Text_t program, cli_command_def_t *commands, Text_t path) {
Text_t man = EMPTY_TEXT;
for (cli_command_def_t *c = commands; c; c = c->next) {
Text_t subpath = path.length > 0 ? Texts(path, " ", c->word) : Text$from_str(c->word);
if (c->binding) {
assert(c->binding->fn_def);
man = Texts(man, ".SS \"", program, " ", subpath, "\"\n");
Text_t comment = Match(c->binding->fn_def, FunctionDef)->comment;
if (comment.length > 0) man = Texts(man, comment, "\n");
man = Texts(man, manpage_options(Match(c->binding->type, FunctionType)->args), "\n");
}
man = Texts(man, manpage_commands(program, c->children, subpath));
}
return man;
}
public
Text_t compile_manpage(Text_t program, ast_t *ast, arg_t *args, cli_command_def_t *commands) {
OptionalText_t user_manpage = ast_metadata(ast, "MANPAGE");
if (user_manpage.tag != TEXT_NONE) {
// Still stamp the marker so `tomo uninstall` recognizes it as ours:
return Texts(Text(TOMO_MANPAGE_MARKER "\n"), user_manpage);
}
OptionalText_t synopsys = ast_metadata(ast, "MANPAGE_SYNOPSYS");
OptionalText_t description = ast_metadata(ast, "MANPAGE_DESCRIPTION");
Text_t date = Text(""); // TODO: use date
Text_t man = Texts(TOMO_MANPAGE_MARKER "\n"
".TH \"",
Text$upper(program, Text("C")), "\" \"1\" \"", date,
"\" \"\" \"\"\n"
".SH NAME\n",
program, " \\- ", synopsys.tag == TEXT_NONE ? Text("a Tomo program") : synopsys, "\n");
if (description.tag != TEXT_NONE) {
man = Texts(man, ".SH DESCRIPTION\n", description, "\n");
}
// A program can have both: `main()`'s own flags, plus subcommands.
// manpage_options() doesn't end in a newline, so add one before any
// section that follows it:
if (args) man = Texts(man, ".SH OPTIONS\n", manpage_options(args), "\n");
if (commands) man = Texts(man, ".SH COMMANDS\n", manpage_commands(program, commands, EMPTY_TEXT));
return man;
}
|