Skip to content

Commit 634c7b1

Browse files
committed
cli.WrapRun(): change to be given to RunE field
this field actually propagates the error through Cobra to the `Command.Execute()`
1 parent c43746e commit 634c7b1

2 files changed

Lines changed: 44 additions & 27 deletions

File tree

app/cli/cobrawrappers.go

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import (
1111
"github.com/spf13/pflag"
1212
)
1313

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.)
14+
// wraps the `Execute()` call of the command to inject boilerplate details for root command:
15+
// - `Use` (for root cmd should be `os.Args[0]`)
16+
// - `Version` (should be program's built-in version info)
17+
// - UX improvements (some Cobra defaults are in poor taste)
18+
// - configure logging
19+
// - deliver context & cancel it on signals
1620
func Execute(app *cobra.Command) {
1721
// dirty to mutate after-the-fact
1822

1923
app.Use = os.Args[0]
2024
app.Version = dynversion.Version
25+
2126
// hide the default "completion" subcommand from polluting UX (it can still be used). https://github.com/spf13/cobra/issues/1507
2227
app.CompletionOptions = cobra.CompletionOptions{HiddenDefaultCmd: true}
2328

@@ -26,32 +31,44 @@ func Execute(app *cobra.Command) {
2631
// https://github.com/spf13/cobra/issues/587#issuecomment-843747825
2732
app.SetHelpCommand(&cobra.Command{Hidden: true})
2833

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

33-
osutil.ExitIfError(app.Execute())
41+
// handle logging
42+
configureLogging()
43+
44+
// handle signals
45+
ctx := notifyContextInterruptOrTerminate()
46+
47+
// this is where the magic happens
48+
_, err := app.ExecuteContextC(ctx)
49+
50+
osutil.ExitIfError(err)
3451
}
3552

3653
// 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
43-
configureLogging()
44-
45-
// handle interrupts
46-
ctx := notifyContextInterruptOrTerminate()
47-
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)
52-
53-
// handle errors
54-
osutil.ExitIfError(err)
54+
// 1. `context.Context` (interrupt handling) has to be fetched from a middle man (not idiomatic Go)
55+
// 2. CLI usage help would be shown for all (even higher app-level) errors
56+
//
57+
// this is intended to be given to `RunE` field in `*cobra.Command`
58+
func WrapRun(run func(ctx context.Context, args []string) error) func(*cobra.Command, []string) error {
59+
return func(cmd *cobra.Command, args []string) error {
60+
// Cobra would displays CLI usage help for any error happened inside `Run` func.
61+
//
62+
// in reality we want the usage to only appear on failed input validation, because it would be confusing to
63+
// show CLI usage help for app-level errors happening above the CLI layer.
64+
//
65+
// since we arrived at the `Run` func we know the error won't be due to input validation anymore.
66+
cmd.SilenceUsage = true
67+
68+
// retrieve back the context that was injected by our `Execute()`
69+
ctx := cmd.Context()
70+
71+
return run(ctx, args)
5572
}
5673
}
5774

os/systemdcli/cli.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func Entrypoint(serviceName string, makeAdditionalCommands func(string) []*cobra
3939
Use: "start",
4040
Short: "Start the service",
4141
Args: cobra.NoArgs,
42-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
42+
RunE: cli.WrapRun(func(ctx context.Context, _ []string) error {
4343
return runSystemctlVerb(ctx, "start")
4444
}),
4545
})
@@ -48,7 +48,7 @@ func Entrypoint(serviceName string, makeAdditionalCommands func(string) []*cobra
4848
Use: "stop",
4949
Short: "Stop the service",
5050
Args: cobra.NoArgs,
51-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
51+
RunE: cli.WrapRun(func(ctx context.Context, _ []string) error {
5252
return runSystemctlVerb(ctx, "stop")
5353
}),
5454
})
@@ -57,7 +57,7 @@ func Entrypoint(serviceName string, makeAdditionalCommands func(string) []*cobra
5757
Use: "restart",
5858
Short: "Restart the service",
5959
Args: cobra.NoArgs,
60-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
60+
RunE: cli.WrapRun(func(ctx context.Context, _ []string) error {
6161
return runSystemctlVerb(ctx, "restart")
6262
}),
6363
})
@@ -66,7 +66,7 @@ func Entrypoint(serviceName string, makeAdditionalCommands func(string) []*cobra
6666
Use: "status",
6767
Short: "Show status of the service",
6868
Args: cobra.NoArgs,
69-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
69+
RunE: cli.WrapRun(func(ctx context.Context, _ []string) error {
7070
translateNonError := func(err error) error {
7171
if err != nil {
7272
// LSB dictates that successful status show of non-running program must return 3:
@@ -89,7 +89,7 @@ func Entrypoint(serviceName string, makeAdditionalCommands func(string) []*cobra
8989
Use: "logs",
9090
Short: "Get logs for the service",
9191
Args: cobra.NoArgs,
92-
Run: cli.WrapRun(func(ctx context.Context, _ []string) error {
92+
RunE: cli.WrapRun(func(ctx context.Context, _ []string) error {
9393
//nolint:gosec // ok, is expected to not be user input.
9494
logsCmd := exec.CommandContext(ctx, "journalctl", "--unit="+serviceName)
9595
logsCmd.Stdout = os.Stdout
@@ -108,7 +108,7 @@ func WithInstallAndUninstallCommands(makeSvc func(string) (*systemdinstaller.Ser
108108
Use: "install",
109109
Short: "Installs the background service",
110110
Args: cobra.NoArgs,
111-
Run: cli.WrapRun(func(_ context.Context, _ []string) error {
111+
RunE: cli.WrapRun(func(_ context.Context, _ []string) error {
112112
svc, err := makeSvc(serviceName)
113113
if err != nil {
114114
return err

0 commit comments

Comments
 (0)