1 Commits

Author SHA1 Message Date
e74fc4f35a feat: Added --format, -f flag.
Allows printing data in tabular or json format.
2026-06-25 17:51:31 -04:00
7 changed files with 175 additions and 9 deletions

View File

@@ -28,6 +28,8 @@
14. Update `read_wire_string` to use a slice.
15. `-h` short flag seems to fail, at least with `envr list`
## Double-check AI output
- [ ] cli.odin

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

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