Skip to content

Commit

Permalink
Add additional generic components and helpers (#224)
Browse files Browse the repository at this point in the history
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
anujc25 authored Feb 4, 2025
1 parent 3942e77 commit 4235cde
Show file tree
Hide file tree
Showing 19 changed files with 1,378 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ issues:
- staticcheck
text: "SA1019:"

# Ignore the copyright header checks in following files
- path: component/tabwriter/internal/tabwriter.go
linters:
- goheader

include:
- EXC0011 # disable excluding of issues about missing package comments from stylecheck

Expand Down
140 changes: 140 additions & 0 deletions command/arguments.go
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)
}
48 changes: 48 additions & 0 deletions command/extend.go
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
}
170 changes: 170 additions & 0 deletions command/extend_test.go
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)
}
}
Loading

0 comments on commit 4235cde

Please sign in to comment.