feat: utils.go — FilterArgs, ParseFlag with short/long flag rules

- FilterArgs: removes empty strings and Go test runner flags
- ParseFlag: single dash (-v, -🔥) must be 1 char, double dash (--verbose) must be 2+ chars
- Cli.Run() now uses FilterArgs and ParseFlag — no test flag awareness in surface layer
- Invalid flags silently ignored (e.g. -verbose, --v)

221 tests, 79.7% coverage.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-20 12:15:57 +00:00
parent c61a2d3dfe
commit 8854d5c79f
3 changed files with 153 additions and 20 deletions

View file

@ -34,13 +34,7 @@ func (cl *Cli) Run(args ...string) Result[any] {
args = os.Args[1:]
}
// Filter out empty args and test flags
var clean []string
for _, a := range args {
if a != "" && !strings.HasPrefix(a, "-test.") {
clean = append(clean, a)
}
}
clean := FilterArgs(args)
if cl.core == nil || cl.core.commands == nil || len(cl.core.commands.commands) == 0 {
// No commands registered — print banner and exit
@ -76,23 +70,17 @@ func (cl *Cli) Run(args ...string) Result[any] {
// Build options from remaining args (flags become Options)
opts := Options{}
for _, arg := range remaining {
if strings.HasPrefix(arg, "--") {
parts := strings.SplitN(strings.TrimPrefix(arg, "--"), "=", 2)
if len(parts) == 2 {
opts = append(opts, Option{K: parts[0], V: parts[1]})
key, val, valid := ParseFlag(arg)
if valid {
if val != "" {
opts = append(opts, Option{K: key, V: val})
} else {
opts = append(opts, Option{K: parts[0], V: true})
opts = append(opts, Option{K: key, V: true})
}
} else if strings.HasPrefix(arg, "-") {
parts := strings.SplitN(strings.TrimPrefix(arg, "-"), "=", 2)
if len(parts) == 2 {
opts = append(opts, Option{K: parts[0], V: parts[1]})
} else {
opts = append(opts, Option{K: parts[0], V: true})
}
} else {
} else if !strings.HasPrefix(arg, "-") {
opts = append(opts, Option{K: "_arg", V: arg})
}
// Invalid flags (e.g. -verbose, --v) are silently ignored
}
return cmd.Run(opts)

67
pkg/core/utils.go Normal file
View file

@ -0,0 +1,67 @@
// SPDX-License-Identifier: EUPL-1.2
// Utility functions for the Core framework.
package core
import (
"strings"
"unicode/utf8"
)
// FilterArgs removes empty strings and Go test runner flags from an argument list.
//
// clean := core.FilterArgs(os.Args[1:])
func FilterArgs(args []string) []string {
var clean []string
for _, a := range args {
if a == "" || strings.HasPrefix(a, "-test.") {
continue
}
clean = append(clean, a)
}
return clean
}
// ParseFlag parses a single flag argument into key, value, and validity.
// Single dash (-) requires exactly 1 character (letter, emoji, unicode).
// Double dash (--) requires 2+ characters.
//
// "-v" → "v", "", true
// "-🔥" → "🔥", "", true
// "--verbose" → "verbose", "", true
// "--port=8080" → "port", "8080", true
// "-verbose" → "", "", false (single dash, 2+ chars)
// "--v" → "", "", false (double dash, 1 char)
// "hello" → "", "", false (not a flag)
func ParseFlag(arg string) (key, value string, valid bool) {
if strings.HasPrefix(arg, "--") {
// Long flag: must be 2+ chars
rest := strings.TrimPrefix(arg, "--")
parts := strings.SplitN(rest, "=", 2)
name := parts[0]
if utf8.RuneCountInString(name) < 2 {
return "", "", false
}
if len(parts) == 2 {
return name, parts[1], true
}
return name, "", true
}
if strings.HasPrefix(arg, "-") {
// Short flag: must be exactly 1 char (rune)
rest := strings.TrimPrefix(arg, "-")
parts := strings.SplitN(rest, "=", 2)
name := parts[0]
if utf8.RuneCountInString(name) != 1 {
return "", "", false
}
if len(parts) == 2 {
return name, parts[1], true
}
return name, "", true
}
return "", "", false
}

78
tests/utils_test.go Normal file
View file

@ -0,0 +1,78 @@
package core_test
import (
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- FilterArgs ---
func TestFilterArgs_Good(t *testing.T) {
args := []string{"deploy", "", "to", "-test.v", "homelab", "-test.paniconexit0"}
clean := FilterArgs(args)
assert.Equal(t, []string{"deploy", "to", "homelab"}, clean)
}
func TestFilterArgs_Empty_Good(t *testing.T) {
clean := FilterArgs(nil)
assert.Nil(t, clean)
}
// --- ParseFlag ---
func TestParseFlag_ShortValid_Good(t *testing.T) {
// Single letter
k, v, ok := ParseFlag("-v")
assert.True(t, ok)
assert.Equal(t, "v", k)
assert.Equal(t, "", v)
// Single emoji
k, v, ok = ParseFlag("-🔥")
assert.True(t, ok)
assert.Equal(t, "🔥", k)
assert.Equal(t, "", v)
// Short with value
k, v, ok = ParseFlag("-p=8080")
assert.True(t, ok)
assert.Equal(t, "p", k)
assert.Equal(t, "8080", v)
}
func TestParseFlag_ShortInvalid_Bad(t *testing.T) {
// Multiple chars with single dash — invalid
_, _, ok := ParseFlag("-verbose")
assert.False(t, ok)
_, _, ok = ParseFlag("-port")
assert.False(t, ok)
}
func TestParseFlag_LongValid_Good(t *testing.T) {
k, v, ok := ParseFlag("--verbose")
assert.True(t, ok)
assert.Equal(t, "verbose", k)
assert.Equal(t, "", v)
k, v, ok = ParseFlag("--port=8080")
assert.True(t, ok)
assert.Equal(t, "port", k)
assert.Equal(t, "8080", v)
}
func TestParseFlag_LongInvalid_Bad(t *testing.T) {
// Single char with double dash — invalid
_, _, ok := ParseFlag("--v")
assert.False(t, ok)
}
func TestParseFlag_NotAFlag_Bad(t *testing.T) {
_, _, ok := ParseFlag("hello")
assert.False(t, ok)
_, _, ok = ParseFlag("")
assert.False(t, ok)
}