8

I am new to Golang and I have been unable to find a solution to this problem using flag.

How can I use flag so my program can handle calls like these, where the -term flag may be present a variable number of times, including 0 times:

./myprogram -f flag1
./myprogram -f flag1 -term t1 -term t2 -term  t3
matt
  • 318
  • 3
  • 11
  • 3
    Bind -term to a variable of your own type implementing https://golang.org/pkg/flag/#Value and collect the values on each Set() call. – Volker Aug 03 '17 at 14:41
  • 1
    Can whoever downvoted explain what the problem with my question is? – matt Aug 03 '17 at 15:31

2 Answers2

14

You need to declare your own type which implements the Value interface. Here is an example.

// Created so that multiple inputs can be accecpted
type arrayFlags []string

func (i *arrayFlags) String() string {
    // change this, this is just can example to satisfy the interface
    return "my string representation"
}

func (i *arrayFlags) Set(value string) error {
    *i = append(*i, strings.TrimSpace(value))
    return nil
}

then in the main function where you are parsing the flags

var myFlags arrayFlags

flag.Var(&myFlags, "term", "my terms")
flag.Parse()

Now all the terms are contained in the slice myFlags

reticentroot
  • 3,612
  • 2
  • 22
  • 39
  • If you mean, does the above work and compile, then yes. That's the answer above – reticentroot Aug 04 '17 at 00:24
  • Thanks, can you tell me if this interpretation is correct?: Since myFlags is referred to as the value argument for Var, myFlags becomes type Value and the String() and Set() methods are run automatically. No sorry I accidentally posted the comment and then was locked out of editing because 5 min passed. – matt Aug 04 '17 at 00:28
  • 1
    Yes, because the argument is of type Value, which is an interface. Any structure that implements the methods of an interface can be used to satisfy the interface. See here for more on interfaces https://tour.golang.org/methods/10 – reticentroot Aug 04 '17 at 00:31
0

This question is an interesting one and can play in many variations.

  • Array
  • Map
  • Struct

The core content is the same as @reticentroot answered,

Complete the definition of this interface: Flag.Value

The following are examples to share and provide relevant links as much as possible

Example

expected usage:

type Books []string

func (*Books) String() string   { return "" }
func (*Books) Set(string) error { return nil }

type Dict map[string]string

func (*Dict) String() string   { return "" }
func (*Dict) Set(string) error { return nil }

type Person struct {
    Name string
    Age  int
}

func (*Person) String() string   { return "" }
func (*Person) Set(string) error { return nil }

func pseudocode() {
    flagSetTest := flag.NewFlagSet("test", flag.ContinueOnError)

    books := Books{}
    flagSetTest.Var(&books, "book", "-book C++ -book Go -book javascript")
    // expected output: books: []string{C++,Go,javascript}

    dict := Dict{}
    flagSetTest.Var(&dict, "dict", "-dict A:65|B:66")
    // expected output: dict: map[string]string{"A":"65", "B":"66"}

    // map
    person := Person{}
    flagSetTest.Var(&person, "person", "-person Name:foo|Age:18")
    // output: {Name:foo Age:18}

    flagSetTest.Parse(os.Args[1:])
    fmt.Println(person, books, dict)
}

Full code

package main

import (
    "bufio"
    "errors"
    "flag"
    "fmt"
    "os"
    "reflect"
    "strconv"
    "strings"
)

type BooksValue []string

// https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L298
func (arr *BooksValue) String() string {
    /*
        value.String(): https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L870
        DefValue string:
            - https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L348
            - https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L914-L920
            - https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L529-L536
            - https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L464
    */
    return ""
}

// https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L299
func (arr *BooksValue) Set(value string) error {
    /*
        value: https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L947
        bool: Set(value): https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L966-L975
        else: Set(value): https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L986-L988
    */
    *arr = append(*arr, strings.TrimSpace(value))
    return nil
}

type DictValue map[string]string

