Skip to content

[1/2] x/cli/output: Add an abstraction to take care of output rendering#27

Merged
AlexisMontagne merged 7 commits into
masterfrom
am/cli-output
May 13, 2026
Merged

[1/2] x/cli/output: Add an abstraction to take care of output rendering#27
AlexisMontagne merged 7 commits into
masterfrom
am/cli-output

Conversation

@AlexisMontagne
Copy link
Copy Markdown
Member

What does this PR do?

A demo is worth 1000 words:

Imagine:

type subValue struct {
	Text string `flag:"text,t"`
}
type config struct {
	Values map[string]subValue `flag:"values,v"`
}

type result struct {
	Message string `json:"message" yaml:"message"`
	Foo     string `json:"foo"     yaml:"foo"`
	Bar     string `json:"bar"     yaml:"bar"`

	SubValue  subValue
	SubStruct map[string]subValue
}

func main() {
	staticCmd := output.DefaultStaticCommand(
		func(_ context.Context, _ cli.CommandContext, cfg config) ([]result, error) {
			return []result{
				{
					Message: "success",
					Foo:     cfg.Values["foo"].Text,
					Bar:     cfg.Values["bar"].Text,
					SubStruct: map[string]subValue{
						"zzz": {Text: "zzz"},
					},
				},
			}, nil
		},
		output.WithShortHelp[config, []result]("example command"),
	)

	cli.NewApp(
		cli.WithName("example"),
		cli.WithCommand(
			output.WrapDefaultCommand(
				staticCmd,
				table.NewDefaultTablePrinter[result](),
				table.NewDefaultCSVPrinter[result](),
			),
		),
	).Run(context.Background())
}

you can now have multi rendering codec program that are "unix philosophy ready".

$ go run ./example
- message: success
  foo: ""
  bar: ""
  subvalue:
    text: ""
  substruct:
    zzz:
        text: zzz

$ go run ./example -o json
[{"message":"success","foo":"","bar":"","SubValue":{"Text":""},"SubStruct":{"zzz":{"Text":"zzz"}}}]

$ go run ./example -o table
Message  Foo  Bar  SubValue.Text  SubStruct
success                           map[zzz:{zzz}]

$ go run ./example -o csv
Message,Foo,Bar,SubValue.Text,SubStruct
success,,,,map[zzz:{zzz}]

What are the observable changes?

Good PR checklist

  • Title makes sense
  • Is against the correct branch
  • Only addresses one issue
  • Properly assigned
  • Added/updated tests
  • Added/updated documentation
  • Properly labeled

Additional Notes

@AlexisMontagne AlexisMontagne self-assigned this Apr 28, 2026
@AlexisMontagne AlexisMontagne requested a review from a team as a code owner April 28, 2026 18:25
@AlexisMontagne AlexisMontagne requested review from FlorianRichardUPF and xgoffin and removed request for a team April 28, 2026 18:25
Comment thread configurator_test.go
Copy link
Copy Markdown
Contributor

@xgoffin xgoffin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I understand nothing of any of this. I understand its purpose but how it gets there from a code standpoint? Nope.
I don't think it's worth changing code over this, but this desperately needs a "walkthrough" doc that says which bit is responsible for what. Also means I can't exactly review this.

Comment thread internal/walker/walker.go
Comment on lines +24 to +27
// Prefixed is an optional interface that a value passed to Walk can
// implement to inject dynamic key prefix segments. When Walk receives a
// Prefixed value it builds a synthetic ancestor chain from the prefix
// segments and walks the inner value returned by WalkValue.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll be 100% straight here: I have no idea what this means.
How about adding a little example to the comment? 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checkout ./configurator_test.go it explicit the behavour quite extensively

enc.SetIndent("", " ")
}

return enc.Encode(v) //nolint:wrapcheck
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have several nolint:wrapcheck in the coming files: do they serve any purpose in asserting errors? Why put them and not simply a wrap?

)
)

walker.Walk( //nolint:errcheck
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is mostly printing, we can consider panicking if the Walk failed, but it is not a hill I will die on

@@ -0,0 +1,156 @@
package output_test
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

package output_test? 🤔 package output at the same level though. Either rename or add a layer?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this file implements some "integration" test i wanted to keep things separated to make sure there is no interference of same package private structs. As the whole shebang relies on quite a bit of reflection

@AlexisMontagne AlexisMontagne merged commit fbbf426 into master May 13, 2026
4 of 5 checks passed
@AlexisMontagne AlexisMontagne deleted the am/cli-output branch May 13, 2026 18:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants