refactor: Fixed a number of memory leaks.

This commit is contained in:
2026-06-12 11:05:34 -04:00
parent 22a517340a
commit 1068458f32
9 changed files with 317 additions and 295 deletions

View File

@@ -1,6 +1,9 @@
package main
import "core:bufio"
import "core:fmt"
import "core:io"
import "core:mem"
import "core:os"
import "core:strings"
@@ -19,10 +22,14 @@ CommandInfo :: struct {
aliases: []string,
}
COMMANDS := []CommandInfo{
{"init", "envr init", "Set up envr",
COMMANDS := []CommandInfo {
{
"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.",
{}},
{},
},
{"scan", "envr scan", "Find and select .env files for backup", "", {}},
{"sync", "envr sync", "Update or restore your env backups", "", {}},
{"backup", "envr backup <path>", "Import a .env file into envr", "", {"add"}},
@@ -30,9 +37,13 @@ COMMANDS := []CommandInfo{
{"list", "envr list", "View your tracked files", "", {}},
{"remove", "envr remove <path>", "Remove a .env file from your database", "", {}},
{"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.",
{}},
{},
},
{"version", "envr version", "Show envr's version", "", {}},
{"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]
if strings.starts_with(arg, "--") {
key := arg[2:]
if i+1 < len(args) && !strings.starts_with(args[i+1], "-") {
cmd.flags[key] = args[i+1]
if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
cmd.flags[key] = args[i + 1]
i += 2
} else {
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 {
key_slice := arg[1:2]
if i+1 < len(args) && !strings.starts_with(args[i+1], "-") {
cmd.flags[key_slice] = args[i+1]
if i + 1 < len(args) && !strings.starts_with(args[i + 1], "-") {
cmd.flags[key_slice] = args[i + 1]
i += 2
} else {
cmd.bool_set[key_slice] = true
@@ -113,45 +124,43 @@ find_command :: proc(name: string) -> (CommandInfo, bool) {
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)
if !found {
return "", false
return false
}
b: strings.Builder
strings.builder_init(&b)
fmt.sbprintf(&b, "Usage: %s [flags]\n\n", info.usage)
fmt.sbprintf(&b, "%s\n", info.short)
fmt.wprintf(w, "Usage: %s [flags]\n\n", info.usage, flush = false)
fmt.wprintf(w, "%s\n", info.short, flush = false)
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 {
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 {
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)
s := strings.clone(strings.to_string(b))
strings.builder_destroy(&b)
return s, true
fmt.wprintf(w, "\nFlags:\n -h, --help help for %s\n", info.name, flush = false)
return true
}
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 {
fmt.printf("Unknown command: %s\n", name)
print_usage()
return
}
fmt.println(text)
bufio.writer_flush(&bw)
}
usage_text :: proc() -> string {
@@ -159,7 +168,10 @@ usage_text :: proc() -> string {
strings.builder_init(&b)
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, "\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, "> envr sync\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, "\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
padding := 20 - name_len
if padding > 0 {
for _ in 0..<padding {
for _ in 0 ..< padding {
strings.write_byte(&b, ' ')
}
}
@@ -224,3 +239,4 @@ usage_text :: proc() -> string {
print_usage :: proc() {
fmt.print(usage_text())
}

View File

@@ -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, "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 restore"),
"step 5 missing 'envr restore'",
)
testing.expect(t, strings.contains(text, "> envr restore"), "step 5 missing 'envr restore'")
}
@(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, "--help"), "missing --help flag")
testing.expect(
t,
strings.contains(text, "Use \"envr [command] --help\""),
"missing help hint",
)
testing.expect(t, strings.contains(text, "Use \"envr [command] --help\""), "missing help hint")
}
@(test)
test_command_help_backup :: proc(t: ^testing.T) {
text, ok := command_help_text("backup")
testing.expect(t, ok, "command_help_text(\"backup\") returned false")
b: strings.Builder
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, "envr backup <path>"),
"missing usage pattern",
)
testing.expect(
t,
strings.contains(text, "Aliases:"),
"missing Aliases section",
)
testing.expect(t, 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, "Flags:"), "missing Flags section")
testing.expect(
t,
strings.contains(text, "--help"),
"missing --help in flags",
)
testing.expect(t, strings.contains(text, "--help"), "missing --help in flags")
}
@(test)
test_command_help_add_alias :: proc(t: ^testing.T) {
text, ok := command_help_text("add")
testing.expect(t, ok, "command_help_text(\"add\") returned false")
b: strings.Builder
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(
t,
strings.contains(text, "envr backup <path>"),
@@ -93,34 +84,43 @@ test_command_help_add_alias :: proc(t: ^testing.T) {
@(test)
test_command_help_init_no_aliases :: proc(t: ^testing.T) {
text, ok := command_help_text("init")
testing.expect(t, ok, "command_help_text(\"init\") returned false")
b: strings.Builder
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, "Aliases:"),
"init should not have Aliases section",
)
testing.expect(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, "help for init"),
"missing 'help for init'",
)
testing.expect(t, strings.contains(text, "help for init"), "missing 'help for init'")
}
@(test)
test_command_help_unknown :: proc(t: ^testing.T) {
text, ok := command_help_text("nonexistent")
testing.expect(t, !ok, "command_help_text(\"nonexistent\") should return false")
b: strings.Builder
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")
}
@(test)
test_command_help_version :: proc(t: ^testing.T) {
text, ok := command_help_text("version")
testing.expect(t, ok, "command_help_text(\"version\") returned false")
b: strings.Builder
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,
@@ -128,3 +128,4 @@ test_command_help_version :: proc(t: ^testing.T) {
"version should not have Aliases section",
)
}

View File

@@ -6,74 +6,79 @@ import "core:path/filepath"
import "core:strings"
cmd_check :: proc(cmd: ^Command) {
check_path: string
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
}
feats := check_features()
abs_path: string
if filepath.is_abs(check_path) {
abs_path = check_path
} else {
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
}
check_path: string
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
}
db, db_ok := db_open()
if !db_ok {
return
}
defer db_close(&db)
abs_path: string
if filepath.is_abs(check_path) {
abs_path = check_path
} else {
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 {
if !can_scan() {
fmt.println("Error: please install fd to use the check command (https://github.com/sharkdp/fd)")
return
}
files_in_path: [dynamic]string
scanned, scan_ok := scan_path(abs_path, db.cfg)
if !scan_ok {
fmt.println("Error scanning directory for .env files")
return
}
files_in_path = scanned
} else {
append(&files_in_path, abs_path)
}
if is_dir {
if cant_scan(feats) {
fmt.println(
"Error: please install fd to use the check command (https://github.com/sharkdp/fd)",
)
return
}
db_files, list_ok := db_list(&db)
if !list_ok {
return
}
scanned, scan_ok := scan_path(abs_path, db.cfg)
if !scan_ok {
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 {
if len(files_in_path) == 0 {
fmt.println("No .env files found in the specified directory.")
} else {
fmt.println("✓ All .env files in the directory are backed up.")
}
} else {
fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed))
for file in not_backed {
fmt.printf(" %s\n", file)
}
fmt.println("\nRun 'envr sync' to back up these files.")
}
not_backed := find_unbacked(files_in_path[:], db_files[:])
if len(not_backed) == 0 {
if len(files_in_path) == 0 {
fmt.println("No .env files found in the specified directory.")
} else {
fmt.println("✓ All .env files in the directory are backed up.")
}
} else {
fmt.printf("Found %d .env file(s) that are not backed up:\n", len(not_backed))
for file in not_backed {
fmt.printf(" %s\n", file)
}
fmt.println("\nRun 'envr sync' to back up these files.")
}
}

View File

@@ -5,39 +5,44 @@ import "core:testing"
@(test)
test_find_unbacked_finds_missing :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env", "/c/.env"}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
local := []string{"/a/.env", "/b/.env", "/c/.env"}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 1, fmt.aprintf("expected 1 unbacked, got %d", len(result)))
if len(result) > 0 {
testing.expect(t, result[0] == "/c/.env", fmt.aprintf("expected /c/.env, got %s", result[0]))
}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 1, fmt.aprintf("expected 1 unbacked, got %d", len(result)))
if len(result) > 0 {
testing.expect(
t,
result[0] == "/c/.env",
fmt.aprintf("expected /c/.env, got %s", result[0]),
)
}
}
@(test)
test_find_unbacked_all_backed :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env"}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
local := []string{"/a/.env", "/b/.env"}
db := []EnvFile{{Path = "/a/.env"}, {Path = "/b/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
}
@(test)
test_find_unbacked_no_local :: proc(t: ^testing.T) {
local: []string
db := []EnvFile{{Path = "/a/.env"}}
local: []string
db := []EnvFile{{Path = "/a/.env"}}
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 0, fmt.aprintf("expected 0 unbacked, got %d", len(result)))
}
@(test)
test_find_unbacked_none_backed :: proc(t: ^testing.T) {
local := []string{"/a/.env", "/b/.env"}
db: []EnvFile
local := []string{"/a/.env", "/b/.env"}
db: []EnvFile
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 2, fmt.aprintf("expected 2 unbacked, got %d", len(result)))
result := find_unbacked(local, db[:])
testing.expect(t, len(result) == 2, fmt.aprintf("expected 2 unbacked, got %d", len(result)))
}