func (m *DictValue) String() string {
    return ""
}

func (m *DictValue) Set(value string) error {
    arr := strings.Split(value, "|") // "key1:val1|key2:val2|..."
    for _, curPairStr := range arr {
        itemArr := strings.Split(curPairStr, ":")
        key := itemArr[0]
        val := itemArr[1]
        (*m)[key] = val
    }
    return nil
}

type PersonValue struct {
    Name     string
    Age      int
    Msg      string
    IsActive bool
}

func (s *PersonValue) String() string {
    return ""
}

func (s *PersonValue) Set(value string) error {
    arr := strings.Split(value, "|") // "Field1:Value1|F2:V2|...|FN:VN"

    for _, curPairStr := range arr {
        itemArr := strings.Split(curPairStr, ":")
        key := itemArr[0]
        val := itemArr[1]

        // [Access struct property by name](https://stackoverflow.com/a/66470232/9935654)
        pointToStruct := reflect.ValueOf(s)
        curStruct := pointToStruct.Elem()
        curField := curStruct.FieldByName(key)
        if !curField.IsValid() {
            return errors.New("not found")
        }

        // CanSet one of conditions: Name starts with a capital
        if !curField.CanSet() {
            return errors.New("can't set")
        }

        t := reflect.TypeOf(*s)
        structFieldXXX, isFound := t.FieldByName(key)
        if !isFound {
            return errors.New("not found")
        }

        switch structFieldXXX.Type.Name() {
        case "int":
            // https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L146-L153
            intValue, err := strconv.ParseInt(val, 0, strconv.IntSize)
            if err != nil {
                return errors.New("parse error: [int]")
            }
            curField.SetInt(intValue)
        case "bool":
            // https://github.com/golang/go/blob/2580d0e/src/flag/flag.go#L117-L121
            boolValue, err := strconv.ParseBool(val)
            if err != nil {
                return errors.New("parse error: [bool]")
            }
            curField.SetBool(boolValue)
        case "string":
            curField.SetString(val)
        default:
            return errors.New("not support type=" + structFieldXXX.Type.Name())
        }
    }

    return nil
}

func main() {

    flagSetTest := flag.NewFlagSet("test", flag.ContinueOnError)

    // array
    books := BooksValue{}
    flagSetTest.Var(&books, "book", "-book Go -book javascript ...")

    // map
    myMap := DictValue{}
    flagSetTest.Var(&myMap, "map", "-dict A:65|B:66")

    // struct
    person := PersonValue{Msg: "Hello world"}
    flagSetTest.Var(&person, "person", "-person Name:string|Age:int|Msg:string|IsActive:bool")

    testArgs := []string{"test",
        "-book", "Go", "-book", "javascript", // testArray
        "-map", "A:65|B:66|Name:Carson", // testMap
        "-person", "Name:Carson|Age:30|IsActive:true", // testStruct
    }

    testFunc := func(args []string, reset bool) {
        if reset {
            books = BooksValue{}
            myMap = DictValue{}
            person = PersonValue{}
        }

        if err := flagSetTest.Parse(args); err != nil {
            fmt.Printf(err.Error())
        }
        fmt.Printf("%+v\n", books)
        fmt.Printf("%+v\n", myMap)
        fmt.Printf("%+v\n", person)
    }

    testFunc(testArgs[1:], false)
    
    // ↓ play by yourself
    scanner := bufio.NewScanner(os.Stdin)
    for {
        fmt.Println("Enter CMD: ") // example: test -book item1 -book item2 -map key1:value1|key2:v2 -person Age:18|Name:Neil|IsActive:true
        scanner.Scan()             // Scans a line from Stdin(Console)
        text := scanner.Text()     // Holds the string that scanned
        args := strings.Split(text, " ")

        switch args[0] {
        case "quit":
            return
        case "test":
            testFunc(args[1:], true)
        }
    }
}

go playground

Carson
  • 6,105
  • 2
  • 37
  • 45