mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 17:45:15 +08:00
feat(shortcuts): typed @file/stdin input + enum completion/help parity
Bring TypedShortcut[T] to parity with legacy common.Shortcut on two flag capabilities that were silently missing on the typed path: - @file / stdin: declare `input:"file,stdin"` on an Args field; the binder resolves @path (file) and - (stdin) before binding, recursing into OneOf buckets and groups. resolveInputForFlag is extracted from the legacy resolveInputFlags so both paths behave identically. - enum: registerLeaf registers shell completion and typed help renders the candidate list, matching legacy. enum/input tags on non-string fields now error at Mount time instead of being silently skipped at runtime. Legacy behavior unchanged. Adds binder_input_enum_test.go (11 cases).
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
)
|
||||
|
||||
// fieldSpec is the binder's intermediate representation of one Args field.
|
||||
@@ -24,6 +25,7 @@ type fieldSpec struct {
|
||||
Description string
|
||||
DefaultValue string
|
||||
EnumValues []string
|
||||
Input []string
|
||||
Required bool
|
||||
IsOneOfBkt bool
|
||||
IsGroup bool
|
||||
@@ -77,6 +79,18 @@ func parseFieldSpec(f reflect.StructField) (fieldSpec, error) {
|
||||
if _, has := f.Tag.Lookup("required"); has {
|
||||
spec.Required = true
|
||||
}
|
||||
if input := f.Tag.Get("input"); input != "" {
|
||||
for _, src := range strings.Split(input, ",") {
|
||||
switch src = strings.TrimSpace(src); src {
|
||||
case "":
|
||||
// tolerate stray commas
|
||||
case File, Stdin:
|
||||
spec.Input = append(spec.Input, src)
|
||||
default:
|
||||
return spec, fmt.Errorf("field %s: unknown input source %q (allowed: %q, %q)", f.Name, src, File, Stdin)
|
||||
}
|
||||
}
|
||||
}
|
||||
ft := f.Type
|
||||
spec.FieldType = ft
|
||||
if ft.Kind() == reflect.Ptr {
|
||||
@@ -92,6 +106,20 @@ func parseFieldSpec(f reflect.StructField) (fieldSpec, error) {
|
||||
} else {
|
||||
spec.IsGroup = true
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
// Leaf field: enum / input tags only make sense on string-kinded leaves
|
||||
// (plain string or string-alias types like ChatID). Reject them on
|
||||
// int / bool / other kinds at Mount time instead of silently skipping the
|
||||
// check at runtime — enum validation reads the flag via GetString and
|
||||
// @file resolution requires a string flag.
|
||||
if ft.Kind() != reflect.String {
|
||||
if len(spec.EnumValues) > 0 {
|
||||
return spec, fmt.Errorf("field %s: enum tag is only supported on string fields", f.Name)
|
||||
}
|
||||
if len(spec.Input) > 0 {
|
||||
return spec, fmt.Errorf("field %s: input tag is only supported on string fields", f.Name)
|
||||
}
|
||||
}
|
||||
return spec, nil
|
||||
}
|
||||
@@ -141,6 +169,46 @@ func registerLeaf(cmd *cobra.Command, s fieldSpec, t reflect.Type) error {
|
||||
if s.Required {
|
||||
_ = cmd.MarkFlagRequired(s.FlagName)
|
||||
}
|
||||
if len(s.EnumValues) > 0 {
|
||||
vals := s.EnumValues
|
||||
cmdutil.RegisterFlagCompletion(cmd, s.FlagName, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
|
||||
return vals, cobra.ShellCompDirectiveNoFileComp
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveTypedInputs applies @file / stdin resolution to every leaf flag that
|
||||
// declared an `input:"file,stdin"` tag, recursing into OneOf buckets and groups
|
||||
// (cobra flags are flat, so a nested variant's flag resolves the same way). It
|
||||
// runs before bindFlags so the file/stdin content is read back into the cobra
|
||||
// flag and then bound into the Args struct. This is the typed counterpart of
|
||||
// runShortcut's resolveInputFlags, which only sees the legacy shell's (empty)
|
||||
// Flags slice — both ultimately call resolveInputForFlag.
|
||||
func resolveTypedInputs(rctx *RuntimeContext, specs []fieldSpec) error {
|
||||
stdinUsed := false
|
||||
return resolveTypedInputsRec(rctx, specs, &stdinUsed)
|
||||
}
|
||||
|
||||
func resolveTypedInputsRec(rctx *RuntimeContext, specs []fieldSpec, stdinUsed *bool) error {
|
||||
for _, s := range specs {
|
||||
if s.IsOneOfBkt || s.IsGroup {
|
||||
inner, err := walkArgs(reflect.PointerTo(s.StructType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resolveTypedInputsRec(rctx, inner, stdinUsed); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s.FlagName == "" || len(s.Input) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := resolveInputForFlag(rctx, s.FlagName, s.Input, stdinUsed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
237
shortcuts/common/binder_input_enum_test.go
Normal file
237
shortcuts/common/binder_input_enum_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
_ "github.com/larksuite/cli/internal/vfs/localfileio"
|
||||
)
|
||||
|
||||
// newTypedInputRuntime registers the given specs on a fresh cobra command,
|
||||
// parses argv, and returns a RuntimeContext wired with a fake stdin — the
|
||||
// typed-binder analogue of newTestRuntimeWithStdin in runner_input_test.go.
|
||||
func newTypedInputRuntime(t *testing.T, specs []fieldSpec, argv []string, stdin string) *RuntimeContext {
|
||||
t.Helper()
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
if err := registerFlags(cmd, specs); err != nil {
|
||||
t.Fatalf("registerFlags: %v", err)
|
||||
}
|
||||
if err := cmd.ParseFlags(argv); err != nil {
|
||||
t.Fatalf("ParseFlags: %v", err)
|
||||
}
|
||||
return &RuntimeContext{
|
||||
Cmd: cmd,
|
||||
Factory: &cmdutil.Factory{
|
||||
IOStreams: &cmdutil.IOStreams{In: strings.NewReader(stdin)},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// --- @file / stdin on typed shortcuts -------------------------------------
|
||||
|
||||
type fileInputArgs struct {
|
||||
Content string `flag:"content" input:"file,stdin"`
|
||||
}
|
||||
|
||||
func TestResolveTypedInputs_File(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cmdutil.TestChdir(t, dir)
|
||||
body := "## Title\n\nbody from a file\n"
|
||||
if err := os.WriteFile("body.md", []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
specs, err := walkArgs(reflect.TypeOf(&fileInputArgs{}))
|
||||
if err != nil {
|
||||
t.Fatalf("walkArgs: %v", err)
|
||||
}
|
||||
rt := newTypedInputRuntime(t, specs, []string{"--content", "@body.md"}, "")
|
||||
if err := resolveTypedInputs(rt, specs); err != nil {
|
||||
t.Fatalf("resolveTypedInputs: %v", err)
|
||||
}
|
||||
out := &fileInputArgs{}
|
||||
if err := bindFlags(rt.Cmd, reflect.ValueOf(out).Elem(), specs); err != nil {
|
||||
t.Fatalf("bindFlags: %v", err)
|
||||
}
|
||||
if out.Content != body {
|
||||
t.Errorf("Content = %q, want file body %q", out.Content, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTypedInputs_Stdin(t *testing.T) {
|
||||
specs, _ := walkArgs(reflect.TypeOf(&fileInputArgs{}))
|
||||
rt := newTypedInputRuntime(t, specs, []string{"--content", "-"}, "piped stdin body")
|
||||
if err := resolveTypedInputs(rt, specs); err != nil {
|
||||
t.Fatalf("resolveTypedInputs: %v", err)
|
||||
}
|
||||
out := &fileInputArgs{}
|
||||
_ = bindFlags(rt.Cmd, reflect.ValueOf(out).Elem(), specs)
|
||||
if out.Content != "piped stdin body" {
|
||||
t.Errorf("Content = %q, want stdin body", out.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveTypedInputs_PlainValueUnchanged(t *testing.T) {
|
||||
specs, _ := walkArgs(reflect.TypeOf(&fileInputArgs{}))
|
||||
rt := newTypedInputRuntime(t, specs, []string{"--content", "literal text"}, "")
|
||||
if err := resolveTypedInputs(rt, specs); err != nil {
|
||||
t.Fatalf("resolveTypedInputs: %v", err)
|
||||
}
|
||||
out := &fileInputArgs{}
|
||||
_ = bindFlags(rt.Cmd, reflect.ValueOf(out).Elem(), specs)
|
||||
if out.Content != "literal text" {
|
||||
t.Errorf("Content = %q, want unchanged literal", out.Content)
|
||||
}
|
||||
}
|
||||
|
||||
// A OneOf variant flag that declares @file/stdin must resolve too — the binder
|
||||
// recurses into buckets because cobra flags are flat regardless of nesting.
|
||||
type nestedInputVariant struct {
|
||||
Body *string `flag:"body" input:"file,stdin"`
|
||||
Raw *string `flag:"raw"`
|
||||
}
|
||||
|
||||
func (nestedInputVariant) OneOf() {}
|
||||
|
||||
type nestedInputArgs struct {
|
||||
Content nestedInputVariant
|
||||
}
|
||||
|
||||
func TestResolveTypedInputs_NestedInOneOf(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cmdutil.TestChdir(t, dir)
|
||||
body := "nested variant body\n"
|
||||
if err := os.WriteFile("v.md", []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
specs, err := walkArgs(reflect.TypeOf(&nestedInputArgs{}))
|
||||
if err != nil {
|
||||
t.Fatalf("walkArgs: %v", err)
|
||||
}
|
||||
rt := newTypedInputRuntime(t, specs, []string{"--body", "@v.md"}, "")
|
||||
if err := resolveTypedInputs(rt, specs); err != nil {
|
||||
t.Fatalf("resolveTypedInputs: %v", err)
|
||||
}
|
||||
out := &nestedInputArgs{}
|
||||
if err := bindBuckets(rt.Cmd, reflect.ValueOf(out).Elem(), specs); err != nil {
|
||||
t.Fatalf("bindBuckets: %v", err)
|
||||
}
|
||||
if out.Content.Body == nil {
|
||||
t.Fatal("Content.Body is nil — variant not bound")
|
||||
}
|
||||
if *out.Content.Body != body {
|
||||
t.Errorf("Content.Body = %q, want file body %q", *out.Content.Body, body)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Mount-time validation: enum / input only on string leaves ------------
|
||||
|
||||
type enumOnIntArgs struct {
|
||||
Level int `flag:"level" enum:"1,2,3"`
|
||||
}
|
||||
|
||||
func TestWalkArgs_EnumOnNonStringErrors(t *testing.T) {
|
||||
_, err := walkArgs(reflect.TypeOf(&enumOnIntArgs{}))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for enum on int field")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "enum tag is only supported on string") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type inputOnBoolArgs struct {
|
||||
Flag bool `flag:"flag" input:"file"`
|
||||
}
|
||||
|
||||
func TestWalkArgs_InputOnNonStringErrors(t *testing.T) {
|
||||
_, err := walkArgs(reflect.TypeOf(&inputOnBoolArgs{}))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for input on bool field")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "input tag is only supported on string") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type unknownInputSrcArgs struct {
|
||||
Content string `flag:"content" input:"bogus"`
|
||||
}
|
||||
|
||||
func TestWalkArgs_UnknownInputSource(t *testing.T) {
|
||||
_, err := walkArgs(reflect.TypeOf(&unknownInputSrcArgs{}))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unknown input source")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown input source") {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A string-alias enum field (e.g. an argstype primitive) must be accepted.
|
||||
type enumOnStringArgs struct {
|
||||
Priority string `flag:"priority" enum:"low,normal,high"`
|
||||
}
|
||||
|
||||
func TestWalkArgs_EnumOnStringOK(t *testing.T) {
|
||||
specs, err := walkArgs(reflect.TypeOf(&enumOnStringArgs{}))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(specs) != 1 || len(specs[0].EnumValues) != 3 {
|
||||
t.Errorf("enum not parsed: %+v", specs)
|
||||
}
|
||||
}
|
||||
|
||||
// --- enum shell completion + help candidate rendering ---------------------
|
||||
|
||||
func TestRegisterLeaf_EnumCompletion(t *testing.T) {
|
||||
prev := cmdutil.FlagCompletionsEnabled()
|
||||
cmdutil.SetFlagCompletionsEnabled(true)
|
||||
defer cmdutil.SetFlagCompletionsEnabled(prev)
|
||||
|
||||
specs, _ := walkArgs(reflect.TypeOf(&enumOnStringArgs{}))
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
if err := registerFlags(cmd, specs); err != nil {
|
||||
t.Fatalf("registerFlags: %v", err)
|
||||
}
|
||||
fn, ok := cmd.GetFlagCompletionFunc("priority")
|
||||
if !ok || fn == nil {
|
||||
t.Fatal("expected a completion func registered for --priority")
|
||||
}
|
||||
vals, _ := fn(cmd, nil, "")
|
||||
want := map[string]bool{"low": true, "normal": true, "high": true}
|
||||
if len(vals) != 3 {
|
||||
t.Fatalf("completion candidates = %v, want low/normal/high", vals)
|
||||
}
|
||||
for _, v := range vals {
|
||||
if !want[v] {
|
||||
t.Errorf("unexpected completion candidate %q", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatLeafLine_EnumCandidates(t *testing.T) {
|
||||
s := fieldSpec{FlagName: "priority", Description: "the priority", EnumValues: []string{"low", "normal", "high"}}
|
||||
line := formatLeafLine(" ", s)
|
||||
if !strings.Contains(line, "(one of: low|normal|high)") {
|
||||
t.Errorf("help line missing enum candidates: %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatLeafLine_EnumAndDefault(t *testing.T) {
|
||||
s := fieldSpec{FlagName: "priority", Description: "the priority", EnumValues: []string{"low", "high"}, DefaultValue: "low"}
|
||||
line := formatLeafLine(" ", s)
|
||||
if !strings.Contains(line, "(one of: low|high)") || !strings.Contains(line, `(default "low")`) {
|
||||
t.Errorf("help line missing enum or default: %q", line)
|
||||
}
|
||||
}
|
||||
@@ -855,56 +855,69 @@ func newRuntimeContext(cmd *cobra.Command, f *cmdutil.Factory, s *Shortcut, conf
|
||||
func resolveInputFlags(rctx *RuntimeContext, flags []Flag) error {
|
||||
stdinUsed := false
|
||||
for _, fl := range flags {
|
||||
if len(fl.Input) == 0 {
|
||||
continue
|
||||
if err := resolveInputForFlag(rctx, fl.Name, fl.Input, &stdinUsed); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := rctx.Cmd.Flags().GetString(fl.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveInputForFlag applies @file / stdin / @@-escape resolution to a single
|
||||
// string flag. sources lists the accepted inputs (File / Stdin); empty sources
|
||||
// or an empty/plain flag value is a no-op. stdinUsed is shared across all flags
|
||||
// of one invocation so stdin (-) is consumed by at most one flag. Both the
|
||||
// legacy resolveInputFlags loop and the typed binder (resolveTypedInputs) call
|
||||
// through here, so @file / stdin behaves identically on TypedShortcut[T].
|
||||
func resolveInputForFlag(rctx *RuntimeContext, name string, sources []string, stdinUsed *bool) error {
|
||||
if len(sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
raw, err := rctx.Cmd.Flags().GetString(name)
|
||||
if err != nil {
|
||||
return FlagErrorf("--%s: Input is only supported for string flags", name)
|
||||
}
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// stdin: -
|
||||
if raw == "-" {
|
||||
if !slices.Contains(sources, Stdin) {
|
||||
return FlagErrorf("--%s does not support stdin (-)", name)
|
||||
}
|
||||
if *stdinUsed {
|
||||
return FlagErrorf("--%s: stdin (-) can only be used by one flag", name)
|
||||
}
|
||||
*stdinUsed = true
|
||||
data, err := io.ReadAll(rctx.IO().In)
|
||||
if err != nil {
|
||||
return FlagErrorf("--%s: Input is only supported for string flags", fl.Name)
|
||||
}
|
||||
if raw == "" {
|
||||
continue
|
||||
return FlagErrorf("--%s: failed to read from stdin: %v", name, err)
|
||||
}
|
||||
rctx.Cmd.Flags().Set(name, string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
// stdin: -
|
||||
if raw == "-" {
|
||||
if !slices.Contains(fl.Input, Stdin) {
|
||||
return FlagErrorf("--%s does not support stdin (-)", fl.Name)
|
||||
}
|
||||
if stdinUsed {
|
||||
return FlagErrorf("--%s: stdin (-) can only be used by one flag", fl.Name)
|
||||
}
|
||||
stdinUsed = true
|
||||
data, err := io.ReadAll(rctx.IO().In)
|
||||
if err != nil {
|
||||
return FlagErrorf("--%s: failed to read from stdin: %v", fl.Name, err)
|
||||
}
|
||||
rctx.Cmd.Flags().Set(fl.Name, string(data))
|
||||
continue
|
||||
}
|
||||
// escape: @@ → literal @
|
||||
if strings.HasPrefix(raw, "@@") {
|
||||
rctx.Cmd.Flags().Set(name, raw[1:]) // strip first @
|
||||
return nil
|
||||
}
|
||||
|
||||
// escape: @@ → literal @
|
||||
if strings.HasPrefix(raw, "@@") {
|
||||
rctx.Cmd.Flags().Set(fl.Name, raw[1:]) // strip first @
|
||||
continue
|
||||
// file: @path
|
||||
if strings.HasPrefix(raw, "@") {
|
||||
if !slices.Contains(sources, File) {
|
||||
return FlagErrorf("--%s does not support file input (@path)", name)
|
||||
}
|
||||
|
||||
// file: @path
|
||||
if strings.HasPrefix(raw, "@") {
|
||||
if !slices.Contains(fl.Input, File) {
|
||||
return FlagErrorf("--%s does not support file input (@path)", fl.Name)
|
||||
}
|
||||
path := strings.TrimSpace(raw[1:])
|
||||
if path == "" {
|
||||
return FlagErrorf("--%s: file path cannot be empty after @", fl.Name)
|
||||
}
|
||||
data, err := cmdutil.ReadInputFile(rctx.FileIO(), path)
|
||||
if err != nil {
|
||||
return FlagErrorf("--%s: %v", fl.Name, err)
|
||||
}
|
||||
rctx.Cmd.Flags().Set(fl.Name, string(data))
|
||||
continue
|
||||
path := strings.TrimSpace(raw[1:])
|
||||
if path == "" {
|
||||
return FlagErrorf("--%s: file path cannot be empty after @", name)
|
||||
}
|
||||
data, err := cmdutil.ReadInputFile(rctx.FileIO(), path)
|
||||
if err != nil {
|
||||
return FlagErrorf("--%s: %v", name, err)
|
||||
}
|
||||
rctx.Cmd.Flags().Set(name, string(data))
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ func buildTypedHelp(specs []fieldSpec, examples []HelpExample) func(*cobra.Comma
|
||||
// same information density as cobra's legacy default help.
|
||||
func formatLeafLine(indent string, s fieldSpec) string {
|
||||
line := fmt.Sprintf("%s--%s %s", indent, s.FlagName, s.Description)
|
||||
if len(s.EnumValues) > 0 {
|
||||
line += fmt.Sprintf(" (one of: %s)", strings.Join(s.EnumValues, "|"))
|
||||
}
|
||||
if s.DefaultValue != "" {
|
||||
line += fmt.Sprintf(" (default %q)", s.DefaultValue)
|
||||
}
|
||||
|
||||
@@ -118,9 +118,12 @@ func (s TypedShortcut[T]) MountWithContext(ctx context.Context, parent *cobra.Co
|
||||
//
|
||||
// 1. identity / scopes / runtime — handled by Shortcut.runShortcut
|
||||
// 2. validateEnumFlags — Shortcut machinery
|
||||
// 3. resolveInputFlags — @file / stdin
|
||||
// 3. resolveInputFlags — @file / stdin (legacy shell only; the
|
||||
// synthesized shell's Flags slice is empty, so this is a no-op for typed —
|
||||
// typed flags declare inputs via the `input` tag, resolved in step 5)
|
||||
// 4. ValidateJqFlags — --jq
|
||||
// 5. shell.Validate — binds T, runs Normalize / ValidateValue /
|
||||
// 5. shell.Validate — resolveTypedInputs (@file / stdin for
|
||||
// `input`-tagged flags), binds T, runs Normalize / ValidateValue /
|
||||
// framework rules / ArgsValidator / user-typed Validate
|
||||
// 6. --dry-run gate — shell.DryRun reads typed args from rt
|
||||
// 7. high-risk-write confirmation — when Risk == "high-risk-write"
|
||||
@@ -161,6 +164,12 @@ func mountTyped[T any](ctx context.Context, parent *cobra.Command, f *cmdutil.Fa
|
||||
}
|
||||
},
|
||||
Validate: func(c context.Context, rt *RuntimeContext) error {
|
||||
// @file / stdin resolution for flags that declared an `input` tag.
|
||||
// Runs before bindFlags so the resolved file/stdin content is what
|
||||
// gets bound into the Args struct.
|
||||
if err := resolveTypedInputs(rt, specs); err != nil {
|
||||
return err
|
||||
}
|
||||
args := reflect.New(argsType.Elem()).Interface().(T)
|
||||
argsVal := reflect.ValueOf(args).Elem()
|
||||
if err := bindFlags(rt.Cmd, argsVal, specs); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user