mirror of
https://github.com/sbrow/envr.git
synced 2026-08-26 18:03:32 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 674f23ad2f | |||
| d4c935ec07 | |||
| e0a339e8ec | |||
| b0635e035c |
@@ -10,9 +10,11 @@
|
|||||||
|
|
||||||
5. Generate md and man pages again.
|
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 purge command?
|
||||||
|
|
||||||
|
|
||||||
## Double-check AI output
|
## Double-check AI output
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ Color_Mode :: enum {
|
|||||||
|
|
||||||
Positional_Arg :: struct {
|
Positional_Arg :: struct {
|
||||||
name: string,
|
name: string,
|
||||||
|
ntype: string, // nushell type: "path", "string". "" defaults to "path"
|
||||||
completion: string,
|
completion: string,
|
||||||
optional: bool,
|
optional: bool,
|
||||||
}
|
}
|
||||||
@@ -132,10 +133,22 @@ key somewhere, otherwise your data could be lost forever.`,
|
|||||||
flags = GLOBAL_FLAGS,
|
flags = GLOBAL_FLAGS,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name = "nushell-completion",
|
name = "completion",
|
||||||
usage = "envr nushell-completion",
|
usage = "envr completion <shell>",
|
||||||
short = "Generate custom completions for nushell",
|
short = "Generate shell completion scripts",
|
||||||
|
long = `Supported shells:
|
||||||
|
|
||||||
|
nushell
|
||||||
|
bash`,
|
||||||
flags = {.Help},
|
flags = {.Help},
|
||||||
|
args = {{name = "shell", ntype = "string", completion = "shells"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = "uninstall",
|
||||||
|
usage = "envr uninstall",
|
||||||
|
short = "Remove envr configuration and database",
|
||||||
|
long = "This will remove all files generated by envr from your system.",
|
||||||
|
flags = GLOBAL_FLAGS + {.Force},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
|
||||||
|
def shells [] {
|
||||||
|
['nushell' 'bash']
|
||||||
|
}
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
cmd_completion :: proc(cmd: ^Command) {
|
||||||
|
if len(cmd.args) == 0 {
|
||||||
|
print_command_help(cmd)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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, bash")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
ntype := len(arg.ntype) > 0 ? arg.ntype : "path"
|
||||||
|
if len(arg.completion) > 0 {
|
||||||
|
return fmt.tprintf("%s: %s@%s", name, ntype, arg.completion)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ test_nushell_completion_contains_commands :: proc(t: ^testing.T) {
|
|||||||
"envr scan",
|
"envr scan",
|
||||||
"envr sync",
|
"envr sync",
|
||||||
"envr version",
|
"envr version",
|
||||||
"envr nushell-completion",
|
"envr completion",
|
||||||
}
|
}
|
||||||
for ext in expected {
|
for ext in expected {
|
||||||
testing.expect(
|
testing.expect(
|
||||||
@@ -76,3 +76,58 @@ test_nushell_completion_no_help_command :: proc(t: ^testing.T) {
|
|||||||
"script should not contain 'envr help' (not a real command)",
|
"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")
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:strings"
|
|
||||||
|
|
||||||
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, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:bufio"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
import "core:path/filepath"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:terminal"
|
||||||
|
|
||||||
|
cmd_uninstall :: proc(cmd: ^Command) {
|
||||||
|
envr_d := envr_dir(cmd.flags.config_file)
|
||||||
|
|
||||||
|
if !os.exists(envr_d) {
|
||||||
|
fmt.wprintln(cmd.out, "Nothing to remove.", flush = false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.read_all_directory_by_path(envr_d, context.temp_allocator)
|
||||||
|
if err != nil {
|
||||||
|
fmt.wprintf(cmd.err, "Error reading %s: %v\n", envr_d, err, flush = false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := make([]string, len(entries), context.temp_allocator)
|
||||||
|
for entry, i in entries {
|
||||||
|
paths[i], _ = filepath.join([]string{envr_d, entry.name}, context.temp_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.wprintln(cmd.out, "This will remove:", flush = false)
|
||||||
|
for path in paths {
|
||||||
|
fmt.wprintf(cmd.out, " %s\n", path, flush = false)
|
||||||
|
}
|
||||||
|
fmt.wprintf(cmd.out, " %s\n", envr_d, flush = false)
|
||||||
|
|
||||||
|
if !cmd.flags.force {
|
||||||
|
if !terminal.is_terminal(os.stdin) {
|
||||||
|
fmt.wprintln(cmd.err, "Run with --force to skip confirmation.", flush = false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.wprintf(cmd.out, "\nAre you sure? [y/N] ", flush = false)
|
||||||
|
bufio.writer_flush(cmd.out_buf)
|
||||||
|
|
||||||
|
buf: [128]u8
|
||||||
|
n, _ := os.read(os.stdin, buf[:])
|
||||||
|
response := strings.trim_space(string(buf[:n]))
|
||||||
|
|
||||||
|
if response != "y" && response != "Y" {
|
||||||
|
fmt.wprintln(cmd.out, "Aborted.", flush = false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_err := os.remove_all(envr_d)
|
||||||
|
if remove_err != nil {
|
||||||
|
fmt.wprintf(cmd.err, "Error removing %s: %v\n", envr_d, remove_err, flush = false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
fmt.wprintf(cmd.out, "Removed %s\n", path, flush = false)
|
||||||
|
}
|
||||||
|
fmt.wprintf(cmd.out, "Removed %s\n", envr_d, flush = false)
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#+test
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "core:bufio"
|
||||||
|
import "core:os"
|
||||||
|
import "core:path/filepath"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:testing"
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_cmd_uninstall_force :: proc(t: ^testing.T) {
|
||||||
|
base := test_temp_dir(t, "envr-test-uninstall-*")
|
||||||
|
|
||||||
|
cfg_path, _ := filepath.join([]string{base, ".envr", "config.json"}, context.temp_allocator)
|
||||||
|
cfg := new_config([]string{"fixtures/keys/insecure-test-key"}, cfg_path)
|
||||||
|
testing.expect(t, save_config(cfg, force = true), "save should succeed")
|
||||||
|
delete_config(&cfg)
|
||||||
|
|
||||||
|
db, db_ok := db_open(cfg_path)
|
||||||
|
testing.expect(t, db_ok, "db should open")
|
||||||
|
if !db_ok do return
|
||||||
|
f := make_test_env_file("/project/.env", "abc123", "SECRET=value")
|
||||||
|
defer delete(f.remotes)
|
||||||
|
db_insert(&db, f)
|
||||||
|
db_close(&db)
|
||||||
|
|
||||||
|
envr_d := filepath.dir(cfg_path)
|
||||||
|
testing.expect(t, os.exists(envr_d), ".envr directory should exist before uninstall")
|
||||||
|
|
||||||
|
out_b: strings.Builder
|
||||||
|
strings.builder_init(&out_b)
|
||||||
|
defer strings.builder_destroy(&out_b)
|
||||||
|
err_b: strings.Builder
|
||||||
|
strings.builder_init(&err_b)
|
||||||
|
defer strings.builder_destroy(&err_b)
|
||||||
|
|
||||||
|
cmd, ok := parse_args(
|
||||||
|
[]string{"envr", "uninstall", "--force", "--config-file", cfg_path},
|
||||||
|
strings.to_stream(&out_b),
|
||||||
|
strings.to_stream(&err_b),
|
||||||
|
)
|
||||||
|
testing.expect(t, ok, "parse_args should succeed")
|
||||||
|
if !ok do return
|
||||||
|
defer delete_command(&cmd)
|
||||||
|
|
||||||
|
cmd_uninstall(&cmd)
|
||||||
|
bufio.writer_flush(cmd.out_buf)
|
||||||
|
|
||||||
|
testing.expect(t, !os.exists(envr_d), ".envr directory should be removed")
|
||||||
|
|
||||||
|
output := strings.to_string(out_b)
|
||||||
|
testing.expect(t, strings.contains(output, "Removed"), "output should list removed files")
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
strings.contains(output, "config.json"),
|
||||||
|
"output should mention config.json",
|
||||||
|
)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
strings.contains(output, "data.envr"),
|
||||||
|
"output should mention data.envr",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_cmd_uninstall_nothing_to_remove :: proc(t: ^testing.T) {
|
||||||
|
base := test_temp_dir(t, "envr-test-uninstall-empty-*")
|
||||||
|
defer os.remove_all(base)
|
||||||
|
|
||||||
|
cfg_path, _ := filepath.join([]string{base, ".envr", "config.json"}, context.temp_allocator)
|
||||||
|
|
||||||
|
out_b: strings.Builder
|
||||||
|
strings.builder_init(&out_b)
|
||||||
|
defer strings.builder_destroy(&out_b)
|
||||||
|
err_b: strings.Builder
|
||||||
|
strings.builder_init(&err_b)
|
||||||
|
defer strings.builder_destroy(&err_b)
|
||||||
|
|
||||||
|
cmd, ok := parse_args(
|
||||||
|
[]string{"envr", "uninstall", "--force", "--config-file", cfg_path},
|
||||||
|
strings.to_stream(&out_b),
|
||||||
|
strings.to_stream(&err_b),
|
||||||
|
)
|
||||||
|
testing.expect(t, ok, "parse_args should succeed")
|
||||||
|
if !ok do return
|
||||||
|
defer delete_command(&cmd)
|
||||||
|
|
||||||
|
cmd_uninstall(&cmd)
|
||||||
|
bufio.writer_flush(cmd.out_buf)
|
||||||
|
|
||||||
|
output := strings.to_string(out_b)
|
||||||
|
testing.expect(
|
||||||
|
t,
|
||||||
|
strings.contains(output, "Nothing to remove"),
|
||||||
|
"output should say nothing to remove",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import "core:fmt"
|
|||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
import "core:path/filepath"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
import "core:sys/posix"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
import "sqlite"
|
import "sqlite"
|
||||||
@@ -559,3 +560,69 @@ test_db_sync_moved :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_db_close_preserves_inodes :: proc(t: ^testing.T) {
|
||||||
|
base := test_temp_dir(t, "envr-test-inode-*")
|
||||||
|
defer os.remove_all(base)
|
||||||
|
|
||||||
|
envr_dir, _ := filepath.join([]string{base, ".envr"}, context.temp_allocator)
|
||||||
|
config_path, _ := filepath.join([]string{envr_dir, "config.json"}, context.temp_allocator)
|
||||||
|
os.mkdir_all(envr_dir)
|
||||||
|
|
||||||
|
key_abs, _ := filepath.abs("fixtures/keys/insecure-test-key", context.temp_allocator)
|
||||||
|
|
||||||
|
cfg := new_config([]string{key_abs}, config_path)
|
||||||
|
defer delete_config(&cfg)
|
||||||
|
testing.expect(t, save_config(cfg, force = true), "save_config should succeed")
|
||||||
|
|
||||||
|
// Create database
|
||||||
|
{
|
||||||
|
db, ok := db_open(config_path)
|
||||||
|
defer db_close(&db)
|
||||||
|
testing.expect(t, ok, "first db_open should succeed")
|
||||||
|
if !ok do return
|
||||||
|
|
||||||
|
env_path, _ := filepath.join([]string{base, ".env"}, context.temp_allocator)
|
||||||
|
f := make_test_env_file(env_path, "sha", "KEY=value")
|
||||||
|
db_insert(&db, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record inode of data.envr
|
||||||
|
data_file, _ := data_path(config_path, context.temp_allocator)
|
||||||
|
cdata := strings.clone_to_cstring(data_file)
|
||||||
|
defer delete(cdata)
|
||||||
|
|
||||||
|
sb1: posix.stat_t
|
||||||
|
testing.expect(t, posix.stat(cdata, &sb1) == .OK, "stat should succeed")
|
||||||
|
inode_before := sb1.st_ino
|
||||||
|
|
||||||
|
// Create hardlink
|
||||||
|
link_path, _ := filepath.join([]string{envr_dir, "data.envr.link"}, context.temp_allocator)
|
||||||
|
clink := strings.clone_to_cstring(link_path)
|
||||||
|
defer delete(clink)
|
||||||
|
testing.expect(t, posix.link(cdata, clink) == .OK, "hardlink should succeed")
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
{
|
||||||
|
db, ok2 := db_open(config_path)
|
||||||
|
defer db_close(&db)
|
||||||
|
testing.expect(t, ok2, "second db_open should succeed")
|
||||||
|
if !ok2 do return
|
||||||
|
|
||||||
|
env_path2, _ := filepath.join([]string{base, ".env2"}, context.temp_allocator)
|
||||||
|
f2 := make_test_env_file(env_path2, "sha2", "KEY2=value2")
|
||||||
|
db_insert(&db, f2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify inode preserved
|
||||||
|
sb2: posix.stat_t
|
||||||
|
testing.expect(t, posix.stat(cdata, &sb2) == .OK, "stat should succeed after rewrite")
|
||||||
|
testing.expect(t, sb2.st_ino == inode_before, "data.envr inode should be unchanged")
|
||||||
|
|
||||||
|
// Verify hardlink still valid
|
||||||
|
sb_link: posix.stat_t
|
||||||
|
testing.expect(t, posix.stat(clink, &sb_link) == .OK, "stat on hardlink should succeed")
|
||||||
|
testing.expect(t, sb_link.st_ino == inode_before, "hardlink inode should match")
|
||||||
|
testing.expect(t, sb_link.st_nlink == 2, "hardlink count should still be 2")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -77,8 +77,10 @@ main :: proc() {
|
|||||||
cmd_scan(&cmd)
|
cmd_scan(&cmd)
|
||||||
case "sync":
|
case "sync":
|
||||||
cmd_sync(&cmd)
|
cmd_sync(&cmd)
|
||||||
case "nushell-completion":
|
case "completion":
|
||||||
cmd_nushell_completion(&cmd)
|
cmd_completion(&cmd)
|
||||||
|
case "uninstall":
|
||||||
|
cmd_uninstall(&cmd)
|
||||||
case:
|
case:
|
||||||
fmt.wprintf(cmd.err, "Unknown command: %s\n", cmd.name)
|
fmt.wprintf(cmd.err, "Unknown command: %s\n", cmd.name)
|
||||||
write_usage(cmd.out)
|
write_usage(cmd.out)
|
||||||
|
|||||||
Reference in New Issue
Block a user