feat: Added --format, -f flag.

Allows printing data in tabular or json format.
This commit is contained in:
2026-06-25 17:34:30 -04:00
parent 6fa68d10b1
commit d5981d7b88
8 changed files with 181 additions and 17 deletions

View File

@@ -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

View File

@@ -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 + " <path>",
`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)

View File

@@ -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)
}

View File

@@ -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(

View File

@@ -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",
)
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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")