diff --git a/TODOS.md b/TODOS.md index 6ba7b35..3db58e7 100644 --- a/TODOS.md +++ b/TODOS.md @@ -14,19 +14,19 @@ 7. Add tests for untested commands. -8. add --format -f flag to commands that draw tables. +8. procedures should be ordered by use, main at the top, then in the order they are called from main. -9. procedures should be ordered by use, main at the top, then in the order they are called from main. +9. Shell completion -10. Shell completion +10. Bring back windows support / cross-compilation. -11. Bring back windows support / cross-compilation. +11. Test all cmds / terminal branches. -12. Test all cmds / terminal branches. +12. Pass allocator to findr? -13. Pass allocator to findr? +13. Update `read_wire_string` to use a slice. -14. Update `read_wire_string` to use a slice. +14. `-h` short flag seems to fail, at least with `envr list` ## Double-check AI output diff --git a/cli.odin b/cli.odin index e39462f..aaf52e3 100644 --- a/cli.odin +++ b/cli.odin @@ -5,6 +5,7 @@ import "core:fmt" import "core:io" import "core:os" import "core:strings" +import "core:terminal" import "core:text/table" Command :: struct { @@ -18,6 +19,11 @@ Command :: struct { err: io.Writer, } +Output_Format :: enum { + Table, + JSON, +} + CommandInfo :: struct { name: string, usage: string, @@ -288,6 +294,11 @@ at before, restore your backup with: COLOR_FLAGS + "-c, --config-file" + ANSI_RESET + " ", `config file (default "~/.envr/config.json")`, ) + table.row( + &tbl, + COLOR_FLAGS + "-f, --format" + ANSI_RESET + " 'json'|'table'", + `the format of output data. (default 'table', unless piping)`, + ) write_borderless_table(w, &tbl) fmt.wprintf( @@ -303,6 +314,22 @@ has_flag :: proc(cmd: ^Command, name: string) -> bool { return name in cmd.flags || name in cmd.bool_set } +get_format :: proc(cmd: ^Command) -> Output_Format { + flags :: []string{"format", "f"} + for name in flags { + if val, ok := cmd.flags[name]; ok { + switch val { + case "json": + return .JSON + case "table": + return .Table + } + } + } + + return terminal.is_terminal(os.stdout) ? .Table : .JSON +} + delete_command :: proc(cmd: ^Command) { bufio.writer_flush(cmd.out_buf) delete(cmd.args) diff --git a/cli_test.odin b/cli_test.odin index 56c2e58..06ce6fc 100644 --- a/cli_test.odin +++ b/cli_test.odin @@ -361,3 +361,43 @@ test_parse_args_config_file_defaults :: proc(t: ^testing.T) { ) } +@(test) +test_get_format_long_json :: proc(t: ^testing.T) { + cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "json"}) + testing.expect(t, ok, "should succeed") + if !ok do return + defer delete_command(&cmd) + + testing.expect_value(t, get_format(&cmd), Output_Format.JSON) +} + +@(test) +test_get_format_short_json :: proc(t: ^testing.T) { + cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "json"}) + testing.expect(t, ok, "should succeed") + if !ok do return + defer delete_command(&cmd) + + testing.expect_value(t, get_format(&cmd), Output_Format.JSON) +} + +@(test) +test_get_format_long_table :: proc(t: ^testing.T) { + cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "--format", "table"}) + testing.expect(t, ok, "should succeed") + if !ok do return + defer delete_command(&cmd) + + testing.expect_value(t, get_format(&cmd), Output_Format.Table) +} + +@(test) +test_get_format_short_table :: proc(t: ^testing.T) { + cmd, ok, _, _ := test_parse_args([]string{"envr", "list", "-f", "table"}) + testing.expect(t, ok, "should succeed") + if !ok do return + defer delete_command(&cmd) + + testing.expect_value(t, get_format(&cmd), Output_Format.Table) +} + diff --git a/cmd_list.odin b/cmd_list.odin index 1cf763d..56720e4 100644 --- a/cmd_list.odin +++ b/cmd_list.odin @@ -5,7 +5,6 @@ import "core:fmt" import "core:os" import "core:path/filepath" import "core:strings" -import "core:terminal" import "core:text/table" ListEntry :: struct { @@ -13,7 +12,6 @@ ListEntry :: struct { path: string `json:"path"`, } -// TODO: Support --format flag // TODO: Improve table rendering cmd_list :: proc(cmd: ^Command) { db, db_ok := db_open(cmd.config_path) @@ -27,7 +25,7 @@ cmd_list :: proc(cmd: ^Command) { return } - if terminal.is_terminal(os.stdout) { + if get_format(cmd) == .Table { t: table.Table table.init(&t, context.temp_allocator, context.temp_allocator) table.padding(&t, 1, 1) @@ -51,7 +49,7 @@ cmd_list :: proc(cmd: ^Command) { table.write_decorated_table(cmd.out, &t, decorations, ansi_aware_width) } else { // TODO: Should we instead print full entries here? - entries: [dynamic]ListEntry + entries := make([dynamic]ListEntry, 0, len(rows), context.temp_allocator) for row in rows { filename := filepath.base(row.path) append( diff --git a/cmd_list_test.odin b/cmd_list_test.odin index ee453e2..8ecefe1 100644 --- a/cmd_list_test.odin +++ b/cmd_list_test.odin @@ -1,7 +1,11 @@ +#+feature dynamic-literals #+test package main +import "core:bufio" +import "core:os" import "core:path/filepath" +import "core:strings" import "core:testing" @(test) @@ -17,3 +21,95 @@ test_filepath_base_equals_rel :: proc(t: ^testing.T) { } } +@(test) +test_cmd_list_format_json :: proc(t: ^testing.T) { + base := test_temp_dir(t, "envr-test-list-json-*") + defer os.remove_all(base) + + cfg_path, _ := filepath.join([]string{base, "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) + testing.expect(t, db_insert(&db, f), "insert should succeed") + db_close(&db) + + 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", "list", "--format", "json", "--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_list(&cmd) + bufio.writer_flush(cmd.out_buf) + output := strings.to_string(out_b) + + testing.expect(t, strings.contains(output, "["), "json output should contain '['") + testing.expect( + t, + strings.contains(output, "\"directory\""), + "json output should contain directory key", + ) +} + +@(test) +test_cmd_list_format_table :: proc(t: ^testing.T) { + base := test_temp_dir(t, "envr-test-list-table-*") + defer os.remove_all(base) + + cfg_path, _ := filepath.join([]string{base, "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) + testing.expect(t, db_insert(&db, f), "insert should succeed") + db_close(&db) + + 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", "list", "--format", "table", "--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_list(&cmd) + bufio.writer_flush(cmd.out_buf) + output := strings.to_string(out_b) + + testing.expect(t, strings.contains(output, "│"), "table output should contain border chars") + testing.expect( + t, + strings.contains(output, "Directory"), + "table output should contain Directory header", + ) +} + diff --git a/cmd_sync.odin b/cmd_sync.odin index 17fc160..420bb16 100644 --- a/cmd_sync.odin +++ b/cmd_sync.odin @@ -2,8 +2,6 @@ package main import "core:encoding/json" import "core:fmt" -import "core:os" -import "core:terminal" import "core:text/table" SyncEntry :: struct { @@ -12,7 +10,6 @@ SyncEntry :: struct { } // TODO: Check for quiet failures. -// TODO: Support --format -f flags cmd_sync :: proc(cmd: ^Command) { db, db_ok := db_open(cmd.config_path) if !db_ok { @@ -49,7 +46,7 @@ cmd_sync :: proc(cmd: ^Command) { } } - if terminal.is_terminal(os.stdout) { + if get_format(cmd) == .Table { t: table.Table table.init(&t, context.temp_allocator, context.temp_allocator) table.padding(&t, 1, 1) diff --git a/db.odin b/db.odin index 5ec8853..4bda676 100644 --- a/db.odin +++ b/db.odin @@ -172,7 +172,7 @@ db_close :: proc(db: ^Db) { } sz: i64 - data := sqlite.serialize(db.conn, "main", &sz, 0) + data := sqlite.serialize(db.conn, "main", &sz, {}) if data == nil { fmt.eprintln("Error: failed to serialize database") return diff --git a/sqlite/sqlite.odin b/sqlite/sqlite.odin index 53f7097..40371cb 100644 --- a/sqlite/sqlite.odin +++ b/sqlite/sqlite.odin @@ -17,6 +17,12 @@ DESERIALIZE_FLAGS :: bit_set[DESERIALIZE_FLAG] DESERIALIZE_FLAG :: enum u32 { FREEONCLOSE = 1, RESIZEABLE = 2, + READONLY = 4, +} + +SERIALIZE_FLAGS :: bit_set[SERIALIZE_FLAG] +SERIALIZE_FLAG :: enum u32 { + NOCOPY = 1, } foreign lib { @@ -43,7 +49,7 @@ foreign lib { @(link_name = "sqlite3_changes") changes :: proc(db: Db) -> c.int --- @(link_name = "sqlite3_serialize") - serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: u32) -> [^]u8 --- + serialize :: proc(db: Db, zSchema: cstring, piSize: ^i64, mFlags: SERIALIZE_FLAGS) -> [^]u8 --- @(link_name = "sqlite3_deserialize") deserialize :: proc(db: Db, zSchema: cstring, pData: [^]u8, szDb: i64, szBuf: i64, mFlags: DESERIALIZE_FLAGS) -> c.int --- @(link_name = "sqlite3_malloc64")