test: commands now accept stdout/stderr fields.

This commit is contained in:
2026-06-15 17:00:45 -04:00
parent ec96dff055
commit 4600c81401
18 changed files with 157 additions and 160 deletions

View File

@@ -32,8 +32,6 @@ Note: These todos can wait until all the subcommands have been ported.
35. **prompt.odin:124**`make([dynamic]bool, len(options))` creates N zero-initialized elements. Works because `false` is the default, but same footgun as original issue 1. Should be `make([dynamic]bool, 0, len(options))`. 35. **prompt.odin:124**`make([dynamic]bool, len(options))` creates N zero-initialized elements. Works because `false` is the default, but same footgun as original issue 1. Should be `make([dynamic]bool, 0, len(options))`.
39. Lots of memory leaks to fix.
## LOW ## LOW
15. **db.odin:115**`json.unmarshal_string` error not checked. Malformed JSON silently produces empty/partial data. 15. **db.odin:115**`json.unmarshal_string` error not checked. Malformed JSON silently produces empty/partial data.

View File

@@ -3,7 +3,6 @@ package main
import "core:bufio" import "core:bufio"
import "core:fmt" import "core:fmt"
import "core:io" import "core:io"
import "core:mem"
import "core:os" import "core:os"
import "core:strings" import "core:strings"
@@ -13,6 +12,9 @@ Command :: struct {
flags: map[string]string, flags: map[string]string,
bool_set: map[string]bool, bool_set: map[string]bool,
config_path: string, config_path: string,
out_buf: ^bufio.Writer,
out: io.Writer,
err: io.Writer,
} }
CommandInfo :: struct { CommandInfo :: struct {
@@ -28,7 +30,10 @@ COMMANDS := []CommandInfo {
"init", "init",
"envr init", "envr init",
"Set up envr", "Set up envr",
"The init command generates your initial config and saves it to\n~/.envr/config in JSON format.\n\nDuring setup, you will be prompted to select one or more ssh keys with which to\nencrypt your databse. **Make 100% sure** that you have **a remote copy** of this\nkey somewhere, otherwise your data could be lost forever.", `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
encrypt your databse. **Make 100% sure** that you have **a remote copy** of this
key somewhere, otherwise your data could be lost forever.`,
{}, {},
}, },
{"scan", "envr scan", "Find and select .env files for backup", "", {}}, {"scan", "envr scan", "Find and select .env files for backup", "", {}},
@@ -60,15 +65,23 @@ delete_command :: proc(cmd: ^Command) {
delete(cmd.args) delete(cmd.args)
delete(cmd.flags) delete(cmd.flags)
delete(cmd.bool_set) delete(cmd.bool_set)
// delete(cmd.config_path) bufio.writer_destroy(cmd.out_buf)
free(cmd.out_buf)
} }
// Caller is responsible for calling delete_command(cmd). // Caller is responsible for calling delete_command(cmd).
// FIXME: Works in kinda a wonky and awkward way. // FIXME: Works in kinda a wonky and awkward way.
parse_args :: proc(args: []string) -> (cmd: Command, ok: bool) { parse_args :: proc(args: []string, out: io.Stream, err: io.Stream) -> (cmd: Command, ok: bool) {
{
cmd.out_buf = new(bufio.Writer)
bufio.writer_init(cmd.out_buf, out)
cmd.out = bufio.writer_to_writer(cmd.out_buf)
cmd.err = err
}
if len(args) < 2 || args[1] == "--help" || args[1] == "-h" { if len(args) < 2 || args[1] == "--help" || args[1] == "-h" {
print_usage() write_usage(cmd.out)
return Command{}, false return cmd, false
} }
cmd.name = args[1] cmd.name = args[1]
@@ -117,8 +130,8 @@ parse_args :: proc(args: []string) -> (cmd: Command, ok: bool) {
} }
if has_flag(&cmd, "help") { if has_flag(&cmd, "help") {
print_command_help(cmd.name) print_command_help(&cmd)
return Command{}, false return cmd, false
} }
return cmd, true return cmd, true
@@ -177,18 +190,12 @@ write_command_help :: proc(name: string, w: io.Writer) -> bool {
return true return true
} }
print_command_help :: proc(name: string) { print_command_help :: proc(cmd: ^Command) {
bw: bufio.Writer ok := write_command_help(cmd.name, cmd.out)
bufio.writer_init(&bw, io.to_writer(os.to_writer(os.stdout)), mem.DEFAULT_PAGE_SIZE)
defer bufio.writer_destroy(&bw)
w := bufio.writer_to_writer(&bw)
ok := write_command_help(name, w)
if !ok { if !ok {
fmt.printf("Unknown command: %s\n", name) fmt.wprintf(cmd.err, "Unknown command: %s\n", cmd.name)
print_usage() write_usage(cmd.out)
} }
bufio.writer_flush(&bw)
} }
// TODO: command args should be shown in usage. // TODO: command args should be shown in usage.
@@ -263,13 +270,3 @@ Use "envr [command] --help" for more information about a command.
) )
} }
// TODO: Look at usages,might want to pass a writer
print_usage :: proc() {
bw: bufio.Writer
bufio.writer_init(&bw, io.to_writer(os.to_writer(os.stdout)), mem.DEFAULT_PAGE_SIZE)
defer bufio.writer_destroy(&bw)
defer bufio.writer_flush(&bw)
write_usage(bufio.writer_to_writer(&bw))
}

