mirror of
https://github.com/sbrow/envr.git
synced 2026-06-27 18:48:33 -04:00
odin: scaffold project with CLI parser, version command, Go fallback
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,4 +7,5 @@ man
|
|||||||
# build artifacts
|
# build artifacts
|
||||||
builds
|
builds
|
||||||
envr
|
envr
|
||||||
|
envr-go
|
||||||
result
|
result
|
||||||
|
|||||||
95
cli.odin
Normal file
95
cli.odin
Normal 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
30
cmd_deps.odin
Normal 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
45
features.odin
Normal 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
|
||||||
|
}
|
||||||
@@ -97,8 +97,8 @@
|
|||||||
gotools
|
gotools
|
||||||
cobra-cli
|
cobra-cli
|
||||||
|
|
||||||
odin
|
unstable.odin
|
||||||
ols
|
unstable.ols
|
||||||
|
|
||||||
# Build tools
|
# Build tools
|
||||||
zip
|
zip
|
||||||
|
|||||||
50
main.odin
50
main.odin
@@ -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
39
stubs.odin
Normal 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
84
table.odin
Normal 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
7
tty.odin
Normal 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
13
version.odin
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user