Skip to content

Commit 774d312

Browse files
committed
cli.WrapRun(): remove as now-unnecessary, RunE field should be used instead
this field actually propagates the error through Cobra to the `Command.Execute()`
1 parent c43746e commit 774d312

2 files changed

Lines changed: 48 additions & 42 deletions

File tree

app/cli/cobrawrappers.go

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package cli
33

44
import (
5-
"context"
65
"os"
76

87
"github.com/function61/gokit/app/dynversion"
@@ -11,13 +10,18 @@ import (
1110
"github.com/spf13/pflag"
1211
)
1312

14-
// wraps the `Execute()` call of the command to inject boilerplate details like `Use`, `Version` and
15-
// handling of error to `Command.Execute()` (such as flag validation, missing command etc.)
13+
// wraps the `Execute()` call of the command to inject boilerplate details for root command:
14+
// - `Use` (for root cmd should be `os.Args[0]`)
15+
// - `Version` (should be program's built-in version info)
16+
// - UX improvements (some Cobra defaults are in poor taste)
17+
// - configure logging
18+
// - deliver context & cancel it on signals
1619
func Execute(app *cobra.Command) {
1720
// dirty to mutate after-the-fact
1821

1922
app.Use = os.Args[0]
2023
app.Version = dynversion.Version
24+
2125
// hide the default "completion" subcommand from polluting UX (it can still be used). https://github.com/spf13/cobra/issues/1507
2226
app.CompletionOptions = cobra.CompletionOptions{HiddenDefaultCmd: true}
2327

@@ -26,33 +30,36 @@ func Execute(app *cobra.Command) {
2630
// https://github.com/spf13/cobra/issues/587#issuecomment-843747825
2731
app.SetHelpCommand(&cobra.Command{Hidden: true})
2832

29-
// cannot `AddLogLevelControls(app.Flags())` here because it gets confusing if:
30-
// a) the root command is not runnable
31-
// b) the root command is runnable BUT it doesn't do logging (or there is no debug-level logs to suppress)
33+
// don't display error internally by Cobra. we do better job of displaying it here (and most importantly, setting exit code)
34+
app.SilenceErrors = true
3235

33-
osutil.ExitIfError(app.Execute())
34-
}
36+
// child commands will "inherit" this.
37+
// the `cmd` will be the child command (and not root), which is the one we need.
38+
app.PersistentPreRun = func(cmd *cobra.Command, args []string) {
39+
// Cobra would displays CLI usage help for any error happened inside (= higher-level app errors) `Run` func.
40+
//
41+
// in reality we want the usage to only appear on failed input validation, because it would be confusing to
42+
// show CLI usage help for app-level errors happening above the CLI layer.
43+
//
44+
// since we arrived at the run family of funcs we know the error won't be due to input validation anymore.
45+
cmd.SilenceUsage = true
3546

36-
// fixes problems of cobra commands' bare run callbacks with regards to application quality:
37-
// 1. logging not configured
38-
// 2. no interrupt handling
39-
// 3. no error handling
40-
func WrapRun(run func(ctx context.Context, args []string) error) func(*cobra.Command, []string) {
41-
return func(_ *cobra.Command, args []string) {
42-
// handle logging
47+
// handle logging configuration. needs to be done here because `AddLogLevelControls()` instructs Cobra to
48+
// possibly modify our global log level verbosity flag but it is only mutated just before run.
4349
configureLogging()
50+
}
4451

45-
// handle interrupts
46-
ctx := notifyContextInterruptOrTerminate()
52+
// cannot `AddLogLevelControls(app.Flags())` here because it gets confusing if:
53+
// a) the root command is not runnable
54+
// b) the root command is runnable BUT it doesn't do logging (or there is no debug-level logs to suppress)
4755

48-
// run the actual code (jump from CLI context to higher-level application context)
49-
// this can be kinda read as:
50-
// output = logic(input)
51-
err := run(ctx, args)
56+
// handle signals
57+
ctx := notifyContextInterruptOrTerminate()
5258

53-
// handle errors
54-
osutil.ExitIfError(err)
55-
}
59+
// this is where the magic happens
60+
_, err := app.ExecuteContextC(ctx)
61+
62+
osutil.ExitIfError(err)
5663
}
5764

5865
// adds CLI flags that control the logging level

os/systemdcli/cli.go

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"os"
88
"os/exec"
99

10-
"github.com/function61/gokit/app/cli"
1110
"github.com/function61/gokit/os/systemdinstaller"
1211
"github.com/spf13/cobra"
1312
)
@@ -39,34 +38,34 @@ func Entrypoint(serviceName string, makeAdditionalCommands func(string) []*cobra
3938
Use: "start",
4039
Short: "Start the service",
4140
Args: cobra.NoArgs,
42-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
43-
return runSystemctlVerb(ctx, "start")
44-
}),
41+
RunE: func(cmd *cobra.Command, _ []string) error {
42+
return runSystemctlVerb(cmd.Context(), "start")
43+
},
4544
})
4645

