3 Commits

7 changed files with 286 additions and 145 deletions
+2
View File
@@ -1 +1,3 @@
use flake use flake
export PATH="./:$PATH"
+2 -2
View File
@@ -8,11 +8,11 @@
4. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`. 4. Check for prealloc opportunities. i.e. `make([dynamic]string)` -> `make([dynamic]string, 5)`.
5. Test all cmds / terminal branches. 5. Test all command branches.
6. Generate md and man pages again. 6. Generate md and man pages again.
7. Shell completion 7. Bash Shell completion
8. Add tests for untested commands. 8. Add tests for untested commands.
+145 -65
View File
@@ -22,8 +22,8 @@ Command :: struct {
Flags :: struct { Flags :: struct {
help: bool `args:"short=h" usage:"show this documentation"`, 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"`, 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"`, 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"`, color: Color_Mode `usage:"Whether or not to colorize output" default:"auto" completion:"color"`,
force: bool `args:"short=f" usage:"Overwrite existing config"`, force: bool `args:"short=f" usage:"Overwrite existing config"`,
} }
@@ -47,6 +47,12 @@ Color_Mode :: enum {
Never, Never,
} }
Positional_Arg :: struct {
name: string,
completion: string,
optional: bool,
}
CommandInfo :: struct { CommandInfo :: struct {
name: string, name: string,
usage: string, usage: string,
@@ -55,59 +61,86 @@ CommandInfo :: struct {
aliases: []string, aliases: []string,
// Flags supported by the command // Flags supported by the command
flags: bit_set[Flag_Type], flags: bit_set[Flag_Type],
args: []Positional_Arg,
} }
GLOBAL_FLAGS: bit_set[Flag_Type] = {.Help, .Config_File, .Color} GLOBAL_FLAGS: bit_set[Flag_Type] = {.Help, .Config_File, .Color}
COMMANDS := []CommandInfo { COMMANDS := []CommandInfo {
{ {
"init", name = "init",
"envr init", usage = "envr init",
"Set up envr", short = "Set up envr",
`The init command generates your initial config and saves it to long = `The init command generates your initial config and saves it to
~/.envr/config in JSON format.\n\nDuring setup, you will be prompted to select one or more ssh keys with which to ~/.envr/config in JSON format.\n\nDuring setup, you will be prompted to select one or more ssh keys with which to
encrypt your databse. **Make 100% sure** that you have **a remote copy** of this encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
key somewhere, otherwise your data could be lost forever.`, key somewhere, otherwise your data could be lost forever.`,
{}, flags = GLOBAL_FLAGS + {.Force},
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 <path>", "Import a .env file into envr", "", {"add"}, GLOBAL_FLAGS},
{
"restore",
"envr restore <path>",
"Restore a .env file from the database",
"",
{},
GLOBAL_FLAGS,
},
{"list", "envr list", "View your tracked files", "", {}, GLOBAL_FLAGS + {.Output}},
{
"remove",
"envr remove <path>",
"Remove a .env file from your database",
"",
{},
GLOBAL_FLAGS,
},
{"check", "envr check [path]", "Check if files are backed up", "", {}, GLOBAL_FLAGS},
{"version", "envr version", "Show envr's version", "", {}, {.Help}},
{
"edit-config",
"envr edit-config",
"Edit your config with your default editor",
"",
{},
GLOBAL_FLAGS,
}, },
{ {
"nushell-completion", name = "scan",
"envr nushell-completion", usage = "envr scan",
"Generate custom completions for nushell", short = "Find and select .env files for backup",
"", flags = GLOBAL_FLAGS,
{}, },
{.Help}, {
name = "sync",
usage = "envr sync",
short = "Update or restore your env backups",
flags = GLOBAL_FLAGS + {.Output},
},
{
name = "backup",
usage = "envr backup <path>",
short = "Import a .env file into envr",
aliases = {"add"},
flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "untracked-paths"}},
},
{
name = "restore",
usage = "envr restore <path>",
short = "Restore a .env file from the database",
flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "tracked-paths"}},
},
{
name = "list",
usage = "envr list",
short = "View your tracked files",
flags = GLOBAL_FLAGS + {.Output},
},
{
name = "remove",
usage = "envr remove <path>",
short = "Remove a .env file from your database",
flags = GLOBAL_FLAGS,
args = {{name = "path", completion = "tracked-paths"}},
},
{
name = "check",
usage = "envr check [path]",
short = "Check if files are backed up",
flags = GLOBAL_FLAGS,
args = {{name = "path", optional = true}},
},
{
name = "version",
usage = "envr version",
short = "Show envr's version",
flags = {.Help},
},
{
name = "edit-config",
usage = "envr edit-config",
short = "Edit your config with your default editor",
flags = GLOBAL_FLAGS,
},
{
name = "nushell-completion",
usage = "envr nushell-completion",
short = "Generate custom completions for nushell",
flags = {.Help},
}, },
} }
@@ -213,13 +246,23 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
return true return true
} }
flag_field_info :: proc( Flag_Kind :: enum {
ft: Flag_Type, Bool,
) -> ( String,
names: string, Enum,
value_hint: string, }
description: string,
) { 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)) field := reflect.struct_field_at(Flags, int(ft))
args_tag := reflect.struct_tag_get(field.tag, "args") args_tag := reflect.struct_tag_get(field.tag, "args")
@@ -229,37 +272,74 @@ flag_field_info :: proc(
} }
short, has_short := get_subtag(args_tag, "short") 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) 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 { 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 { } else if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string {
value_hint = " <value>" kind = .String
} else if enum_ti, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum { } 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) parts := make([dynamic]string, 0, len(enum_ti.names), context.temp_allocator)
for name in enum_ti.names { for name in enum_ti.names {
lower := strings.to_lower(name, context.temp_allocator) lower := strings.to_lower(name, context.temp_allocator)
append(&parts, fmt.tprintf("'%s'", lower)) 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) delete(parts)
} }
usage := reflect.struct_tag_get(field.tag, "usage") usage := reflect.struct_tag_get(field.tag, "usage")
default_val := reflect.struct_tag_get(field.tag, "default") default_val := reflect.struct_tag_get(field.tag, "default")
completion := reflect.struct_tag_get(field.tag, "completion")
description = usage return {
if len(default_val) > 0 { long_name = long_name,
if _, is_string := base_ti.variant.(runtime.Type_Info_String); is_string { short_name = has_short ? short : "",
description = fmt.tprintf(`%s (default "%s")`, usage, default_val) kind = kind,
} else if _, is_enum := base_ti.variant.(runtime.Type_Info_Enum); is_enum { usage = usage,
description = fmt.tprintf("%s (default '%s')", usage, default_val) 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 = " <value>"
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)
} }
} }
+91 -2
View File
@@ -1,10 +1,99 @@
package main package main
import "core:fmt" 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) { 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)
} }
+45 -4
View File
@@ -7,31 +7,72 @@ import "core:testing"
@(test) @(test)
test_nushell_completion_nonempty :: proc(t: ^testing.T) { 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)
test_nushell_completion_contains_externs :: proc(t: ^testing.T) { test_nushell_completion_contains_commands :: proc(t: ^testing.T) {
script := generate_nushell_completion()
expected := []string{ expected := []string{
"tracked-paths", "tracked-paths",
"untracked-paths", "untracked-paths",
"envr backup", "envr backup",
"envr check", "envr check",
"envr edit-config", "envr edit-config",
"envr help",
"envr init", "envr init",
"envr list", "envr list",
"envr remove", "envr remove",
"envr restore", "envr restore",
"envr scan", "envr scan",
"envr sync", "envr sync",
"envr version",
"envr nushell-completion", "envr nushell-completion",
} }
for ext in expected { for ext in expected {
testing.expect( testing.expect(
t, t,
strings.contains(COMPLETION_SCRIPT, ext), strings.contains(script, ext),
fmt.tprintf("expected script to contain %q", 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)",
)
}
+1 -1
View File
@@ -5,7 +5,7 @@ import "core:mem"
import "core:os" import "core:os"
MAGIC :: "ENVR" MAGIC :: "ENVR"
MAGIC_BYTES := [4]u8{u8('E'), u8('N'), u8('V'), u8('R')} MAGIC_BYTES: [4]u8 = MAGIC
RECIPIENT_ENTRY_SIZE :: RECIPIENT_ENTRY_SIZE ::
CRYPTO_BOX_PUBLICKEY_BYTES + CRYPTO_BOX_PUBLICKEY_BYTES +
-71
View File
@@ -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
]