Skip to content

Commit 55c2d91

Browse files
authored
Merge pull request urfave#2028 from fjl/cmd-before-hook
Run Before actions after setting up subcommand
2 parents cf01a1d + 085f748 commit 55c2d91

File tree

2 files changed

+74
-17
lines changed

2 files changed

+74
-17
lines changed

command.go

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"path/filepath"
1111
"reflect"
12+
"slices"
1213
"sort"
1314
"strings"
1415
"unicode"
@@ -561,23 +562,7 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
561562
}
562563
}
563564

564-
if cmd.Before != nil && !cmd.Root().shellCompletion {
565-
if bctx, err := cmd.Before(ctx, cmd); err != nil {
566-
deferErr = cmd.handleExitCoder(ctx, err)
567-
return deferErr
568-
} else if bctx != nil {
569-
ctx = bctx
570-
}
571-
}
572-
573-
tracef("running flag actions (cmd=%[1]q)", cmd.Name)
574-
575-
if err := cmd.runFlagActions(ctx); err != nil {
576-
return err
577-
}
578-
579565
var subCmd *Command
580-
581566
if args.Present() {
582567
tracef("checking positional args %[1]q (cmd=%[2]q)", args, cmd.Name)
583568

@@ -613,11 +598,46 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
613598
}
614599
}
615600

601+
// If a subcommand has been resolved, let it handle the remaining execution.
616602
if subCmd != nil {
617603
tracef("running sub-command %[1]q with arguments %[2]q (cmd=%[3]q)", subCmd.Name, cmd.Args(), cmd.Name)
618604
return subCmd.Run(ctx, cmd.Args().Slice())
619605
}
620606

607+
// This code path is the innermost command execution. Here we actually
608+
// perform the command action.
609+
//
610+
// First, resolve the chain of nested commands up to the parent.
611+
var cmdChain []*Command
612+
for p := cmd; p != nil; p = p.parent {
613+
cmdChain = append(cmdChain, p)
614+
}
615+
slices.Reverse(cmdChain)
616+
617+
// Run Before actions in order.
618+
for _, cmd := range cmdChain {
619+
if cmd.Before == nil {
620+
continue
621+
}
622+
if bctx, err := cmd.Before(ctx, cmd); err != nil {
623+
deferErr = cmd.handleExitCoder(ctx, err)
624+
return deferErr
625+
} else if bctx != nil {
626+
ctx = bctx
627+
}
628+
}
629+
630+
// Run flag actions in order.
631+
// These take a context, so this has to happen after Before actions.
632+
for _, cmd := range cmdChain {
633+
tracef("running flag actions (cmd=%[1]q)", cmd.Name)
634+
if err := cmd.runFlagActions(ctx); err != nil {
635+
deferErr = cmd.handleExitCoder(ctx, err)
636+
return deferErr
637+
}
638+
}
639+
640+
// Run the command action.
621641
if cmd.Action == nil {
622642
cmd.Action = helpCommandAction
623643
} else {

command_test.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,43 @@ func TestCommand_BeforeFunc(t *testing.T) {
14381438
assert.Zero(t, counts.SubCommand, "Subcommand executed when NOT expected")
14391439
}
14401440

1441+
func TestCommand_BeforeFuncPersistentFlag(t *testing.T) {
1442+
counts := &opCounts{}
1443+
beforeError := fmt.Errorf("fail")
1444+
var err error
1445+
1446+
cmd := &Command{
1447+
Before: func(_ context.Context, cmd *Command) (context.Context, error) {
1448+
counts.Before++
1449+
s := cmd.String("opt")
1450+
if s != "value" {
1451+
return nil, beforeError
1452+
}
1453+
return nil, nil
1454+
},
1455+
Commands: []*Command{
1456+
{
1457+
Name: "sub",
1458+
Action: func(context.Context, *Command) error {
1459+
counts.SubCommand++
1460+
return nil
1461+
},
1462+
},
1463+
},
1464+
Flags: []Flag{
1465+
&StringFlag{Name: "opt"},
1466+
},
1467+
Writer: io.Discard,
1468+
}
1469+
1470+
// Check that --opt value is available in root command Before hook,
1471+
// even when it was set on the subcommand.
1472+
err = cmd.Run(buildTestContext(t), []string{"command", "sub", "--opt", "value"})
1473+
require.NoError(t, err)
1474+
assert.Equal(t, 1, counts.Before, "Before() not executed when expected")
1475+
assert.Equal(t, 1, counts.SubCommand, "Subcommand not executed when expected")
1476+
}
1477+
14411478
func TestCommand_BeforeAfterFuncShellCompletion(t *testing.T) {
14421479
t.Skip("TODO: is '--generate-shell-completion' (flag) still supported?")
14431480

@@ -2649,7 +2686,7 @@ func TestFlagAction(t *testing.T) {
26492686
{
26502687
name: "mixture",
26512688
args: []string{"app", "--f_string=app", "--f_uint=1", "--f_int_slice=1,2,3", "--f_duration=1h30m20s", "c1", "--f_string=c1", "sub1", "--f_string=sub1"},
2652-
exp: "app 1h30m20s [1 2 3] 1 c1 sub1 ",
2689+
exp: "sub1 1h30m20s [1 2 3] 1 sub1 sub1 ",
26532690
},
26542691
{
26552692
name: "flag_string_map",

0 commit comments

Comments
 (0)