diff options
| author | Bruce Hill <bruce@bruce-hill.com> | 2025-08-31 12:51:47 -0400 |
|---|---|---|
| committer | Bruce Hill <bruce@bruce-hill.com> | 2025-08-31 12:51:47 -0400 |
| commit | 07c5287760ca7a80c06dd80defe007ad009695fc (patch) | |
| tree | da8145974f100c7c8f96fbf73170d9991a1f75cf | |
| parent | c3bcb504a07823ec8a096d34d0f4fe8dc5b27196 (diff) | |
Add argument aliases so programs can use `func main(verbose|v=no)` to do
single-letter argument flags.
| -rw-r--r-- | docs/command-line-parsing.md | 58 | ||||
| -rw-r--r-- | src/ast.h | 2 | ||||
| -rw-r--r-- | src/compile/cli.c | 47 | ||||
| -rw-r--r-- | src/compile/functions.c | 46 | ||||
| -rw-r--r-- | src/parse/functions.c | 20 | ||||
| -rw-r--r-- | src/tomo.c | 12 | ||||
| -rw-r--r-- | src/typecheck.c | 20 | ||||
| -rw-r--r-- | src/types.h | 2 |
8 files changed, 137 insertions, 70 deletions
diff --git a/docs/command-line-parsing.md b/docs/command-line-parsing.md index 2c1af0e6..7acfe95c 100644 --- a/docs/command-line-parsing.md +++ b/docs/command-line-parsing.md @@ -5,7 +5,7 @@ Here's a simple example: ```tomo # greet.tm -func main(name:Text, be_excited=no) +func main(name:Text, be_excited|E:Bool=no) if be_excited say("Hello $name!!!") else @@ -16,30 +16,45 @@ This program will automatically support command line argument parsing for the arguments to `main()`: ```bash -$ tomo greet.tm +$ tomo -e greet.tm +Compiled executable: greet + +$ ./greet greet: Required argument 'name' was not provided! -Signature: greet [--help] <name> [--be-excited] +Signature: greet [--help] <name> [--be-excited|-E|--no-be-exited] -$ tomo greet.tm --help -Signature: greet [--help] <name> [--be-excited] +$ ./greet --help +Signature: greet [--help] <name> [--be-excited|-E|--no-be-excited] -$ tomo greet.tm "Zaphod" +$ ./greet "Zaphod" Hi Zaphod. -$ tomo greet.tm "Zaphod" --be-excited +$ ./greet "Zaphod" --be-excited +Hello Zaphod!!! + +$ ./greet "Zaphod" -E Hello Zaphod!!! -$ tomo greet.tm --no-be-excited --name="Zaphod" +$ ./greet --no-be-excited --name="Zaphod" Hi Zaphod. -$ tomo greet.tm --not-a-real-argument "Bob" +$ ./greet --not-a-real-argument "Bob" greet: Unrecognized argument: --not-a-real-argument -Signature: greet [--help] <name> [--be-excited] +Signature: greet [--help] <name> [--be-excited|-E|--no-be-excited] ``` Underscores in argument names are converted to dashes when parsing command line arguments. +## Running Programs Directly + +If you want to run a program directly (instead of compiling to an executable +with `tomo -e`), you can run the program with `tomo program.tm -- [program +arguments...]`. The `--` is required to separate the arguments passed to the +Tomo compiler from those being passed to your program. For example, `tomo +greet.tm -- --help` will pass the argument `--help` to your program, whereas +`tomo greet.tm --help` will pass `--help` to `tomo`. + ## Positional vs Default Arguments Any arguments with a default value must be specified with a `--flag=value` or @@ -121,3 +136,26 @@ $ tomo many-texts.tm --args=one,two,three $ tomo many-texts.tm -- one --not-a-flag 'a space' >> ["one", "--not-a-flag", "a space"] : [Text] ``` + +## Aliases and Flag Arguments + +Each argument may optionally have an alias of the form `name|alias`. This allows +you to specify a long-form argument and a single-letter flag like `verbose|v = +no`. Single letter flags (whether as an alias or as a main flag name) have +slightly different command line parsing rules: + +- Single letter flags use only a single dash: `-v` vs `--verbose` +- Single letter flags can coalesce with other single letter flags: `-abc` is the +same as `-a -b -c` + +When single letter flags coalesce together, the first flags in the cluster must +be boolean values, while the last one is allowed to be any type. This lets you +specify several flags at once while still providing arguments: + +```tomo +func main(output|o:Path? = none, verbose|v:Bool = no) + ... +``` +```bash +$ tomo -e program.tm && ./program -vo outfile.txt` +``` @@ -65,7 +65,7 @@ typedef struct ast_list_s { } ast_list_t; typedef struct arg_ast_s { - const char *name; + const char *name, *alias; type_ast_t *type; ast_t *value; struct arg_ast_s *next; diff --git a/src/compile/cli.c b/src/compile/cli.c index 4a3be333..b082239d 100644 --- a/src/compile/cli.c +++ b/src/compile/cli.c @@ -2,6 +2,7 @@ #include "../environment.h" #include "../stdlib/datatypes.h" +#include "../stdlib/optionals.h" #include "../stdlib/text.h" #include "../stdlib/util.h" #include "../typecheck.h" @@ -25,6 +26,14 @@ static Text_t get_flag_options(type_t *t, const char *separator) { } } +static OptionalText_t flagify(const char *name, bool prefix) { + if (!name) return NONE_TEXT; + Text_t flag = Text$from_str(name); + flag = Text$replace(flag, Text("_"), Text("-")); + if (prefix) flag = flag.length == 1 ? Texts("-", flag) : Texts("--", flag); + return flag; +} + public Text_t compile_cli_arg_call(env_t *env, Text_t fn_name, type_t *fn_type, const char *version) { DeclareMatch(fn_info, fn_type, FunctionType); @@ -49,20 +58,22 @@ Text_t compile_cli_arg_call(env_t *env, Text_t fn_name, type_t *fn_type, const c for (arg_t *arg = fn_info->args; arg; arg = arg->next) { usage = Texts(usage, " "); type_t *t = get_arg_type(main_env, arg); - Text_t flag = Text$replace(Text$from_str(arg->name), Text("_"), Text("-")); if (arg->default_val || arg->type->tag == OptionalType) { - if (strlen(arg->name) == 1) { - if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType)) - usage = Texts(usage, "[-", flag, "]"); - else usage = Texts(usage, "[-", flag, " ", get_flag_options(t, "|"), "]"); - } else { - if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType)) - usage = Texts(usage, "[--", flag, "]"); - else if (t->tag == ListType) usage = Texts(usage, "[--", flag, " ", get_flag_options(t, "|"), "]"); - else usage = Texts(usage, "[--", flag, "=", get_flag_options(t, "|"), "]"); - } + OptionalText_t flag = flagify(arg->name, true); + assert(flag.length >= 0); + OptionalText_t alias_flag = flagify(arg->alias, true); + Text_t flags = alias_flag.length >= 0 ? Texts(flag, "|", alias_flag) : flag; + if (t->tag == BoolType || (t->tag == OptionalType && Match(t, OptionalType)->type->tag == BoolType)) + usage = Texts(usage, "[", flags, "]"); + else if (t->tag == ListType) usage = Texts(usage, "[", flags, " ", get_flag_options(t, "|"), "]"); + else usage = Texts(usage, "[", flags, "=", get_flag_options(t, "|"), "]"); } else { - if (t->tag == BoolType) usage = Texts(usage, "<--", flag, "|--no-", flag, ">"); + OptionalText_t flag = flagify(arg->name, false); + assert(flag.length >= 0); + OptionalText_t alias_flag = flagify(arg->alias, true); + if (t->tag == BoolType) + usage = Texts(usage, "<--", flag, alias_flag.length >= 0 ? Texts("|", alias_flag) : EMPTY_TEXT, + "|--no-", flag, ">"); else if (t->tag == EnumType) usage = Texts(usage, get_flag_options(t, "|")); else if (t->tag == ListType) usage = Texts(usage, "[", flag, "...]"); else usage = Texts(usage, "<", flag, ">"); @@ -76,9 +87,10 @@ Text_t compile_cli_arg_call(env_t *env, Text_t fn_name, type_t *fn_type, const c for (arg_t *arg = fn_info->args; arg; arg = arg->next) { type_t *opt_type = arg->type->tag == OptionalType ? arg->type : Type(OptionalType, .type = arg->type); - code = Texts(code, compile_declaration(opt_type, Texts("_$", arg->name))); + code = Texts(code, compile_declaration(opt_type, Texts("_$", Text$from_str(arg->name)))); if (arg->default_val) { - Text_t default_val = compile(env, arg->default_val); + Text_t default_val = + arg->type ? compile_to_type(env, arg->default_val, arg->type) : compile(env, arg->default_val); if (arg->type->tag != OptionalType) default_val = promote_to_optional(arg->type, default_val); code = Texts(code, " = ", default_val); } else { @@ -92,7 +104,12 @@ Text_t compile_cli_arg_call(env_t *env, Text_t fn_name, type_t *fn_type, const c for (arg_t *arg = fn_info->args; arg; arg = arg->next) { code = Texts(code, ",\n{", quoted_text(Text$replace(Text$from_str(arg->name), Text("_"), Text("-"))), ", ", (arg->default_val || arg->type->tag == OptionalType) ? "false" : "true", ", ", - compile_type_info(arg->type), ", &", Texts("_$", arg->name), "}"); + compile_type_info(arg->type), ", &", Texts("_$", Text$from_str(arg->name)), "}"); + if (arg->alias) { + code = Texts(code, ",\n{", quoted_text(Text$replace(Text$from_str(arg->alias), Text("_"), Text("-"))), ", ", + (arg->default_val || arg->type->tag == OptionalType) ? "false" : "true", ", ", + compile_type_info(arg->type), ", &", Texts("_$", Text$from_str(arg->name)), "}"); + } } code = Texts(code, ");\n"); diff --git a/src/compile/functions.c b/src/compile/functions.c index 665c7379..01de26e3 100644 --- a/src/compile/functions.c +++ b/src/compile/functions.c @@ -71,29 +71,31 @@ Text_t compile_arguments(env_t *env, ast_t *call_ast, arg_t *spec_args, arg_ast_ for (arg_t *spec_arg = spec_args; spec_arg; spec_arg = spec_arg->next) { int64_t i = 1; // Find keyword: - if (spec_arg->name) { - for (arg_ast_t *call_arg = call_args; call_arg; call_arg = call_arg->next) { - if (call_arg->name && streq(call_arg->name, spec_arg->name)) { - Text_t value; - if (spec_arg->type->tag == IntType && call_arg->value->tag == Int) { - value = compile_int_to_type(env, call_arg->value, spec_arg->type); - } else if (spec_arg->type->tag == NumType && call_arg->value->tag == Int) { - OptionalInt_t int_val = Int$from_str(Match(call_arg->value, Int)->str); - if (int_val.small == 0) code_err(call_arg->value, "Failed to parse this integer"); - if (Match(spec_arg->type, NumType)->bits == TYPE_NBITS64) - value = Text$from_str(String(hex_double(Num$from_int(int_val, false)))); - else value = Text$from_str(String(hex_double((double)Num32$from_int(int_val, false)), "f")); - } else { - env_t *arg_env = with_enum_scope(env, spec_arg->type); - value = compile_maybe_incref(arg_env, call_arg->value, spec_arg->type); - } - Table$str_set(&used_args, call_arg->name, call_arg); - if (code.length > 0) code = Texts(code, ", "); - code = Texts(code, value); - goto found_it; - } + assert(spec_arg->name); + for (arg_ast_t *call_arg = call_args; call_arg; call_arg = call_arg->next) { + if (!call_arg->name) continue; + if (!(streq(call_arg->name, spec_arg->name) || (spec_arg->alias && streq(call_arg->name, spec_arg->alias)))) + continue; + + Text_t value; + if (spec_arg->type->tag == IntType && call_arg->value->tag == Int) { + value = compile_int_to_type(env, call_arg->value, spec_arg->type); + } else if (spec_arg->type->tag == NumType && call_arg->value->tag == Int) { + OptionalInt_t int_val = Int$from_str(Match(call_arg->value, Int)->str); + if (int_val.small == 0) code_err(call_arg->value, "Failed to parse this integer"); + if (Match(spec_arg->type, NumType)->bits == TYPE_NBITS64) + value = Text$from_str(String(hex_double(Num$from_int(int_val, false)))); + else value = Text$from_str(String(hex_double((double)Num32$from_int(int_val, false)), "f")); + } else { + env_t *arg_env = with_enum_scope(env, spec_arg->type); + value = compile_maybe_incref(arg_env, call_arg->value, spec_arg->type); } + Table$str_set(&used_args, call_arg->name, call_arg); + if (code.length > 0) code = Texts(code, ", "); + code = Texts(code, value); + goto found_it; } + // Find positional: for (arg_ast_t *call_arg = call_args; call_arg; call_arg = call_arg->next) { if (call_arg->name) continue; @@ -161,7 +163,7 @@ Text_t compile_function_call(env_t *env, ast_t *ast) { args = new (arg_t, .name = a->name, .type = get_type(env, a->value), .next = args); REVERSE_LIST(args); code_err(ast, - "This function's public signature doesn't match this call site.\n" + "This function's signature doesn't match this call site.\n" "The signature is: ", type_to_text(fn_t), "\n" diff --git a/src/parse/functions.c b/src/parse/functions.c index 0779bb7b..37505ac5 100644 --- a/src/parse/functions.c +++ b/src/parse/functions.c @@ -13,8 +13,8 @@ #include "context.h" #include "controlflow.h" #include "errors.h" -#include "functions.h" #include "expressions.h" +#include "functions.h" #include "types.h" #include "utils.h" @@ -26,7 +26,7 @@ arg_ast_t *parse_args(parse_ctx_t *ctx, const char **pos) { type_ast_t *type = NULL; typedef struct name_list_s { - const char *name; + const char *name, *alias; struct name_list_s *next; } name_list_t; @@ -37,19 +37,26 @@ arg_ast_t *parse_args(parse_ctx_t *ctx, const char **pos) { if (!name) break; whitespace(pos); + const char *alias = NULL; + if (match(pos, "|")) { + whitespace(pos); + alias = get_id(pos); + if (!alias) parser_err(ctx, *pos, *pos, "I expected an argument alias after `|`"); + } + if (match(pos, ":")) { type = expect(ctx, *pos - 1, pos, parse_type, "I expected a type here"); - names = new (name_list_t, .name = name, .next = names); + names = new (name_list_t, .name = name, .alias = alias, .next = names); whitespace(pos); if (match(pos, "=")) default_val = expect(ctx, *pos - 1, pos, parse_term, "I expected a value after this '='"); break; } else if (strncmp(*pos, "==", 2) != 0 && match(pos, "=")) { default_val = expect(ctx, *pos - 1, pos, parse_term, "I expected a value after this '='"); - names = new (name_list_t, .name = name, .next = names); + names = new (name_list_t, .name = name, .alias = alias, .next = names); break; } else if (name) { - names = new (name_list_t, .name = name, .next = names); + names = new (name_list_t, .name = name, .alias = alias, .next = names); spaces(pos); if (!match(pos, ",")) break; } else { @@ -64,7 +71,8 @@ arg_ast_t *parse_args(parse_ctx_t *ctx, const char **pos) { REVERSE_LIST(names); for (; names; names = names->next) - args = new (arg_ast_t, .name = names->name, .type = type, .value = default_val, .next = args); + args = new (arg_ast_t, .name = names->name, .alias = names->alias, .type = type, .value = default_val, + .next = args); if (!match_separator(pos)) break; } @@ -289,7 +289,7 @@ int main(int argc, char *argv[]) { if (files.length < 1) print_err("No file specified!"); - quiet = !verbose; + if (!compile_exe && !stop_at_transpile && !stop_at_obj_compilation) quiet = !verbose; for (int64_t i = 0; i < files.length; i++) { Path_t path = *(Path_t *)(files.data + i * files.stride); @@ -404,7 +404,7 @@ void build_library(Path_t lib_dir) { int status = pclose(prog); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) exit(EXIT_FAILURE); - if (!quiet) print("Compiled library:\t", shared_lib); + if (!quiet) print("Compiled library:\t", Path$relative_to(shared_lib, Path$current_dir())); } void install_library(Path_t lib_dir) { @@ -726,7 +726,7 @@ void transpile_header(env_t *base_env, Path_t path) { Text$print(header, h_code); if (fclose(header) == -1) print_err("Failed to write header file: ", h_filename); - if (!quiet) print("Transpiled header:\t", h_filename); + if (!quiet) print("Transpiled header:\t", Path$relative_to(h_filename, Path$current_dir())); if (show_codegen.length > 0) xsystem(show_codegen, " <", h_filename); } @@ -765,7 +765,7 @@ void transpile_code(env_t *base_env, Path_t path) { if (fclose(c_file) == -1) print_err("Failed to output C code to ", c_filename); - if (!quiet) print("Transpiled code:\t", c_filename); + if (!quiet) print("Transpiled code:\t", Path$relative_to(c_filename, Path$current_dir())); if (show_codegen.length > 0) xsystem(show_codegen, " <", c_filename); } @@ -781,7 +781,7 @@ void compile_object_file(Path_t path) { Path$write(build_file(path, ".config"), config_summary, 0644); - if (!quiet) print("Compiled object:\t", obj_file); + if (!quiet) print("Compiled object:\t", Path$relative_to(obj_file, Path$current_dir())); } Path_t compile_executable(env_t *base_env, Path_t path, Path_t exe_path, List_t object_files, List_t extra_ldlibs) { @@ -821,6 +821,6 @@ Path_t compile_executable(env_t *base_env, Path_t path, Path_t exe_path, List_t int status = pclose(runner); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) exit(EXIT_FAILURE); - if (!quiet) print("Compiled executable:\t", exe_path); + if (!quiet) print("Compiled executable:\t", Path$relative_to(exe_path, Path$current_dir())); return exe_path; } diff --git a/src/typecheck.c b/src/typecheck.c index 828d2509..50df9327 100644 --- a/src/typecheck.c +++ b/src/typecheck.c @@ -104,7 +104,7 @@ type_t *parse_type_ast(env_t *env, type_ast_t *ast) { "longer exist on the stack."); arg_t *type_args = NULL; for (arg_ast_t *arg = fn->args; arg; arg = arg->next) { - type_args = new (arg_t, .name = arg->name, .next = type_args); + type_args = new (arg_t, .name = arg->name, .alias = arg->alias, .next = type_args); if (arg->type) type_args->type = parse_type_ast(env, arg->type); else if (arg->value) type_args->type = get_type(env, arg->value); @@ -397,8 +397,8 @@ void bind_statement(env_t *env, ast_t *statement) { "\nTry using a @", type_to_str(field_t), " pointer for this field."); } - fields = new (arg_t, .name = field_ast->name, .type = field_t, .default_val = field_ast->value, - .next = fields); + fields = new (arg_t, .name = field_ast->name, .alias = field_ast->alias, .type = field_t, + .default_val = field_ast->value, .next = fields); } REVERSE_LIST(fields); type->__data.StructType.fields = fields; // populate placeholder @@ -453,8 +453,8 @@ void bind_statement(env_t *env, ast_t *statement) { "\nTry using a @", type_to_str(field_t), " pointer for this field."); } - fields = new (arg_t, .name = field_ast->name, .type = field_t, .default_val = field_ast->value, - .next = fields); + fields = new (arg_t, .name = field_ast->name, .alias = field_ast->alias, .type = field_t, + .default_val = field_ast->value, .next = fields); } REVERSE_LIST(fields); env_t *member_ns = namespace_env(env, String(def->name, "$", tag_ast->name)); @@ -586,7 +586,7 @@ type_t *get_function_def_type(env_t *env, ast_t *ast) { env_t *scope = fresh_scope(env); for (arg_ast_t *arg = arg_asts; arg; arg = arg->next) { type_t *t = arg->type ? parse_type_ast(env, arg->type) : get_type(env, arg->value); - args = new (arg_t, .name = arg->name, .type = t, .default_val = arg->value, .next = args); + args = new (arg_t, .name = arg->name, .alias = arg->alias, .type = t, .default_val = arg->value, .next = args); set_binding(scope, arg->name, t, EMPTY_TEXT); } REVERSE_LIST(args); @@ -918,7 +918,8 @@ type_t *get_type(env_t *env, ast_t *ast) { return t; // Constructor arg_t *arg_types = NULL; for (arg_ast_t *arg = call->args; arg; arg = arg->next) - arg_types = new (arg_t, .type = get_type(env, arg->value), .name = arg->name, .next = arg_types); + arg_types = new (arg_t, .type = get_type(env, arg->value), .name = arg->name, .alias = arg->alias, + .next = arg_types); REVERSE_LIST(arg_types); code_err(call->fn, "I couldn't find a type constructor for ", type_to_text(Type(FunctionType, .args = arg_types, .ret = t))); @@ -1399,7 +1400,7 @@ type_t *get_type(env_t *env, ast_t *ast) { env_t *scope = fresh_scope(env); // For now, just use closed variables in scope normally for (arg_ast_t *arg = lambda->args; arg; arg = arg->next) { type_t *t = get_arg_ast_type(env, arg); - args = new (arg_t, .name = arg->name, .type = t, .next = args); + args = new (arg_t, .name = arg->name, .alias = arg->alias, .type = t, .next = args); set_binding(scope, arg->name, t, EMPTY_TEXT); } REVERSE_LIST(args); @@ -1617,7 +1618,8 @@ bool is_valid_call(env_t *env, arg_t *spec_args, arg_ast_t *call_args, call_opts for (arg_t *spec_arg = spec_args; spec_arg; spec_arg = spec_arg->next) { if (!options.underscores && spec_arg->name[0] == '_') continue; - if (!streq(call_arg->name, spec_arg->name)) continue; + if (!(streq(call_arg->name, spec_arg->name) || (spec_arg->alias && streq(call_arg->name, spec_arg->alias)))) + continue; type_t *spec_type = get_arg_type(env, spec_arg); env_t *arg_scope = with_enum_scope(env, spec_type); type_t *call_type = get_arg_ast_type(arg_scope, call_arg); diff --git a/src/types.h b/src/types.h index ed5628e8..5b5c4fb9 100644 --- a/src/types.h +++ b/src/types.h @@ -9,7 +9,7 @@ typedef struct type_s type_t; typedef struct arg_s { - const char *name; + const char *name, *alias; type_t *type; ast_t *default_val; struct arg_s *next; |
