Search code examples
goicmpflag-go

accept empty commandline argument value for flag.String in Go lang


I'm creating a CLI tool and in that i want to accept value of string for "-l" flag. if the "-l" flag is passed without argument then use the empty string value. when i run this with empt argument:app.exe -l

I'm running into error: flag needs an argument: -l

Code block: flag.StringVar(&listening, "l", "", "Receive only")

how can i solve this error? bool type work with empty arguments but this case also accept string like: app.exe -l "127.0.0.1"

I'm using "flag" Go package.

Context: I'm creating a tool to send data using the ICMP protocol. In that, the "-l" flag can be used in two ways: if it is present, it acts as a bool value, and it starts listening for all incoming ICMP packets. If we want to filter those packets to receive from a specific IP only, then it will accept a string argument like "-l 192.168.56.27". In that case, it will listen for incoming packets, but it will discard all those with non-matching IPs and only process the ones that match the IP 192.168.56.27.

Note: This problem is now solved. I may have done it in a complicated way, but it is working😊. If you are curious, you can check out the code at GitHub: https://github.com/Aftab700/pingo


Solution

  • I will try my best to explain: I'm defining a custom flag type that implements the flag.Value interface.

    
    type stringFlag struct {
        set   bool
        value string
    }
    
    

    But this will still give an error if "-l" is empty. To deal with that, we will use flag.ContinueOnError in flag.NewFlagSet. and we will check if

    err.Error() == "flag needs an argument: -l" then we set the our stringFlag.set = true.

    But this will introduce a new issue: if we pass arguments like -l -h then the -h will not be considered a separate flag but a string value to the -l.

    To deal with that, I need to modify the os.Args array before passing it to myFlagSet.Parse(Args).

    We will check if the -l is present, and if yes then if the next item in the array starts with - character, we will append an empty string, so the -h flag is considered a separate flag.

    Code:

    package main
    
    import (
        "flag"
        "fmt"
        "os"
    )
    
    var listeningStr stringFlag
    var myFlagSet = flag.NewFlagSet("Options", flag.ContinueOnError)
    
    type stringFlag struct {
        set   bool
        value string
    }
    
    func (sf *stringFlag) Set(x string) error {
        sf.value = x
        sf.set = true
        return nil
    }
    func (sf *stringFlag) String() string {
        return sf.value
    }
    
    func parseFlags() {
        myFlagSet.Var(&listeningStr, "l", "Listen for incoming ICMP packets\nProvide an IP address to Receive ICMP packets from provided IP address only")
    
        // -l is string so if there is flag after -l[-l -h] will not consider -h as flag but value to -l
        var Args []string
        var temp = len(os.Args)
        for i, v := range os.Args[1:] {
            Args = append(Args, v)
            if v == "-l" && (i+2) < temp && os.Args[i+2][0] == 45 { // 45 = "-"
                Args = append(Args, "")
            }
        }
    
        err := myFlagSet.Parse(Args)
        myFlagSet.SetOutput(nil)
    
        if err != nil {
            if err.Error() == "flag needs an argument: -l" {
                listeningStr.set = true
            } else {
                fmt.Println(err)
                os.Exit(0)
            }
        }
    }
    
    func init() {
        parseFlags()
    }
    

    For more info checkout the code at GitHub: https://github.com/Aftab700/pingo/blob/main/main.go