View File

@@ -2,6 +2,7 @@
package main package main
import "core:bufio" import "core:bufio"
import "core:fmt"
import "core:strings" import "core:strings"
import "core:testing" import "core:testing"
@@ -189,9 +190,35 @@ test_has_flag_empty_command :: proc(t: ^testing.T) {
} }
test_parse_args :: proc( test_parse_args :: proc(
args: []string,
) -> (
cmd: Command,
ok: bool,
out_text: string,
err_text: string,
) {
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(args, strings.to_stream(&out_b), strings.to_stream(&err_b))
if ok {
bufio.writer_flush(cmd.out_buf)
out_text = strings.to_string(out_b)
err_text = strings.to_string(err_b)
}
return
}
@(test)
test_parse_args_bare_command :: proc(t: ^testing.T) { test_parse_args_bare_command :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "list"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -204,10 +231,9 @@ test_parse_args_bare_command :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_positional :: proc(t: ^testing.T) { test_parse_args_positional :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "/project/.env"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "/project/.env"})
testing.expect(t, ok, "should succeed") defer delete_command(&cmd)
if !ok do return
defer delete_command(&cmd)
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
testing.expect(t, cmd.name == "backup") testing.expect(t, cmd.name == "backup")
testing.expect(t, len(cmd.args) == 1) testing.expect(t, len(cmd.args) == 1)
testing.expect(t, cmd.args[0] == "/project/.env") testing.expect(t, cmd.args[0] == "/project/.env")
@@ -216,7 +242,7 @@ test_parse_args_positional :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_long_flag_with_value :: proc(t: ^testing.T) { test_parse_args_long_flag_with_value :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "--config", "x.json"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "--config", "x.json"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -226,7 +252,7 @@ test_parse_args_long_flag_with_value :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_short_flag_with_value :: proc(t: ^testing.T) { test_parse_args_short_flag_with_value :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "-c", "x.json"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "sync", "-c", "x.json"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -236,7 +262,7 @@ test_parse_args_short_flag_with_value :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_long_bool_flag :: proc(t: ^testing.T) { test_parse_args_long_bool_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "init", "--force"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "init", "--force"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -246,7 +272,7 @@ test_parse_args_long_bool_flag :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_short_bool_flag :: proc(t: ^testing.T) { test_parse_args_short_bool_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "version", "-l"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "version", "-l"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -256,7 +282,7 @@ test_parse_args_short_bool_flag :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_multiple_positionals :: proc(t: ^testing.T) { test_parse_args_multiple_positionals :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "a", "b"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "a", "b"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -268,7 +294,7 @@ test_parse_args_multiple_positionals :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_mixed_flags_and_positionals :: proc(t: ^testing.T) { test_parse_args_mixed_flags_and_positionals :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "/project/.env", "--force"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "/project/.env", "--force"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -280,16 +306,16 @@ test_parse_args_mixed_flags_and_positionals :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_no_args :: proc(t: ^testing.T) { test_parse_args_no_args :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr"}) cmd, ok, _, _ := test_parse_args([]string{"envr"})
testing.expect(t, !ok, "no args should return false") defer delete_command(&cmd)
testing.expect(t, !ok, "no args should return false")
} }
@(test) @(test)
test_parse_args_flag_then_positional_then_flag :: proc(t: ^testing.T) { test_parse_args_flag_then_positional_then_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "a.env", "--force", "--verbose"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "backup", "a.env", "--force", "--verbose"})
testing.expect(t, ok, "should succeed") defer delete_command(&cmd)
if !ok do return
defer delete_command(&cmd)
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
testing.expect(t, cmd.bool_set["force"] == true) testing.expect(t, cmd.bool_set["force"] == true)
testing.expect(t, cmd.bool_set["verbose"] == true) testing.expect(t, cmd.bool_set["verbose"] == true)
testing.expect(t, len(cmd.args) == 1) testing.expect(t, len(cmd.args) == 1)
@@ -299,7 +325,9 @@ test_parse_args_flag_then_positional_then_flag :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_config_file_long_flag :: proc(t: ^testing.T) { test_parse_args_config_file_long_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args( cmd, ok, _, _ := test_parse_args(
testing.expect(t, ok, "should succeed") []string{"envr", "list", "--config-file", "/custom/config.json"},
)
testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -313,7 +341,7 @@ test_parse_args_config_file_long_flag :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_config_file_short_flag :: proc(t: ^testing.T) { test_parse_args_config_file_short_flag :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-c", "/custom/config.json"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-c", "/custom/config.json"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)
@@ -327,7 +355,7 @@ test_parse_args_config_file_short_flag :: proc(t: ^testing.T) {
@(test) @(test)
test_parse_args_config_file_defaults :: proc(t: ^testing.T) { test_parse_args_config_file_defaults :: proc(t: ^testing.T) {
cmd, ok, _, _ := test_parse_args([]string{"envr", "list"}) cmd, ok, _, _ := test_parse_args([]string{"envr", "list"})
testing.expect(t, ok, "should succeed") testing.expect(t, ok, "should succeed")
if !ok do return if !ok do return
defer delete_command(&cmd) defer delete_command(&cmd)

View File

@@ -5,13 +5,13 @@ import "core:strings"
cmd_backup :: proc(cmd: ^Command) { cmd_backup :: proc(cmd: ^Command) {
if len(cmd.args) != 1 { if len(cmd.args) != 1 {
print_command_help("backup") print_command_help(cmd)
return return
} }
path := cmd.args[0] path := cmd.args[0]
if len(strings.trim_space(path)) == 0 { if len(strings.trim_space(path)) == 0 {
fmt.println("Error: No path provided") fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
return return
} }
@@ -30,6 +30,6 @@ cmd_backup :: proc(cmd: ^Command) {
return return
} }
fmt.printf("Saved %s into the database\n", path) fmt.wprintf(cmd.out, "Saved %s into the database\n", path, flush = false)
} }

View File

@@ -13,7 +13,7 @@ cmd_check :: proc(cmd: ^Command) {
} else { } else {
cwd, cwd_err := os.get_working_directory(context.temp_allocator) cwd, cwd_err := os.get_working_directory(context.temp_allocator)
if cwd_err != nil { if cwd_err != nil {
fmt.printf("Error getting current directory: %v\n", cwd_err) fmt.wprintf(cmd.err, "Error getting current directory: %v\n", cwd_err, flush = false)
return return
} }
check_path = cwd check_path = cwd
@@ -25,7 +25,7 @@ cmd_check :: proc(cmd: ^Command) {
} else { } else {
resolved, abs_err := filepath.abs(check_path) resolved, abs_err := filepath.abs(check_path)
if abs_err != nil { if abs_err != nil {
fmt.printf("Error getting absolute path: %v\n", abs_err) fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return return
} }
abs_path = resolved abs_path = resolved
@@ -43,15 +43,17 @@ cmd_check :: proc(cmd: ^Command) {
if is_dir { if is_dir {
if cant_scan(feats) { if cant_scan(feats) {
fmt.println( fmt.wprintln(
cmd.err,
"Error: please install fd to use the check command (https://github.com/sharkdp/fd)", "Error: please install fd to use the check command (https://github.com/sharkdp/fd)",
flush = false,
) )
return return
} }
scanned, scan_ok := scan_path(abs_path, db.cfg) scanned, scan_ok := scan_path(abs_path, db.cfg)
if !scan_ok { if !scan_ok {
fmt.println("Error scanning directory for .env files") fmt.wprintln(cmd.err, "Error scanning directory for .env files", flush = false)
return return
} }
files_in_path = scanned files_in_path = scanned
@@ -68,16 +70,15 @@ cmd_check :: proc(cmd: ^Command) {
if len(not_backed) == 0 { if len(not_backed) == 0 {
if len(files_in_path) == 0 { if len(files_in_path) == 0 {
fmt.println("No .env files found in the specified directory.") fmt.wprintln(cmd.out, "No .env files found in the specified directory.", flush = false)
} else { } else {
fmt.println("✓ All .env files in the directory are backed up.") fmt.wprintln(cmd.out, "✓ All .env files in the directory are backed up.", flush = false)
} }
} else { } else {
fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed)) fmt.wprintf(cmd.out, "Found %d .env file(s) that are not backed up:\n", len(not_backed), flush = false)
for file in not_backed { for file in not_backed {
fmt.printf(" %s\n", file) fmt.wprintf(cmd.out, " %s\n", file, flush = false)
} }
fmt.println("\nRun 'envr sync' to back up these files.") fmt.wprintln(cmd.out, "\nRun 'envr sync' to back up these files.", flush = false)
} }
} }

View File

@@ -1,6 +1,6 @@
package main package main
import "core:io" import "core:fmt"
import "core:os" import "core:os"
import "core:terminal" import "core:terminal"
@@ -24,12 +24,10 @@ cmd_deps :: proc(cmd: ^Command) {
} }
if terminal.is_terminal(os.stdout) { if terminal.is_terminal(os.stdout) {
w := io.to_writer(os.to_writer(os.stdout)) render_table(cmd.out, headers, rows[:])
render_table(w, headers, rows[:])
} else { } else {
w := io.to_writer(os.to_writer(os.stdout)) render_json_rows(cmd.out, headers, rows[:])
render_json_rows(w, headers, rows[:]) fmt.wprint(cmd.out, "\n", flush = false)
io.write_string(w, "\n")
} }
} }

View File

@@ -6,7 +6,7 @@ import "core:os"
cmd_edit_config :: proc(cmd: ^Command) { cmd_edit_config :: proc(cmd: ^Command) {
editor := os.get_env("EDITOR", context.allocator) editor := os.get_env("EDITOR", context.allocator)
if len(editor) == 0 { if len(editor) == 0 {
fmt.println("Error: $EDITOR environment variable is not set") fmt.wprintln(cmd.err, "Error: $EDITOR environment variable is not set", flush = false)
return return
} }
@@ -14,7 +14,12 @@ cmd_edit_config :: proc(cmd: ^Command) {
_, stat_err := os.stat(config_path, context.allocator) _, stat_err := os.stat(config_path, context.allocator)
if stat_err != nil { if stat_err != nil {
fmt.printf("Config file does not exist at %s. Run 'envr init' first.\n", config_path) fmt.wprintf(
cmd.err,
"Config file does not exist at %s. Run 'envr init' first.\n",
config_path,
flush = false,
)
return return
} }
@@ -28,13 +33,13 @@ cmd_edit_config :: proc(cmd: ^Command) {
p, start_err := os.process_start(desc) p, start_err := os.process_start(desc)
if start_err != nil { if start_err != nil {
fmt.printf("Error running editor: %v\n", start_err) fmt.wprintf(cmd.err, "Error running editor: %v\n", start_err, flush = false)
return return
} }
state, wait_err := os.process_wait(p) state, wait_err := os.process_wait(p)
if wait_err != nil { if wait_err != nil {
fmt.printf("Error waiting for editor: %v\n", wait_err) fmt.wprintf(cmd.err, "Error waiting for editor: %v\n", wait_err, flush = false)
return return
} }
if state.exit_code != 0 { if state.exit_code != 0 {

View File

@@ -5,13 +5,15 @@ import "core:fmt"
cmd_init :: proc(cmd: ^Command) { cmd_init :: proc(cmd: ^Command) {
force := has_flag(cmd, "force") || has_flag(cmd, "f") force := has_flag(cmd, "force") || has_flag(cmd, "f")
fmt.println(cmd.config_path) fmt.wprintln(cmd.out, cmd.config_path, flush = false)
_, cfg_exists := load_config(cmd.config_path) _, cfg_exists := load_config(cmd.config_path)
if cfg_exists && !force { if cfg_exists && !force {
fmt.println( fmt.wprintln(
cmd.out,
`You have already initialized envr. `You have already initialized envr.
Run again with the --force flag if you want to reinitialize.`, Run again with the --force flag if you want to reinitialize.`,
flush = false,
) )
return return
} }
@@ -22,15 +24,15 @@ Run again with the --force flag if you want to reinitialize.`,
} }
if len(keys) == 0 { if len(keys) == 0 {
fmt.println(`No ssh-ed25519 keys found in ~/.ssh fmt.wprintln(cmd.err, `No ssh-ed25519 keys found in ~/.ssh
Generate one with: ssh-keygen -t ed25519`) Generate one with: ssh-keygen -t ed25519`, flush = false)
return return
} }
selected, result := multi_select("Select SSH private keys:", keys[:]) selected, result := multi_select("Select SSH private keys:", keys[:])
defer delete(selected) defer delete(selected)
if result == .Cancel { if result == .Cancel {
fmt.println("\x1b[2mCancelled.\x1b[0m") fmt.wprintln(cmd.out, "\x1b[2mCancelled.\x1b[0m", flush = false)
return return
} }
@@ -42,7 +44,7 @@ Generate one with: ssh-keygen -t ed25519`)
} }
if len(selected_paths) == 0 { if len(selected_paths) == 0 {
fmt.println("No SSH keys selected - Config not created") fmt.wprintln(cmd.err, "No SSH keys selected - Config not created", flush = false)
return return
} }
@@ -51,9 +53,10 @@ Generate one with: ssh-keygen -t ed25519`)
return return
} }
fmt.printf( fmt.wprintf(
cmd.out,
"Config initialized with %d SSH key(s). You are ready to use envr.\n", "Config initialized with %d SSH key(s). You are ready to use envr.\n",
len(selected_paths), len(selected_paths),
flush = false,
) )
} }

View File

@@ -2,7 +2,6 @@ package main
import "core:encoding/json" import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:io"
import "core:os" import "core:os"
import "core:path/filepath" import "core:path/filepath"
import "core:strings" import "core:strings"
@@ -41,8 +40,7 @@ cmd_list :: proc(cmd: ^Command) {
append(&table_rows, row_slice) append(&table_rows, row_slice)
} }
w := io.to_writer(os.to_writer(os.stdout)) render_table(cmd.out, headers, table_rows[:])
render_table(w, headers, table_rows[:])
} else { } else {
// TODO: Should we instead print full entries here? // TODO: Should we instead print full entries here?
entries: [dynamic]ListEntry entries: [dynamic]ListEntry
@@ -59,10 +57,10 @@ cmd_list :: proc(cmd: ^Command) {
data, marshal_err := json.marshal(entries[:], allocator = context.temp_allocator) data, marshal_err := json.marshal(entries[:], allocator = context.temp_allocator)
if marshal_err != nil { if marshal_err != nil {
fmt.printf("Error marshaling JSON: %v\n", marshal_err) fmt.wprintf(cmd.err, "Error marshaling JSON: %v\n", marshal_err, flush = false)
return return
} }
fmt.println(string(data)) fmt.wprintln(cmd.out, string(data), flush = false)
} }
} }

View File

@@ -5,7 +5,6 @@ import "core:fmt"
COMPLETION_SCRIPT: string : string(#load("mod.nu")) COMPLETION_SCRIPT: string : string(#load("mod.nu"))
cmd_nushell_completion :: proc(cmd: ^Command) { cmd_nushell_completion :: proc(cmd: ^Command) {
// TODO: Use buffered writer? fmt.wprint(cmd.out, COMPLETION_SCRIPT, flush = false)
fmt.print(COMPLETION_SCRIPT)
} }

View File

@@ -6,13 +6,13 @@ import "core:strings"
cmd_remove :: proc(cmd: ^Command) { cmd_remove :: proc(cmd: ^Command) {
if len(cmd.args) != 1 { if len(cmd.args) != 1 {
print_command_help("remove") print_command_help(cmd)
return return
} }
path := cmd.args[0] path := cmd.args[0]
if len(strings.trim_space(path)) == 0 { if len(strings.trim_space(path)) == 0 {
fmt.println("Error: No path provided") fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
return return
} }
@@ -23,7 +23,7 @@ cmd_remove :: proc(cmd: ^Command) {
} else { } else {
resolved, abs_err := filepath.abs(path) resolved, abs_err := filepath.abs(path)
if abs_err != nil { if abs_err != nil {
fmt.printf("Error getting absolute path: %v\n", abs_err) fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return return
} }
abs_path = resolved abs_path = resolved
@@ -39,6 +39,6 @@ cmd_remove :: proc(cmd: ^Command) {
return return
} }
fmt.printf("Removed %s from the database\n", abs_path) fmt.wprintf(cmd.out, "Removed %s from the database\n", abs_path, flush = false)
} }

View File

@@ -7,13 +7,13 @@ import "core:strings"
cmd_restore :: proc(cmd: ^Command) { cmd_restore :: proc(cmd: ^Command) {
if len(cmd.args) != 1 { if len(cmd.args) != 1 {
print_command_help("restore") print_command_help(cmd)
return return
} }
path := cmd.args[0] path := cmd.args[0]
if len(strings.trim_space(path)) == 0 { if len(strings.trim_space(path)) == 0 {
fmt.println("Error: No path provided") fmt.wprintln(cmd.err, "Error: No path provided", flush = false)
return return
} }
@@ -24,7 +24,7 @@ cmd_restore :: proc(cmd: ^Command) {
} else { } else {
resolved, abs_err := filepath.abs(path) resolved, abs_err := filepath.abs(path)
if abs_err != nil { if abs_err != nil {
fmt.printf("Error getting absolute path: %v\n", abs_err) fmt.wprintf(cmd.err, "Error getting absolute path: %v\n", abs_err, flush = false)
return return
} }
abs_path = resolved abs_path = resolved
@@ -46,10 +46,10 @@ cmd_restore :: proc(cmd: ^Command) {
write_err := os.write_entire_file(file.Path, file.contents) write_err := os.write_entire_file(file.Path, file.contents)
if write_err != nil { if write_err != nil {
fmt.printf("Error writing file: %v\n", write_err) fmt.wprintf(cmd.err, "Error writing file: %v\n", write_err, flush = false)
return return
} }
fmt.printf("Restored %s\n", file.Path) fmt.wprintf(cmd.out, "Restored %s\n", file.Path, flush = false)
} }

View File

@@ -8,8 +8,10 @@ import "core:terminal"
cmd_scan :: proc(cmd: ^Command) { cmd_scan :: proc(cmd: ^Command) {
feats := check_features() feats := check_features()
if cant_scan(feats) { if cant_scan(feats) {
fmt.println( fmt.wprintln(
cmd.err,
"Error: please install fd to use the scan command (https://github.com/sharkdp/fd)", "Error: please install fd to use the scan command (https://github.com/sharkdp/fd)",
flush = false,
) )
return return
} }
@@ -22,7 +24,11 @@ cmd_scan :: proc(cmd: ^Command) {
search_dirs := search_paths(db.cfg) search_dirs := search_paths(db.cfg)
if len(search_dirs) == 0 { if len(search_dirs) == 0 {
fmt.println("No search paths configured. Please run `envr init -f` or edit your config.") fmt.wprintln(
cmd.err,
"No search paths configured. Please run `envr init -f` or edit your config.",
flush = false,
)
return return
} }
@@ -31,7 +37,7 @@ cmd_scan :: proc(cmd: ^Command) {
for dir in search_dirs { for dir in search_dirs {
found, scan_ok := scan_path(dir, db.cfg) found, scan_ok := scan_path(dir, db.cfg)
if !scan_ok { if !scan_ok {
fmt.printf("Error scanning %s\n", dir) fmt.wprintf(cmd.err, "Error scanning %s\n", dir, flush = false)
continue continue
} }
for f in found { for f in found {
@@ -47,24 +53,29 @@ cmd_scan :: proc(cmd: ^Command) {
files := find_unbacked(all_files[:], db_files[:]) files := find_unbacked(all_files[:], db_files[:])
if len(files) == 0 { if len(files) == 0 {
fmt.println("No .env files found to add.") fmt.wprintln(cmd.out, "No .env files found to add.", flush = false)
return return
} }
if !terminal.is_terminal(os.stdout) { if !terminal.is_terminal(os.stdout) {
output, marshal_err := json.marshal(files[:]) output, marshal_err := json.marshal(files[:])
if marshal_err != nil { if marshal_err != nil {
fmt.printf("Error marshaling files to JSON: %v\n", marshal_err) fmt.wprintf(
cmd.err,
"Error marshaling files to JSON: %v\n",
marshal_err,
flush = false,
)
return return
} }
fmt.println(string(output)) fmt.wprintln(cmd.out, string(output), flush = false)
return return
} }
selected, result := multi_select("Select .env files to backup:", files[:]) selected, result := multi_select("Select .env files to backup:", files[:])
defer delete(selected) defer delete(selected)
if result == .Cancel { if result == .Cancel {
fmt.println("\x1b[2mCancelled.\x1b[0m") fmt.wprintln(cmd.out, "\x1b[2mCancelled.\x1b[0m", flush = false)
return return
} }
@@ -75,20 +86,25 @@ cmd_scan :: proc(cmd: ^Command) {
} }
env_file, ok := new_env_file(files[i]) env_file, ok := new_env_file(files[i])
if !ok { if !ok {
fmt.printf("Error reading %s\n", files[i]) fmt.wprintf(cmd.err, "Error reading %s\n", files[i], flush = false)
continue continue
} }
if !db_insert(&db, env_file) { if !db_insert(&db, env_file) {
fmt.printf("Error adding %s\n", files[i]) fmt.wprintf(cmd.err, "Error adding %s\n", files[i], flush = false)
continue continue
} }
added_count += 1 added_count += 1
} }
if added_count > 0 { if added_count > 0 {
fmt.printf("\x1b[1;32mSuccessfully added %d file(s) to backup.\x1b[0m\n", added_count) fmt.wprintf(
cmd.out,
"\x1b[1;32mSuccessfully added %d file(s) to backup.\x1b[0m\n",
added_count,
flush = false,
)
} else { } else {
fmt.println("\x1b[2mNo files were added.\x1b[0m") fmt.wprintln(cmd.out, "\x1b[2mNo files were added.\x1b[0m", flush = false)
} }
} }

View File

@@ -2,7 +2,6 @@ package main
import "core:encoding/json" import "core:encoding/json"
import "core:fmt" import "core:fmt"
import "core:io"
import "core:os" import "core:os"
import "core:strings" import "core:strings"
import "core:terminal" import "core:terminal"
@@ -84,15 +83,14 @@ cmd_sync :: proc(cmd: ^Command) {
append(&table_rows, row_slice) append(&table_rows, row_slice)
} }
w := io.to_writer(os.to_writer(os.stdout)) render_table(cmd.out, headers, table_rows[:])
render_table(w, headers, table_rows[:])
} else { } else {
data, marshal_err := json.marshal(results[:]) data, marshal_err := json.marshal(results[:])
if marshal_err != nil { if marshal_err != nil {
fmt.printf("Error marshaling JSON: %v\n", marshal_err) fmt.wprintf(cmd.err, "Error marshaling JSON: %v\n", marshal_err, flush = false)
return return
} }
fmt.println(string(data)) fmt.wprintln(cmd.out, string(data), flush = false)
} }
} }

View File

@@ -5,6 +5,6 @@ import "core:fmt"
VERSION :: #load("version.txt", string) VERSION :: #load("version.txt", string)
cmd_version :: proc(cmd: ^Command) { cmd_version :: proc(cmd: ^Command) {
fmt.println(VERSION) fmt.wprintln(cmd.out, VERSION, flush = false)
} }

View File

@@ -51,7 +51,6 @@ delete_envfile :: proc(f: ^EnvFile) {
delete(f.contents) delete(f.contents)
} }
// TODO: Leak?
make_temp_path :: proc() -> string { make_temp_path :: proc() -> string {
ts := time.time_to_unix(time.now()) ts := time.time_to_unix(time.now())
b: strings.Builder b: strings.Builder

View File

@@ -1,10 +1,12 @@
package main package main
import "core:bufio"
import "core:fmt" import "core:fmt"
import "core:os" import "core:os"
main :: proc() { main :: proc() {
cmd, ok := parse_args(os.args) cmd, ok := parse_args(os.args, os.to_writer(os.stdout), os.to_writer(os.stderr))
defer bufio.writer_flush(cmd.out_buf)
if !ok { if !ok {
return return
} }
@@ -35,10 +37,9 @@ main :: proc() {
case "nushell-completion": case "nushell-completion":
cmd_nushell_completion(&cmd) cmd_nushell_completion(&cmd)
case: case:
fmt.printf("Unknown command: %s\n", cmd.name) fmt.wprintf(cmd.err, "Unknown command: %s\n", cmd.name)
print_usage() write_usage(cmd.out)
os.exit(1) os.exit(1)
} }
} }

View File

@@ -1,44 +0,0 @@
package main
import "core:fmt"
import "core:os"
main :: proc() {
cmd, ok := parse_args()
if !ok {
return
}
switch cmd.name {
case "init":
cmd_init(&cmd)
case "version":
cmd_version(&cmd)
case "deps":
cmd_deps(&cmd)
case "list":
cmd_list(&cmd)
case "backup", "add":
cmd_backup(&cmd)
case "remove":
cmd_remove(&cmd)
case "restore":
cmd_restore(&cmd)
case "edit-config":
cmd_edit_config(&cmd)
case "check":
cmd_check(&cmd)
case "scan":
cmd_scan(&cmd)
case "sync":
cmd_sync(&cmd)
case "nushell-completion":
cmd_nushell_completion(&cmd)
case:
fmt.printf("Unknown command: %s\n", cmd.name)
print_usage()
os.exit(1)
}
}