Skip to content

Commit

Permalink
Add FlagMetadata instead of required flags slice
Browse files Browse the repository at this point in the history
  • Loading branch information
mfridman committed Dec 24, 2024
1 parent 2323f88 commit 5a458be
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 25 deletions.
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ root := &cli.Command{
// Add a flag to capitalize the input
f.Bool("c", false, "capitalize the input")
}),
FlagsMetadata: []cli.FlagMetadata{
{Name: "c", Required: true},
},
Exec: func(ctx context.Context, s *cli.State) error {
if len(s.Args) == 0 {
// Return a new error with the error code ErrShowHelp
Expand Down Expand Up @@ -73,13 +76,14 @@ Each command in your CLI application is represented by a `Command` struct:

```go
type Command struct {
Name string
Exec func(ctx context.Context, s *State) error
Usage string
ShortHelp string
Flags *flag.FlagSet
SubCommands []*Command
UsageFunc func(*Command) string
Name string
Usage string
ShortHelp string
UsageFunc func(*Command) string
Flags *flag.FlagSet
FlagsMetadata []FlagMetadata
SubCommands []*Command
Exec func(ctx context.Context, s *State) error
}
```

Expand Down Expand Up @@ -182,7 +186,7 @@ git (pull|push) [remote]
This project is in active development and undergoing changes as the API is refined. Please open an
issue if you encounter any problems or have suggestions for improvement.

- [ ] Nail down required flags implementation
- [x] Nail down required flags implementation
- [ ] Add tests for typos and command suggestions, crude levenstein distance for now
- [ ] Internal implementation (not user-facing), track selected `*Command` in `*State` and remove
`flags *flag.FlagSet` from `*State`
Expand Down
18 changes: 12 additions & 6 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,9 @@ type Command struct {
// Flags holds the command-specific flag definitions. Each command maintains its own flag set
// for parsing arguments.
Flags *flag.FlagSet
// RequiredFlags is a list of flag names that are required for the command to run. If any of
// these flags are missing, the command will not execute and will show its help text instead.
//
// TODO(mf): maybe thise should be a proper data structure instead of a list of strings to allow
// for more flexibility in the future.
RequiredFlags []string
// FlagsMetadata is an optional list of flag information to extend the FlagSet with additional
// metadata. This is useful for tracking required flags.
FlagsMetadata []FlagMetadata

// SubCommands is a list of nested commands that exist under this command.
SubCommands []*Command
Expand All @@ -55,6 +52,15 @@ type Command struct {
selected *Command
}

// FlagMetadata holds additional metadata for a flag, such as whether it is required.
type FlagMetadata struct {
// Name is the flag's name. Must match the flag name in the flag set.
Name string

// Required indicates whether the flag is required.
Required bool
}

// FlagsFunc is a helper function that creates a new [flag.FlagSet] and applies the given function
// to it. Intended for use in command definitions to simplify flag setup. Example usage:
//
Expand Down
6 changes: 3 additions & 3 deletions examples/cmd/echo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ func main() {
// Add a flag to capitalize the input
f.Bool("c", false, "capitalize the input")
}),
RequiredFlags: []string{
"c",
FlagsMetadata: []cli.FlagMetadata{
{Name: "c", Required: true},
},
Exec: func(ctx context.Context, s *cli.State) error {
if len(s.Args) == 0 {
Expand All @@ -30,7 +30,7 @@ func main() {
}
output := strings.Join(s.Args, " ")
// If -c flag is set, capitalize the output
if cli.GetFlag[bool](s, "c") || cli.GetFlag[bool](s, "capitalize") {
if cli.GetFlag[bool](s, "c") {
output = strings.ToUpper(output)
}
fmt.Fprintln(s.Stdout, output)
Expand Down
20 changes: 12 additions & 8 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,27 +111,31 @@ func Parse(root *Command, args []string) error {
}

// Check required flags by inspecting the args string for their presence
if len(current.RequiredFlags) > 0 {
if len(current.FlagsMetadata) > 0 {
var missingFlags []string
for _, flagName := range current.RequiredFlags {
flag := combinedFlags.Lookup(flagName)
for _, flagMetadata := range current.FlagsMetadata {
if !flagMetadata.Required {
continue
}
// TODO(mf): we need to validate that the metadata flag is known to the flag set
flag := combinedFlags.Lookup(flagMetadata.Name)
if flag == nil {
return fmt.Errorf("command %q: internal error: required flag %q not found in flag set", current.Name, flagName)
return fmt.Errorf("command %q: internal error: required flag %q not found in flag set", current.Name, flagMetadata.Name)
}

// Look for the flag in the original args before any delimiter
found := false
for _, arg := range argsToParse {
// Match either -flag or --flag
if arg == "-"+flagName || arg == "--"+flagName ||
strings.HasPrefix(arg, "-"+flagName+"=") ||
strings.HasPrefix(arg, "--"+flagName+"=") {
if arg == "-"+flagMetadata.Name || arg == "--"+flagMetadata.Name ||
strings.HasPrefix(arg, "-"+flagMetadata.Name+"=") ||
strings.HasPrefix(arg, "--"+flagMetadata.Name+"=") {
found = true
break
}
}
if !found {
missingFlags = append(missingFlags, flagName)
missingFlags = append(missingFlags, flagMetadata.Name)
}
}
if len(missingFlags) > 0 {
Expand Down

0 comments on commit 5a458be

Please sign in to comment.