cli
cli copied to clipboard
How to set up a cli.StringMapFlag of Slices (v3)
My question is how to set up a string map of a string slice.
Input: --cidrs "dev=10.0.0.0/8,192.168.1.1/32" --ip-list "prod=10.1.0.0/8"
Currently configured like this:
&cli.StringMapFlag{
Name: "cidrs",
Value: map[string]string{},
Validator: func(m map[string]string) error {
for _, value := range m {
cidrs := strings.Split(value, ",")
if len(cidrs) < 1 {
return fmt.Errorf("expected at least on cidr")
}
for _, cidr := range cidrs {
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("%w, parsing at least one of the entered CIDR was unsuccessful, expect format per CIDR of '<IP>/<suffix>'", err)
}
}
}
return nil
},
},
This results now in this error: Incorrect Usage: invalid value "dev=10.0.0.0/8,192.168.1.1/32" for flag -cidrs: item "192.168.1.1/32" is missing separator "="
I ended up implementing it like this:
Own implementation
package commands
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/urfave/cli/v3"
)
// StringMapSliceFlag is a flag that represents a map of string slices
type StringMapSliceFlag struct {
Name string
Aliases []string
Usage string
EnvVars []string
Value map[string][]string
Destination *map[string][]string
Required bool
Hidden bool
Validator func(map[string][]string) error
hasBeenSet bool
}
// Names returns the flag name and aliases
func (f *StringMapSliceFlag) Names() []string {
return append([]string{f.Name}, f.Aliases...)
}
// IsSet checks if this flag is set
func (f *StringMapSliceFlag) IsSet() bool {
return f.hasBeenSet
}
// String returns the stringified flag value
func (f *StringMapSliceFlag) String() string {
if len(f.Value) == 0 {
return ""
}
// Create sorted keys for consistent output
keys := make([]string, 0, len(f.Value))
for key := range f.Value {
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(f.Value))
for _, key := range keys {
values := f.Value[key]
parts = append(parts, fmt.Sprintf("%s=%s", key, strings.Join(values, ",")))
}
return strings.Join(parts, ";")
}
// Get returns the flag's value
func (f *StringMapSliceFlag) Get() any {
return f.Value
}
// PreParse is called before parsing
func (f *StringMapSliceFlag) PreParse() error {
// Initialize default value if needed
if f.Value == nil {
f.Value = make(map[string][]string)
}
return nil
}
// PostParse is called after parsing
func (f *StringMapSliceFlag) PostParse() error {
if f.Validator != nil && f.Value != nil {
return f.Validator(f.Value)
}
return nil
}
// Set sets the flag value
func (f *StringMapSliceFlag) Set(name, value string) error {
if value == "" {
return nil
}
// Check if it's a JSON serialized value (with slPfx prefix)
if strings.HasPrefix(value, "[") {
var mapSlice map[string][]string
if err := json.Unmarshal([]byte(value), &mapSlice); err == nil {
f.Value = mapSlice
f.hasBeenSet = true
if f.Destination != nil {
*f.Destination = f.Value
}
return nil
}
}
// If not initialized yet
if f.Value == nil {
f.Value = make(map[string][]string)
}
// Parse the string like "key1=val1,val2;key2=val3,val4"
for _, part := range strings.Split(value, ";") {
if part == "" {
continue
}
keyValue := strings.SplitN(part, "=", 2)
if len(keyValue) != 2 {
return fmt.Errorf("invalid format for key-value pair: %s (expected key=value format)", part)
}
key := keyValue[0]
valueStr := keyValue[1]
values := strings.Split(valueStr, ",")
f.Value[key] = values
}
f.hasBeenSet = true
if f.Destination != nil {
*f.Destination = f.Value
}
return nil
}
// GetStringMapSlice returns the map string slice value from the context
func GetStringMapSlice(cmd *cli.Command, name string) map[string][]string {
if v, ok := cmd.Value(name).(map[string][]string); ok {
return v
}
return nil
}
// GetJSONStringMapSlice parses a JSON string into a map of string slices
func GetJSONStringMapSlice(c *cli.Command, name string) map[string][]string {
val := c.String(name)
if val == "" {
return map[string][]string{}
}
var result map[string][]string
if err := json.Unmarshal([]byte(val), &result); err != nil {
return map[string][]string{}
}
return result
}
There are probably better ways? It does the job for me though 😄
Previously I implemented it like this (v2), which failed on me (panic: interface conversion: cli.Value is nil, not *commands.CIDRMapFlag while providing a valid config which worked perfectly with v2):
v2 Implementation
package commands
import (
"fmt"
"net"
"path/filepath"
"strings"
)
// CIDRMapFlag is used to add an advanced flag for CIDR maps.
type CIDRMapFlag struct {
M map[string][]string
}
// Set creates the expected CIDR map structure based on the initial string structure.
func (k *CIDRMapFlag) Set(value string) error {
if k.M == nil {
k.M = make(map[string][]string)
}
expectedElements := 2
parts := strings.SplitN(value, "=", expectedElements)
// Validate the major formatting of flags
if len(parts) != expectedElements {
return fmt.Errorf("expects the format '--cidrs <env>=<CIDR 0>[,<CIDR n>]'")
}
// Split and validate CIDRs
cidrs := strings.Split(parts[1], ",")
if len(cidrs) < 1 {
return fmt.Errorf("expected at least on cidr")
}
for _, cidr := range cidrs {
_, _, err := net.ParseCIDR(cidr)
if err != nil {
return fmt.Errorf("%w, parsing at least one of the entered CIDR was unsuccessful, expect format per CIDR of '<IP>/<suffix>'", err)
}
}
k.M[parts[0]] = cidrs
return nil
}
// String returns the CIDR map as string.
func (k *CIDRMapFlag) String() string {
return fmt.Sprintf("%s", k.M)
}
// Get returns the CIDR map as object.
func (k *CIDRMapFlag) Get() any {
return k.M
}
v2 usage via GenericFlag
&cli.GenericFlag{
Name: "cidrs",
Value: &CIDRMapFlag{},
}