odin: scaffold project with CLI parser, version command, Go fallback

This commit is contained in:
2026-06-11 20:16:00 -04:00
parent 28f96df4c0
commit d84e43d044
10 changed files with 365 additions and 3 deletions

1
.gitignore vendored
View File

@@ -7,4 +7,5 @@ man
# build artifacts # build artifacts
builds builds
envr envr
envr-go
result result

95
cli.odin Normal file
View File

@@ -0,0 +1,95 @@
package main
import "core:fmt"
import "core:os"
import "core:strings"
Command :: struct {
name: string,
args: [dynamic]string,
flags: map[string]string,
bool_set: map[string]bool,
}
IMPLEMENTED_COMMANDS := []string{
"version",
}
parse_args :: proc() -> (cmd: Command, ok: bool) {
args := os.args
if len(args) < 2 {
print_usage()
return Command{}, false
}
cmd.name = args[1]
cmd.args = make([dynamic]string)
cmd.flags = make(map[string]string)
cmd.bool_set = make(map[string]bool)
i := 2
for i < len(args) {
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]
i += 2
} else {
cmd.bool_set[key] = true
i += 1
}
} 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]
i += 2
} else {
cmd.bool_set[key_slice] = true
i += 1
}
} else {
append(&cmd.args, arg)
i += 1
}
}
return cmd, true
}
is_implemented :: proc(name: string) -> bool {
for c in IMPLEMENTED_COMMANDS {
if c == name {
return true
}
}
return false
}
has_flag :: proc(cmd: ^Command, name: string) -> bool {
_, ok := cmd.flags[name]
if ok {
return true
}
_, ok2 := cmd.bool_set[name]
return ok2
}
print_usage :: proc() {
fmt.println("envr - Manage your .env files.")
fmt.println("")
fmt.println("Usage: envr <command> [args]")
fmt.println("")
fmt.println("Commands:")
fmt.println(" init Set up envr")
fmt.println(" scan Find and select .env files for backup")
fmt.println(" sync Update or restore your env backups")
fmt.println(" backup <path> Import a .env file into envr")
fmt.println(" restore <path> Restore a .env file from the database")
fmt.println(" list View your tracked files")
fmt.println(" remove <path> Remove a .env file from your database")
fmt.println(" check [path] Check if files are backed up")
fmt.println(" deps Check for missing binaries")
fmt.println(" version Show envr's version")
fmt.println(" edit-config Edit your config with your default editor")
}

30
cmd_deps.odin Normal file
View File

@@ -0,0 +1,30 @@
package main
import "core:fmt"
cmd_deps :: proc(cmd: ^Command) {
feats := check_features()
headers := []string{"Feature", "Status"}
rows: [dynamic][]string
if .Git in feats {
append(&rows, []string{"Git", "\u2713 Available"})
} else {
append(&rows, []string{"Git", "\u2717 Missing"})
}
if .Fd in feats {
append(&rows, []string{"fd", "\u2713 Available"})
} else {
append(&rows, []string{"fd", "\u2717 Missing"})
}
if .Age in feats {
append(&rows, []string{"age", "\u2713 Available"})
} else {
append(&rows, []string{"age", "\u2717 Missing"})
}
render_table(headers, rows[:])
}

45
features.odin Normal file
View File

@@ -0,0 +1,45 @@
package main
import "core:os"
import "core:strings"
Feature :: enum {
Git,
Fd,
Age,
}
AvailableFeatures :: bit_set[Feature]
check_features :: proc() -> AvailableFeatures {
feats: AvailableFeatures
if find_binary("git") != "" {
feats += {.Git}
}
if find_binary("fd") != "" {
feats += {.Fd}
}
if find_binary("age") != "" {
feats += {.Age}
}
return feats
}
find_binary :: proc(name: string) -> string {
path_env := os.get_env("PATH", context.allocator)
paths := strings.split(path_env, ":")
for p in paths {
candidate := strings.join({strings.trim_right(p, "/"), name}, "/")
_, err := os.stat(candidate, context.allocator)
if err == nil {
return candidate
}
}
return ""
}
has_feature :: proc(feats: AvailableFeatures, f: Feature) -> bool {
return f in feats
}

View File

@@ -97,8 +97,8 @@
gotools gotools
cobra-cli cobra-cli
odin unstable.odin
ols unstable.ols
# Build tools # Build tools
zip zip

View File

