-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add additional generic components and helpers (#224)
1. Adds Cobra command's `Arg`, `Error` and `Extend` helpers under `command` package 2. Adds Emoji and special characters under `component/icons` package 3. Adds string and color utils under `component/stringutils` package 4. Adds TabWriter component under `component/tabwriter` package
- Loading branch information
Showing
19 changed files
with
1,378 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// Copyright 2025 VMware, Inc. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package command | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
var ErrIgnoreArg = fmt.Errorf("ignore argument") | ||
|
||
type Arg struct { | ||
Name string | ||
Arity int | ||
Optional bool | ||
Set func(cmd *cobra.Command, args []string, offset int) error | ||
} | ||
|
||
func Args(cmd *cobra.Command, argDefs ...Arg) { | ||
cmd.Args = func(cmd *cobra.Command, args []string) error { | ||
offset := 0 | ||
|
||
for _, argDef := range argDefs { | ||
arity := argDef.Arity | ||
if arity == -1 { | ||
// consume all remaining args | ||
arity = len(args) - offset | ||
} | ||
if len(args)-offset < arity { | ||
if argDef.Optional { | ||
continue | ||
} | ||
// TODO create a better message saying what is missing | ||
return fmt.Errorf("missing required argument(s)") | ||
} | ||
|
||
if err := argDef.Set(cmd, args, offset); err != nil { | ||
if err == ErrIgnoreArg { | ||
continue | ||
} | ||
return err | ||
} | ||
|
||
offset += arity | ||
} | ||
|
||
// no additional args | ||
return cobra.NoArgs(cmd, args[offset:]) | ||
} | ||
|
||
addArgsToUseString(cmd, argDefs) | ||
} | ||
|
||
func Argument(name string, val *string) Arg { | ||
return Arg{ | ||
Name: name, | ||
Arity: 1, | ||
Set: func(_ *cobra.Command, args []string, offset int) error { | ||
*val = args[offset] | ||
return nil | ||
}, | ||
} | ||
} | ||
|
||
func OptionalArgument(name string, val *string) Arg { | ||
arg := Argument(name, val) | ||
arg.Optional = true | ||
return arg | ||
} | ||
|
||
func RemainingArguments(name string, values *[]string) Arg { | ||
return Arg{ | ||
Name: name, | ||
Arity: -1, | ||
Set: func(_ *cobra.Command, args []string, offset int) error { | ||
*values = args[offset:] | ||
return nil | ||
}, | ||
} | ||
} | ||
|
||
func OptionalRemainingArguments(name string, values *[]string) Arg { | ||
arg := RemainingArguments(name, values) | ||
arg.Optional = true | ||
return arg | ||
} | ||
|
||
func BareDoubleDashArgs(values *[]string) Arg { | ||
return Arg{ | ||
Arity: -1, | ||
Set: func(cmd *cobra.Command, args []string, _ int) error { | ||
if cmd.ArgsLenAtDash() == -1 { | ||
return nil | ||
} | ||
*values = args[cmd.ArgsLenAtDash():] | ||
return nil | ||
}, | ||
} | ||
} | ||
|
||
// addArgsToUseString automatically adds the argument names to the Use field of the command | ||
func addArgsToUseString(cmd *cobra.Command, argDefs []Arg) { | ||
for i := range argDefs { | ||
name := argDefs[i].Name | ||
if name == "" { | ||
continue | ||
} | ||
|
||
if argDefs[i].Optional { | ||
name = fmt.Sprintf("[%s]", name) | ||
} else { | ||
name = fmt.Sprintf("<%s>", name) | ||
} | ||
|
||
cmd.Use += " " + name | ||
} | ||
} | ||
|
||
// Name argument specific helpers | ||
|
||
const ( | ||
NameArgumentName = "name" | ||
NamesArgumentName = "name(s)" | ||
) | ||
|
||
func NameArg(val *string) Arg { | ||
return Argument(NameArgumentName, val) | ||
} | ||
|
||
func OptionalNameArg(val *string) Arg { | ||
arg := NameArg(val) | ||
arg.Optional = true | ||
return arg | ||
} | ||
|
||
func NamesArg(vals *[]string) Arg { | ||
return RemainingArguments(NamesArgumentName, vals) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Copyright 2025 VMware, Inc. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package command | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/spf13/cobra" | ||
) | ||
|
||
func Sequence(items ...func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error { | ||
return func(cmd *cobra.Command, args []string) error { | ||
for i := range items { | ||
if err := items[i](cmd, args); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
func Visit(cmd *cobra.Command, f func(c *cobra.Command) error) error { | ||
err := f(cmd) | ||
if err != nil { | ||
return err | ||
} | ||
for _, c := range cmd.Commands() { | ||
err := Visit(c, f) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
type commandKey struct{} | ||
|
||
func ContextWithCommand(ctx context.Context, cmd *cobra.Command) context.Context { | ||
return context.WithValue(ctx, commandKey{}, cmd) | ||
} | ||
|
||
func CommandFromContext(ctx context.Context) *cobra.Command { | ||
if cmd, ok := ctx.Value(commandKey{}).(*cobra.Command); ok { | ||
return cmd | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
// Copyright 2025 VMware, Inc. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package command | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"fmt" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/spf13/cobra" | ||
) | ||
|
||
func TestSequence(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
args []string | ||
items []func(cmd *cobra.Command, args []string) error | ||
output string | ||
err error | ||
}{{ | ||
name: "empty", | ||
}, { | ||
name: "single item", | ||
args: []string{"a", "b", "c"}, | ||
items: []func(cmd *cobra.Command, args []string) error{ | ||
func(cmd *cobra.Command, args []string) error { | ||
fmt.Fprintf(cmd.OutOrStdout(), "step %v\n", args) | ||
return nil | ||
}, | ||
}, | ||
output: ` | ||
step [a b c] | ||
`, | ||
}, { | ||
name: "multiple items", | ||
items: []func(cmd *cobra.Command, _ []string) error{ | ||
func(cmd *cobra.Command, _ []string) error { | ||
fmt.Fprintln(cmd.OutOrStdout(), "step 1") | ||
return nil | ||
}, | ||
func(cmd *cobra.Command, _ []string) error { | ||
fmt.Fprintln(cmd.OutOrStdout(), "step 2") | ||
return nil | ||
}, | ||
func(cmd *cobra.Command, _ []string) error { | ||
fmt.Fprintln(cmd.OutOrStdout(), "step 3") | ||
return nil | ||
}, | ||
}, | ||
output: ` | ||
step 1 | ||
step 2 | ||
step 3 | ||
`, | ||
}, { | ||
name: "stops on error", | ||
items: []func(cmd *cobra.Command, _ []string) error{ | ||
func(cmd *cobra.Command, _ []string) error { | ||
fmt.Fprintln(cmd.OutOrStdout(), "step 1") | ||
return nil | ||
}, | ||
func(cmd *cobra.Command, _ []string) error { | ||
fmt.Fprintln(cmd.OutOrStdout(), "step 2") | ||
return fmt.Errorf("test error") | ||
}, | ||
func(cmd *cobra.Command, _ []string) error { | ||
fmt.Fprintln(cmd.OutOrStdout(), "step 3") | ||
return nil | ||
}, | ||
}, | ||
output: ` | ||
step 1 | ||
step 2 | ||
`, | ||
err: fmt.Errorf("test error"), | ||
}} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
output := &bytes.Buffer{} | ||
cmd := &cobra.Command{} | ||
cmd.SetOutput(output) | ||
|
||
err := Sequence(test.items...)(cmd, test.args) | ||
|
||
if expected, actual := fmt.Sprintf("%s", test.err), fmt.Sprintf("%s", err); expected != actual { | ||
t.Errorf("Expected error %q, actually %q", expected, actual) | ||
} | ||
if diff := cmp.Diff(strings.TrimSpace(test.output), strings.TrimSpace(output.String())); diff != "" { | ||
t.Errorf("Unexpected output (-expected, +actual): %s", diff) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestVisit(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
cmd func() *cobra.Command | ||
visitor func(*cobra.Command) error | ||
err error | ||
}{{ | ||
name: "single command", | ||
cmd: func() *cobra.Command { | ||
return &cobra.Command{Use: "root"} | ||
}, | ||
visitor: func(_ *cobra.Command) error { | ||
return nil | ||
}, | ||
}, { | ||
name: "parent-child", | ||
cmd: func() *cobra.Command { | ||
root := &cobra.Command{Use: "root"} | ||
root.AddCommand(&cobra.Command{Use: "child"}) | ||
return root | ||
}, | ||
visitor: func(_ *cobra.Command) error { | ||
return nil | ||
}, | ||
}, { | ||
name: "error", | ||
cmd: func() *cobra.Command { | ||
return &cobra.Command{Use: "root"} | ||
}, | ||
visitor: func(cmd *cobra.Command) error { | ||
return fmt.Errorf("%s", cmd.Name()) | ||
}, | ||
err: fmt.Errorf("root"), | ||
}, { | ||
name: "child error", | ||
cmd: func() *cobra.Command { | ||
root := &cobra.Command{Use: "root"} | ||
root.AddCommand(&cobra.Command{Use: "child"}) | ||
return root | ||
}, | ||
visitor: func(cmd *cobra.Command) error { | ||
if cmd.Name() == "child" { | ||
return fmt.Errorf("%s", cmd.Name()) | ||
} | ||
return nil | ||
}, | ||
err: fmt.Errorf("child"), | ||
}} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
err := Visit(test.cmd(), test.visitor) | ||
if expected, actual := fmt.Sprintf("%s", test.err), fmt.Sprintf("%s", err); expected != actual { | ||
t.Errorf("Expected error %q, actually %q", expected, actual) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestCommandFromContext_WithCommand(t *testing.T) { | ||
cmd := &cobra.Command{} | ||
parentCtx := context.Background() | ||
childCtx := ContextWithCommand(parentCtx, cmd) | ||
|
||
if expected, actual := (*cobra.Command)(nil), CommandFromContext(parentCtx); expected != actual { | ||
t.Errorf("expected command %v, actually %v", expected, actual) | ||
} | ||
if expected, actual := cmd, CommandFromContext(childCtx); expected != actual { | ||
t.Errorf("expected command %v, actually %v", expected, actual) | ||
} | ||
} |
Oops, something went wrong.