mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 10:38:33 -04:00
refactor: Fixed a number of memory leaks.
This commit is contained in:
78
cli.odin
78
cli.odin
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import "core:bufio"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
import "core:io"
|
||||||
|
import "core:mem"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
@@ -19,10 +22,14 @@ CommandInfo :: struct {
|
|||||||
aliases: []string,
|
aliases: []string,
|
||||||
}
|
}
|
||||||
|
|
||||||
COMMANDS := []CommandInfo{
|
COMMANDS := []CommandInfo {
|
||||||
{"init", "envr init", "Set up envr",
|
{
|
||||||
|
"init",
|
||||||
|
"envr init",
|
||||||
|
"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\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.",
|
||||||
{}},
|
{},
|
||||||
|
},
|
||||||
{"scan", "envr scan", "Find and select .env files for backup", "", {}},
|
{"scan", "envr scan", "Find and select .env files for backup", "", {}},
|
||||||
{"sync", "envr sync", "Update or restore your env backups", "", {}},
|
{"sync", "envr sync", "Update or restore your env backups", "", {}},
|
||||||
{"backup", "envr backup <path>", "Import a .env file into envr", "", {"add"}},
|
{"backup", "envr backup <path>", "Import a .env file into envr", "", {"add"}},
|
||||||
@@ -30,9 +37,13 @@ COMMANDS := []CommandInfo{
|
|||||||
{"list", "envr list", "View your tracked files", "", {}},
|
{"list", "envr list", "View your tracked files", "", {}},
|
||||||
{"remove", "envr remove <path>", "Remove a .env file from your database", "", {}},
|
{"remove", "envr remove <path>", "Remove a .env file from your database", "", {}},
|
||||||
{"check", "envr check [path]", "Check if files are backed up", "", {}},
|
{"check", "envr check [path]", "Check if files are backed up", "", {}},
|
||||||
{"deps", "envr deps", "Check for missing binaries",
|
{
|
||||||
|
"deps",
|
||||||
|
"envr deps",
|
||||||
|
"Check for missing binaries",
|
||||||
"envr relies on external binaries for certain functionality.\n\nThe check command reports on which binaries are available and which are not.",
|
"envr relies on external binaries for certain functionality.\n\nThe check command reports on which binaries are available and which are not.",
|
||||||
{}},
|
{},
|
||||||
|
},
|
||||||
{"version", "envr version", "Show envr's version", "", {}},
|
{"version", "envr version", "Show envr's version", "", {}},
|
||||||
{"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}},
|
{"edit-config", "envr edit-config", "Edit your config with your default editor", "", {}},
|
||||||
}
|
}
|
||||||
@@ -60,8 +71,8 @@ parse_args :: proc() -> (cmd: Command, ok: bool) {
|
|||||||
arg := args[i]
|
arg := args[i]
|
||||||
if strings.starts_with(arg, "--") {
|
if strings.starts_with(arg, "--") {
|
||||||
key := arg[2:]
|
key := arg[2:]
|
||||||
if i+1 < len(args) && !strings.starts_with(args[i+1], "-") {
|
if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
|
||||||
cmd.flags[key] = args[i+1]
|
cmd.flags[key] = args[i + 1]
|
||||||
i += 2
|
i += 2
|
||||||
} else {
|
} else {
|
||||||
cmd.bool_set[key] = true
|
cmd.bool_set[key] = true
|
||||||
@@ -69,8 +80,8 @@ parse_args :: proc() -> (cmd: Command, ok: bool) {
|
|||||||
}
|
}
|
||||||
} else if strings.starts_with(arg, "-") && len(arg) == 2 {
|
} else if strings.starts_with(arg, "-") && len(arg) == 2 {
|
||||||
key_slice := arg[1:2]
|
key_slice := arg[1:2]
|
||||||
if i+1 < len(args) && !strings.starts_with(args[i+1], "-") {
|
if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
|
||||||
cmd.flags[key_slice] = args[i+1]
|
cmd.flags[key_slice] = args[i + 1]
|
||||||
i += 2
|
i += 2
|
||||||
} else {
|
} else {
|
||||||
cmd.bool_set[key_slice] = true
|
cmd.bool_set[key_slice] = true
|
||||||
@@ -113,45 +124,43 @@ find_command :: proc(name: string) -> (CommandInfo, bool) {
|
|||||||
return CommandInfo{}, false
|
return CommandInfo{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
command_help_text :: proc(name: string) -> (string, bool) {
|
write_command_help :: proc(name: string, w: io.Writer) -> bool {
|
||||||
info, found := find_command(name)
|
info, found := find_command(name)
|
||||||
if !found {
|
if !found {
|
||||||
return "", false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
b: strings.Builder
|
fmt.wprintf(w, "Usage: %s [flags]\n\n", info.usage, flush = false)
|
||||||
strings.builder_init(&b)
|
fmt.wprintf(w, "%s\n", info.short, flush = false)
|
||||||
|
|
||||||
fmt.sbprintf(&b, "Usage: %s [flags]\n\n", info.usage)
|
|
||||||
fmt.sbprintf(&b, "%s\n", info.short)
|
|
||||||
|
|
||||||
if len(info.aliases) > 0 {
|
if len(info.aliases) > 0 {
|
||||||
fmt.sbprintf(&b, "\nAliases:\n %s", info.name)
|
fmt.wprintf(w, "\nAliases:\n %s", info.name, flush = false)
|
||||||
for a in info.aliases {
|
for a in info.aliases {
|
||||||
fmt.sbprintf(&b, ", %s", a)
|
fmt.wprintf(w, ", %s", a, flush = false)
|
||||||
}
|
}
|
||||||
fmt.sbprintf(&b, "\n")
|
fmt.wprintf(w, "\n", flush = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(info.long) > 0 {
|
if len(info.long) > 0 {
|
||||||
fmt.sbprintf(&b, "\n%s\n", info.long)
|
fmt.wprintf(w, "\n%s\n", info.long, flush = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.sbprintf(&b, "\nFlags:\n -h, --help help for %s\n", info.name)
|
fmt.wprintf(w, "\nFlags:\n -h, --help help for %s\n", info.name, flush = false)
|
||||||
|
return true
|
||||||
s := strings.clone(strings.to_string(b))
|
|
||||||
strings.builder_destroy(&b)
|
|
||||||
return s, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print_command_help :: proc(name: string) {
|
print_command_help :: proc(name: string) {
|
||||||
text, ok := command_help_text(name)
|
bw: bufio.Writer
|
||||||
|
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.printf("Unknown command: %s\n", name)
|
||||||
print_usage()
|
print_usage()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
fmt.println(text)
|
bufio.writer_flush(&bw)
|
||||||
}
|
}
|
||||||
|
|
||||||
usage_text :: proc() -> string {
|
usage_text :: proc() -> string {
|
||||||
@@ -159,7 +168,10 @@ usage_text :: proc() -> string {
|
|||||||
strings.builder_init(&b)
|
strings.builder_init(&b)
|
||||||
|
|
||||||
fmt.sbprintf(&b, "envr keeps your .env synced to a local, age encrypted database.\n")
|
fmt.sbprintf(&b, "envr keeps your .env synced to a local, age encrypted database.\n")
|
||||||
fmt.sbprintf(&b, "Is a safe and easy way to gather all your .env files in one place where they can\n")
|
fmt.sbprintf(
|
||||||
|
&b,
|
||||||
|
"Is a safe and easy way to gather all your .env files in one place where they can\n",
|
||||||
|
)
|
||||||
fmt.sbprintf(&b, "easily be backed by another tool such as restic or git.\n")
|
fmt.sbprintf(&b, "easily be backed by another tool such as restic or git.\n")
|
||||||
fmt.sbprintf(&b, "\n")
|
fmt.sbprintf(&b, "\n")
|
||||||
fmt.sbprintf(&b, "All your data is stored in ~/data.age\n")
|
fmt.sbprintf(&b, "All your data is stored in ~/data.age\n")
|
||||||
@@ -184,7 +196,10 @@ usage_text :: proc() -> string {
|
|||||||
fmt.sbprintf(&b, "\n")
|
fmt.sbprintf(&b, "\n")
|
||||||
fmt.sbprintf(&b, "> envr sync\n")
|
fmt.sbprintf(&b, "> envr sync\n")
|
||||||
fmt.sbprintf(&b, "\n")
|
fmt.sbprintf(&b, "\n")
|
||||||
fmt.sbprintf(&b, "5. If you lose a repository, after re-cloning the repo into the same path it was\n")
|
fmt.sbprintf(
|
||||||
|
&b,
|
||||||
|
"5. If you lose a repository, after re-cloning the repo into the same path it was\n",
|
||||||
|
)
|
||||||
fmt.sbprintf(&b, "at before, restore your backup with:\n")
|
fmt.sbprintf(&b, "at before, restore your backup with:\n")
|
||||||
fmt.sbprintf(&b, "\n")
|
fmt.sbprintf(&b, "\n")
|
||||||
fmt.sbprintf(&b, "> envr restore ~/<path to repository>/.env\n")
|
fmt.sbprintf(&b, "> envr restore ~/<path to repository>/.env\n")
|
||||||
@@ -203,7 +218,7 @@ usage_text :: proc() -> string {
|
|||||||
name_len := len(b.buf) - name_start
|
name_len := len(b.buf) - name_start
|
||||||
padding := 20 - name_len
|
padding := 20 - name_len
|
||||||
if padding > 0 {
|
if padding > 0 {
|
||||||
for _ in 0..<padding {
|
for _ in 0 ..< padding {
|
||||||
strings.write_byte(&b, ' ')
|
strings.write_byte(&b, ' ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,3 +239,4 @@ usage_text :: proc() -> string {
|
|||||||
print_usage :: proc() {
|
print_usage :: proc() {
|
||||||
fmt.print(usage_text())
|
fmt.print(usage_text())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,7 @@ test_usage_text_contains_steps :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, strings.contains(text, "4."), "missing step 4")
|
testing.expect(t, strings.contains(text, "4."), "missing step 4")
|
||||||
testing.expect(t, strings.contains(text, "5."), "missing step 5")
|
testing.expect(t, strings.contains(text, "5."), "missing step 5")
|
||||||
testing.expect(t, strings.contains(text, "> envr sync\n"), "step 4 missing 'envr sync'")
|
testing.expect(t, strings.contains(text, "> envr sync\n"), "step 4 missing 'envr sync'")
|
||||||
testing.expect(
|
testing.expect(t, strings.contains(text, "> envr restore"), "step 5 missing 'envr restore'")
|
||||||
t,
|
|
||||||
strings.contains(text, "> envr restore"),
|
|
||||||
"step 5 missing 'envr restore'",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -47,42 +43,37 @@ test_usage_text_contains_flags_and_help_hint :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
|
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
|
||||||
testing.expect(t, strings.contains(text, "--help"), "missing --help flag")
|
testing.expect(t, strings.contains(text, "--help"), "missing --help flag")
|
||||||
testing.expect(
|
testing.expect(t, strings.contains(text, "Use \"envr [command] --help\""), "missing help hint")
|
||||||
t,
|
|
||||||
strings.contains(text, "Use \"envr [command] --help\""),
|
|
||||||
"missing help hint",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_command_help_backup :: proc(t: ^testing.T) {
|
test_command_help_backup :: proc(t: ^testing.T) {
|
||||||
text, ok := command_help_text("backup")
|
b: strings.Builder
|
||||||
testing.expect(t, ok, "command_help_text(\"backup\") returned false")
|
strings.builder_init(&b)
|
||||||
|
defer strings.builder_destroy(&b)
|
||||||
|
|
||||||
|
ok := write_command_help("backup", strings.to_writer(&b))
|
||||||
|
testing.expect(t, ok, "write_command_help(\"backup\") returned false")
|
||||||
|
|
||||||
|
text := strings.to_string(b)
|
||||||
testing.expect(t, strings.contains(text, "Usage:"), "missing Usage line")
|
testing.expect(t, strings.contains(text, "Usage:"), "missing Usage line")
|
||||||
testing.expect(
|
testing.expect(t, strings.contains(text, "envr backup <path>"), "missing usage pattern")
|
||||||
t,
|
testing.expect(t, strings.contains(text, "Aliases:"), "missing Aliases section")
|
||||||
strings.contains(text, "envr backup <path>"),
|
|
||||||
"missing usage pattern",
|
|
||||||
)
|
|
||||||
testing.expect(
|
|
||||||
t,
|
|
||||||
strings.contains(text, "Aliases:"),
|
|
||||||
"missing Aliases section",
|
|
||||||
)
|
|
||||||
testing.expect(t, strings.contains(text, "add"), "missing 'add' alias")
|
testing.expect(t, strings.contains(text, "add"), "missing 'add' alias")
|
||||||
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
|
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
|
||||||
testing.expect(
|
testing.expect(t, strings.contains(text, "--help"), "missing --help in flags")
|
||||||
t,
|
|
||||||
strings.contains(text, "--help"),
|
|
||||||
"missing --help in flags",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_command_help_add_alias :: proc(t: ^testing.T) {
|
test_command_help_add_alias :: proc(t: ^testing.T) {
|
||||||
text, ok := command_help_text("add")
|
b: strings.Builder
|
||||||
testing.expect(t, ok, "command_help_text(\"add\") returned false")
|
strings.builder_init(&b)
|
||||||
|
defer strings.builder_destroy(&b)
|
||||||
|
|
||||||
|
ok := write_command_help("add", strings.to_writer(&b))
|
||||||
|
testing.expect(t, ok, "write_command_help(\"add\") returned false")
|
||||||
|
|
||||||
|
text := strings.to_string(b)
|
||||||
testing.expect(
|
testing.expect(
|
||||||
t,
|
t,
|
||||||
strings.contains(text, "envr backup <path>"),
|
strings.contains(text, "envr backup <path>"),
|
||||||
@@ -93,34 +84,43 @@ test_command_help_add_alias :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_command_help_init_no_aliases :: proc(t: ^testing.T) {
|
test_command_help_init_no_aliases :: proc(t: ^testing.T) {
|
||||||
text, ok := command_help_text("init")
|
b: strings.Builder
|
||||||
testing.expect(t, ok, "command_help_text(\"init\") returned false")
|
strings.builder_init(&b)
|
||||||
|
defer strings.builder_destroy(&b)
|
||||||
|
|
||||||
|
ok := write_command_help("init", strings.to_writer(&b))
|
||||||
|
testing.expect(t, ok, "write_command_help(\"init\") returned false")
|
||||||
|
|
||||||
|
text := strings.to_string(b)
|
||||||
testing.expect(t, strings.contains(text, "Usage:"), "missing Usage line")
|
testing.expect(t, strings.contains(text, "Usage:"), "missing Usage line")
|
||||||
testing.expect(
|
testing.expect(t, !strings.contains(text, "Aliases:"), "init should not have Aliases section")
|
||||||
t,
|
|
||||||
!strings.contains(text, "Aliases:"),
|
|
||||||
"init should not have Aliases section",
|
|
||||||
)
|
|
||||||
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
|
testing.expect(t, strings.contains(text, "Flags:"), "missing Flags section")
|
||||||
testing.expect(
|
testing.expect(t, strings.contains(text, "help for init"), "missing 'help for init'")
|
||||||
t,
|
|
||||||
strings.contains(text, "help for init"),
|
|
||||||
"missing 'help for init'",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_command_help_unknown :: proc(t: ^testing.T) {
|
test_command_help_unknown :: proc(t: ^testing.T) {
|
||||||
text, ok := command_help_text("nonexistent")
|
b: strings.Builder
|
||||||
testing.expect(t, !ok, "command_help_text(\"nonexistent\") should return false")
|
strings.builder_init(&b)
|
||||||
|
defer strings.builder_destroy(&b)
|
||||||
|
|
||||||
|
ok := write_command_help("nonexistent", strings.to_writer(&b))
|
||||||
|
testing.expect(t, !ok, "write_command_help(\"nonexistent\") should return false")
|
||||||
|
|
||||||
|
text := strings.to_string(b)
|
||||||
testing.expect(t, len(text) == 0, "text should be empty for unknown command")
|
testing.expect(t, len(text) == 0, "text should be empty for unknown command")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_command_help_version :: proc(t: ^testing.T) {
|
test_command_help_version :: proc(t: ^testing.T) {
|
||||||
text, ok := command_help_text("version")
|
b: strings.Builder
|
||||||
testing.expect(t, ok, "command_help_text(\"version\") returned false")
|
strings.builder_init(&b)
|
||||||
|
defer strings.builder_destroy(&b)
|
||||||
|
|
||||||
|
ok := write_command_help("version", strings.to_writer(&b))
|
||||||
|
testing.expect(t, ok, "write_command_help(\"version\") returned false")
|
||||||
|
|
||||||
|
text := strings.to_string(b)
|
||||||
testing.expect(t, strings.contains(text, "Usage:"), "missing Usage line")
|
testing.expect(t, strings.contains(text, "Usage:"), "missing Usage line")
|
||||||
testing.expect(
|
testing.expect(
|
||||||
t,
|
t,
|
||||||
@@ -128,3 +128,4 @@ test_command_help_version :: proc(t: ^testing.T) {
|
|||||||
"version should not have Aliases section",
|
"version should not have Aliases section",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
127
cmd_check.odin
127
cmd_check.odin
@@ -6,74 +6,79 @@ import "core:path/filepath"
|
|||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
cmd_check :: proc(cmd: ^Command) {
|
cmd_check :: proc(cmd: ^Command) {
|
||||||
check_path: string
|
feats := check_features()
|
||||||
if len(cmd.args) > 0 {
|
|
||||||
check_path = cmd.args[0]
|
|
||||||
} else {
|
|
||||||
cwd, cwd_err := os.get_working_directory(context.allocator)
|
|
||||||
if cwd_err != nil {
|
|
||||||
fmt.printf("Error getting current directory: %v\n", cwd_err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
check_path = cwd
|
|
||||||
}
|
|
||||||
|
|
||||||
abs_path: string
|
check_path: string
|
||||||
if filepath.is_abs(check_path) {
|
if len(cmd.args) > 0 {
|
||||||
abs_path = check_path
|
check_path = cmd.args[0]
|
||||||
} else {
|
} else {
|
||||||
resolved, abs_err := filepath.abs(check_path)
|
cwd, cwd_err := os.get_working_directory(context.allocator)
|
||||||
if abs_err != nil {
|
if cwd_err != nil {
|
||||||
fmt.printf("Error getting absolute path: %v\n", abs_err)
|
fmt.printf("Error getting current directory: %v\n", cwd_err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
abs_path = resolved
|
check_path = cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
db, db_ok := db_open()
|
abs_path: string
|
||||||
if !db_ok {
|
if filepath.is_abs(check_path) {
|
||||||
return
|
abs_path = check_path
|
||||||
}
|
} else {
|
||||||
defer db_close(&db)
|
resolved, abs_err := filepath.abs(check_path)
|
||||||
|
if abs_err != nil {
|
||||||
|
fmt.printf("Error getting absolute path: %v\n", abs_err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
abs_path = resolved
|
||||||
|
}
|
||||||
|
|
||||||
is_dir := os.is_directory(abs_path)
|
db, db_ok := db_open()
|
||||||
|
if !db_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer db_close(&db)
|
||||||
|
|
||||||
files_in_path: [dynamic]string
|
is_dir := os.is_directory(abs_path)
|
||||||
|
|
||||||
if is_dir {
|
files_in_path: [dynamic]string
|
||||||
if !can_scan() {
|
|
||||||
fmt.println("Error: please install fd to use the check command (https://github.com/sharkdp/fd)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scanned, scan_ok := scan_path(abs_path, db.cfg)
|
if is_dir {
|
||||||
if !scan_ok {
|
if cant_scan(feats) {
|
||||||
fmt.println("Error scanning directory for .env files")
|
fmt.println(
|
||||||
return
|
"Error: please install fd to use the check command (https://github.com/sharkdp/fd)",
|
||||||
}
|
)
|
||||||
files_in_path = scanned
|
return
|
||||||
} else {
|
}
|
||||||
append(&files_in_path, abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
db_files, list_ok := db_list(&db)
|
scanned, scan_ok := scan_path(abs_path, db.cfg)
|
||||||
if !list_ok {
|
if !scan_ok {
|
||||||
return
|
fmt.println("Error scanning directory for .env files")
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
files_in_path = scanned
|
||||||
|
} else {
|
||||||
|
append(&files_in_path, abs_path)
|
||||||
|
}
|
||||||
|
|
||||||
not_backed := find_unbacked(files_in_path[:], db_files[:])
|
db_files, list_ok := db_list(&db)
|
||||||
|
if !list_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(not_backed) == 0 {
|
not_backed := find_unbacked(files_in_path[:], db_files[:])
|
||||||
if len(files_in_path) == 0 {
|
|
||||||
fmt.println("No .env files found in the specified directory.")
|
if len(not_backed) == 0 {
|
||||||
} else {
|
if len(files_in_path) == 0 {
|
||||||
fmt.println("✓ All .env files in the directory are backed up.")
|
fmt.println("No .env files found in the specified directory.")
|
||||||
}
|
} else {
|
||||||
} else {
|
fmt.println("✓ All .env files in the directory are backed up.")
|
||||||
fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed))
|
}
|
||||||
for file in not_backed {
|
} else {
|
||||||
fmt.printf(" %s\n", file)
|
fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed))
|
||||||
}
|
for file in not_backed {
|
||||||
fmt.println("\nRun 'envr sync' to back up these files.")
|
fmt.printf(" %s\n", file)
|
||||||
}
|
}
|
||||||
|
fmt.println("\nRun 'envr sync' to back up these files.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,39 +5,44 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
|
test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
|
||||||
local := []string{"/a/.env", "/b/.env", "/c/.env"}
|
local := []string{"/a/.env", "/b/.env", "/c/.env"}
|
||||||
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
|
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
|
||||||
|
|
||||||
result := find_unbacked(local, db[:])
|
result := find_unbacked(local, db[:])
|
||||||
testing.expect(t, len(result) == 1, fmt.aprintf("expected 1 unbacked, got %d", len(result)))
|
testing.expect(t, len(result) == 1, fmt.aprintf("expected 1 unbacked, got %d", len(result)))
|
||||||
if len(result) > 0 {
|
if len(result) > 0 {
|
||||||
testing.expect(t, result[0] == "/c/.env", fmt.aprintf("expected /c/.env, got %s", result[0]))
|
testing.expect(
|
||||||
}
|
t,
|
||||||
|
result[0] == "/c/.env",
|
||||||
|
fmt.aprintf("expected /c/.env, got %s", result[0]),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
|
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
|
||||||
local := []string{"/a/.env", "/b/.env"}
|
local := []string{"/a/.env", "/b/.env"}
|
||||||
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
|
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
|
||||||
|
|
||||||
result := find_unbacked(local, db[:])
|
result := find_unbacked(local, db[:])
|
||||||
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
|
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_find_unbacked_no_local :: proc(t: ^testing.T) {
|
test_find_unbacked_no_local :: proc(t: ^testing.T) {
|
||||||
local: []string
|
local: []string
|
||||||
db := []EnvFile{{Path = "/a/.env"}}
|
db := []EnvFile{{Path = "/a/.env"}}
|
||||||
|
|
||||||
result := find_unbacked(local, db[:])
|
result := find_unbacked(local, db[:])
|
||||||
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
|
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_find_unbacked_none_backed :: proc(t: ^testing.T) {
|
test_find_unbacked_none_backed :: proc(t: ^testing.T) {
|
||||||
local := []string{"/a/.env", "/b/.env"}
|
local := []string{"/a/.env", "/b/.env"}
|
||||||
db: []EnvFile
|
db: []EnvFile
|
||||||
|
|
||||||
result := find_unbacked(local, db[:])
|
result := find_unbacked(local, db[:])
|
||||||
testing.expect(t, len(result) == 2, fmt.aprintf("expected 2 unbacked, got %d", len(result)))
|
testing.expect(t, len(result) == 2, fmt.aprintf("expected 2 unbacked, got %d", len(result)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import "core:encoding/json"
|
|||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
|
|
||||||
cmd_scan :: proc(cmd: ^Command) {
|
cmd_scan :: proc(cmd: ^Command) {
|
||||||
if !can_scan() {
|
feats := check_features()
|
||||||
|
if cant_scan(feats) {
|
||||||
fmt.println(
|
fmt.println(
|
||||||
"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)",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_find_binary_exists :: proc(t: ^testing.T) {
|
test_find_binary_exists :: proc(t: ^testing.T) {
|
||||||
path := os.get_env("PATH", context.allocator)
|
path := os.get_env("PATH", context.temp_allocator)
|
||||||
paths := strings.split(path, ":")
|
paths := strings.split(path, ":", context.temp_allocator)
|
||||||
|
|
||||||
result := find_binary(paths, "sh")
|
result := find_binary(paths, "sh")
|
||||||
testing.expect(t, result != "", "sh should be found on PATH")
|
testing.expect(t, result != "", "sh should be found on PATH")
|
||||||
@@ -15,7 +15,7 @@ test_find_binary_exists :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_find_binary_not_exists :: proc(t: ^testing.T) {
|
test_find_binary_not_exists :: proc(t: ^testing.T) {
|
||||||
old_path := os.get_env("PATH", context.allocator)
|
old_path := os.get_env("PATH", context.temp_allocator)
|
||||||
defer {
|
defer {
|
||||||
if old_path != "" {
|
if old_path != "" {
|
||||||
os.set_env("PATH", old_path)
|
os.set_env("PATH", old_path)
|
||||||
@@ -24,8 +24,8 @@ test_find_binary_not_exists :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
os.set_env("PATH", "/tmp/envr-nope")
|
os.set_env("PATH", "/tmp/envr-nope")
|
||||||
|
|
||||||
path := os.get_env("PATH", context.allocator)
|
path := os.get_env("PATH", context.temp_allocator)
|
||||||
paths := strings.split(path, ":")
|
paths := strings.split(path, ":", context.temp_allocator)
|
||||||
|
|
||||||
|
|
||||||
result := find_binary(paths, "no_such_binary_xyz")
|
result := find_binary(paths, "no_such_binary_xyz")
|
||||||
|
|||||||
109
scan.odin
109
scan.odin
@@ -2,23 +2,49 @@ package main
|
|||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:path/filepath"
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:sync"
|
import "core:sync"
|
||||||
|
|
||||||
fd_counter: sync.Atomic_Mutex
|
fd_counter: sync.Atomic_Mutex
|
||||||
fd_seq: int
|
fd_seq: int
|
||||||
|
|
||||||
next_fd_tmp_path :: proc() -> string {
|
// Caller is responsible for freeing paths
|
||||||
sync.atomic_mutex_lock(&fd_counter)
|
scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) {
|
||||||
n := fd_seq
|
if is_tty() {
|
||||||
fd_seq += 1
|
fmt.printf("Searching for all files in \"%s\"...\n", search_path)
|
||||||
sync.atomic_mutex_unlock(&fd_counter)
|
}
|
||||||
return fmt.aprintf("/tmp/envr-fd-%d-%d", os.get_pid(), n)
|
all_files, all_ok := run_fd(build_fd_args(search_path, cfg, true))
|
||||||
|
if !all_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_tty() {
|
||||||
|
fmt.printf("Search for unignored fies in \"%s\"...\n", search_path)
|
||||||
|
}
|
||||||
|
unignored_files, unignored_ok := run_fd(build_fd_args(search_path, cfg, false))
|
||||||
|
if !unignored_ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
unignored_set := make(map[string]bool, len(unignored_files), context.temp_allocator)
|
||||||
|
for file in unignored_files {
|
||||||
|
unignored_set[file] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in all_files {
|
||||||
|
if !(file in unignored_set) {
|
||||||
|
append(&paths, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(private = "file")
|
||||||
build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -> []string {
|
build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -> []string {
|
||||||
args := make([dynamic]string, 0, 3 + 2 * len(cfg.ScanConfig.Exclude) + 2)
|
args_len := 3 + 2 * len(cfg.ScanConfig.Exclude) + 2
|
||||||
|
args := make([dynamic]string, 0, args_len, context.temp_allocator)
|
||||||
append(&args, "fd")
|
append(&args, "fd")
|
||||||
append(&args, "-a")
|
append(&args, "-a")
|
||||||
append(&args, cfg.ScanConfig.Matcher)
|
append(&args, cfg.ScanConfig.Matcher)
|
||||||
@@ -38,7 +64,7 @@ build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -
|
|||||||
return args[:]
|
return args[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
|
run_fd :: proc(args: []string) -> (lines: []string, ok: bool) {
|
||||||
tmp_path := next_fd_tmp_path()
|
tmp_path := next_fd_tmp_path()
|
||||||
tmp_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC)
|
tmp_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC)
|
||||||
if tmp_err != nil {
|
if tmp_err != nil {
|
||||||
@@ -64,7 +90,7 @@ run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, read_err := os.read_entire_file_from_path(tmp_path, context.allocator)
|
data, read_err := os.read_entire_file_from_path(tmp_path, context.temp_allocator)
|
||||||
os.remove(tmp_path)
|
os.remove(tmp_path)
|
||||||
if read_err != nil {
|
if read_err != nil {
|
||||||
return
|
return
|
||||||
@@ -77,69 +103,44 @@ run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
raw_lines := strings.split(output, "\n")
|
raw_lines := strings.split(output, "\n", context.temp_allocator)
|
||||||
|
result := make([dynamic]string, 0, len(raw_lines), context.temp_allocator)
|
||||||
for line in raw_lines {
|
for line in raw_lines {
|
||||||
trimmed, _ := strings.clone(strings.trim_space(line))
|
trimmed := strings.trim_space(line)
|
||||||
if len(trimmed) > 0 {
|
if len(trimmed) > 0 {
|
||||||
append(&lines, trimmed)
|
append(&result, trimmed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true
|
return result[:], true
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) {
|
@(private = "file")
|
||||||
if is_tty() {
|
next_fd_tmp_path :: proc() -> string {
|
||||||
fmt.printf("Searching for all files in \"%s\"...\n", search_path)
|
sync.atomic_mutex_lock(&fd_counter)
|
||||||
}
|
n := fd_seq
|
||||||
all_args := build_fd_args(search_path, cfg, true)
|
fd_seq += 1
|
||||||
all_files, all_ok := run_fd(all_args)
|
sync.atomic_mutex_unlock(&fd_counter)
|
||||||
if !all_ok {
|
return fmt.aprintf("/tmp/envr-fd-%d-%d", os.get_pid(), n, allocator = context.temp_allocator)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_tty() {
|
|
||||||
fmt.printf("Search for unignored fies in \"%s\"...\n", search_path)
|
|
||||||
}
|
|
||||||
unignored_args := build_fd_args(search_path, cfg, false)
|
|
||||||
unignored_files, unignored_ok := run_fd(unignored_args)
|
|
||||||
if !unignored_ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
unignored_set: map[string]bool
|
|
||||||
for file in unignored_files {
|
|
||||||
unignored_set[file] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for file in all_files {
|
|
||||||
if !(file in unignored_set) {
|
|
||||||
append(&paths, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = true
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
can_scan :: proc() -> bool {
|
cant_scan :: proc(feats: AvailableFeatures) -> bool {
|
||||||
feats := check_features()
|
return Feature.Fd not_in feats
|
||||||
return Feature.Fd in feats
|
|
||||||
}
|
}
|
||||||
|
|
||||||
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> [dynamic]string {
|
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> []string {
|
||||||
backed_set: map[string]bool
|
// Lives until the end of the function
|
||||||
|
backed_set := make(map[string]bool, len(db_files), context.temp_allocator)
|
||||||
for file in db_files {
|
for file in db_files {
|
||||||
backed_set[file.Path] = true
|
backed_set[file.Path] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
unbacked: [dynamic]string
|
unbacked := make([dynamic]string, 0, len(db_files) / 2, context.temp_allocator)
|
||||||
for file in local_files {
|
for file in local_files {
|
||||||
if !(file in backed_set) {
|
if !(file in backed_set) {
|
||||||
append(&unbacked, file)
|
append(&unbacked, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return unbacked
|
return unbacked[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
129
scan_test.odin
129
scan_test.odin
@@ -7,88 +7,81 @@ import "core:testing"
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
|
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
|
||||||
if !can_scan() {
|
feats := check_features()
|
||||||
return
|
testing.expect(t, cant_scan(feats) == false)
|
||||||
}
|
|
||||||
|
|
||||||
base := fmt.aprintf("/tmp/envr-scan-test-%d", os.get_pid())
|
base := fmt.aprintf("/tmp/envr-scan-test-%d", os.get_pid())
|
||||||
os.mkdir_all(base)
|
os.mkdir_all(base)
|
||||||
defer os.remove_all(base)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
git_init := os.Process_Desc{
|
git_init := os.Process_Desc {
|
||||||
command = []string{"git", "-c", "advice.defaultBranchName=false", "init"},
|
command = []string{"git", "-c", "advice.defaultBranchName=false", "init"},
|
||||||
working_dir = base,
|
working_dir = base,
|
||||||
stdout = os.stderr,
|
stdout = os.stderr,
|
||||||
stderr = os.stderr,
|
stderr = os.stderr,
|
||||||
}
|
}
|
||||||
p, err := os.process_start(git_init)
|
p, err := os.process_start(git_init)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, wait_err := os.process_wait(p)
|
_, wait_err := os.process_wait(p)
|
||||||
if wait_err != nil {
|
if wait_err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
gitignore_path := fmt.aprintf("%s/.gitignore", base)
|
gitignore_path := fmt.aprintf("%s/.gitignore", base)
|
||||||
_ = os.write_entire_file(gitignore_path, ".env*\n")
|
_ = os.write_entire_file(gitignore_path, ".env*\n")
|
||||||
|
|
||||||
_ = os.write_entire_file(fmt.aprintf("%s/.env", base), "SECRET=1")
|
_ = os.write_entire_file(fmt.aprintf("%s/.env", base), "SECRET=1")
|
||||||
_ = os.write_entire_file(fmt.aprintf("%s/.env.testing", base), "TEST=1")
|
_ = os.write_entire_file(fmt.aprintf("%s/.env.testing", base), "TEST=1")
|
||||||
_ = os.write_entire_file(fmt.aprintf("%s/config.yaml", base), "key: value")
|
_ = os.write_entire_file(fmt.aprintf("%s/config.yaml", base), "key: value")
|
||||||
|
|
||||||
cfg := Config{
|
cfg := Config {
|
||||||
ScanConfig = ScanConfig{
|
ScanConfig = ScanConfig{Matcher = "\\.env", Exclude = []string{}, Include = []string{}},
|
||||||
Matcher = "\\.env",
|
}
|
||||||
Exclude = []string{},
|
|
||||||
Include = []string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
results, ok := scan_path(base, cfg)
|
results, ok := scan_path(base, cfg)
|
||||||
testing.expect(t, ok, "scan_path should succeed")
|
defer delete(results)
|
||||||
|
testing.expect(t, ok, "scan_path should succeed")
|
||||||
|
|
||||||
found_env := false
|
found_env := false
|
||||||
found_testing := false
|
found_testing := false
|
||||||
found_config := false
|
found_config := false
|
||||||
|
|
||||||
for path in results {
|
for path in results {
|
||||||
_, filename := filepath.split(path)
|
_, filename := filepath.split(path)
|
||||||
if filename == ".env" {
|
if filename == ".env" {
|
||||||
found_env = true
|
found_env = true
|
||||||
}
|
}
|
||||||
if filename == ".env.testing" {
|
if filename == ".env.testing" {
|
||||||
found_testing = true
|
found_testing = true
|
||||||
}
|
}
|
||||||
if filename == "config.yaml" {
|
if filename == "config.yaml" {
|
||||||
found_config = true
|
found_config = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testing.expect(t, found_env, "should find .env (gitignored)")
|
testing.expect(t, found_env, "should find .env (gitignored)")
|
||||||
testing.expect(t, found_testing, "should find .env.testing (gitignored)")
|
testing.expect(t, found_testing, "should find .env.testing (gitignored)")
|
||||||
testing.expect(t, !found_config, "should NOT find config.yaml (not gitignored)")
|
testing.expect(t, !found_config, "should NOT find config.yaml (not gitignored)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_scan_path_empty_dir :: proc(t: ^testing.T) {
|
test_scan_path_empty_dir :: proc(t: ^testing.T) {
|
||||||
if !can_scan() {
|
feats := check_features()
|
||||||
return
|
testing.expect(t, cant_scan(feats) == false)
|
||||||
}
|
|
||||||
|
|
||||||
base := fmt.aprintf("/tmp/envr-scan-empty-%d", os.get_pid())
|
base := fmt.aprintf("/tmp/envr-scan-empty-%d", os.get_pid())
|
||||||
os.mkdir_all(base)
|
os.mkdir_all(base)
|
||||||
defer os.remove_all(base)
|
defer os.remove_all(base)
|
||||||
|
|
||||||
cfg := Config{
|
cfg := Config {
|
||||||
ScanConfig = ScanConfig{
|
ScanConfig = ScanConfig{Matcher = "\\.env", Exclude = []string{}, Include = []string{}},
|
||||||
Matcher = "\\.env",
|
}
|
||||||
Exclude = []string{},
|
|
||||||
Include = []string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
results, ok := scan_path(base, cfg)
|
results, ok := scan_path(base, cfg)
|
||||||
testing.expect(t, ok, "scan_path should succeed")
|
defer delete(results)
|
||||||
testing.expect(t, len(results) == 0, fmt.aprintf("expected 0 results, got %d", len(results)))
|
testing.expect(t, ok, "scan_path should succeed")
|
||||||
|
testing.expect(t, len(results) == 0, fmt.aprintf("expected 0 results, got %d", len(results)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
table.odin
22
table.odin
@@ -15,11 +15,11 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
col_widths := make([dynamic]int, 0, len(headers))
|
col_widths := make([dynamic]int, 0, len(headers))
|
||||||
for i in 0..<len(headers) {
|
for i in 0 ..< len(headers) {
|
||||||
append(&col_widths, strings.rune_count(headers[i]))
|
append(&col_widths, strings.rune_count(headers[i]))
|
||||||
}
|
}
|
||||||
for r in rows {
|
for r in rows {
|
||||||
for i in 0..<len(r) {
|
for i in 0 ..< len(r) {
|
||||||
w := strings.rune_count(r[i])
|
w := strings.rune_count(r[i])
|
||||||
if i < len(col_widths) && w > col_widths[i] {
|
if i < len(col_widths) && w > col_widths[i] {
|
||||||
col_widths[i] = w
|
col_widths[i] = w
|
||||||
@@ -34,11 +34,11 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
|||||||
|
|
||||||
hline :: proc(b: ^strings.Builder, left, mid, right: string, widths: [dynamic]int) {
|
hline :: proc(b: ^strings.Builder, left, mid, right: string, widths: [dynamic]int) {
|
||||||
strings.write_string(b, left)
|
strings.write_string(b, left)
|
||||||
for i in 0..<len(widths) {
|
for i in 0 ..< len(widths) {
|
||||||
for _ in 0..<widths[i]+2 {
|
for _ in 0 ..< widths[i] + 2 {
|
||||||
strings.write_string(b, "\u2500")
|
strings.write_string(b, "\u2500")
|
||||||
}
|
}
|
||||||
if i < len(widths)-1 {
|
if i < len(widths) - 1 {
|
||||||
strings.write_string(b, mid)
|
strings.write_string(b, mid)
|
||||||
} else {
|
} else {
|
||||||
strings.write_string(b, right)
|
strings.write_string(b, right)
|
||||||
@@ -56,7 +56,7 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
strings.write_string(&b, "\u2502")
|
strings.write_string(&b, "\u2502")
|
||||||
for i in 0..<len(headers) {
|
for i in 0 ..< len(headers) {
|
||||||
cell(&b, headers[i], col_widths[i])
|
cell(&b, headers[i], col_widths[i])
|
||||||
}
|
}
|
||||||
fmt.println(strings.to_string(b))
|
fmt.println(strings.to_string(b))
|
||||||
@@ -66,7 +66,7 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
|||||||
|
|
||||||
for r in rows {
|
for r in rows {
|
||||||
strings.write_string(&b, "\u2502")
|
strings.write_string(&b, "\u2502")
|
||||||
for i in 0..<len(r) {
|
for i in 0 ..< len(r) {
|
||||||
cell(&b, r[i], col_widths[i])
|
cell(&b, r[i], col_widths[i])
|
||||||
}
|
}
|
||||||
fmt.println(strings.to_string(b))
|
fmt.println(strings.to_string(b))
|
||||||
@@ -77,12 +77,11 @@ render_table :: proc(headers: []string, rows: [][]string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render_json_rows :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
render_json_rows :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
||||||
entries := make([dynamic]map[string]string, 0, len(rows))
|
entries := make([dynamic]map[string]string, 0, len(rows), context.temp_allocator)
|
||||||
defer delete(entries)
|
|
||||||
|
|
||||||
for row in rows {
|
for row in rows {
|
||||||
entry: map[string]string
|
entry := make(map[string]string, len(headers), context.temp_allocator)
|
||||||
for i in 0..<len(headers) {
|
for i in 0 ..< len(headers) {
|
||||||
entry[headers[i]] = row[i]
|
entry[headers[i]] = row[i]
|
||||||
}
|
}
|
||||||
append(&entries, entry)
|
append(&entries, entry)
|
||||||
@@ -95,3 +94,4 @@ render_json_rows :: proc(w: io.Writer, headers: []string, rows: [][]string) {
|
|||||||
}
|
}
|
||||||
io.write_string(w, string(data))
|
io.write_string(w, string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user