mirror of
https://github.com/sbrow/envr.git
synced 2026-08-26 09:53:32 -04:00
feat: Added bash completion support.
This commit is contained in:
@@ -10,15 +10,13 @@
|
||||
|
||||
5. Generate md and man pages again.
|
||||
|
||||
6. Bash Shell completion
|
||||
6. Add tests for untested commands.
|
||||
|
||||
7. Add tests for untested commands.
|
||||
7. Add uninstall command.
|
||||
|
||||
8. Add uninstall command.
|
||||
8. Add purge command?
|
||||
|
||||
9. Add purge command?
|
||||
|
||||
10. Ensure updates to db don't break hardlinks.
|
||||
9. Ensure updates to db don't break hardlinks.
|
||||
|
||||
|
||||
## Double-check AI output
|
||||
|
||||
@@ -138,7 +138,8 @@ key somewhere, otherwise your data could be lost forever.`,
|
||||
short = "Generate shell completion scripts",
|
||||
long = `Supported shells:
|
||||
|
||||
nushell`,
|
||||
nushell
|
||||
bash`,
|
||||
flags = {.Help},
|
||||
args = {{name = "shell", ntype = "string", completion = "shells"}},
|
||||
},
|
||||
|
||||
+155
-2
@@ -29,7 +29,7 @@ def output [] {
|
||||
}
|
||||
|
||||
def shells [] {
|
||||
['nushell']
|
||||
['nushell' 'bash']
|
||||
}
|
||||
|
||||
`
|
||||
@@ -43,9 +43,11 @@ cmd_completion :: proc(cmd: ^Command) {
|
||||
switch cmd.args[0] {
|
||||
case "nushell":
|
||||
fmt.wprint(cmd.out, generate_nushell_completion(), flush = true)
|
||||
case "bash":
|
||||
fmt.wprint(cmd.out, generate_bash_completion(), flush = true)
|
||||
case:
|
||||
fmt.wprintf(cmd.err, "Unsupported shell: %s\n", cmd.args[0])
|
||||
fmt.wprintln(cmd.err, "Supported shells: nushell")
|
||||
fmt.wprintln(cmd.err, "Supported shells: nushell, bash")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,3 +115,154 @@ nushell_positional_line :: proc(arg: Positional_Arg) -> string {
|
||||
return fmt.tprintf("%s: %s", name, ntype)
|
||||
}
|
||||
|
||||
bash_header :: `
|
||||
_envr() {
|
||||
local cur prev cmd
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
cmd="${COMP_WORDS[1]}"
|
||||
|
||||
`
|
||||
|
||||
bash_footer :: `
|
||||
}
|
||||
|
||||
complete -F _envr envr
|
||||
`
|
||||
|
||||
generate_bash_completion :: proc() -> string {
|
||||
sb: strings.Builder
|
||||
strings.builder_init(&sb, context.temp_allocator)
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
fmt.sbprint(&sb, bash_header)
|
||||
|
||||
// B: Subcommand completion
|
||||
fmt.sbprintf(&sb, " if [[ $COMP_CWORD -eq 1 ]]; then\n")
|
||||
fmt.sbprintf(&sb, " COMPREPLY=( $(compgen -W \"")
|
||||
first := true
|
||||
for c in COMMANDS {
|
||||
if !first do fmt.sbprintf(&sb, " ")
|
||||
fmt.sbprintf(&sb, "%s", c.name)
|
||||
first = false
|
||||
for a in c.aliases {
|
||||
fmt.sbprintf(&sb, " %s", a)
|
||||
}
|
||||
}
|
||||
fmt.sbprintf(&sb, "\" -- \"$cur\") )\n")
|
||||
fmt.sbprintf(&sb, " return\n")
|
||||
fmt.sbprintf(&sb, " fi\n\n")
|
||||
|
||||
// C: Flag value completion
|
||||
fmt.sbprintf(&sb, " case \"$prev\" in\n")
|
||||
for ft in Flag_Type {
|
||||
f := flag_field(ft)
|
||||
if f.kind == .Bool do continue
|
||||
|
||||
pattern := fmt.tprintf("--%s", f.long_name)
|
||||
if len(f.short_name) > 0 {
|
||||
pattern = fmt.tprintf("%s|-%s", pattern, f.short_name)
|
||||
}
|
||||
fmt.sbprintf(&sb, " %s)\n", pattern)
|
||||
|
||||
switch f.kind {
|
||||
case .Enum:
|
||||
fmt.sbprintf(
|
||||
&sb,
|
||||
" COMPREPLY=( $(compgen -W \"%s\" -- \"$cur\") )\n",
|
||||
bash_enum_values(f.enum_values),
|
||||
)
|
||||
case .String:
|
||||
fmt.sbprintf(&sb, " COMPREPLY=( $(compgen -f -- \"$cur\") )\n")
|
||||
case .Bool:
|
||||
panic("unexpected")
|
||||
}
|
||||
fmt.sbprintf(&sb, " return\n")
|
||||
fmt.sbprintf(&sb, " ;;\n")
|
||||
}
|
||||
fmt.sbprintf(&sb, " esac\n\n")
|
||||
|
||||
// D: Flag name completion per command
|
||||
fmt.sbprintf(&sb, " case \"$cur\" in -*)\n")
|
||||
fmt.sbprintf(&sb, " case \"$cmd\" in\n")
|
||||
for c in COMMANDS {
|
||||
if c.flags == {} do continue
|
||||
|
||||
cmd_pattern := c.name
|
||||
for a in c.aliases {
|
||||
cmd_pattern = fmt.tprintf("%s|%s", cmd_pattern, a)
|
||||
}
|
||||
fmt.sbprintf(&sb, " %s)\n", cmd_pattern)
|
||||
fmt.sbprintf(&sb, " COMPREPLY=( $(compgen -W \"")
|
||||
|
||||
flag_first := true
|
||||
for ft in Flag_Type {
|
||||
if ft not_in c.flags do continue
|
||||
f := flag_field(ft)
|
||||
if !flag_first do fmt.sbprintf(&sb, " ")
|
||||
fmt.sbprintf(&sb, "--%s", f.long_name)
|
||||
if len(f.short_name) > 0 {
|
||||
fmt.sbprintf(&sb, " -%s", f.short_name)
|
||||
}
|
||||
flag_first = false
|
||||
}
|
||||
fmt.sbprintf(&sb, "\" -- \"$cur\") )\n")
|
||||
fmt.sbprintf(&sb, " return\n")
|
||||
fmt.sbprintf(&sb, " ;;\n")
|
||||
}
|
||||
fmt.sbprintf(&sb, " esac\n")
|
||||
fmt.sbprintf(&sb, " ;;\n")
|
||||
fmt.sbprintf(&sb, " esac\n\n")
|
||||
|
||||
// E: Positional completion
|
||||
fmt.sbprintf(&sb, " case \"$cmd\" in\n")
|
||||
for c in COMMANDS {
|
||||
has_comp := false
|
||||
for arg in c.args {
|
||||
if len(arg.completion) > 0 {
|
||||
has_comp = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !has_comp do continue
|
||||
|
||||
cmd_pattern := c.name
|
||||
for a in c.aliases {
|
||||
cmd_pattern = fmt.tprintf("%s|%s", cmd_pattern, a)
|
||||
}
|
||||
fmt.sbprintf(&sb, " %s)\n", cmd_pattern)
|
||||
|
||||
for arg in c.args {
|
||||
if len(arg.completion) == 0 do continue
|
||||
|
||||
comp: string
|
||||
switch arg.completion {
|
||||
case "tracked-paths":
|
||||
comp = "$(envr list --output json 2>/dev/null)"
|
||||
case "untracked-paths":
|
||||
comp = "$(envr scan --output json 2>/dev/null)"
|
||||
case "shells":
|
||||
comp = "nushell bash"
|
||||
case:
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.sbprintf(&sb, " COMPREPLY=( $(compgen -W \"%s\" -- \"$cur\") )\n", comp)
|
||||
}
|
||||
fmt.sbprintf(&sb, " return\n")
|
||||
fmt.sbprintf(&sb, " ;;\n")
|
||||
}
|
||||
fmt.sbprintf(&sb, " esac\n")
|
||||
|
||||
fmt.sbprint(&sb, bash_footer)
|
||||
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
bash_enum_values :: proc(enum_values: string) -> string {
|
||||
s, _ := strings.replace(enum_values, "'", "", -1, context.temp_allocator)
|
||||
s, _ = strings.replace(s, "|", " ", -1, context.temp_allocator)
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -76,3 +76,58 @@ test_nushell_completion_no_help_command :: proc(t: ^testing.T) {
|
||||
"script should not contain 'envr help' (not a real command)",
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_bash_completion_nonempty :: proc(t: ^testing.T) {
|
||||
script := generate_bash_completion()
|
||||
testing.expect(t, len(script) > 0, "bash completion script should not be empty")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_bash_completion_contains_registration :: proc(t: ^testing.T) {
|
||||
script := generate_bash_completion()
|
||||
testing.expect(
|
||||
t,
|
||||
strings.contains(script, "complete -F _envr envr"),
|
||||
"expected bash script to register completion function",
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_bash_completion_contains_commands :: proc(t: ^testing.T) {
|
||||
script := generate_bash_completion()
|
||||
expected := []string{
|
||||
"init", "scan", "sync", "backup", "add",
|
||||
"restore", "list", "remove", "check",
|
||||
"version", "edit-config", "completion",
|
||||
}
|
||||
for cmd in expected {
|
||||
testing.expect(
|
||||
t,
|
||||
strings.contains(script, cmd),
|
||||
fmt.tprintf("expected bash script to contain %q", cmd),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_bash_completion_contains_flags :: proc(t: ^testing.T) {
|
||||
script := generate_bash_completion()
|
||||
expected := []string{
|
||||
"--help", "--config-file", "--color", "--force", "--output",
|
||||
}
|
||||
for flag in expected {
|
||||
testing.expect(
|
||||
t,
|
||||
strings.contains(script, flag),
|
||||
fmt.tprintf("expected bash script to contain %q", flag),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_bash_completion_contains_enum_values :: proc(t: ^testing.T) {
|
||||
script := generate_bash_completion()
|
||||
testing.expect(t, strings.contains(script, "auto table json"), "missing output enum values")
|
||||
testing.expect(t, strings.contains(script, "auto always never"), "missing color enum values")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
|
||||
_envr() {
|
||||
local cur prev cmd
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
cmd="${COMP_WORDS[1]}"
|
||||
|
||||
if [[ $COMP_CWORD -eq 1 ]]; then
|
||||
COMPREPLY=( $(compgen -W "init scan sync backup add restore list remove check version edit-config completion" -- "$cur") )
|
||||
return
|
||||
fi
|
||||
|
||||
case "$prev" in
|
||||
--config-file|-c)
|
||||
COMPREPLY=( $(compgen -f -- "$cur") )
|
||||
return
|
||||
;;
|
||||
--output|-o)
|
||||
COMPREPLY=( $(compgen -W "auto table json" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=( $(compgen -W "auto always never" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cur" in -*)
|
||||
case "$cmd" in
|
||||
init)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --color --force -f" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
scan)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
sync)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --output -o --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
backup|add)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
restore)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
list)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --output -o --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
remove)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
check)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
version)
|
||||
COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
edit-config)
|
||||
COMPREPLY=( $(compgen -W "--help -h --config-file -c --color" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
completion)
|
||||
COMPREPLY=( $(compgen -W "--help -h" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$cmd" in
|
||||
backup|add)
|
||||
COMPREPLY=( $(compgen -W "$(envr scan --output json 2>/dev/null)" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
restore)
|
||||
COMPREPLY=( $(compgen -W "$(envr list --output json 2>/dev/null)" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
remove)
|
||||
COMPREPLY=( $(compgen -W "$(envr list --output json 2>/dev/null)" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
completion)
|
||||
COMPREPLY=( $(compgen -W "nushell bash" -- "$cur") )
|
||||
return
|
||||
;;
|
||||
esac
|
||||
|
||||
}
|
||||
|
||||
complete -F _envr envr
|
||||
Reference in New Issue
Block a user