Skip to content

Commit a0ee3cf

Browse files
authored
Fix/feedback (#25)
* refactor: rename to helloworld * fix: improve naming, fix bugs, import go-common from same dir * fix: feedback from ASH 1. Link to packages in tut ["cmd", "errors", "ui"] 2. link from first mention of ExecutionContext to https://github.com/neticdk/go-common/blob/main/pkg/cli/TUTORIAL.md#the-execution-context 3. Explain var version = "HEAD" 4. Limited explanation of what the different examples does in section: https://github.com/neticdk/go-common/blob/main/pkg/cli/TUTORIAL.md#using-the-executioncontext 5. OutputFormat Table please 6. perhaps make it more clear that <flag>Enabled does not enable it but "simply" makes the pflag available for use https://github.com/neticdk/go-common/blob/main/pkg/cli/TUTORIAL.md#persistentglobal-flags 7. Example of good vs bad Run() i.e. "short vs long" "When should i move code out of Run() to usecases" * fix: tests * fix: tests
1 parent 17ee624 commit a0ee3cf

File tree

5 files changed

+79
-32
lines changed

5 files changed

+79
-32
lines changed

pkg/cli/TUTORIAL.md

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ to [Scroll 11](https://scr.k8s.netic.dk/0011/).
77

88
The packages are:
99

10-
- `cmd` — provides helpers for building the root command and
10+
- [`cmd`](cmd/) — provides helpers for building the root command and
1111
sub-commands. It sets sensible defaults, global flags, configuration
1212
files, etc. It also adds an interface to sub-commands that makes
13-
completing, validating and running commands more uniform.
14-
- `errors` — provides error handling and error types.
15-
- `ui` — provides UI elements such as tables, spinners, select boxes,
16-
prompts, etc.
13+
completing, validating and running commands more predictable.
14+
- [`errors`](errors/) — provides error handling and error types.
15+
- [`ui`](ui/) — provides UI elements such as tables, spinners, select
16+
boxes, prompts, etc.
1717

1818
This tutorial covers basic usage and the core concepts of these packages.
1919

@@ -74,8 +74,8 @@ func NewContext() *Context {
7474
Here we set up the Application Context . Think of it as the container for
7575
application information and dependencies. It will vary from project to project.
7676

77-
For now it just holds a pointer to the `ExecutionContext` which you will learn
78-
more about later.
77+
For now it just holds a pointer to the
78+
[`ExecutionContext`](#the-execution-context) which you will learn more about later.
7979

8080
Create the `cmd` directory which will hold the code for the CLI commands:
8181

@@ -237,6 +237,17 @@ func main() {
237237
}
238238
```
239239

240+
This runs the `Execute` function and exits with the status code returned.
241+
242+
We use the `version` variable to set the version of the CLI when building it.
243+
The `go build` flag `-ldflags` can override variable values at build time and we
244+
use that to set the version to the current git tag. Example:
245+
246+
```bash
247+
VERSION=$(git describe --tags --always --match=v* 2>/dev/null || echo v0 | sed -e s/^v//)
248+
go build -o bin/ -ldflags '-s -w -X main.version=${VERSION}'
249+
```
250+
240251
Install dependencies:
241252

242253
```bash
@@ -360,8 +371,8 @@ are used.
360371

361372
#### Using the `ExecutionContext`
362373

363-
The `ExecutionContext` can be used by itself but works better in most cases if
364-
embedded in other context structs such an application context:
374+
The `ExecutionContext` can be used by itself but works better when embedded in
375+
other context structs such an application context:
365376

366377
```go
367378
package myapp
@@ -378,8 +389,8 @@ ac := &Context{
378389
}
379390
```
380391

381-
That way you will always have things like the `Logger` and `ErrorHandler`
382-
as well as the I/O pipes available.
392+
That way you will always have things like the `Logger`, `ErrorHandler`
393+
and I/O pipes available.
383394

384395
Create it and pass it to `newRootCmd` from `Execute()` in `cmd/root.go`:
385396

@@ -391,8 +402,8 @@ ac := &myapp.Context{EC: ec}
391402
rootCmd := newRootCmd(ac)
392403
```
393404
394-
`newRootCmd` passes the `ExecutionContext` to the functions used to create
395-
commands:
405+
The pass it from `newRootCmd` via `myapp.Context` to the functions used to
406+
create commands:
396407
397408
```go
398409
func newRootCmd(ac *myapp.Context) {
@@ -403,7 +414,7 @@ func newRootCmd(ac *myapp.Context) {
403414
)
404415
}
405416

406-
// or
417+
// or for sub-commands
407418

408419
func newSubCmd(ac *myapp.Context) {
409420
o := &options{}
@@ -412,6 +423,11 @@ func newSubCmd(ac *myapp.Context) {
412423
}
413424
```
414425
426+
The [Commands and Sub-Commands](#commands-and-sub-commands) section explains
427+
the `NewRootCommand` and `NewSubCommand` functions in detail. For now it's fine
428+
to know that they create the respective `cobra.Command` and make the `Context`
429+
available later on.
430+
415431
In the root command the `ExecutionContext` is available for all fields of the
416432
`*cobra.Command` passed to or returned from `NewRootCommand`:
417433
@@ -431,6 +447,11 @@ func newRootCmd(ac *myapp.Context) {
431447
}
432448
```
433449
450+
Here the `WithInitFunc` uses functions on the `Context` to do some
451+
initialization. `WithInitFunc` is a way to add code to the `PersistentPreRunE`
452+
function on the `cobra.Command` struct. It can be used like here to setup
453+
defaults or dependencies on the `Concext`.
454+
434455
For sub-commands, the `NewSubCommand` passes down the context to the
435456
`Complete`, `Validate` and `Run` functions:
436457
@@ -471,21 +492,24 @@ func (o *options) Run(ctx context.Context, ac *myapp.Context) error {
471492
472493
Note that the third argument passed to `NewSubCommand` is generic, so anything
473494
you pass there will end up becoming the second argument to the three functions.
474-
More on that later.
495+
In most cases this will be the Application Context. More on that later.
475496
476497
### Persistent/Global Flags
477498
478499
The `cmd` package comes with default persistent flags, some of which are
479500
permanent/mandatory and some of which can be toggled. Persistent flags are
480501
always present for all commands.
481502
482-
To enable a flag, set `<FLAG>Enabled` to `true`:
503+
To enable setting a flag (not enabling the flag itself), set `<FLAG>Enabled`
504+
to `true`:
483505
484506
```go
485507
ec := cmd.NewExecutionContext(...)
486508
ec.PFlags.DryRunEnabled = true
487509
```
488510
511+
This makes it possible to use the `--dry-run` flag.
512+
489513
See [`cmd/flags.go`](cmd/flags.go) for more information about available flags.
490514
491515
Flags that can't be disabled:
@@ -507,6 +531,7 @@ like JSON for machines. This can be set via flags.
507531
- `cmd.OutputFormatJSON`
508532
- `cmd.OutputFormatYAML`
509533
- `cmd.OutputFormatMarkdown`
534+
- `cmd.OutputFormatTable`
510535
511536
To set it, enable the format flags you want to support.
512537
@@ -516,6 +541,7 @@ For now, these are:
516541
- `--json`
517542
- `--yaml`
518543
- `--markdown`
544+
- `--table`
519545
520546
They are mutually exclusive. Enable them like this:
521547
@@ -524,6 +550,7 @@ ec.PFlags.PlainEnabled = true
524550
ec.PFlags.JSONEnabled = true
525551
ec.PFlags.YAMLEnabled = true
526552
ec.PFlags.MarkdownEnabled = true
553+
ec.PFlags.TableEnabled = true
527554
```
528555
529556
#### Enabling flags
@@ -984,7 +1011,7 @@ func (o *helloOptions) Run(_ context.Context, ac *helloworld.Context) error {
9841011
```
9851012
9861013
In real life applications things are rarely so simple. So, we encourage the use
987-
of the 'Use cases' term taken from Clean Architecture.
1014+
of 'Use cases', a term taken from Clean Architecture.
9881015
9891016
From the Clean Architecture book:
9901017
@@ -1038,6 +1065,16 @@ func (o *options) Run(ctx context.Context, ac *myapp.Context) error {
10381065
10391066
This makes it easier to separate concerns.
10401067
1068+
#### When to Create Use Cases
1069+
1070+
As mentioned, we encourage the use of use cases. But there is no need to
1071+
needlessly complicate things. If you are just doing something that borders what
1072+
may be called business logic or something small (think a few lines of code) it
1073+
is OK to keep the code in `Run`. But keep in mind that code in `cmd/` is *meant*
1074+
to handle command line logic, not business logic. So in the long run and for
1075+
maintainability it might be smart to create use cases even though they don't do
1076+
much.
1077+
10411078
### UI Elements
10421079
10431080
There's a couple of UI elements included in the `ui` package. There are:

pkg/cli/cmd/context.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ type ExecutionContext struct {
5959
PFlags PFlags
6060

6161
// OutputFormat is the format used for outputting data
62-
// Flags: --plain, --json, --yaml, --markdown, etc
63-
OutputFormat OutputFormat
62+
// Flags: --plain, --json, --yaml, --markdown, --table
63+
OutputFormat string
6464

6565
// for changing log level
6666
LogLevel *slog.LevelVar

pkg/cli/cmd/flags.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ type PFlags struct {
6464
JSONEnabled bool
6565
YAMLEnabled bool
6666
MarkdownEnabled bool
67+
TableEnabled bool
6768

6869
// NoHeaders is used to control whether headers are printed
6970
// Flag: --no-headers
@@ -75,7 +76,7 @@ type PFlags struct {
7576

7677
// AddPersistentFlags adds global flags to the command and does some initialization
7778
func AddPersistentFlags(cmd *cobra.Command, ec *ExecutionContext) *pflag.FlagSet {
78-
var plain, json, yaml, markdown bool
79+
var plain, json, yaml, markdown, table bool
7980

8081
f := cmd.PersistentFlags()
8182

@@ -113,6 +114,9 @@ func AddPersistentFlags(cmd *cobra.Command, ec *ExecutionContext) *pflag.FlagSet
113114
if ec.PFlags.MarkdownEnabled {
114115
f.BoolVar(&markdown, "markdown", false, "Output in Markdown format")
115116
}
117+
if ec.PFlags.TableEnabled {
118+
f.BoolVar(&table, "table", false, "Output in table format")
119+
}
116120

117121
_ = cmd.PersistentFlags().Parse(os.Args[1:])
118122

@@ -143,6 +147,11 @@ func AddPersistentFlags(cmd *cobra.Command, ec *ExecutionContext) *pflag.FlagSet
143147
outputFlags = append(outputFlags, "markdown")
144148
}
145149

150+
if tableArg, err := cmd.PersistentFlags().GetBool("table"); err == nil && tableArg {
151+
ec.OutputFormat = OutputFormatTable
152+
outputFlags = append(outputFlags, "table")
153+
}
154+
146155
cmd.MarkFlagsMutuallyExclusive(outputFlags...)
147156

148157
if noColor, err := cmd.PersistentFlags().GetBool("no-color"); err == nil {

pkg/cli/cmd/flags_test.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func TestAddPersistentFlags(t *testing.T) {
1313
tests := []struct {
1414
name string
1515
args []string
16-
expectedFormat cmd.OutputFormat
16+
expectedFormat string
1717
expectedFlags []string
1818
}{
1919
{
@@ -40,6 +40,12 @@ func TestAddPersistentFlags(t *testing.T) {
4040
expectedFormat: cmd.OutputFormatMarkdown,
4141
expectedFlags: []string{"markdown"},
4242
},
43+
{
44+
name: "Table format",
45+
args: []string{"--table"},
46+
expectedFormat: cmd.OutputFormatTable,
47+
expectedFlags: []string{"table"},
48+
},
4349
}
4450

4551
for _, tt := range tests {
@@ -49,6 +55,7 @@ func TestAddPersistentFlags(t *testing.T) {
4955
ec.PFlags.JSONEnabled = true
5056
ec.PFlags.YAMLEnabled = true
5157
ec.PFlags.MarkdownEnabled = true
58+
ec.PFlags.TableEnabled = true
5259

5360
os.Args = append([]string{"cmd"}, tt.args...)
5461
cmd.AddPersistentFlags(c, ec)

pkg/cli/cmd/output.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
package cmd
22

3-
// OutputFormat represents the output format for the command
4-
type OutputFormat string
5-
63
const (
7-
OutputFormatPlain OutputFormat = "plain"
8-
OutputFormatJSON OutputFormat = "json"
9-
OutputFormatYAML OutputFormat = "yaml"
10-
OutputFormatMarkdown OutputFormat = "markdown"
4+
OutputFormatPlain = "plain"
5+
OutputFormatJSON = "json"
6+
OutputFormatYAML = "yaml"
7+
OutputFormatMarkdown = "markdown"
8+
OutputFormatTable = "table"
119
)
12-
13-
func (o OutputFormat) String() string {
14-
return string(o)
15-
}

0 commit comments

Comments
 (0)