1 // This file defines how to compile CLI argument parsing
3 #include "../stdlib/cli.h"
4 #include "../environment.h"
5 #include "../stdlib/datatypes.h"
6 #include "../stdlib/optionals.h"
7 #include "../stdlib/text.h"
8 #include "../stdlib/util.h"
9 #include "../typecheck.h"
11 #include "compilation.h"
13 static Text_t get_flag_options(type_t *t, Text_t separator) {
14 if (t->tag == BoolType) {
15 return Text("yes|no");
16 } else if (t->tag == PathType) {
18 } else if (t->tag == EnumType) {
19 Text_t options = EMPTY_TEXT;
20 for (tag_t *tag = Match(t, EnumType)->tags; tag; tag = tag->next) {
21 if (Match(tag->type, StructType)->fields)
22 options = Texts(options, tag->name, " ", get_flag_options(tag->type, separator));
23 else options = Texts(options, tag->name);
24 if (tag->next) options = Texts(options, separator);
27 } else if (t->tag == StructType) {
28 Text_t options = EMPTY_TEXT;
29 for (arg_t *field = Match(t, StructType)->fields; field; field = field->next) {
30 options = Texts(options, get_flag_options(field->type, separator));
31 if (field->next) options = Texts(options, " ");
34 } else if (is_numeric_type(t)) {
36 } else if (t->tag == TextType || t->tag == CStringType) {
38 } else if (t->tag == ListType) {
39 Text_t item_option = get_flag_options(Match(t, ListType)->item_type, separator);
40 return Texts(item_option, "1 ", item_option, "2...");
41 } else if (t->tag == TableType && Match(t, TableType)->value_type == PRESENT_TYPE) {
42 Text_t item_option = get_flag_options(Match(t, TableType)->key_type, separator);
43 return Texts(item_option, "1 ", item_option, "2...");
44 } else if (t->tag == TableType) {
45 Text_t key_option = get_flag_options(Match(t, TableType)->key_type, separator);
46 Text_t value_option = get_flag_options(Match(t, TableType)->value_type, separator);
47 return Texts(key_option, "1:", value_option, "1 ", key_option, "2:", value_option, "2...");
53 static OptionalText_t flagify(const char *name, bool prefix) {
54 if (!name) return NONE_TEXT;
55 Text_t flag = Text$from_str(name);
56 flag = Text$replace(flag, Text("_"), Text("-"));
57 if (prefix) flag = flag.length == 1 ? Texts("-", flag) : Texts("--", flag);
61 static Text_t generate_usage(env_t *env, type_t *fn_type) {
62 DeclareMatch(fn_info, fn_type, FunctionType);
63 bool explicit_help_flag = false;
64 for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
65 if (streq(arg->name, "help")) {
66 explicit_help_flag = true;
70 env_t *main_env = fresh_scope(env);
71 Text_t usage = explicit_help_flag ? EMPTY_TEXT : Text(" [\x1b[1m--help\x1b[m]");
72 for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
73 usage = Texts(usage, " ");
74 type_t *t = get_arg_type(main_env, arg);
75 OptionalText_t flag = flagify(arg->name, arg->default_val != NULL);
76 assert(flag.tag != TEXT_NONE);
77 Text_t flags = Texts("\x1b[1m", flag, "\x1b[m");
78 if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
79 flags = Texts(flags, "|\x1b[1m--no-", Text$without_prefix(flag, Text("--")), "\x1b[m");
80 if (arg->default_val || value_type(t)->tag == BoolType) {
81 if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
82 usage = Texts(usage, "[", flags, "]");
83 else if (t->tag == ListType) usage = Texts(usage, "[", flags, " ", get_flag_options(t, Text("|")), "]");
84 else if (t->tag == EnumType) usage = Texts(usage, "[", flags, " val]");
85 else usage = Texts(usage, "[", flags, " ", get_flag_options(t, Text("|")), "]");
87 usage = Texts(usage, "\x1b[1m", flag, "\x1b[m");
93 static Text_t generate_help(env_t *env, type_t *fn_type) {
94 DeclareMatch(fn_info, fn_type, FunctionType);
95 env_t *main_env = fresh_scope(env);
96 Text_t help_text = EMPTY_TEXT;
98 for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
99 help_text = Texts(help_text, "\n");
100 type_t *t = get_arg_type(main_env, arg);
101 OptionalText_t flag = flagify(arg->name, true);
102 assert(flag.tag != TEXT_NONE);
103 OptionalText_t alias_flag = flagify(arg->alias, true);
104 Text_t flags = Texts("\x1b[33;1m", flag, "\x1b[m");
105 if (alias_flag.tag != TEXT_NONE) flags = Texts("\x1b[33;1m", alias_flag, "\x1b[0;2m,\x1b[m ", flags);
106 if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
107 flags = Texts(flags, "|\x1b[33;1m--no-", Text$without_prefix(flag, Text("--")), "\x1b[m");
108 if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType))
109 help_text = Texts(help_text, " ", flags);
111 help_text = Texts(help_text, " ", flags, " \x1b[1;34m", get_flag_options(t, Text("\x1b[m | \x1b[1;34m")),
114 if (arg->comment.length > 0) help_text = Texts(help_text, " \x1b[3m", arg->comment, "\x1b[m");
115 if (arg->default_val) {
116 Text_t default_text =
117 Text$from_strn(arg->default_val->start, (size_t)(arg->default_val->end - arg->default_val->start));
118 help_text = Texts(help_text, " \x1b[2m(default:", default_text, ")\x1b[m");
125 Text_t compile_cli_arg_call(env_t *env, ast_t *ast, Text_t fn_name, type_t *fn_type) {
126 DeclareMatch(fn_info, fn_type, FunctionType);
128 Text_t code = EMPTY_TEXT;
129 OptionalText_t usage = ast_metadata(ast, "USAGE");
130 if (usage.tag == TEXT_NONE) usage = generate_usage(env, fn_type);
132 OptionalText_t help = ast_metadata(ast, "HELP");
133 if (help.tag == TEXT_NONE) help = generate_help(env, fn_type);
136 "Text_t usage = Texts(Text(\"\\x1b[1mUsage:\\x1b[m \"), "
137 "Text$from_str(argv[0]), Text(\" \")",
138 usage.length == 0 ? EMPTY_TEXT : Texts(", Text(", quoted_text(usage), ")"), ");\n",
139 "Text_t help = Texts(usage, Text(\"\\n\"", quoted_text(help), "\"\\n\"));\n");
141 for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
142 code = Texts(code, compile_declaration(arg->type, Texts("_$", Text$from_str(arg->name))), " = ",
143 compile_empty(arg->type), ";\n");
146 OptionalText_t version = ast_metadata(ast, "VERSION");
147 if (version.tag == TEXT_NONE) version = Text("0.0.1");
148 Text_t version_code = Text$quoted(version, false, Text("\""));
149 code = Texts(code, "cli_arg_t cli_args[] = {\n");
150 for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
151 code = Texts(code, "{", quoted_text(Text$replace(Text$from_str(arg->name), Text("_"), Text("-"))), ", &",
152 Texts("_$", Text$from_str(arg->name)), ", ", compile_type_info(arg->type),
153 arg->default_val ? Text("") : Text(", .required=true"),
154 arg->alias ? Texts(", .short_flag=", quoted_text(Text$from_str(arg->alias)),
155 "[0]") // TODO: escape char properly
160 code = Texts(code, "};\n");
161 code = Texts(code, "tomo_parse_args(argc, argv, usage, help, ", version_code,
162 ", sizeof(cli_args)/sizeof(cli_args[0]), cli_args);\n");
164 // Lazily initialize default values to prevent side effects
166 for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
167 if (arg->default_val) {
170 default_val = compile_to_type(env, arg->default_val, arg->type);
172 default_val = compile(env, arg->default_val);
174 code = Texts(code, "if (!cli_args[", i, "].populated) ", Texts("_$", Text$from_str(arg->name)), " = ",
180 code = Texts(code, fn_name, "(");
181 for (arg_t *arg = fn_info->args; arg; arg = arg->next) {
182 code = Texts(code, Texts("_$", arg->name));
183 if (arg->next) code = Texts(code, ", ");
185 code = Texts(code, ");\n");
190 Text_t compile_manpage(Text_t program, ast_t *ast, arg_t *args) {
191 OptionalText_t user_manpage = ast_metadata(ast, "MANPAGE");
192 if (user_manpage.tag != TEXT_NONE) {
196 OptionalText_t synopsys = ast_metadata(ast, "MANPAGE_SYNOPSYS");
197 OptionalText_t description = ast_metadata(ast, "MANPAGE_DESCRIPTION");
198 Text_t date = Text(""); // TODO: use date
199 Text_t man = Texts(".\\\" Automatically generated by Tomo\n"
201 Text$upper(program, Text("C")), "\" \"1\" \"", date,
204 program, " \\- ", synopsys.tag == TEXT_NONE ? Text("a Tomo program") : synopsys, "\n");
206 if (description.tag != TEXT_NONE) {
207 man = Texts(man, ".SH DESCRIPTION\n", description, "\n");
210 man = Texts(man, ".SH OPTIONS\n");
211 for (arg_t *arg = args; arg; arg = arg->next) {
212 OptionalText_t flag = flagify(arg->name, true);
213 assert(flag.tag != TEXT_NONE);
214 Text_t flags = Texts("\\f[B]", flag, "\\f[R]");
215 if (arg->alias) flags = Texts(flags, ", \\f[B]", flagify(arg->alias, true), "\\f[R]");
216 if (non_optional(arg->type)->tag == BoolType)
217 flags = Texts(flags, " | \\f[B]--no-", Text$without_prefix(flag, Text("--")), "\\f[R]");
219 man = Texts(man, "\n.TP\n", flags);
220 if (non_optional(arg->type)->tag != BoolType) {
221 Text_t options = Texts("\\f[I]", get_flag_options(arg->type, Text("\\f[R] | \\f[I]")), "\\f[R]");
222 man = Texts(man, " ", options);
225 if (arg->comment.length > 0) {
226 man = Texts(man, "\n", arg->comment);