4746
cmd.AddCommand(&cobra.Command{
4847
Use: "stop",
4948
Short: "Stop the service",
5049
Args: cobra.NoArgs,
51-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
52-
return runSystemctlVerb(ctx, "stop")
53-
}),
50+
RunE: func(cmd *cobra.Command, _ []string) error {
51+
return runSystemctlVerb(cmd.Context(), "stop")
52+
},
5453
})
5554

5655
cmd.AddCommand(&cobra.Command{
5756
Use: "restart",
5857
Short: "Restart the service",
5958
Args: cobra.NoArgs,
60-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
61-
return runSystemctlVerb(ctx, "restart")
62-
}),
59+
RunE: func(cmd *cobra.Command, args []string) error {
60+
return runSystemctlVerb(cmd.Context(), "restart")
61+
},
6362
})
6463

6564
cmd.AddCommand(&cobra.Command{
6665
Use: "status",
6766
Short: "Show status of the service",
6867
Args: cobra.NoArgs,
69-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
68+
RunE: func(cmd *cobra.Command, args []string) error {
7069
translateNonError := func(err error) error {
7170
if err != nil {
7271
// LSB dictates that successful status show of non-running program must return 3:
@@ -81,21 +80,21 @@ func Entrypoint(serviceName string, makeAdditionalCommands func(string) []*cobra
8180
}
8281
}
8382

84-
return translateNonError(runSystemctlVerb(ctx, "status"))
85-
}),
83+
return translateNonError(runSystemctlVerb(cmd.Context(), "status"))
84+
},
8685
})
8786

8887
cmd.AddCommand(&cobra.Command{
8988
Use: "logs",
9089
Short: "Get logs for the service",
9190
Args: cobra.NoArgs,
92-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
91+
RunE: func(cmd *cobra.Command, args []string) error {
9392
//nolint:gosec // ok, is expected to not be user input.
94-
logsCmd := exec.CommandContext(ctx, "journalctl", "--unit="+serviceName)
93+
logsCmd := exec.CommandContext(cmd.Context(), "journalctl", "--unit="+serviceName)
9594
logsCmd.Stdout = os.Stdout
9695
logsCmd.Stderr = os.Stderr
9796
return logsCmd.Run()
98-
}),
97+
},
9998
})
10099

101100
return cmd
@@ -108,7 +107,7 @@ func WithInstallAndUninstallCommands(makeSvc func(string) (*systemdinstaller.Ser
108107
Use: "install",
109108
Short: "Installs the background service",
110109
Args: cobra.NoArgs,
111-
Run: cli.WrapRun(func(_ context.Context, _ []string) error {
110+
RunE: func(_ *cobra.Command, args []string) error {
112111
svc, err := makeSvc(serviceName)
113112
if err != nil {
114113
return err
@@ -121,7 +120,7 @@ func WithInstallAndUninstallCommands(makeSvc func(string) (*systemdinstaller.Ser
121120
fmt.Println(systemdinstaller.EnableAndStartCommandHints(*svc))
122121

123122
return nil
124-
}),
123+
},
125124
},
126125
// TODO: add uninstall command
127126
}

0 commit comments

Comments
 (0)