Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .version/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.0.3
v0.0.4
35 changes: 29 additions & 6 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,18 +164,18 @@ func (c *Command) FullOptions() OptionSet {
}

// GetGlobalFlags returns the global flags from the root command
// All non-hidden options in the root command are considered global flags
func (c *Command) GetGlobalFlags() OptionSet {
// Traverse to the root command
root := c
for root.parent != nil {
root = root.parent
}

// Find global flags (they have specific names)
// Return all non-hidden options from root command as global flags
var globalFlags OptionSet
for _, opt := range root.Options {
switch opt.Flag {
case "help", "list-commands", "list-flags":
if opt.Flag != "" && !opt.Hidden {
globalFlags = append(globalFlags, opt)
}
}
Expand Down Expand Up @@ -620,6 +620,30 @@ func (inv *Invocation) run(state *runState) error {
}
}

// Execute Action callbacks for options that were set
// Don't execute actions if help was requested
if !isHelpRequested && !errors.Is(state.flagParseErr, pflag.ErrHelp) && inv.Flags != nil {
// Use a map to track which flags we've already processed
// This prevents executing Action multiple times for the same flag
processedFlags := make(map[string]bool)

// Execute actions for flags, starting from the current command and moving up to the root.
// This ensures that if a flag is defined in multiple commands (e.g., overridden),
// the action of the most specific command (the current one) is executed.
for cmd := inv.Command; cmd != nil; cmd = cmd.parent {
for _, opt := range cmd.Options {
if opt.Action != nil && opt.Flag != "" && !processedFlags[opt.Flag] {
if ff := inv.Flags.Lookup(opt.Flag); ff != nil && ff.Changed {
if err := opt.Action(ff.Value); err != nil {
return fmt.Errorf("action for flag %q failed: %w", opt.Flag, err)
}
processedFlags[opt.Flag] = true
}
}
}
}
}

// Parse and assign arguments
if inv.Command.RawArgs {
// If we're at the root command, then the name is omitted
Expand Down Expand Up @@ -670,12 +694,11 @@ func (inv *Invocation) run(state *runState) error {
// to get [root, parent, ..., child] order. Chain() will reverse again
// to ensure execution order is root -> parent -> ... -> child -> handler
var middlewareChain []MiddlewareFunc
cmd := inv.Command
for cmd != nil {
for cmd := inv.Command; cmd != nil; cmd = cmd.parent {
if cmd.Middleware != nil {
middlewareChain = append(middlewareChain, cmd.Middleware)
}
cmd = cmd.parent

}
// Reverse to get order from root (parent) to current (child)
// This ensures Chain() will execute them in the correct order: root -> parent -> child -> handler
Expand Down
5 changes: 3 additions & 2 deletions example/demo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func main() {
ok := true
ok := "true"
// Create root command
rootCmd := &redant.Command{
Use: "myapp",
Expand All @@ -20,7 +20,8 @@ func main() {
Options: redant.OptionSet{
{
Flag: "upper",
Value: redant.BoolOf(&ok),
Value: redant.StringOf(&ok),
Shorthand: "u",
Description: "Prints the text in upper case.",
},
},
Expand Down
5 changes: 5 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ type Option struct {
Deprecated string

Category string

// Action is called after the flag is parsed and set.
// It receives the flag value and can perform additional validation or side effects.
// If Action returns an error, command execution will fail.
Action func(val pflag.Value) error `json:"-"`
}

// OptionSet is a group of options that can be applied to a command.
Expand Down