Skip to content

Commit f1f4087

Browse files
committed
Feature: allow running persistent run hooks of all parents
Currently, only one of the persistent pre-runs and post-runs is executed. It is always the first one found in the parents chain, starting at this command. Expected behavior is to execute all parents' persistent pre-runs and post-runs. Dependent projects implemented various workarounds for this: - manually building persistent hook chains (in every hook). - applying some kind of monkey-patching on top of Cobra. This change eliminates the necessity for such workarounds by allowing to set a global variable EnableTraverseRunHooks. Tickets: - spf13#216 - spf13#252 Signed-off-by: Volodymyr Khoroz <[email protected]>
1 parent 95d8a1e commit f1f4087

File tree

4 files changed

+83
-66
lines changed

4 files changed

+83
-66
lines changed

cobra.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ var initializers []func()
4343
var finalizers []func()
4444

4545
const (
46-
defaultPrefixMatching = false
47-
defaultCommandSorting = true
48-
defaultCaseInsensitive = false
46+
defaultPrefixMatching = false
47+
defaultCommandSorting = true
48+
defaultCaseInsensitive = false
49+
defaultTraverseRunHooks = false
4950
)
5051

5152
// EnablePrefixMatching allows setting automatic prefix matching. Automatic prefix matching can be a dangerous thing
@@ -60,6 +61,10 @@ var EnableCommandSorting = defaultCommandSorting
6061
// EnableCaseInsensitive allows case-insensitive commands names. (case sensitive by default)
6162
var EnableCaseInsensitive = defaultCaseInsensitive
6263

64+
// EnableTraverseRunHooks executes persistent pre-run and post-run hooks from all parents.
65+
// By default this is disabled, which means only the first run hook to be found is executed.
66+
var EnableTraverseRunHooks = defaultTraverseRunHooks
67+
6368
// MousetrapHelpText enables an information splash screen on Windows
6469
// if the CLI is started from explorer.exe.
6570
// To disable the mousetrap, just set this variable to blank string ("").

command.go

+24-4
Original file line numberDiff line numberDiff line change
@@ -934,15 +934,31 @@ func (c *Command) execute(a []string) (err error) {
934934
return err
935935
}
936936

937+
parents := make([]*Command, 0, 5)
937938
for p := c; p != nil; p = p.Parent() {
939+
if EnableTraverseRunHooks {
940+
// When EnableTraverseRunHooks is set:
941+
// - Execute all persistent pre-runs from the root parent till this command.
942+
// - Execute all persistent post-runs from this command till the root parent.
943+
parents = append([]*Command{p}, parents...)
944+
} else {
945+
// Otherwise, execute only the first found persistent hook.
946+
parents = append(parents, p)
947+
}
948+
}
949+
for _, p := range parents {
938950
if p.PersistentPreRunE != nil {
939951
if err := p.PersistentPreRunE(c, argWoFlags); err != nil {
940952
return err
941953
}
942-
break
954+
if !EnableTraverseRunHooks {
955+
break
956+
}
943957
} else if p.PersistentPreRun != nil {
944958
p.PersistentPreRun(c, argWoFlags)
945-
break
959+
if !EnableTraverseRunHooks {
960+
break
961+
}
946962
}
947963
}
948964
if c.PreRunE != nil {
@@ -979,10 +995,14 @@ func (c *Command) execute(a []string) (err error) {
979995
if err := p.PersistentPostRunE(c, argWoFlags); err != nil {
980996
return err
981997
}
982-
break
998+
if !EnableTraverseRunHooks {
999+
break
1000+
}
9831001
} else if p.PersistentPostRun != nil {
9841002
p.PersistentPostRun(c, argWoFlags)
985-
break
1003+
if !EnableTraverseRunHooks {
1004+
break
1005+
}
9861006
}
9871007
}
9881008

command_test.go

+47-59
Original file line numberDiff line numberDiff line change
@@ -1530,57 +1530,73 @@ func TestHooks(t *testing.T) {
15301530
}
15311531

