Add shared report format flag
This commit is contained in:
48
builtins.go
48
builtins.go
@@ -4,10 +4,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
internalcobra "go.maximumdirect.net/commandkit/internal/cobra"
|
internalcobra "go.maximumdirect.net/commandkit/internal/cobra"
|
||||||
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
|
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
|
||||||
yaml "go.yaml.in/yaml/v4"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const commandKitModulePath = "go.maximumdirect.net/commandkit"
|
const commandKitModulePath = "go.maximumdirect.net/commandkit"
|
||||||
@@ -18,15 +18,16 @@ type helpInput struct {
|
|||||||
|
|
||||||
type versionInput struct {
|
type versionInput struct {
|
||||||
Scope string
|
Scope string
|
||||||
Format string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type versionDocument struct {
|
type versionDocument struct {
|
||||||
Name string `yaml:"name"`
|
Name string `json:"name"`
|
||||||
Version string `yaml:"version"`
|
Version string `json:"version"`
|
||||||
Revision string `yaml:"revision"`
|
Revision string `json:"revision"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var versionTextTemplate = template.Must(ParseTextTemplate("version", "{{.Name}} {{.Version}}{{if .Revision}} ({{.Revision}}){{end}}\n", nil))
|
||||||
|
|
||||||
func (a *App) builtinCommands() []Declaration {
|
func (a *App) builtinCommands() []Declaration {
|
||||||
help := captureCommand(CommandSpec[helpInput]{
|
help := captureCommand(CommandSpec[helpInput]{
|
||||||
Name: "help",
|
Name: "help",
|
||||||
@@ -39,14 +40,12 @@ func (a *App) builtinCommands() []Declaration {
|
|||||||
return current.renderHelpTargets(input.Targets.Value)
|
return current.renderHelpTargets(input.Targets.Value)
|
||||||
})
|
})
|
||||||
scopeDefault := "app"
|
scopeDefault := "app"
|
||||||
formatDefault := "text"
|
|
||||||
version := NewCommand(CommandSpec[versionInput]{
|
version := NewCommand(CommandSpec[versionInput]{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Summary: "Show application or CommandKit version.",
|
Summary: "Show application or CommandKit version.",
|
||||||
Description: "Report identity and build metadata without loading application configuration.",
|
Description: "Report identity and build metadata without loading application configuration.",
|
||||||
Flags: []Flag[versionInput]{
|
Flags: []Flag[versionInput]{
|
||||||
ValueFlag(FlagMeta{Name: "scope", Usage: "version subject", Completion: CompleteChoices(CompletionChoice{Value: "app"}, CompletionChoice{Value: "commandkit"})}, &scopeDefault, parseVersionScope, func(input *versionInput, value Supplied[string]) { input.Scope = value.Value }),
|
ValueFlag(FlagMeta{Name: "scope", Usage: "version subject", Completion: CompleteChoices(CompletionChoice{Value: "app"}, CompletionChoice{Value: "commandkit"})}, &scopeDefault, parseVersionScope, func(input *versionInput, value Supplied[string]) { input.Scope = value.Value }),
|
||||||
ValueFlag(FlagMeta{Name: "format", Usage: "output format", Completion: CompleteChoices(CompletionChoice{Value: "text"}, CompletionChoice{Value: "yaml"})}, &formatDefault, parseVersionFormat, func(input *versionInput, value Supplied[string]) { input.Format = value.Value }),
|
|
||||||
},
|
},
|
||||||
Run: func(invocation Invocation, input versionInput) Outcome {
|
Run: func(invocation Invocation, input versionInput) Outcome {
|
||||||
identity := a.identity
|
identity := a.identity
|
||||||
@@ -56,7 +55,7 @@ func (a *App) builtinCommands() []Declaration {
|
|||||||
if identity.Version == "" {
|
if identity.Version == "" {
|
||||||
identity.Version = "dev"
|
identity.Version = "dev"
|
||||||
}
|
}
|
||||||
return renderVersionOutcome(identity, input.Format, invocation.Reporter())
|
return renderVersionOutcome(identity, invocation.Reporter())
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return []Declaration{help, version}
|
return []Declaration{help, version}
|
||||||
@@ -69,13 +68,6 @@ func parseVersionScope(value string) (string, error) {
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseVersionFormat(value string) (string, error) {
|
|
||||||
if value != "text" && value != "yaml" {
|
|
||||||
return "", fmt.Errorf("format must be text or yaml, got %q", value)
|
|
||||||
}
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) renderHelpViewOutcomeWithSession(view commandmeta.HelpView, reporter *Reporter, session *internalcobra.Session) Outcome {
|
func (a *App) renderHelpViewOutcomeWithSession(view commandmeta.HelpView, reporter *Reporter, session *internalcobra.Session) Outcome {
|
||||||
content, failure := session.RenderHelpView(view)
|
content, failure := session.RenderHelpView(view)
|
||||||
if failure != nil {
|
if failure != nil {
|
||||||
@@ -91,36 +83,14 @@ func (a *App) renderHelpViewOutcomeWithSession(view commandmeta.HelpView, report
|
|||||||
return Success()
|
return Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderVersionOutcome(identity Identity, format string, reporter *Reporter) Outcome {
|
func renderVersionOutcome(identity Identity, reporter *Reporter) Outcome {
|
||||||
if identity.Version == "" {
|
if identity.Version == "" {
|
||||||
identity.Version = "dev"
|
identity.Version = "dev"
|
||||||
}
|
}
|
||||||
switch format {
|
_ = reporter.Report(Report{Value: versionDocument{Name: identity.Name, Version: identity.Version, Revision: identity.Revision}, TextTemplate: versionTextTemplate})
|
||||||
case "text":
|
|
||||||
line := identity.Name + " " + identity.Version
|
|
||||||
if identity.Revision != "" {
|
|
||||||
line += " (" + identity.Revision + ")"
|
|
||||||
}
|
|
||||||
_ = reporter.RenderOutput(func(w io.Writer) error {
|
|
||||||
_, err := io.WriteString(w, line+"\n")
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
case "yaml":
|
|
||||||
_ = reporter.RenderOutput(func(w io.Writer) error {
|
|
||||||
return encodeYAML(w, versionDocument{Name: identity.Name, Version: identity.Version, Revision: identity.Revision})
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return Usage(fmt.Sprintf("unsupported version format %q", format))
|
|
||||||
}
|
|
||||||
return Success()
|
return Success()
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeYAML(writer io.Writer, document any) error {
|
|
||||||
encoder := yaml.NewEncoder(writer)
|
|
||||||
encoder.SetIndent(2)
|
|
||||||
return encoder.Encode(document)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBuildInfo() *debug.BuildInfo {
|
func readBuildInfo() *debug.BuildInfo {
|
||||||
info, ok := debug.ReadBuildInfo()
|
info, ok := debug.ReadBuildInfo()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ func TestCompletionQueryDispatch(t *testing.T) {
|
|||||||
{"alias value", []string{"__complete", "shortcut", "--colour=bl"}, "blue\tcool color\n", false},
|
{"alias value", []string{"__complete", "shortcut", "--colour=bl"}, "blue\tcool color\n", false},
|
||||||
{"quiet is data", []string{"__complete", "run", "--quiet", "--color=bl"}, "blue\tcool color\n", false},
|
{"quiet is data", []string{"__complete", "run", "--quiet", "--color=bl"}, "blue\tcool color\n", false},
|
||||||
{"debug is data", []string{"__complete", "run", "--debug", "--color=bl"}, "blue\tcool color\n", false},
|
{"debug is data", []string{"__complete", "run", "--debug", "--color=bl"}, "blue\tcool color\n", false},
|
||||||
|
{"format value", []string{"__complete", "run", "--format", "j"}, "json\n", false},
|
||||||
{"short attached choice", []string{"__complete", "run", "-cfoo=ba"}, "foo=bar\n", false},
|
{"short attached choice", []string{"__complete", "run", "-cfoo=ba"}, "foo=bar\n", false},
|
||||||
{"unknown route", []string{"__complete", "missing", ""}, emptyReply, true},
|
{"unknown route", []string{"__complete", "missing", ""}, emptyReply, true},
|
||||||
} {
|
} {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestCompletionHintsFollowCapturedBindings(t *testing.T) {
|
|||||||
t.Fatalf("no completion: %#v", got)
|
t.Fatalf("no completion: %#v", got)
|
||||||
}
|
}
|
||||||
for _, flag := range append(commandmeta.ReportingFlags(), commandmeta.HelpFlag()) {
|
for _, flag := range append(commandmeta.ReportingFlags(), commandmeta.HelpFlag()) {
|
||||||
if !reflect.DeepEqual(flag.Completion.Choices, []commandmeta.CompletionChoice{{Value: "true"}, {Value: "false"}}) {
|
if flag.Boolean && !reflect.DeepEqual(flag.Completion.Choices, []commandmeta.CompletionChoice{{Value: "true"}, {Value: "false"}}) {
|
||||||
t.Fatalf("reporting flag choices: %#v", flag)
|
t.Fatalf("reporting flag choices: %#v", flag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,15 @@ func TestPublicInstallationAcceptsMixedDeclarations(t *testing.T) {
|
|||||||
Name: "show",
|
Name: "show",
|
||||||
Summary: "Show a value.",
|
Summary: "Show a value.",
|
||||||
Flags: []commandkit.Flag[struct{}]{
|
Flags: []commandkit.Flag[struct{}]{
|
||||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, commandkit.Supplied[string]) {}),
|
commandkit.ValueFlag(commandkit.FlagMeta{Name: "style"}, (*string)(nil), func(value string) (string, error) { return value, nil }, func(*struct{}, commandkit.Supplied[string]) {}),
|
||||||
},
|
},
|
||||||
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() },
|
Run: func(commandkit.Invocation, struct{}) commandkit.Outcome { return commandkit.Success() },
|
||||||
})
|
})
|
||||||
declarations := []commandkit.Declaration{
|
declarations := []commandkit.Declaration{
|
||||||
command,
|
command,
|
||||||
commandkit.Alias("inspect", "show"),
|
commandkit.Alias("inspect", "show"),
|
||||||
commandkit.FlagAlias("--fmt", "--format"),
|
commandkit.FlagAlias("--fmt", "--style"),
|
||||||
commandkit.FlagAliasFor("show", "-f", "--format"),
|
commandkit.FlagAliasFor("show", "-f", "--style"),
|
||||||
}
|
}
|
||||||
app, err := commandkit.New(commandkit.Identity{Name: "Example"}, declarations...)
|
app, err := commandkit.New(commandkit.Identity{Name: "Example"}, declarations...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -84,7 +84,7 @@ func TestPublicAliasesPreserveCommandsAcrossDeclarationOrders(t *testing.T) {
|
|||||||
func(value string) (string, error) { optionalParses++; return value, nil },
|
func(value string) (string, error) { optionalParses++; return value, nil },
|
||||||
func(target *showInput, value commandkit.Supplied[string]) { target.profile = value },
|
func(target *showInput, value commandkit.Supplied[string]) { target.profile = value },
|
||||||
)},
|
)},
|
||||||
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil),
|
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "style"}, (*string)(nil),
|
||||||
func(value string) (string, error) { return value, nil },
|
func(value string) (string, error) { return value, nil },
|
||||||
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
||||||
)},
|
)},
|
||||||
@@ -99,7 +99,7 @@ func TestPublicAliasesPreserveCommandsAcrossDeclarationOrders(t *testing.T) {
|
|||||||
commandkit.Alias("r full", "inspect"),
|
commandkit.Alias("r full", "inspect"),
|
||||||
commandkit.Alias("inspect-shortcut", "inspect"),
|
commandkit.Alias("inspect-shortcut", "inspect"),
|
||||||
commandkit.Alias("i", "inspect-shortcut"),
|
commandkit.Alias("i", "inspect-shortcut"),
|
||||||
commandkit.FlagAlias("--fmt", "--format"),
|
commandkit.FlagAlias("--fmt", "--style"),
|
||||||
}
|
}
|
||||||
declarations := append([]commandkit.Declaration(nil), commands...)
|
declarations := append([]commandkit.Declaration(nil), commands...)
|
||||||
if test.rulesFirst {
|
if test.rulesFirst {
|
||||||
@@ -154,7 +154,7 @@ func TestPublicRootGroupRegistrationAndFailedBatchRecovery(t *testing.T) {
|
|||||||
var formats []string
|
var formats []string
|
||||||
show := commandkit.NewCommand(commandkit.CommandSpec[showInput]{
|
show := commandkit.NewCommand(commandkit.CommandSpec[showInput]{
|
||||||
Name: "show",
|
Name: "show",
|
||||||
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "format"}, (*string)(nil),
|
Flags: []commandkit.Flag[showInput]{commandkit.ValueFlag(commandkit.FlagMeta{Name: "style"}, (*string)(nil),
|
||||||
func(value string) (string, error) { formatParses++; return value, nil },
|
func(value string) (string, error) { formatParses++; return value, nil },
|
||||||
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
func(target *showInput, value commandkit.Supplied[string]) { target.format = value },
|
||||||
)},
|
)},
|
||||||
@@ -181,7 +181,7 @@ func TestPublicRootGroupRegistrationAndFailedBatchRecovery(t *testing.T) {
|
|||||||
if err := app.Register(family,
|
if err := app.Register(family,
|
||||||
commandkit.Alias("cfg-show", "config show"),
|
commandkit.Alias("cfg-show", "config show"),
|
||||||
commandkit.Alias("cs", "cfg-show"),
|
commandkit.Alias("cs", "cfg-show"),
|
||||||
commandkit.FlagAlias("--fmt", "--format"),
|
commandkit.FlagAlias("--fmt", "--style"),
|
||||||
); err != nil {
|
); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
18
docs/cli.md
18
docs/cli.md
@@ -46,7 +46,7 @@ The supported flags are:
|
|||||||
| Flag | Values | Default | Meaning |
|
| Flag | Values | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `--scope` | `app`, `commandkit` | `app` | Select the caller's application identity or the embedded CommandKit module identity. |
|
| `--scope` | `app`, `commandkit` | `app` | Select the caller's application identity or the embedded CommandKit module identity. |
|
||||||
| `--format` | `text`, `yaml` | `text` | Select the output format. |
|
| `--format` | `text`, `yaml`, `json` | automatic | Select the output format; automatic uses text for version. |
|
||||||
|
|
||||||
Text output is `NAME VERSION`, followed by ` (REVISION)` when the selected
|
Text output is `NAME VERSION`, followed by ` (REVISION)` when the selected
|
||||||
identity has a revision. An empty application version is displayed as `dev`;
|
identity has a revision. An empty application version is displayed as `dev`;
|
||||||
@@ -58,7 +58,7 @@ versionless replacement, an untagged module, or missing build information
|
|||||||
displays `dev`. The CommandKit identity name is `CommandKit`; dependency build
|
displays `dev`. The CommandKit identity name is `CommandKit`; dependency build
|
||||||
metadata does not supply a revision.
|
metadata does not supply a revision.
|
||||||
|
|
||||||
YAML output uses these explicit fields, including an empty revision:
|
YAML and JSON output use these fields, including an empty revision:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: ExampleApp
|
name: ExampleApp
|
||||||
@@ -111,6 +111,14 @@ caller passes result and diagnostic writers to `App.Execute`; substantive
|
|||||||
results go to `Streams.Out`, and categorized messages, terminal explanations,
|
results go to `Streams.Out`, and categorized messages, terminal explanations,
|
||||||
and usage guidance go to `Streams.Err`.
|
and usage guidance go to `Streams.Err`.
|
||||||
|
|
||||||
|
The shared `--format` flag selects `text`, `yaml`, or `json` for reports
|
||||||
|
submitted through the core reporter. When omitted, a report's text template
|
||||||
|
selects text; otherwise scalar values and nonempty string lists use text, while
|
||||||
|
structured values and empty collections use YAML. Explicit text without a
|
||||||
|
template uses YAML for structured values. Invalid or empty format values are
|
||||||
|
usage errors. Raw output, help, completion, and diagnostics are not encoded as
|
||||||
|
reports.
|
||||||
|
|
||||||
### Modes and visibility
|
### Modes and visibility
|
||||||
|
|
||||||
Each invocation starts in ordinary mode. `--quiet`, `--verbose`, and `--debug`
|
Each invocation starts in ordinary mode. `--quiet`, `--verbose`, and `--debug`
|
||||||
@@ -154,9 +162,9 @@ example version --quiet
|
|||||||
Reporting flags do not go before or between command-path components. Tokens
|
Reporting flags do not go before or between command-path components. Tokens
|
||||||
consumed as values and tokens after `--` retain their normal meaning.
|
consumed as values and tokens after `--` retain their normal meaning.
|
||||||
|
|
||||||
The reserved long spellings are `--quiet`, `--verbose`, `--debug`, and
|
The reserved long spellings are `--quiet`, `--verbose`, `--debug`,
|
||||||
`--progress`; application declarations cannot reuse them. There are no built-in
|
`--progress`, and `--format`; application declarations cannot reuse them. There
|
||||||
short spellings. Applications may define aliases such as `-q → --quiet`,
|
are no built-in short spellings. Applications may define aliases such as `-q → --quiet`,
|
||||||
`-v → --verbose`, and `-d → --debug`; canonical and aliased occurrences share
|
`-v → --verbose`, and `-d → --debug`; canonical and aliased occurrences share
|
||||||
the same ordering. Thus `example export --verbose -q` selects quiet mode.
|
the same ordering. Thus `example export --verbose -q` selects quiet mode.
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ installation alongside aliases and integration commands.
|
|||||||
## Invocation reporting
|
## Invocation reporting
|
||||||
|
|
||||||
`Invocation.Reporter()` supplies the reporter for the current handler. Use
|
`Invocation.Reporter()` supplies the reporter for the current handler. Use
|
||||||
`Output()` or `RenderOutput()` for substantive results; use `Warn`, `Info`,
|
`Report()` for formatted substantive values and optional text templates;
|
||||||
|
use `Output()` or `RenderOutput()` for deliberate raw output. Use `Warn`, `Info`,
|
||||||
`Progress`, and `Debug` for categorized messages. Queue a summary with
|
`Progress`, and `Debug` for categorized messages. Queue a summary with
|
||||||
`AtEnd().Info` when it belongs after the handler returns. The
|
`AtEnd().Info` when it belongs after the handler returns. The
|
||||||
[CLI reference](../cli.md#reporting) owns flag grammar, visibility, prefixes,
|
[CLI reference](../cli.md#reporting) owns flag grammar, visibility, prefixes,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit v0.20.0 implementation plan
|
# Shared reporting and PromptKit v0.20.0 implementation plan
|
||||||
|
|
||||||
Status: implementation in progress; Stages 1–3 complete.
|
Status: implementation in progress; Stages 1–4 complete.
|
||||||
|
|
||||||
## Scope, authority, and execution
|
## Scope, authority, and execution
|
||||||
|
|
||||||
@@ -160,6 +160,11 @@ boundary in GoDoc.
|
|||||||
|
|
||||||
## Stage 4: Universal format input, core version, and a core consumer
|
## Stage 4: Universal format input, core version, and a core consumer
|
||||||
|
|
||||||
|
**Complete.** Core format selection, version reporting, and a maintained core
|
||||||
|
consumer use the shared report path. Focused parser, routing, completion, and
|
||||||
|
reporting tests and the root workspace suite (`GOTOOLCHAIN=local go test ./...`
|
||||||
|
with Go 1.27.0) passed. Integration migrations remain in later stages.
|
||||||
|
|
||||||
Extend neutral core flag metadata and parser selection in
|
Extend neutral core flag metadata and parser selection in
|
||||||
[metadata.go](../../internal/commandmeta/metadata.go),
|
[metadata.go](../../internal/commandmeta/metadata.go),
|
||||||
[parser.go](../../parser.go), and the internal Cobra adapter. Add core-owned
|
[parser.go](../../parser.go), and the internal Cobra adapter. Add core-owned
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
commandkit "go.maximumdirect.net/commandkit"
|
commandkit "go.maximumdirect.net/commandkit"
|
||||||
)
|
)
|
||||||
@@ -20,6 +21,15 @@ type reportInput struct {
|
|||||||
Scenario string
|
Scenario string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type greetingReport struct {
|
||||||
|
Count string `json:"count"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
Entered string `json:"entered"`
|
||||||
|
Canonical string `json:"canonical"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var greetingTemplate = template.Must(commandkit.ParseTextTemplate("greeting", "Hello; count={{.Count}}; tags={{.Tags}}; entered={{printf \"%q\" .Entered}}; canonical={{printf \"%q\" .Canonical}}\n", nil))
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errRecoveredFixture = errors.New("a local read failed and was recovered")
|
errRecoveredFixture = errors.New("a local read failed and was recovered")
|
||||||
errWarningFixture = errors.New("a local warning has a cause")
|
errWarningFixture = errors.New("a local warning has a cause")
|
||||||
@@ -50,10 +60,7 @@ func newApp() (*commandkit.App, error) {
|
|||||||
if input.Tags.Set {
|
if input.Tags.Set {
|
||||||
tags = fmt.Sprintf("explicit %v", input.Tags.Value)
|
tags = fmt.Sprintf("explicit %v", input.Tags.Value)
|
||||||
}
|
}
|
||||||
_ = reporter.RenderOutput(func(writer io.Writer) error {
|
_ = reporter.Report(commandkit.Report{Value: greetingReport{Count: count, Tags: tags, Entered: invocation.EnteredPath(), Canonical: invocation.CanonicalPath()}, TextTemplate: greetingTemplate})
|
||||||
_, err := fmt.Fprintf(writer, "Hello; count=%s; tags=%s; entered=%q; canonical=%q\n", count, tags, invocation.EnteredPath(), invocation.CanonicalPath())
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return commandkit.Success()
|
return commandkit.Success()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -475,10 +475,15 @@ func TestVersionCommandFormatsApplicationAndCommandKitIdentity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
out.Reset()
|
out.Reset()
|
||||||
yamlOutcome := app.Execute(context.Background(), []string{"version", "--format=yaml"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
yamlOutcome := app.Execute(context.Background(), []string{"version", "--format=yaml"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
||||||
if yamlOutcome.Kind != commandkit.OutcomeSuccess || out.String() != "name: ExampleApp\nversion: dev\nrevision: abc123\n" {
|
if yamlOutcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), "name: ExampleApp\n") || !strings.Contains(out.String(), "version: dev\n") || !strings.Contains(out.String(), "revision: abc123\n") {
|
||||||
t.Fatalf("YAML version = %#v, output %q", yamlOutcome, out.String())
|
t.Fatalf("YAML version = %#v, output %q", yamlOutcome, out.String())
|
||||||
}
|
}
|
||||||
out.Reset()
|
out.Reset()
|
||||||
|
jsonOutcome := app.Execute(context.Background(), []string{"version", "--format=json"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
||||||
|
if jsonOutcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(out.String(), `"revision": "abc123"`) {
|
||||||
|
t.Fatalf("JSON version = %#v, output %q", jsonOutcome, out.String())
|
||||||
|
}
|
||||||
|
out.Reset()
|
||||||
component := app.Execute(context.Background(), []string{"version", "--scope=commandkit"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
component := app.Execute(context.Background(), []string{"version", "--scope=commandkit"}, commandkit.Streams{Out: &out, Err: &diagnostic})
|
||||||
if component.Kind != commandkit.OutcomeSuccess || !strings.HasPrefix(out.String(), "CommandKit ") || !strings.HasSuffix(out.String(), "\n") {
|
if component.Kind != commandkit.OutcomeSuccess || !strings.HasPrefix(out.String(), "CommandKit ") || !strings.HasSuffix(out.String(), "\n") {
|
||||||
t.Fatalf("CommandKit version = %#v, output %q", component, out.String())
|
t.Fatalf("CommandKit version = %#v, output %q", component, out.String())
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ func stringFlag(name, short string) Flag[struct{}] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagAliasChainsUseScopedOverridesAndCanonicalEndpoints(t *testing.T) {
|
func TestFlagAliasChainsUseScopedOverridesAndCanonicalEndpoints(t *testing.T) {
|
||||||
inspect := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("format", "f"))
|
inspect := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("style", "f"))
|
||||||
show := flagAliasCommand("show", stringFlag("color", "c"), stringFlag("format", "f"))
|
show := flagAliasCommand("show", stringFlag("color", "c"), stringFlag("style", "f"))
|
||||||
group := Group("Tools", "tools", show)
|
group := Group("Tools", "tools", show)
|
||||||
app, err := New(Identity{Name: "Example"},
|
app, err := New(Identity{Name: "Example"},
|
||||||
inspect,
|
inspect,
|
||||||
@@ -35,8 +35,8 @@ func TestFlagAliasChainsUseScopedOverridesAndCanonicalEndpoints(t *testing.T) {
|
|||||||
FlagAlias("-x", "--color"),
|
FlagAlias("-x", "--color"),
|
||||||
FlagAlias("--tint", "-c"),
|
FlagAlias("--tint", "-c"),
|
||||||
FlagAliasFor("Tools", "--group-help", "--help"),
|
FlagAliasFor("Tools", "--group-help", "--help"),
|
||||||
FlagAliasFor("inspect", "--hue", "--format"),
|
FlagAliasFor("inspect", "--hue", "--style"),
|
||||||
FlagAliasFor("inspect", "--local", "--format"),
|
FlagAliasFor("inspect", "--local", "--style"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -44,11 +44,11 @@ func TestFlagAliasChainsUseScopedOverridesAndCanonicalEndpoints(t *testing.T) {
|
|||||||
|
|
||||||
inspectAliases := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling
|
inspectAliases := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling
|
||||||
for source, target := range map[string]string{
|
for source, target := range map[string]string{
|
||||||
"--colour": "--format",
|
"--colour": "--style",
|
||||||
"--hue": "--format",
|
"--hue": "--style",
|
||||||
"-x": "--color",
|
"-x": "--color",
|
||||||
"--tint": "-c",
|
"--tint": "-c",
|
||||||
"--local": "--format",
|
"--local": "--style",
|
||||||
} {
|
} {
|
||||||
alias, ok := inspectAliases[flagSpellingKey(capturedSpelling(source))]
|
alias, ok := inspectAliases[flagSpellingKey(capturedSpelling(source))]
|
||||||
if !ok || alias.target.token() != target {
|
if !ok || alias.target.token() != target {
|
||||||
@@ -126,7 +126,7 @@ func TestFlagAliasDiscoveryContextsUseGlobalReportingAliases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReportingFlagsAreReservedAndAcceptApplicationShortAliases(t *testing.T) {
|
func TestReportingFlagsAreReservedAndAcceptApplicationShortAliases(t *testing.T) {
|
||||||
for _, name := range []string{"quiet", "verbose", "debug", "progress"} {
|
for _, name := range []string{"quiet", "verbose", "debug", "progress", "format"} {
|
||||||
command := flagAliasCommand("reserved", stringFlag(name, ""))
|
command := flagAliasCommand("reserved", stringFlag(name, ""))
|
||||||
if len(command.record.issues) == 0 {
|
if len(command.record.issues) == 0 {
|
||||||
t.Errorf("capturing --%s did not retain a declaration error", name)
|
t.Errorf("capturing --%s did not retain a declaration error", name)
|
||||||
@@ -154,7 +154,7 @@ func TestReportingFlagsAreReservedAndAcceptApplicationShortAliases(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReportingFlagSpellingsConflictWithAliasSources(t *testing.T) {
|
func TestReportingFlagSpellingsConflictWithAliasSources(t *testing.T) {
|
||||||
for _, name := range []string{"quiet", "verbose", "debug", "progress"} {
|
for _, name := range []string{"quiet", "verbose", "debug", "progress", "format"} {
|
||||||
if _, err := New(Identity{Name: "Example"}, FlagAlias("--"+name, "--help")); err == nil || !strings.Contains(err.Error(), "conflicts with a canonical spelling") {
|
if _, err := New(Identity{Name: "Example"}, FlagAlias("--"+name, "--help")); err == nil || !strings.Contains(err.Error(), "conflicts with a canonical spelling") {
|
||||||
t.Errorf("alias source --%s returned %v, want a canonical spelling collision", name, err)
|
t.Errorf("alias source --%s returned %v, want a canonical spelling collision", name, err)
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ func TestReportingFlagSpellingsConflictWithAliasSources(t *testing.T) {
|
|||||||
|
|
||||||
func TestRoutingSnapshotCarriesReportingMetadataOutsideConsumerBindings(t *testing.T) {
|
func TestRoutingSnapshotCarriesReportingMetadataOutsideConsumerBindings(t *testing.T) {
|
||||||
app, err := New(Identity{Name: "Example"},
|
app, err := New(Identity{Name: "Example"},
|
||||||
flagAliasCommand("run", stringFlag("format", "f")),
|
flagAliasCommand("run", stringFlag("style", "f")),
|
||||||
Group("tools", "", flagAliasCommand("show")),
|
Group("tools", "", flagAliasCommand("show")),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -174,7 +174,7 @@ func TestRoutingSnapshotCarriesReportingMetadataOutsideConsumerBindings(t *testi
|
|||||||
}
|
}
|
||||||
var check func(*commandmeta.Command)
|
var check func(*commandmeta.Command)
|
||||||
check = func(node *commandmeta.Command) {
|
check = func(node *commandmeta.Command) {
|
||||||
if len(node.CoreFlags) != 4 {
|
if len(node.CoreFlags) != 5 {
|
||||||
t.Errorf("%s core reporting flags = %#v", node.Name, node.CoreFlags)
|
t.Errorf("%s core reporting flags = %#v", node.Name, node.CoreFlags)
|
||||||
}
|
}
|
||||||
for _, flag := range node.Flags {
|
for _, flag := range node.Flags {
|
||||||
@@ -190,15 +190,15 @@ func TestRoutingSnapshotCarriesReportingMetadataOutsideConsumerBindings(t *testi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagAliasDuplicatesAndScopesAreDeterministic(t *testing.T) {
|
func TestFlagAliasDuplicatesAndScopesAreDeterministic(t *testing.T) {
|
||||||
command := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("format", "f"))
|
command := flagAliasCommand("inspect", stringFlag("color", "c"), stringFlag("style", "f"))
|
||||||
duplicate := FlagAlias("--colour", "--color")
|
duplicate := FlagAlias("--colour", "--color")
|
||||||
app, err := New(Identity{Name: "Example"}, command, flagAliasCommand("plain", stringFlag("color", "c")), duplicate, FlagAlias("--colour", "--color"),
|
app, err := New(Identity{Name: "Example"}, command, flagAliasCommand("plain", stringFlag("color", "c")), duplicate, FlagAlias("--colour", "--color"),
|
||||||
FlagAliasFor("inspect", "--colour", "--format"))
|
FlagAliasFor("inspect", "--colour", "--style"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("identical duplicates and scoped override were rejected: %v", err)
|
t.Fatalf("identical duplicates and scoped override were rejected: %v", err)
|
||||||
}
|
}
|
||||||
if got := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--format" {
|
if got := app.flagAliases[commandpath.Path{"inspect"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--style" {
|
||||||
t.Fatalf("scoped alias target = %s, want --format", got)
|
t.Fatalf("scoped alias target = %s, want --style", got)
|
||||||
}
|
}
|
||||||
if got := app.flagAliases[commandpath.Path{"plain"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--color" {
|
if got := app.flagAliases[commandpath.Path{"plain"}.Key()].bySpelling[flagSpellingKey(capturedFlagSpelling{name: "colour"})].target.token(); got != "--color" {
|
||||||
t.Fatalf("effective global alias target = %s, want --color", got)
|
t.Fatalf("effective global alias target = %s, want --color", got)
|
||||||
@@ -207,11 +207,11 @@ func TestFlagAliasDuplicatesAndScopesAreDeterministic(t *testing.T) {
|
|||||||
for name, rules := range map[string][]RoutingRule{
|
for name, rules := range map[string][]RoutingRule{
|
||||||
"competing global targets": {
|
"competing global targets": {
|
||||||
FlagAlias("--colour", "--color"),
|
FlagAlias("--colour", "--color"),
|
||||||
FlagAlias("--colour", "--format"),
|
FlagAlias("--colour", "--style"),
|
||||||
},
|
},
|
||||||
"competing scoped targets": {
|
"competing scoped targets": {
|
||||||
FlagAliasFor("inspect", "--colour", "--color"),
|
FlagAliasFor("inspect", "--colour", "--color"),
|
||||||
FlagAliasFor("inspect", "--colour", "--format"),
|
FlagAliasFor("inspect", "--colour", "--style"),
|
||||||
},
|
},
|
||||||
"unknown scope": {FlagAliasFor("missing", "--colour", "--color")},
|
"unknown scope": {FlagAliasFor("missing", "--colour", "--color")},
|
||||||
"missing scoped destination": {FlagAliasFor("inspect", "--colour", "--missing")},
|
"missing scoped destination": {FlagAliasFor("inspect", "--colour", "--missing")},
|
||||||
|
|||||||
82
format_flag_test.go
Normal file
82
format_flag_test.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package commandkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSharedFormatFlag(t *testing.T) {
|
||||||
|
factories, runs := 0, 0
|
||||||
|
command := NewCommand(CommandSpec[struct{}]{Name: "run", NewInput: func() struct{} { factories++; return struct{}{} }, Run: func(inv Invocation, _ struct{}) Outcome {
|
||||||
|
runs++
|
||||||
|
_ = inv.Reporter().Report(Report{Value: map[string]any{"answer": 42}})
|
||||||
|
return Success()
|
||||||
|
}})
|
||||||
|
app, err := New(Identity{Name: "Example", Version: "1"}, command, FlagAlias("--fmt", "--format"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
args []string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{[]string{"run", "--format", "text"}, "yaml"},
|
||||||
|
{[]string{"run", "--format=yaml"}, "yaml"},
|
||||||
|
{[]string{"run", "--fmt=json"}, "json"},
|
||||||
|
{[]string{"run", "--format=yaml", "--fmt", "json"}, "json"},
|
||||||
|
{[]string{"run"}, "yaml"},
|
||||||
|
} {
|
||||||
|
var out bytes.Buffer
|
||||||
|
got := app.Execute(context.Background(), tc.args, Streams{Out: &out})
|
||||||
|
if got.Status != 0 {
|
||||||
|
t.Fatalf("%v: %#v", tc.args, got)
|
||||||
|
}
|
||||||
|
var value any
|
||||||
|
if tc.expected == "json" {
|
||||||
|
dec := json.NewDecoder(&out)
|
||||||
|
dec.UseNumber()
|
||||||
|
err = dec.Decode(&value)
|
||||||
|
} else {
|
||||||
|
value, err = DecodeYAMLReport(&out)
|
||||||
|
}
|
||||||
|
if err != nil || !reflect.DeepEqual(value, map[string]any{"answer": json.Number("42")}) {
|
||||||
|
t.Fatalf("%v: %#v %v", tc.args, value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
beforeFactories, beforeRuns := factories, runs
|
||||||
|
for _, args := range [][]string{{"run", "--format="}, {"run", "--fmt=xml"}, {"run", "--format=bad", "--format=json"}, {"run", "--format", "--quiet"}} {
|
||||||
|
var diagnostics bytes.Buffer
|
||||||
|
got := app.Execute(context.Background(), args, Streams{Err: &diagnostics})
|
||||||
|
if got.Kind != OutcomeUsage || !strings.Contains(diagnostics.String(), "format") {
|
||||||
|
t.Fatalf("%v: %#v %q", args, got, &diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if factories != beforeFactories || runs != beforeRuns {
|
||||||
|
t.Fatalf("invalid formats ran domain code: factories %d, runs %d", factories, runs)
|
||||||
|
}
|
||||||
|
var help bytes.Buffer
|
||||||
|
got := app.Execute(context.Background(), []string{"run", "--help"}, Streams{Out: &help})
|
||||||
|
if got.Status != 0 || !strings.Contains(help.String(), "--format") || factories != beforeFactories || runs != beforeRuns {
|
||||||
|
t.Fatalf("help: %#v, %q", got, &help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSharedFormatParseOrderAndTerminator(t *testing.T) {
|
||||||
|
app, err := New(Identity{Name: "Example"}, flagAliasCommand("run", stringFlag("message", "")), FlagAlias("--fmt", "--format"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
result := parseReportingArgs(app, []string{"run", "--verbose", "--fmt", "json", "--quiet", "--message=--format", "--", "--format=yaml"})
|
||||||
|
if result.Failure != nil || result.Reporting.Format != "json" || result.Reporting.Mode != commandmeta.ReportingModeQuiet || !reflect.DeepEqual(result.Operands, []string{"--format=yaml"}) {
|
||||||
|
t.Fatalf("%#v", result)
|
||||||
|
}
|
||||||
|
if result.FlagOccurrences[1].Spelling != "--fmt" || result.FlagOccurrences[1].RawValue != "json" {
|
||||||
|
t.Fatalf("%#v", result.FlagOccurrences)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,10 +80,10 @@ func TestParseInterspersedOperandsBooleanFlagsAndTerminator(t *testing.T) {
|
|||||||
func TestReportingFlagsParseWithoutChangingConsumerFlagIndexes(t *testing.T) {
|
func TestReportingFlagsParseWithoutChangingConsumerFlagIndexes(t *testing.T) {
|
||||||
tree := &Command{Name: "app", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{
|
tree := &Command{Name: "app", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{
|
||||||
{Name: "config", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{
|
{Name: "config", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{
|
||||||
{Name: "show", Runnable: true, CoreFlags: commandmeta.ReportingFlags(), Flags: []Flag{{Name: "format"}}},
|
{Name: "show", Runnable: true, CoreFlags: commandmeta.ReportingFlags(), Flags: []Flag{{Name: "style"}}},
|
||||||
}},
|
}},
|
||||||
}}
|
}}
|
||||||
result := Parse(tree, []string{"config", "show", "--quiet", "-v", "--format=yaml"}, ParseOptions{
|
result := Parse(tree, []string{"config", "show", "--quiet", "-v", "--style=yaml"}, ParseOptions{
|
||||||
FlagAliases: []commandmeta.FlagAlias{{Source: "-v", Target: "--verbose"}},
|
FlagAliases: []commandmeta.FlagAlias{{Source: "-v", Target: "--verbose"}},
|
||||||
})
|
})
|
||||||
if result.Failure != nil {
|
if result.Failure != nil {
|
||||||
@@ -92,7 +92,7 @@ func TestReportingFlagsParseWithoutChangingConsumerFlagIndexes(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(result.FlagValues, [][]string{{"yaml"}}) {
|
if !reflect.DeepEqual(result.FlagValues, [][]string{{"yaml"}}) {
|
||||||
t.Fatalf("consumer binding values = %#v", result.FlagValues)
|
t.Fatalf("consumer binding values = %#v", result.FlagValues)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(result.FlagSpellings, [][]string{{"--format"}}) {
|
if !reflect.DeepEqual(result.FlagSpellings, [][]string{{"--style"}}) {
|
||||||
t.Fatalf("consumer binding spellings = %#v", result.FlagSpellings)
|
t.Fatalf("consumer binding spellings = %#v", result.FlagSpellings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ var reportingFlags = [...]Flag{
|
|||||||
{Name: "verbose", Usage: "show warning and informational messages", Default: "false", Boolean: true},
|
{Name: "verbose", Usage: "show warning and informational messages", Default: "false", Boolean: true},
|
||||||
{Name: "debug", Usage: "show debugging messages", Default: "false", Boolean: true},
|
{Name: "debug", Usage: "show debugging messages", Default: "false", Boolean: true},
|
||||||
{Name: "progress", Usage: "show progress messages", Default: "false", Boolean: true},
|
{Name: "progress", Usage: "show progress messages", Default: "false", Boolean: true},
|
||||||
|
{Name: "format", Usage: "report format: text, yaml, or json", Completion: CompletionHint{Kind: CompletionChoices, Choices: []CompletionChoice{{Value: "text"}, {Value: "yaml"}, {Value: "json"}}}},
|
||||||
}
|
}
|
||||||
|
|
||||||
// BooleanCompletion returns the ordinary explicit boolean suggestions.
|
// BooleanCompletion returns the ordinary explicit boolean suggestions.
|
||||||
@@ -95,7 +96,11 @@ func HelpFlag() Flag {
|
|||||||
func ReportingFlags() []Flag {
|
func ReportingFlags() []Flag {
|
||||||
flags := append([]Flag(nil), reportingFlags[:]...)
|
flags := append([]Flag(nil), reportingFlags[:]...)
|
||||||
for index := range flags {
|
for index := range flags {
|
||||||
|
if flags[index].Boolean {
|
||||||
flags[index].Completion = BooleanCompletion()
|
flags[index].Completion = BooleanCompletion()
|
||||||
|
} else {
|
||||||
|
flags[index].Completion.Choices = append([]CompletionChoice(nil), flags[index].Completion.Choices...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
@@ -160,6 +165,7 @@ type ReportingSelection struct {
|
|||||||
Mode ReportingMode
|
Mode ReportingMode
|
||||||
Progress bool
|
Progress bool
|
||||||
ProgressSet bool
|
ProgressSet bool
|
||||||
|
Format string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Result contains the selected command and raw input collected by the adapter.
|
// Result contains the selected command and raw input collected by the adapter.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import "testing"
|
|||||||
func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) {
|
func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) {
|
||||||
flags := ReportingFlags()
|
flags := ReportingFlags()
|
||||||
want := []string{"quiet", "verbose", "debug", "progress"}
|
want := []string{"quiet", "verbose", "debug", "progress"}
|
||||||
if len(flags) != len(want) {
|
if len(flags) != len(want)+1 {
|
||||||
t.Fatalf("reporting flags = %#v", flags)
|
t.Fatalf("reporting flags = %#v", flags)
|
||||||
}
|
}
|
||||||
for index, name := range want {
|
for index, name := range want {
|
||||||
@@ -16,6 +16,13 @@ func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) {
|
|||||||
t.Errorf("%q is not recognized as a reporting flag", name)
|
t.Errorf("%q is not recognized as a reporting flag", name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if flags[4].Name != "format" || flags[4].Boolean || !IsReportingFlag("format") {
|
||||||
|
t.Fatalf("format metadata = %#v", flags[4])
|
||||||
|
}
|
||||||
|
flags[4].Completion.Choices[0].Value = "changed"
|
||||||
|
if ReportingFlags()[4].Completion.Choices[0].Value != "text" {
|
||||||
|
t.Fatal("caller mutation changed format choices")
|
||||||
|
}
|
||||||
flags[0].Name = "changed"
|
flags[0].Name = "changed"
|
||||||
if ReportingFlags()[0].Name != "quiet" {
|
if ReportingFlags()[0].Name != "quiet" {
|
||||||
t.Fatal("caller mutation changed shared reporting metadata")
|
t.Fatal("caller mutation changed shared reporting metadata")
|
||||||
|
|||||||
12
parser.go
12
parser.go
@@ -24,6 +24,18 @@ func reduceReportingOccurrences(result commandmeta.Result) commandmeta.Result {
|
|||||||
if !commandmeta.IsReportingFlag(occurrence.Name) {
|
if !commandmeta.IsReportingFlag(occurrence.Name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if occurrence.Name == "format" {
|
||||||
|
switch Format(occurrence.RawValue) {
|
||||||
|
case FormatText, FormatYAML, FormatJSON:
|
||||||
|
selection.Format = occurrence.RawValue
|
||||||
|
default:
|
||||||
|
result.FlagOccurrences = result.FlagOccurrences[:index+1]
|
||||||
|
result.Reporting = selection
|
||||||
|
result.Failure = &commandmeta.Failure{Message: fmt.Sprintf("invalid format %q for %s (expected text, yaml, or json)", occurrence.RawValue, occurrence.Spelling), Usage: true}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
enabled, err := strconv.ParseBool(occurrence.RawValue)
|
enabled, err := strconv.ParseBool(occurrence.RawValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.FlagOccurrences = result.FlagOccurrences[:index+1]
|
result.FlagOccurrences = result.FlagOccurrences[:index+1]
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ func (r *Reporter) applySelection(selection commandmeta.ReportingSelection) {
|
|||||||
}
|
}
|
||||||
state.progress = selection.Progress
|
state.progress = selection.Progress
|
||||||
state.progressSet = selection.ProgressSet
|
state.progressSet = selection.ProgressSet
|
||||||
|
state.format = Format(selection.Format)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output returns a managed writer for substantive command results. Writes are
|
// Output returns a managed writer for substantive command results. Writes are
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestRunnableParentKeepsNonCommandTokensAsOperands(t *testing.T) {
|
|||||||
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "ARG"}, 0, -1,
|
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "ARG"}, 0, -1,
|
||||||
func(value string) (string, error) { return value, nil },
|
func(value string) (string, error) { return value, nil },
|
||||||
func(target *input, value Supplied[[]string]) { target.words = value.Value })},
|
func(target *input, value Supplied[[]string]) { target.words = value.Value })},
|
||||||
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "format", Short: "f"}, (*string)(nil),
|
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "style", Short: "f"}, (*string)(nil),
|
||||||
func(value string) (string, error) { return value, nil },
|
func(value string) (string, error) { return value, nil },
|
||||||
func(target *input, value Supplied[string]) { target.format = value })},
|
func(target *input, value Supplied[string]) { target.format = value })},
|
||||||
Run: func(inv Invocation, value input) Outcome {
|
Run: func(inv Invocation, value input) Outcome {
|
||||||
@@ -36,7 +36,7 @@ func TestRunnableParentKeepsNonCommandTokensAsOperands(t *testing.T) {
|
|||||||
return Success()
|
return Success()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app, err := New(Identity{Name: "Demo"}, parent, Alias("r", "run"), FlagAliasFor("run", "--fmt", "--format"))
|
app, err := New(Identity{Name: "Demo"}, parent, Alias("r", "run"), FlagAliasFor("run", "--fmt", "--style"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ func TestRunnableParentKeepsNonCommandTokensAsOperands(t *testing.T) {
|
|||||||
}
|
}
|
||||||
outcome = app.Execute(context.Background(), append(args, "--help"), Streams{Out: &output, Err: &diagnostics})
|
outcome = app.Execute(context.Background(), append(args, "--help"), Streams{Out: &output, Err: &diagnostics})
|
||||||
if outcome.Kind != OutcomeSuccess || built != beforeBuilt+1 || parentRuns != beforeRuns+1 ||
|
if outcome.Kind != OutcomeSuccess || built != beforeBuilt+1 || parentRuns != beforeRuns+1 ||
|
||||||
!strings.Contains(output.String(), "Demo "+spelling+" [ARG...]") || !strings.Contains(output.String(), "--fmt → --format") {
|
!strings.Contains(output.String(), "Demo "+spelling+" [ARG...]") || !strings.Contains(output.String(), "--fmt → --style") {
|
||||||
t.Fatalf("parent help = %#v, output %q, diagnostics %q", outcome, output.String(), diagnostics.String())
|
t.Fatalf("parent help = %#v, output %q, diagnostics %q", outcome, output.String(), diagnostics.String())
|
||||||
}
|
}
|
||||||
outcome = app.Execute(context.Background(), []string{spelling, "--", operand, "child", "--fmt=yaml"}, Streams{Out: io.Discard, Err: &diagnostics})
|
outcome = app.Execute(context.Background(), []string{spelling, "--", operand, "child", "--fmt=yaml"}, Streams{Out: io.Discard, Err: &diagnostics})
|
||||||
|
|||||||
@@ -67,14 +67,14 @@ func TestParserFailuresRetainAliasUsageAndFlagSpellings(t *testing.T) {
|
|||||||
command := NewCommand(CommandSpec[struct{}]{
|
command := NewCommand(CommandSpec[struct{}]{
|
||||||
Name: "inspect",
|
Name: "inspect",
|
||||||
NewInput: func() struct{} { built++; return struct{}{} },
|
NewInput: func() struct{} { built++; return struct{}{} },
|
||||||
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
|
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "style"}, (*string)(nil),
|
||||||
func(value string) (string, error) { return value, nil },
|
func(value string) (string, error) { return value, nil },
|
||||||
func(*struct{}, Supplied[string]) {})},
|
func(*struct{}, Supplied[string]) {})},
|
||||||
Run: func(Invocation, struct{}) Outcome { t.Fatal("invalid input executed"); return Success() },
|
Run: func(Invocation, struct{}) Outcome { t.Fatal("invalid input executed"); return Success() },
|
||||||
})
|
})
|
||||||
app, err := New(Identity{Name: "Demo"}, command, Alias("i", "inspect"),
|
app, err := New(Identity{Name: "Demo"}, command, Alias("i", "inspect"),
|
||||||
FlagAliasFor("inspect", "--fmt", "--format"),
|
FlagAliasFor("inspect", "--fmt", "--style"),
|
||||||
FlagAliasFor("inspect", "-F", "--format"), FlagAlias("--hlep", "--help"))
|
FlagAliasFor("inspect", "-F", "--style"), FlagAlias("--hlep", "--help"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) {
|
|||||||
usage bool
|
usage bool
|
||||||
}{
|
}{
|
||||||
{name: "interspersed alias", args: []string{"first", "--fmt=yaml", "second"}},
|
{name: "interspersed alias", args: []string{"first", "--fmt=yaml", "second"}},
|
||||||
{name: "interspersed canonical flag", args: []string{"first", "--format=yaml", "second"}},
|
{name: "interspersed canonical flag", args: []string{"first", "--style=yaml", "second"}},
|
||||||
{name: "partial help", args: []string{"first", "--help"}, help: true},
|
{name: "partial help", args: []string{"first", "--help"}, help: true},
|
||||||
{name: "partial help with flag alias", args: []string{"first", "--fmt=yaml", "--help"}, help: true},
|
{name: "partial help with flag alias", args: []string{"first", "--fmt=yaml", "--help"}, help: true},
|
||||||
{name: "excess operands", args: []string{"first", "second", "third", "--fmt=yaml"}, usage: true},
|
{name: "excess operands", args: []string{"first", "second", "third", "--fmt=yaml"}, usage: true},
|
||||||
@@ -125,7 +125,7 @@ func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) {
|
|||||||
built, ran := 0, false
|
built, ran := 0, false
|
||||||
command := NewCommand(CommandSpec[input]{
|
command := NewCommand(CommandSpec[input]{
|
||||||
Name: "run", NewInput: func() input { built++; return input{} },
|
Name: "run", NewInput: func() input { built++; return input{} },
|
||||||
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
|
Flags: []Flag[input]{ValueFlag(FlagMeta{Name: "style"}, (*string)(nil),
|
||||||
func(value string) (string, error) { return value, nil },
|
func(value string) (string, error) { return value, nil },
|
||||||
func(target *input, value Supplied[string]) { target.format = value })},
|
func(target *input, value Supplied[string]) { target.format = value })},
|
||||||
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "ARG"}, 2, 2,
|
Arguments: []Argument[input]{RepeatArgument(ArgumentMeta{Name: "ARG"}, 2, 2,
|
||||||
@@ -142,7 +142,7 @@ func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) {
|
|||||||
return Success()
|
return Success()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
app, err := New(Identity{Name: "Demo"}, command, Alias("r", "run"), FlagAliasFor("run", "--fmt", "--format"))
|
app, err := New(Identity{Name: "Demo"}, command, Alias("r", "run"), FlagAliasFor("run", "--fmt", "--style"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ func TestCommandContextSurvivesIncompleteAndInvalidOperandCounts(t *testing.T) {
|
|||||||
} else if outcome.Kind != OutcomeSuccess {
|
} else if outcome.Kind != OutcomeSuccess {
|
||||||
t.Fatalf("outcome = %#v, diagnostics %q", outcome, diagnostics.String())
|
t.Fatalf("outcome = %#v, diagnostics %q", outcome, diagnostics.String())
|
||||||
}
|
}
|
||||||
if test.help && (!strings.Contains(output.String(), "Demo "+spelling+" ARG") || !strings.Contains(output.String(), "--fmt → --format")) {
|
if test.help && (!strings.Contains(output.String(), "Demo "+spelling+" ARG") || !strings.Contains(output.String(), "--fmt → --style")) {
|
||||||
t.Errorf("command help = %q", output.String())
|
t.Errorf("command help = %q", output.String())
|
||||||
}
|
}
|
||||||
if test.help || test.usage {
|
if test.help || test.usage {
|
||||||
@@ -229,13 +229,13 @@ func TestDiscoveryHelpComposesGroupShortcutsAndOverrides(t *testing.T) {
|
|||||||
initialized := false
|
initialized := false
|
||||||
inspect := NewCommand(CommandSpec[struct{}]{
|
inspect := NewCommand(CommandSpec[struct{}]{
|
||||||
Name: "inspect", NewInput: func() struct{} { initialized = true; return struct{}{} },
|
Name: "inspect", NewInput: func() struct{} { initialized = true; return struct{}{} },
|
||||||
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
|
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "style"}, (*string)(nil),
|
||||||
func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
||||||
Run: func(Invocation, struct{}) Outcome { return Success() },
|
Run: func(Invocation, struct{}) Outcome { return Success() },
|
||||||
})
|
})
|
||||||
declarations := []Declaration{Group("box", "Box"), inspect, simpleRunnable("alternate"),
|
declarations := []Declaration{Group("box", "Box"), inspect, simpleRunnable("alternate"),
|
||||||
Alias("pk", "box"), Alias("box admin pick", "inspect"), Alias("box admin other", "inspect"),
|
Alias("pk", "box"), Alias("box admin pick", "inspect"), Alias("box admin other", "inspect"),
|
||||||
FlagAlias("--hlep", "--help"), FlagAliasFor("inspect", "--fmt", "--format")}
|
FlagAlias("--hlep", "--help"), FlagAliasFor("inspect", "--fmt", "--style")}
|
||||||
wantPick := "pick → inspect"
|
wantPick := "pick → inspect"
|
||||||
if override {
|
if override {
|
||||||
declarations = append(declarations, Alias("pk admin pick", "alternate"), Alias("pk admin nested choose", "inspect"))
|
declarations = append(declarations, Alias("pk admin pick", "alternate"), Alias("pk admin nested choose", "inspect"))
|
||||||
@@ -308,7 +308,7 @@ func TestHelpDiscoversRoutesThroughIntermediateAliases(t *testing.T) {
|
|||||||
makeCommand := func(name string) Command {
|
makeCommand := func(name string) Command {
|
||||||
return NewCommand(CommandSpec[struct{}]{
|
return NewCommand(CommandSpec[struct{}]{
|
||||||
Name: name, NewInput: func() struct{} { built++; return struct{}{} },
|
Name: name, NewInput: func() struct{} { built++; return struct{}{} },
|
||||||
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
|
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "style"}, (*string)(nil),
|
||||||
func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
func(value string) (string, error) { return value, nil }, func(*struct{}, Supplied[string]) {})},
|
||||||
Run: func(inv Invocation, _ struct{}) Outcome {
|
Run: func(inv Invocation, _ struct{}) Outcome {
|
||||||
runs++
|
runs++
|
||||||
@@ -327,7 +327,7 @@ func TestHelpDiscoversRoutesThroughIntermediateAliases(t *testing.T) {
|
|||||||
}
|
}
|
||||||
declarations := []Declaration{group, makeCommand("inspect"), makeCommand("alternate"),
|
declarations := []Declaration{group, makeCommand("inspect"), makeCommand("alternate"),
|
||||||
Alias("p", "q"), Alias("q", target), Alias(middle+" pick", "inspect"), Alias(middle+" other", "inspect"),
|
Alias("p", "q"), Alias("q", target), Alias(middle+" pick", "inspect"), Alias(middle+" other", "inspect"),
|
||||||
FlagAlias("--hlep", "--help"), FlagAliasFor("inspect", "--fmt", "--format")}
|
FlagAlias("--hlep", "--help"), FlagAliasFor("inspect", "--fmt", "--style")}
|
||||||
wantTarget := "inspect"
|
wantTarget := "inspect"
|
||||||
if test.override {
|
if test.override {
|
||||||
declarations = append(declarations, Alias(prefix+" pick", "alternate"))
|
declarations = append(declarations, Alias(prefix+" pick", "alternate"))
|
||||||
@@ -352,7 +352,7 @@ func TestHelpDiscoversRoutesThroughIntermediateAliases(t *testing.T) {
|
|||||||
t.Errorf("help %q omits %q", output.String(), fragment)
|
t.Errorf("help %q omits %q", output.String(), fragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.Contains(output.String(), "--fmt") || strings.Contains(output.String(), "--format") ||
|
if strings.Contains(output.String(), "--fmt") || strings.Contains(output.String(), "--style") ||
|
||||||
(test.override && strings.Contains(output.String(), "pick → inspect")) {
|
(test.override && strings.Contains(output.String(), "pick → inspect")) {
|
||||||
t.Errorf("help exposes a descendant flag or overridden destination: %q", output.String())
|
t.Errorf("help exposes a descendant flag or overridden destination: %q", output.String())
|
||||||
}
|
}
|
||||||
@@ -920,7 +920,7 @@ func TestAliasHelpTargetsGroupsAndDiscoveryViews(t *testing.T) {
|
|||||||
inputBuilds++
|
inputBuilds++
|
||||||
return struct{}{}
|
return struct{}{}
|
||||||
},
|
},
|
||||||
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "format"}, (*string)(nil),
|
Flags: []Flag[struct{}]{ValueFlag(FlagMeta{Name: "style"}, (*string)(nil),
|
||||||
func(value string) (string, error) { return value, nil },
|
func(value string) (string, error) { return value, nil },
|
||||||
func(*struct{}, Supplied[string]) {},
|
func(*struct{}, Supplied[string]) {},
|
||||||
)},
|
)},
|
||||||
@@ -939,7 +939,7 @@ func TestAliasHelpTargetsGroupsAndDiscoveryViews(t *testing.T) {
|
|||||||
Alias("pk", "promptkit"),
|
Alias("pk", "promptkit"),
|
||||||
Alias("pk list bundles", "promptkit show prompt"),
|
Alias("pk list bundles", "promptkit show prompt"),
|
||||||
FlagAlias("--hlep", "--help"),
|
FlagAlias("--hlep", "--help"),
|
||||||
FlagAliasFor("promptkit show prompt", "--fmt", "--format"),
|
FlagAliasFor("promptkit show prompt", "--fmt", "--style"),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -985,13 +985,13 @@ func TestAliasHelpTargetsGroupsAndDiscoveryViews(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "scoped flag alias appears on canonical command help",
|
name: "scoped flag alias appears on canonical command help",
|
||||||
args: []string{"promptkit", "show", "prompt", "--hlep"},
|
args: []string{"promptkit", "show", "prompt", "--hlep"},
|
||||||
want: []string{"--fmt → --format"},
|
want: []string{"--fmt → --style"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "group excludes descendant scoped flag aliases",
|
name: "group excludes descendant scoped flag aliases",
|
||||||
args: []string{"promptkit", "--help"},
|
args: []string{"promptkit", "--help"},
|
||||||
want: []string{"Command Aliases:"},
|
want: []string{"Command Aliases:"},
|
||||||
omit: []string{"--fmt → --format"},
|
omit: []string{"--fmt → --style"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "root lists explicit command aliases separately",
|
name: "root lists explicit command aliases separately",
|
||||||
|
|||||||
Reference in New Issue
Block a user