2

I'm building a CLI using Go and Cobra library. I've the following JSON that needs to be deserialized in the corresponding struct. Argument as JSON array:

"[
    (stringA, stringB), 
    stringC
 ]"

Struct

type MyStruct struct {
    StringArray []string
}

I'm using Cobra's StringSicceVarP as follows to

cmd.PersistentFlags().StringSliceVarP(&opts.StringParam, "paramname", "", nil, `this is the description`)

But cobra is reading the incoming json as one single string [(stringA, stringB), stringC] whereas I want the array to be of length 2, such that StringArray[0]: (stringA, stringB) and StringArray[1]:stringC.

I can't use the StringSliceVarP as it will split based on , which I don't want as my array string could itself has a ,.

How can I achieve this?

tortuga
  • 737
  • 2
  • 13
  • 34

1 Answers1

2

I personally advice you against this option. Supplying formatted data is conventionally done through reading STDIN or from a file. Such solution is usually more flexible by allowing you to add flags to specify the file's format (JSON, XML, etc.).

Supplying a filename instead of the raw JSON string in the arguments adds better interoperability with other software, and other benefits such as using the computer's disk for buffering data instead of the computer's memory/RAM.

My personal recommendations is that:

  • Use flags for options and configs, similar to a HTTP's query parameters.
  • Use stdin/file handles for data, similar to a HTTP's request body.

However, if you insist on using a flag:

Cobra does not have built-in support for JSON structures. However, the pflag package (the flag library used by Cobra) allows you to define custom value types to be used as flags through the pflag.(*FlagSet).Var() method. You have to make a new type that implements the pflag.Value interface:

type Value interface {
    String() string
    Set(string) error
    Type() string
}

To make a custom JSON-parsing type, you could write the following to use the built-in encoding/json package:

import (
    "encoding/json"
)

type JSONFlag struct {
    Target interface{}
}

// String is used both by fmt.Print and by Cobra in help text
func (f *JSONFlag) String() string {
    b, err := json.Marshal(f.Target)
    if err != nil {
        return "failed to marshal object"
    }
    return string(b)
}

// Set must have pointer receiver so it doesn't change the value of a copy
func (f *JSONFlag) Set(v string) error {
    return json.Unmarshal([]byte(v), f.Target)
}

// Type is only used in help text
func (f *JSONFlag) Type() string {
    return "json"
}

Then to use this new pflag.Value-compatible type, you may write something like this:

import (
    "fmt"

    "github.com/spf13/cobra"
)

type MyStruct struct {
    StringArray []string
}

func init() {
    var flagMyStringArray []string

    var myCmd = &cobra.Command{
        Use:   "mycmd",
        Short: "A brief description of your command",
        Run: func(cmd *cobra.Command, args []string) {
            myStruct := MyStruct{StringArray: flagMyStringArray}
            fmt.Printf("myStruct.StringArray contains %d elements:\n", len(myStruct.StringArray))
            for i, s := range myStruct.StringArray {
                fmt.Printf("idx=%d: %q", i, s)
            }
        },
    }

    rootCmd.AddCommand(myCmd)

    myCmd.Flags().Var(&JSONFlag{&flagMyStringArray}, "paramname", `this is the description`)
}

Example usage:

$ go run . mycmd --paramname 'hello'
Error: invalid argument "hello" for "--paramname" flag: invalid character 'h' looking for beginning of value
Usage:
  test mycmd [flags]

Flags:
  -h, --help             help for mycmd
      --paramname json   this is the description

exit status 1
$ go run . mycmd --paramname '["(stringA, stringB)", "stringC"]'
myStruct.StringArray contains 2 elements:
idx=0: "(stringA, stringB)"
idx=1: "stringC"
Applejag
  • 1,068
  • 10
  • 17
  • Thanks for the detailed response. Although I had figured out a way using the `SliceVarP()` of the CLI, your response will help for similar implementations in the future. – tortuga Jan 13 '22 at 19:45