Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added cli
Binary file not shown.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/Tylerx3udv/cli

go 1.22.2
369 changes: 367 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,372 @@
package main

import "fmt"
import (
"fmt"
"os"
"strconv"
"strings"
)

// --- Flag Value Interface ---

// Value is the interface to the dynamic value stored in a flag.
type Value interface {
String() string
Set(string) error
}

// --- Slice Types ---

// StringSlice is a flag value backed by a slice of strings.
// Tracks whether values were set by CLI args vs env vars/defaults so that
// CLI args completely override (not append to) env/default values.
type StringSlice struct {
value []string
hasEnvValue bool // true if env vars populated the slice
cliStarted bool // true once CLI Set() is called for the first time
}

func NewStringSlice(defaults ...string) *StringSlice {
// Copy defaults to avoid mutating the caller's backing array on append
val := make([]string, len(defaults))
copy(val, defaults)
return &StringSlice{value: val}
}

func (s *StringSlice) Set(val string) error {
// First CLI Set(): clear any env/default values to ensure CLI overrides
if !s.cliStarted {
s.value = nil
s.cliStarted = true
}
s.value = append(s.value, val)
return nil
}

// SetFromEnv sets values from an environment variable (comma-separated).
// Env values replace any defaults that were set during construction.
func (s *StringSlice) SetFromEnv(val string) error {
parts := strings.Split(val, ",")
s.value = make([]string, 0, len(parts))
for _, p := range parts {
s.value = append(s.value, strings.TrimSpace(p))
}
s.hasEnvValue = true
return nil
}

func (s *StringSlice) String() string {
return "[" + strings.Join(s.value, ",") + "]"
}

// IntSlice is a flag value backed by a slice of ints.
type IntSlice struct {
value []int
hasEnvValue bool
cliStarted bool
}

func NewIntSlice(defaults ...int) *IntSlice {
// Copy defaults to avoid mutating the caller's backing array on append
val := make([]int, len(defaults))
copy(val, defaults)
return &IntSlice{value: val}
}

func (i *IntSlice) Set(val string) error {
n, err := strconv.Atoi(val)
if err != nil {
return err
}
if !i.cliStarted {
i.value = nil
i.cliStarted = true
}
i.value = append(i.value, n)
return nil
}

func (i *IntSlice) SetFromEnv(val string) error {
parts := strings.Split(val, ",")
i.value = make([]int, 0, len(parts))
for _, p := range parts {
n, err := strconv.Atoi(strings.TrimSpace(p))
if err != nil {
return err
}
i.value = append(i.value, n)
}
i.hasEnvValue = true
return nil
}

func (i *IntSlice) String() string {
strs := make([]string, len(i.value))
for idx, v := range i.value {
strs[idx] = strconv.Itoa(v)
}
return "[" + strings.Join(strs, ",") + "]"
}

// Float64Slice is a flag value backed by a slice of float64s.
type Float64Slice struct {
value []float64
hasEnvValue bool
cliStarted bool
}

func NewFloat64Slice(defaults ...float64) *Float64Slice {
// Copy defaults to avoid mutating the caller's backing array on append
val := make([]float64, len(defaults))
copy(val, defaults)
return &Float64Slice{value: val}
}

func (f *Float64Slice) Set(val string) error {
n, err := strconv.ParseFloat(val, 64)
if err != nil {
return err
}
if !f.cliStarted {
f.value = nil
f.cliStarted = true
}
f.value = append(f.value, n)
return nil
}

func (f *Float64Slice) SetFromEnv(val string) error {
parts := strings.Split(val, ",")
f.value = make([]float64, 0, len(parts))
for _, p := range parts {
n, err := strconv.ParseFloat(strings.TrimSpace(p), 64)
if err != nil {
return err
}
f.value = append(f.value, n)
}
f.hasEnvValue = true
return nil
}

func (f *Float64Slice) String() string {
strs := make([]string, len(f.value))
for idx, v := range f.value {
strs[idx] = strconv.FormatFloat(v, 'f', -1, 64)
}
return "[" + strings.Join(strs, ",") + "]"
}

// --- Flag Types ---

// Flag is the interface for all flag types.
type Flag interface {
Names() []string
Apply(set *FlagSet) error
}

// StringSliceFlag represents a --flag that accepts multiple string values.
type StringSliceFlag struct {
Name string
EnvVars []string
Default []string
value *StringSlice
}

func (f *StringSliceFlag) Names() []string { return []string{f.Name} }