15321532
func TestPersistentHooks(t *testing.T) {
1533-
var (
1534-
parentPersPreArgs string
1535-
parentPreArgs string
1536-
parentRunArgs string
1537-
parentPostArgs string
1538-
parentPersPostArgs string
1539-
)
1533+
EnableTraverseRunHooks = true
1534+
testPersistentHooks(t, []string{
1535+
"parent PersistentPreRun",
1536+
"child PersistentPreRun",
1537+
"child PreRun",
1538+
"child Run",
1539+
"child PostRun",
1540+
"child PersistentPostRun",
1541+
"parent PersistentPostRun",
1542+
})
15401543

1541-
var (
1542-
childPersPreArgs string
1543-
childPreArgs string
1544-
childRunArgs string
1545-
childPostArgs string
1546-
childPersPostArgs string
1547-
)
1544+
EnableTraverseRunHooks = false
1545+
testPersistentHooks(t, []string{
1546+
"child PersistentPreRun",
1547+
"child PreRun",
1548+
"child Run",
1549+
"child PostRun",
1550+
"child PersistentPostRun",
1551+
})
1552+
}
1553+
1554+
func testPersistentHooks(t *testing.T, expectedHookRunOrder []string) {
1555+
var hookRunOrder []string
1556+
1557+
validateHook := func(args []string, hookName string) {
1558+
hookRunOrder = append(hookRunOrder, hookName)
1559+
got := strings.Join(args, " ")
1560+
if onetwo != got {
1561+
t.Errorf("Expected %s %q, got %q", hookName, onetwo, got)
1562+
}
1563+
}
15481564

15491565
parentCmd := &Command{
15501566
Use: "parent",
15511567
PersistentPreRun: func(_ *Command, args []string) {
1552-
parentPersPreArgs = strings.Join(args, " ")
1568+
validateHook(args, "parent PersistentPreRun")
15531569
},
15541570
PreRun: func(_ *Command, args []string) {
1555-
parentPreArgs = strings.Join(args, " ")
1571+
validateHook(args, "parent PreRun")
15561572
},
15571573
Run: func(_ *Command, args []string) {
1558-
parentRunArgs = strings.Join(args, " ")
1574+
validateHook(args, "parent Run")
15591575
},
15601576
PostRun: func(_ *Command, args []string) {
1561-
parentPostArgs = strings.Join(args, " ")
1577+
validateHook(args, "parent PostRun")
15621578
},
15631579
PersistentPostRun: func(_ *Command, args []string) {
1564-
parentPersPostArgs = strings.Join(args, " ")
1580+
validateHook(args, "parent PersistentPostRun")
15651581
},
15661582
}
15671583

15681584
childCmd := &Command{
15691585
Use: "child",
15701586
PersistentPreRun: func(_ *Command, args []string) {
1571-
childPersPreArgs = strings.Join(args, " ")
1587+
validateHook(args, "child PersistentPreRun")
15721588
},
15731589
PreRun: func(_ *Command, args []string) {
1574-
childPreArgs = strings.Join(args, " ")
1590+
validateHook(args, "child PreRun")
15751591
},
15761592
Run: func(_ *Command, args []string) {
1577-
childRunArgs = strings.Join(args, " ")
1593+
validateHook(args, "child Run")
15781594
},
15791595
PostRun: func(_ *Command, args []string) {
1580-
childPostArgs = strings.Join(args, " ")
1596+
validateHook(args, "child PostRun")
15811597
},
15821598
PersistentPostRun: func(_ *Command, args []string) {
1583-
childPersPostArgs = strings.Join(args, " ")
1599+
validateHook(args, "child PersistentPostRun")
15841600
},
15851601
}
15861602
parentCmd.AddCommand(childCmd)
@@ -1593,41 +1609,13 @@ func TestPersistentHooks(t *testing.T) {
15931609
t.Errorf("Unexpected error: %v", err)
15941610
}
15951611

1596-
for _, v := range []struct {
1597-
name string
1598-
got string
1599-
}{
1600-
// TODO: currently PersistentPreRun* defined in parent does not
1601-
// run if the matching child subcommand has PersistentPreRun.
1602-
// If the behavior changes (https://github.com/spf13/cobra/issues/252)
1603-
// this test must be fixed.
1604-
{"parentPersPreArgs", parentPersPreArgs},
1605-
{"parentPreArgs", parentPreArgs},
1606-
{"parentRunArgs", parentRunArgs},
1607-
{"parentPostArgs", parentPostArgs},
1608-
// TODO: currently PersistentPostRun* defined in parent does not
1609-
// run if the matching child subcommand has PersistentPostRun.
1610-
// If the behavior changes (https://github.com/spf13/cobra/issues/252)
1611-
// this test must be fixed.
1612-
{"parentPersPostArgs", parentPersPostArgs},
1613-
} {
1614-
if v.got != "" {
1615-
t.Errorf("Expected blank %s, got %q", v.name, v.got)
1616-
}
1617-
}
1618-
1619-
for _, v := range []struct {
1620-
name string
1621-
got string
1622-
}{
1623-
{"childPersPreArgs", childPersPreArgs},
1624-
{"childPreArgs", childPreArgs},
1625-
{"childRunArgs", childRunArgs},
1626-
{"childPostArgs", childPostArgs},
1627-
{"childPersPostArgs", childPersPostArgs},
1628-
} {
1629-
if v.got != onetwo {
1630-
t.Errorf("Expected %s %q, got %q", v.name, onetwo, v.got)
1612+
for idx, exp := range expectedHookRunOrder {
1613+
if len(hookRunOrder) > idx {
1614+
if act := hookRunOrder[idx]; act != exp {
1615+
t.Errorf("Expected %q at %d, got %q", exp, idx, act)
1616+
}
1617+
} else {
1618+
t.Errorf("Expected %q at %d, got nothing", exp, idx)
16311619
}
16321620
}
16331621
}

site/content/user_guide.md

+4
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,10 @@ Inside subCmd PostRun with args: [arg1 arg2]
687687
Inside subCmd PersistentPostRun with args: [arg1 arg2]
688688
```
689689

690+
By default, only the first persistent hook found in the command chain is executed.
691+
That is why in the above output, the `rootCmd PersistentPostRun` was not called for a child command.
692+
Set `EnableTraverseRunHooks` global variable to `true` if you want to execute all parents' persistent hooks.
693+
690694
## Suggestions when "unknown command" happens
691695

692696
Cobra will print automatic suggestions when "unknown command" errors happen. This allows Cobra to behave similarly to the `git` command when a typo happens. For example:

0 commit comments

Comments
 (0)