View File

@@ -4,7 +4,8 @@ import "core:encoding/json"
import "core:fmt"
cmd_scan :: proc(cmd: ^Command) {
if !can_scan() {
feats := check_features()
if cant_scan(feats) {
fmt.println(
"Error: please install fd to use the scan command (https://github.com/sharkdp/fd)",
)

View File

@@ -6,8 +6,8 @@ import "core:testing"
@(test)
test_find_binary_exists :: proc(t: ^testing.T) {
path := os.get_env("PATH", context.allocator)
paths := strings.split(path, ":")
path := os.get_env("PATH", context.temp_allocator)
paths := strings.split(path, ":", context.temp_allocator)
result := find_binary(paths, "sh")
testing.expect(t, result != "", "sh should be found on PATH")
@@ -15,7 +15,7 @@ test_find_binary_exists :: proc(t: ^testing.T) {
@(test)
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 {
if 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")
path := os.get_env("PATH", context.allocator)
paths := strings.split(path, ":")
path := os.get_env("PATH", context.temp_allocator)
paths := strings.split(path, ":", context.temp_allocator)
result := find_binary(paths, "no_such_binary_xyz")

109
scan.odin
View File

@@ -2,23 +2,49 @@ package main
import "core:fmt"
import "core:os"
import "core:path/filepath"
import "core:strings"
import "core:sync"
fd_counter: sync.Atomic_Mutex
fd_seq: int
next_fd_tmp_path :: proc() -> string {
sync.atomic_mutex_lock(&fd_counter)
n := fd_seq
fd_seq += 1
sync.atomic_mutex_unlock(&fd_counter)
return fmt.aprintf("/tmp/envr-fd-%d-%d", os.get_pid(), n)
// Caller is responsible for freeing paths
scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) {
if is_tty() {
fmt.printf("Searching for all files in \"%s\"...\n", search_path)
}
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 {
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, "-a")
append(&args, cfg.ScanConfig.Matcher)
@@ -38,7 +64,7 @@ build_fd_args :: proc(search_path: string, cfg: Config, include_ignored: bool) -
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_file, tmp_err := os.open(tmp_path, os.O_CREATE | os.O_WRONLY | os.O_TRUNC)
if tmp_err != nil {
@@ -64,7 +90,7 @@ run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
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)
if read_err != nil {
return
@@ -77,69 +103,44 @@ run_fd :: proc(args: []string) -> (lines: [dynamic]string, ok: bool) {
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 {
trimmed, _ := strings.clone(strings.trim_space(line))
trimmed := strings.trim_space(line)
if len(trimmed) > 0 {
append(&lines, trimmed)
append(&result, trimmed)
}
}
ok = true
return
return result[:], true
}
scan_path :: proc(search_path: string, cfg: Config) -> (paths: [dynamic]string, ok: bool) {
if is_tty() {
fmt.printf("Searching for all files in \"%s\"...\n", search_path)
}
all_args := build_fd_args(search_path, cfg, true)
all_files, all_ok := run_fd(all_args)
if !all_ok {
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
@(private = "file")
next_fd_tmp_path :: proc() -> string {
sync.atomic_mutex_lock(&fd_counter)
n := fd_seq
fd_seq += 1
sync.atomic_mutex_unlock(&fd_counter)
return fmt.aprintf("/tmp/envr-fd-%d-%d", os.get_pid(), n, allocator = context.temp_allocator)
}
can_scan :: proc() -> bool {
feats := check_features()
return Feature.Fd in feats
cant_scan :: proc(feats: AvailableFeatures) -> bool {
return Feature.Fd not_in feats
}
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> [dynamic]string {
backed_set: map[string]bool
find_unbacked :: proc(local_files: []string, db_files: []EnvFile) -> []string {
// Lives until the end of the function
backed_set := make(map[string]bool, len(db_files), context.temp_allocator)
for file in db_files {
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 {
if !(file in backed_set) {
append(&unbacked, file)
}
}
return unbacked
return unbacked[:]
}

View File

@@ -7,88 +7,81 @@ import "core:testing"
@(test)
test_scan_path_finds_gitignored_env_files :: proc(t: ^testing.T) {
if !can_scan() {
return
}
feats := check_features()
testing.expect(t, cant_scan(feats) == false)
base := fmt.aprintf("/tmp/envr-scan-test-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
base := fmt.aprintf("/tmp/envr-scan-test-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
git_init := os.Process_Desc{
command = []string{"git", "-c", "advice.defaultBranchName=false", "init"},
working_dir = base,
stdout = os.stderr,
stderr = os.stderr,
}
p, err := os.process_start(git_init)
if err != nil {
return
}
_, wait_err := os.process_wait(p)
if wait_err != nil {
return
}
git_init := os.Process_Desc {
command = []string{"git", "-c", "advice.defaultBranchName=false", "init"},
working_dir = base,
stdout = os.stderr,
stderr = os.stderr,
}
p, err := os.process_start(git_init)
if err != nil {
return
}
_, wait_err := os.process_wait(p)
if wait_err != nil {
return
}
gitignore_path := fmt.aprintf("%s/.gitignore", base)
_ = os.write_entire_file(gitignore_path, ".env*\n")
gitignore_path := fmt.aprintf("%s/.gitignore", base)
_ = 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.testing", base), "TEST=1")
_ = os.write_entire_file(fmt.aprintf("%s/config.yaml", base), "key: value")
_ = 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/config.yaml", base), "key: value")
cfg := Config{
ScanConfig = ScanConfig{
Matcher = "\\.env",
Exclude = []string{},
Include = []string{},
},
}
cfg := Config {
ScanConfig = ScanConfig{Matcher = "\\.env", Exclude = []string{}, Include = []string{}},
}
results, ok := scan_path(base, cfg)
testing.expect(t, ok, "scan_path should succeed")
results, ok := scan_path(base, cfg)
defer delete(results)
testing.expect(t, ok, "scan_path should succeed")
found_env := false
found_testing := false
found_config := false
found_env := false
found_testing := false
found_config := false
for path in results {
_, filename := filepath.split(path)
if filename == ".env" {
found_env = true
}
if filename == ".env.testing" {
found_testing = true
}
if filename == "config.yaml" {
found_config = true
}
}
for path in results {
_, filename := filepath.split(path)
if filename == ".env" {
found_env = true
}
if filename == ".env.testing" {
found_testing = true
}
if filename == "config.yaml" {
found_config = true
}
}
testing.expect(t, found_env, "should find .env (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_env, "should find .env (gitignored)")
testing.expect(t, found_testing, "should find .env.testing (gitignored)")
testing.expect(t, !found_config, "should NOT find config.yaml (not gitignored)")
}
@(test)
test_scan_path_empty_dir :: proc(t: ^testing.T) {
if !can_scan() {
return
}
feats := check_features()
testing.expect(t, cant_scan(feats) == false)
base := fmt.aprintf("/tmp/envr-scan-empty-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
base := fmt.aprintf("/tmp/envr-scan-empty-%d", os.get_pid())
os.mkdir_all(base)
defer os.remove_all(base)
cfg := Config{
ScanConfig = ScanConfig{
Matcher = "\\.env",
Exclude = []string{},
Include = []string{},
},
}
cfg := Config {
ScanConfig = ScanConfig{Matcher = "\\.env", Exclude = []string{}, Include = []string{}},
}
results, ok := scan_path(base, cfg)
testing.expect(t, ok, "scan_path should succeed")
testing.expect(t, len(results) == 0, fmt.aprintf("expected 0 results, got %d", len(results)))
results, ok := scan_path(base, cfg)
defer delete(results)
testing.expect(t, ok, "scan_path should succeed")
testing.expect(t, len(results) == 0, fmt.aprintf("expected 0 results, got %d", len(results)))
}

View File

@@ -15,11 +15,11 @@ render_table :: proc(headers: []string, rows: [][]string) {
}
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]))
}
for r in rows {
for i in 0..<len(r) {
for i in 0 ..< len(r) {
w := strings.rune_count(r[i])
if i < len(col_widths) && w > col_widths[i] {
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) {
strings.write_string(b, left)
for i in 0..<len(widths) {
for _ in 0..<widths[i]+2 {
for i in 0 ..< len(widths) {
for _ in 0 ..< widths[i] + 2 {
strings.write_string(b, "\u2500")
}
if i < len(widths)-1 {
if i < len(widths) - 1 {
strings.write_string(b, mid)
} else {
strings.write_string(b, right)
@@ -56,7 +56,7 @@ render_table :: proc(headers: []string, rows: [][]string) {
}
strings.write_string(&b, "\u2502")
for i in 0..<len(headers) {
for i in 0 ..< len(headers) {
cell(&b, headers[i], col_widths[i])
}
fmt.println(strings.to_string(b))
@@ -66,7 +66,7 @@ render_table :: proc(headers: []string, rows: [][]string) {
for r in rows {
strings.write_string(&b, "\u2502")
for i in 0..<len(r) {
for i in 0 ..< len(r) {
cell(&b, r[i], col_widths[i])
}
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) {
entries := make([dynamic]map[string]string, 0, len(rows))
defer delete(entries)
entries := make([dynamic]map[string]string, 0, len(rows), context.temp_allocator)
for row in rows {
entry: map[string]string
for i in 0..<len(headers) {
entry := make(map[string]string, len(headers), context.temp_allocator)
for i in 0 ..< len(headers) {
entry[headers[i]] = row[i]
}
append(&entries, entry)
@@ -95,3 +94,4 @@ render_json_rows :: proc(w: io.Writer, headers: []string, rows: [][]string) {
}
io.write_string(w, string(data))
}