Skip to content

Commit

Permalink
Merge pull request #67 from jranson/boolean-flag-args
Browse files Browse the repository at this point in the history
accept boolean arg pairs like "--arg-name true"
  • Loading branch information
gwprice115 authored Feb 4, 2025
2 parents 665fef5 + 88d2d8f commit 1747420
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 2 deletions.
4 changes: 3 additions & 1 deletion hathora/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import (
)

func main() {
if err := commands.App().Run(context.Background(), os.Args); err != nil {
app := commands.App()
args := commands.NormalizeArgs(app, os.Args)
if err := app.Run(context.Background(), args); err != nil {
red := color.New(color.FgRed)
errStr := fmt.Sprintf("%v", err)
errorLines := strings.Split(errStr, "\n")
Expand Down
3 changes: 2 additions & 1 deletion internal/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ func App() *cli.Command {
return nil
}

err = altsrc.InitializeValueSourcesFromFlags(ctx, cmd, os.Args[1:])
err = altsrc.InitializeValueSourcesFromFlags(ctx, cmd,
NormalizeArgs(cmd, os.Args)[1:])
if err != nil {
return err
}
Expand Down
97 changes: 97 additions & 0 deletions internal/commands/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,100 @@ func handleNewVersionAvailable(currentVersion string) {
zap.L().Warn("Version " + release.TagName + " is available for download.")
}
}

// NormalizeArgs accepts an args slice (e.g., os.Args) and transforms it to
// collapse any Boolean flags with separate value parameters into a single
// parameter=value arg. This allows users to provide arg lists like:
// --idle-timeout-enabled false
// which would normally ignore the provided value and always register as True,
// as Flag's required syntax for bools is strict: --idle-timeout-enabled=false
// In all cases, a new slice is returned.
func NormalizeArgs(cmd *cli.Command, args []string) []string {
l := len(args)
if l == 0 {
return make([]string, 0)
}
// this returns a map of boolean flag names from the command tree
bfl := getBooleanFlags(cmd)
if len(bfl) == 0 {
out := make([]string, l)
copy(out, args)
return out
}
// this checks if there are any boolean flags provided by the user where
// the next flag is a value instead of a flag. in those cases, this will
// merge the elements. so ["--my-boolean", "true"] becomes ["--my-boolean=true"]
var skipNext bool
out := make([]string, 0, l)
for i, arg := range args {
// this skips whenever the previous arg just merged the current into it
if skipNext {
skipNext = false
continue
}
// this adds the current arg to the output
out = append(out, arg)
// this moves on when the current arg is not a flag name
if !strings.HasPrefix(arg, "-") {
continue
}
// this provides the flag name without a leading - or --
flagName := strings.TrimPrefix(strings.TrimPrefix(arg, "-"), "-")
// this skips any flag names that are not boolean
if _, ok := bfl[flagName]; !ok {
continue
}
// this skips when the iterator is on the last arg or whenever it's not
// the last but another flag name arg immediately follows this one
if i >= l-1 || strings.HasPrefix(args[i+1], "-") {
continue
}
// the current arg must be a boolean flag name followed by a value arg.
// this therefore merges them into a single element with an = delimiter
// by updating the just-appended element and skipping the next element
out[len(out)-1] = fmt.Sprintf("%s=%s", arg, args[i+1])
skipNext = true
}
return out
}

// getBooleanFlags iterates the command tree and returns a lookup table of all
// boolean flags
func getBooleanFlags(cmd *cli.Command) map[string]any {
out := make(map[string]any)
getBooleanFlagsIter(cmd, out)
return out
}

func getBooleanFlagsIter(cmd *cli.Command, dst map[string]any) {
// this skips over help, as it is allowed to have a value in the next arg
// (e.g., --help deploy)
omit := func(names []string) bool {
for _, name := range names {
if name == "help" {
return true
}
}
return false
}
// this iterates and registers the current Command's boolean Flag names
for _, flag := range cmd.Flags {
// this skips non-bools
if _, ok := flag.(*cli.BoolFlag); !ok {
continue
}
// this skips any other explicit omissions
fns := flag.Names()
if omit(fns) {
continue
}
// otherwise this add the Flag's names to the output map
for _, name := range fns {
dst[name] = nil
}
}
// this sends the passed command's subcommands through this iterator func
for _, sub := range cmd.Commands {
getBooleanFlagsIter(sub, dst)
}
}
47 changes: 47 additions & 0 deletions internal/commands/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package commands

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)

func TestNormalizeArgs(t *testing.T) {
cmd := &cli.Command{
Flags: []cli.Flag{
idleTimeoutFlag,
},
}
tests := []struct {
input, expected []string
}{
{
input: []string{"main", "--config", "abcd", "--idle-timeout-enabled", "false"},
expected: []string{"main", "--config", "abcd", "--idle-timeout-enabled=false"},
},
{
input: []string{"main", "--config", "abcd", "--idle-timeout-enabled", "--next-flag"},
expected: []string{"main", "--config", "abcd", "--idle-timeout-enabled", "--next-flag"},
},
{
input: []string{"main", "--config", "abcd", "--idle-timeout-enabled", "-n", "abc"},
expected: []string{"main", "--config", "abcd", "--idle-timeout-enabled", "-n", "abc"},
},
{
input: []string{"main", "--config", "abcd", "--idle-timeout-enabled", "T", "-n", "abc"},
expected: []string{"main", "--config", "abcd", "--idle-timeout-enabled=T", "-n", "abc"},
},
{
input: []string{"main", "--config", "abcd", "--idle-timeout-enabled"},
expected: []string{"main", "--config", "abcd", "--idle-timeout-enabled"},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
got := NormalizeArgs(cmd, test.input)
assert.Equal(t, fmt.Sprintf("%v", got), fmt.Sprintf("%v", test.expected))
})
}
}

0 comments on commit 1747420

Please sign in to comment.