diff --git a/.envrc b/.envrc index 3550a30..20da18f 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,3 @@ use flake + +export PATH="./:$PATH" diff --git a/TODOS.md b/TODOS.md index 7dfa8aa..a4e178a 100644 --- a/TODOS.md +++ b/TODOS.md @@ -12,7 +12,7 @@ 6. Generate md and man pages again. -7. Shell completion +7. Bash Shell completion 8. Add tests for untested commands. diff --git a/cli.odin b/cli.odin index a94d17d..df92995 100644 --- a/cli.odin +++ b/cli.odin @@ -22,8 +22,8 @@ Command :: struct { Flags :: struct { help: bool `args:"short=h" usage:"show this documentation"`, config_file: string `args:"name=config-file,short=c" usage:"config file" default:"~/.envr/config.json"`, - output: Output_Format `args:"short=o" usage:"the format of output data" default:"table"`, - color: Color_Mode `usage:"Whether or not to colorize output" default:"auto"`, + output: Output_Format `args:"short=o" usage:"the format of output data" default:"table" completion:"output"`, + color: Color_Mode `usage:"Whether or not to colorize output" default:"auto" completion:"color"`, force: bool `args:"short=f" usage:"Overwrite existing config"`, } @@ -47,6 +47,12 @@ Color_Mode :: enum { Never, } +Positional_Arg :: struct { + name: string, + completion: string, + optional: bool, +} + CommandInfo :: struct { name: string, usage: string, @@ -55,6 +61,7 @@ CommandInfo :: struct { aliases: []string, // Flags supported by the command flags: bit_set[Flag_Type], + args: []Positional_Arg, } GLOBAL_FLAGS: bit_set[Flag_Type] = {.Help, .Config_File, .Color} @@ -70,10 +77,27 @@ encrypt your databse. **Make 100% sure** that you have **a remote copy** of this key somewhere, otherwise your data could be lost forever.`, {}, GLOBAL_FLAGS + {.Force}, + {}, + }, + {"scan", "envr scan", "Find and select .env files for backup", "", {}, GLOBAL_FLAGS, {}}, + { + "sync", + "envr sync", + "Update or restore your env backups", + "", + {}, + GLOBAL_FLAGS + {.Output}, + {}, + }, + { + "backup", + "envr backup ", + "Import a .env file into envr", + "", + {"add"}, + GLOBAL_FLAGS, + {{name = "path", completion = "untracked-paths"}}, }, - {"scan", "envr scan", "Find and select .env files for backup", "", {}, GLOBAL_FLAGS}, - {"sync", "envr sync", "Update or restore your env backups", "", {}, GLOBAL_FLAGS + {.Output}}, - {"backup", "envr backup ", "Import a .env file into envr", "", {"add"}, GLOBAL_FLAGS}, { "restore", "envr restore ", @@ -81,8 +105,9 @@ key somewhere, otherwise your data could be lost forever.`, "", {}, GLOBAL_FLAGS, + {{name = "path", completion = "tracked-paths"}}, }, - {"list", "envr list", "View your tracked files", "", {}, GLOBAL_FLAGS + {.Output}}, + {"list", "envr list", "View your tracked files", "", {}, GLOBAL_FLAGS + {.Output}, {}}, { "remove", "envr remove ", @@ -90,9 +115,18 @@ key somewhere, otherwise your data could be lost forever.`, "", {}, GLOBAL_FLAGS, + {{name = "path", completion = "tracked-paths"}}, }, - {"check", "envr check [path]", "Check if files are backed up", "", {}, GLOBAL_FLAGS}, - {"version", "envr version", "Show envr's version", "", {}, {.Help}}, + { + "check", + "envr check [path]", + "Check if files are backed up", + "", + {}, + GLOBAL_FLAGS, + {{name = "path", optional = true}}, + }, + {"version", "envr version", "Show envr's version", "", {}, {.Help}, {}}, { "edit-config", "envr edit-config", @@ -100,6 +134,7 @@ key somewhere, otherwise your data could be lost forever.`, "", {}, GLOBAL_FLAGS, + {}, }, { "nushell-completion", @@ -108,6 +143,7 @@ key somewhere, otherwise your data could be lost forever.`, "", {}, {.Help}, + {}, }, } @@ -213,13 +249,23 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool { return true } -flag_field_info :: proc( - ft: Flag_Type, -) -> ( - names: string, - value_hint: string, - description: string, -) { +Flag_Kind :: enum { + Bool, + String, + Enum, +} + +Flag_Field :: struct { + long_name: string, + short_name: string, + kind: Flag_Kind, + usage: string, + default_val: string, + enum_values: string, + completion: string, +} + +flag_field :: proc(ft: Flag_Type) -> Flag_Field { field := reflect.struct_field_at(Flags, int(ft)) args_tag := reflect.struct_tag_get(field.tag, "args") @@ -229,37 +275,74 @@ flag_field_info :: proc( } short, has_short := get_subtag(args_tag, "short") - if has_short { - names = fmt.tprintf("-%s, --%s", short, long_name) - } else { - names = fmt.tprintf("--%s", long_name) - } base_ti := runtime.type_info_base(field.type) + kind: Flag_Kind + enum_values: string if _, is_bool := base_ti.variant.(runtime.Type_Info_Boolean); is_bool { - value_hint = "" + kind = .Bool } else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string { - value_hint = " " + kind = .String } else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum { + kind = .Enum parts := make([dynamic]string, 0, len(enum_ti.names), context.temp_allocator) for name in enum_ti.names { lower := strings.to_lower(name, context.temp_allocator) append(&parts, fmt.tprintf("'%s'", lower)) } - value_hint = fmt.tprintf(" %s", strings.join(parts[:], "|", context.temp_allocator)) + enum_values = strings.join(parts[:], "|", context.temp_allocator) delete(parts) } usage := reflect.struct_tag_get(field.tag, "usage") default_val := reflect.struct_tag_get(field.tag, "default") + completion := reflect.struct_tag_get(field.tag, "completion") - description = usage - if len(default_val) > 0 { - if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string { - description = fmt.tprintf(`%s (default "%s")`, usage, default_val) - } else if _, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum { - description = fmt.tprintf("%s (default '%s')", usage, default_val) + return { + long_name = long_name, + short_name = has_short ? short : "", + kind = kind, + usage = usage, + default_val = default_val, + enum_values = enum_values, + completion = completion, + } +} + +flag_field_info :: proc( + ft: Flag_Type, +) -> ( + names: string, + value_hint: string, + description: string, +) { + f := flag_field(ft) + + if len(f.short_name) > 0 { + names = fmt.tprintf("-%s, --%s", f.short_name, f.long_name) + } else { + names = fmt.tprintf("--%s", f.long_name) + } + + switch f.kind { + case .Bool: + value_hint = "" + case .String: + value_hint = " " + case .Enum: + value_hint = fmt.tprintf(" %s", f.enum_values) + } + + description = f.usage + if len(f.default_val) > 0 { + switch f.kind { + case .Bool: + // do nothing + case .String: + description = fmt.tprintf(`%s (default "%s")`, f.usage, f.default_val) + case .Enum: + description = fmt.tprintf("%s (default '%s')", f.usage, f.default_val) } } diff --git a/cmd_nushell_completion.odin b/cmd_nushell_completion.odin index 9da7034..39c4400 100644 --- a/cmd_nushell_completion.odin +++ b/cmd_nushell_completion.odin @@ -1,10 +1,99 @@ package main import "core:fmt" +import "core:strings" -COMPLETION_SCRIPT: string : string(#load("mod.nu")) +nushell_header :: `def tracked-paths [] { + ( + ^envr list + | from json + | each { + [$in.directory $in.path] | path join + } + ) +} +def untracked-paths [] { + ( + ^envr scan + | from json + ) +} + +def color [] { + ['auto' 'always' 'never'] +} + +def output [] { + ['auto' 'table' 'json'] +} + +` + cmd_nushell_completion :: proc(cmd: ^Command) { - fmt.wprint(cmd.out, COMPLETION_SCRIPT, flush = false) + fmt.wprint(cmd.out, generate_nushell_completion(), flush = true) +} + +generate_nushell_completion :: proc() -> string { + sb: strings.Builder + strings.builder_init(&sb, context.temp_allocator) + defer strings.builder_destroy(&sb) + + fmt.sbprint(&sb, nushell_header) + + for c in COMMANDS { + fmt.sbprintf(&sb, "# %s\n", c.short) + fmt.sbprintf(&sb, "export extern \"envr %s\" [\n", c.name) + for ft in Flag_Type { + if ft not_in c.flags do continue + f := flag_field(ft) + fmt.sbprintf(&sb, " %s\n", nushell_flag_line(f)) + } + for arg in c.args { + fmt.sbprintf(&sb, " %s\n", nushell_positional_line(arg)) + } + fmt.sbprintf(&sb, "]\n") + + for a in c.aliases { + fmt.sbprintf(&sb, "\nexport alias \"envr %s\" = envr %s\n", a, c.name) + } + fmt.sbprintf(&sb, "\n") + } + + return strings.to_string(sb) +} + +nushell_flag_line :: proc(f: Flag_Field) -> string { + line: string + if len(f.short_name) > 0 { + line = fmt.tprintf("--%s(-%s)", f.long_name, f.short_name) + } else { + line = fmt.tprintf("--%s", f.long_name) + } + + switch f.kind { + case .Bool: + case .String: + line = fmt.tprintf("%s: path", line) + case .Enum: + if len(f.completion) > 0 { + line = fmt.tprintf("%s: string@%s", line, f.completion) + } else { + line = fmt.tprintf("%s: string", line) + } + } + + return fmt.tprintf("%s # %s", line, f.usage) +} + +nushell_positional_line :: proc(arg: Positional_Arg) -> string { + name := arg.name + if arg.optional { + name = fmt.tprintf("%s?", name) + } + if len(arg.completion) > 0 { + return fmt.tprintf("%s: path@%s", name, arg.completion) + } + return fmt.tprintf("%s: path", name) } diff --git a/cmd_nushell_completion_test.odin b/cmd_nushell_completion_test.odin index 0d91e81..258a5fb 100644 --- a/cmd_nushell_completion_test.odin +++ b/cmd_nushell_completion_test.odin @@ -7,31 +7,72 @@ import "core:testing" @(test) test_nushell_completion_nonempty :: proc(t: ^testing.T) { - testing.expect(t, len(COMPLETION_SCRIPT) > 0, "completion script should not be empty") + script := generate_nushell_completion() + testing.expect(t, len(script) > 0, "completion script should not be empty") } @(test) -test_nushell_completion_contains_externs :: proc(t: ^testing.T) { +test_nushell_completion_contains_commands :: proc(t: ^testing.T) { + script := generate_nushell_completion() expected := []string{ "tracked-paths", "untracked-paths", "envr backup", "envr check", "envr edit-config", - "envr help", "envr init", "envr list", "envr remove", "envr restore", "envr scan", "envr sync", + "envr version", "envr nushell-completion", } for ext in expected { testing.expect( t, - strings.contains(COMPLETION_SCRIPT, ext), + strings.contains(script, ext), fmt.tprintf("expected script to contain %q", ext), ) } } + +@(test) +test_nushell_completion_contains_flags :: proc(t: ^testing.T) { + script := generate_nushell_completion() + expected_flags := []string{ + "--help(-h)", + "--config-file(-c)", + "--color", + "--force(-f)", + "--output(-o)", + } + for flag in expected_flags { + testing.expect( + t, + strings.contains(script, flag), + fmt.tprintf("expected script to contain %q", flag), + ) + } +} + +@(test) +test_nushell_completion_contains_aliases :: proc(t: ^testing.T) { + script := generate_nushell_completion() + testing.expect( + t, + strings.contains(script, "envr add"), + "expected script to contain 'envr add' alias", + ) +} + +@(test) +test_nushell_completion_no_help_command :: proc(t: ^testing.T) { + script := generate_nushell_completion() + testing.expect( + t, + !strings.contains(script, "envr help"), + "script should not contain 'envr help' (not a real command)", + ) +} diff --git a/mod.nu b/mod.nu deleted file mode 100644 index c16b57f..0000000 --- a/mod.nu +++ /dev/null @@ -1,71 +0,0 @@ -# envr command extern definitions for Nushell -# A tool for managing environment files and backups - -export def tracked-paths [] { - ( - ^envr list - | from json - | each { - [$in.directory $in.path] | path join - } - ) -} - -export def untracked-paths [] { - ( - ^envr scan - | from json - ) -} - -export extern envr [ - ...args: any - --help(-h) # Show help information -] - -export extern "envr backup" [ - --help(-h) # Show help for backup command - path: path@untracked-paths # Path to .env file to backup -] - -export extern "envr check" [ - --help(-h) # Show help for check command -] - -export extern "envr edit-config" [ - --help(-h) # Show help for edit-config command -] - -export extern "envr help" [ - command?: string # Show help for specific command -] - -export extern "envr init" [ - --help(-h) # Show help for init command -] - -export extern "envr list" [ - --help(-h) # Show help for list command -] - -export extern "envr remove" [ - --help(-h) # Show help for remove command - path: path@tracked-paths -] - -export extern "envr restore" [ - --help(-h) # Show help for restore command - path: path@tracked-paths -] - -export extern "envr scan" [ - --help(-h) # Show help for scan command -] - -export extern "envr sync" [ - --help(-h) # Show help for sync command -] - -export extern "envr nushell-completion" [ - --help(-h) # Show help for nushell-completion command -]