func (f *StringSliceFlag) Apply(set *FlagSet) error {
f.value = NewStringSlice(f.Default...)

// Apply env vars first (lowest precedence above defaults)
for _, env := range f.EnvVars {
if val, ok := os.LookupEnv(env); ok {
if err := f.value.SetFromEnv(val); err != nil {
return err
}
break
}
}

set.flags[f.Name] = f.value
return nil
}

func (f *StringSliceFlag) Value() []string { return f.value.value }

// IntSliceFlag represents a --flag that accepts multiple int values.
type IntSliceFlag struct {
Name string
EnvVars []string
Default []int
value *IntSlice
}

func (f *IntSliceFlag) Names() []string { return []string{f.Name} }

func (f *IntSliceFlag) Apply(set *FlagSet) error {
f.value = NewIntSlice(f.Default...)
for _, env := range f.EnvVars {
if val, ok := os.LookupEnv(env); ok {
if err := f.value.SetFromEnv(val); err != nil {
return err
}
break
}
}
set.flags[f.Name] = f.value
return nil
}

func (f *IntSliceFlag) Value() []int { return f.value.value }

// Float64SliceFlag represents a --flag that accepts multiple float64 values.
type Float64SliceFlag struct {
Name string
EnvVars []string
Default []float64
value *Float64Slice
}

func (f *Float64SliceFlag) Names() []string { return []string{f.Name} }

func (f *Float64SliceFlag) Apply(set *FlagSet) error {
f.value = NewFloat64Slice(f.Default...)
for _, env := range f.EnvVars {
if val, ok := os.LookupEnv(env); ok {
if err := f.value.SetFromEnv(val); err != nil {
return err
}
break
}
}
set.flags[f.Name] = f.value
return nil
}

func (f *Float64SliceFlag) Value() []float64 { return f.value.value }

// --- FlagSet ---

// FlagSet holds parsed flags.
type FlagSet struct {
flags map[string]Value
}

func NewFlagSet() *FlagSet {
return &FlagSet{flags: make(map[string]Value)}
}

// StringSlice returns the []string value of a named flag.
func (fs *FlagSet) StringSlice(name string) []string {
if v, ok := fs.flags[name].(*StringSlice); ok {
return v.value
}
return nil
}

// IntSlice returns the []int value of a named flag.
func (fs *FlagSet) IntSlice(name string) []int {
if v, ok := fs.flags[name].(*IntSlice); ok {
return v.value
}
return nil
}

// Float64Slice returns the []float64 value of a named flag.
func (fs *FlagSet) Float64Slice(name string) []float64 {
if v, ok := fs.flags[name].(*Float64Slice); ok {
return v.value
}
return nil
}

// --- App ---

// App is the main CLI application.
type App struct {
Flags []Flag
Action func(*Context) error
}

// Context provides access to parsed flags during Action.
type Context struct {
flagSet *FlagSet
}

func (c *Context) StringSlice(name string) []string { return c.flagSet.StringSlice(name) }
func (c *Context) IntSlice(name string) []int { return c.flagSet.IntSlice(name) }
func (c *Context) Float64Slice(name string) []float64 { return c.flagSet.Float64Slice(name) }

// Run parses args and executes the app's action.
func (a *App) Run(args []string) error {
fs := NewFlagSet()

// Apply all flags (reads env vars + sets defaults)
for _, flag := range a.Flags {
if err := flag.Apply(fs); err != nil {
return err
}
}

// Parse CLI args (overrides env vars)
remaining, err := parseArgs(args[1:], fs)
if err != nil {
return err
}
_ = remaining

ctx := &Context{flagSet: fs}
return a.Action(ctx)
}

// parseArgs parses command-line arguments and applies them to the flag set.
// When a slice flag receives its first CLI value, it clears any env/default
// values that were set during Apply, ensuring CLI args override completely.
func parseArgs(args []string, fs *FlagSet) ([]string, error) {
i := 0
for i < len(args) {
arg := args[i]
if !strings.HasPrefix(arg, "--") {
i++
continue
}

name := strings.TrimPrefix(arg, "--")
var val string
hasVal := false

if idx := strings.Index(name, "="); idx >= 0 {
val = name[idx+1:]
name = name[:idx]
hasVal = true
}

v, ok := fs.flags[name]
if !ok {
i++
continue
}

if hasVal {
if err := v.Set(val); err != nil {
return nil, err
}
} else if i+1 < len(args) && !strings.HasPrefix(args[i+1], "--") {
// Consume the next argument as the value
if err := v.Set(args[i+1]); err != nil {
return nil, err
}
i++
} else {
// Flag with no value (boolean-like), skip
}
i++
}

return nil, nil
}

func main() {
fmt.Println("Hello, Bounty Hunter!")
fmt.Println("CLI flag parser with precedence fix.")
}
Loading