@@ -1,8 +1,56 @@
package main package main
import "core:fmt" import "core:fmt"
import "core:os"
GO_BINARY :: "./envr-go"
main :: proc() { main :: proc() {
fmt.println("Hello!") cmd, ok := parse_args()
if !ok {
return
} }
if !is_implemented(cmd.name) {
fallback_to_go()
return
}
switch cmd.name {
case "version":
cmd_version(&cmd)
case:
fmt.printf("Unknown command: %s\n", cmd.name)
print_usage()
os.exit(1)
}
}
fallback_to_go :: proc() {
args := make([dynamic]string)
append(&args, "./envr-go")
for i in 1..<len(os.args) {
append(&args, os.args[i])
}
desc := os.Process_Desc{
command = args[:],
stdin = os.stdin,
stdout = os.stdout,
stderr = os.stderr,
}
p, err1 := os.process_start(desc)
if err1 != nil {
fmt.printf("Error: failed to run envr-go: %v\n", err1)
os.exit(1)
}
state, err2 := os.process_wait(p)
if err2 != nil {
fmt.printf("Error waiting for envr-go: %v\n", err2)
os.exit(1)
}
os.exit(int(state.exit_code))
}

39
stubs.odin Normal file
View File

@@ -0,0 +1,39 @@
package main
import "core:fmt"
cmd_init :: proc(cmd: ^Command) {
fmt.println("TODO: init")
}
cmd_list :: proc(cmd: ^Command) {
fmt.println("TODO: list")
}
cmd_scan :: proc(cmd: ^Command) {
fmt.println("TODO: scan")
}
cmd_sync :: proc(cmd: ^Command) {
fmt.println("TODO: sync")
}
cmd_backup :: proc(cmd: ^Command) {
fmt.println("TODO: backup")
}
cmd_restore :: proc(cmd: ^Command) {
fmt.println("TODO: restore")
}
cmd_remove :: proc(cmd: ^Command) {
fmt.println("TODO: remove")
}
cmd_check :: proc(cmd: ^Command) {
fmt.println("TODO: check")
}
cmd_edit_config :: proc(cmd: ^Command) {
fmt.println("TODO: edit-config")
}

84
table.odin Normal file
View File

@@ -0,0 +1,84 @@
package main
import "core:fmt"
import "core:strings"
render_table :: proc(headers: []string, rows: [][]string) {
if !is_tty() {
render_json_rows(headers, rows)
return
}
col_widths := make([dynamic]int, len(headers))
for i in 0..<len(headers) {
append(&col_widths, len(headers[i]))
}
for r in rows {
for i in 0..<len(r) {
if i < len(col_widths) && len(r[i]) > col_widths[i] {
col_widths[i] = len(r[i])
}
}
}
b: strings.Builder
strings.builder_init(&b)
defer strings.builder_destroy(&b)
defer delete(col_widths)
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 {
strings.write_string(b, "\u2500")
}
if i < len(widths)-1 {
strings.write_string(b, mid)
} else {
strings.write_string(b, right)
}
}
fmt.println(strings.to_string(b^))
strings.builder_reset(b)
}
hline(&b, "\u250c", "\u252c", "\u2510", col_widths)
strings.write_string(&b, "\u2502")
for i in 0..<len(headers) {
fmt.sbprintf(&b, " %-*s \u2502", col_widths[i], headers[i])
}
fmt.println(strings.to_string(b))
strings.builder_reset(&b)
hline(&b, "\u251c", "\u253c", "\u2524", col_widths)
for r in rows {
strings.write_string(&b, "\u2502")
for i in 0..<len(r) {
fmt.sbprintf(&b, " %-*s \u2502", col_widths[i], r[i])
}
fmt.println(strings.to_string(b))
strings.builder_reset(&b)
}
hline(&b, "\u2514", "\u2534", "\u2518", col_widths)
}
render_json_rows :: proc(headers: []string, rows: [][]string) {
fmt.print("[")
for i in 0..<len(rows) {
if i > 0 {
fmt.print(",")
}
fmt.print("{")
for j in 0..<len(headers) {
if j > 0 {
fmt.print(",")
}
fmt.printf("\"%s\":\"%s\"", headers[j], rows[i][j])
}
fmt.print("}")
}
fmt.println("]")
}

7
tty.odin Normal file
View File

@@ -0,0 +1,7 @@
package main
import "core:sys/posix"
is_tty :: proc() -> bool {
return bool(posix.isatty(1))
}

13
version.odin Normal file
View File

@@ -0,0 +1,13 @@
package main
import "core:fmt"
VERSION :: "0.2.0"
cmd_version :: proc(cmd: ^Command) {
if has_flag(cmd, "long") || has_flag(cmd, "l") {
fmt.printf("envr version %s\n", VERSION)
} else {
fmt.println(VERSION)
}
}