jawher / mow.cli

A versatile library for building CLI applications in Go
MIT License
872 stars 55 forks source link

Inter-option ordering #92

Open nim-nim opened 5 years ago

nim-nim commented 5 years ago

I'm writing a command that needs to filter a file list.

Some filters will cause files to be selected, others to be deselected. A file can match several filters so filtering order matters: <something that selects the file> followed by <something that deselects the file> does not give the same result that <something that deselects the file> followed by <something that selects the file>

I was planning to use a different option for each type of filter (basically --filtertype filterexpression) but mow.cli does not seem to preserve inter-option ordering. So I'm stuck in hardcoding the filter application ordering by type (all filters of type A, then all filters of type B, etc)

Would it be possible to enhance mow.cli so related options generate a slice of {option, value} and not separate per-option value slices?

jawher commented 5 years ago

How about implementing a custom multi-valued option type to represent an ordered slice of typed file filters (include or exclude) ?

Something like:

type Type string

const (
    Include Type = "include"
    Exclude Type = "exclude"
)

type Filter struct {
    Type    Type
    Pattern string
}

type Filters []Filter

var (
    BadFilter            = errors.New("invalid filter, accepted formats: +pattern (include filter) or -pattern (exclude filter)")
    _         flag.Value = &Filters{}
)

func (ff *Filters) Set(v string) error {
    var (
        pattern string
        typ     Type
    )
    switch {
    case strings.HasPrefix(v, "+"):
        pattern = strings.TrimPrefix(v, "+")
        typ = Include
    case strings.HasPrefix(v, "-"):
        pattern = strings.TrimPrefix(v, "-")
        typ = Exclude
    default:
        return BadFilter
    }
    if pattern == "" {
        return BadFilter
    }

    *ff = append(*ff, Filter{Type: typ, Pattern: pattern})
    return nil
}

// make Filters a multi-valued option
func (ff *Filters) Clear() {
    *ff = Filters{}
}

func (ff *Filters) String() string {
    return fmt.Sprintf("%+v", *ff)
}

func main() {
    var (
        filters Filters
        app     = cli.App("app", "")
    )
    app.Spec = "[-f...]"

    app.VarOpt("f", &filters, "file filters, accepted formats: +pattern (include filter) or -pattern (exclude filter)")

    app.Action = func() {
        fmt.Printf("filter files using:\n")
        for i, f := range filters {
            fmt.Printf("  %d. %+v\n", i+1, f)
        }
    }

    app.Run(os.Args)
}

You can then call this app with:

$ ./app -f='-bak_*' -f='+*.png'
filter files using:
  1. {Type:exclude Pattern:bak_*}
  2. {Type:include Pattern:*.png}
nim-nim commented 5 years ago

Thank you for looking at this. I realise now I should have been clearer, by filter type I meant not just "+" and "-" but several types of filters that exclude and several types of filter that include

[-f=<path> | -e=<extension> | -d=<path> | -t=<tree root path> | -r=<regular expression> | -F=<path> | -E=<extension> | -D=<path> | -T=<tree root path> | -R=<regular expression>]

So shoving the type in the option value does not cut it and quickly becomes cryptic and user-unfriendly.

Not to mention users are familiar with a previous-gen implementation in another tech, that used human-friendly flag names such as --ignore-tree and --include-extension

jawher commented 5 years ago

Ah, I see.

Since each option stores its value in a separate variable, the order is indeed lost.

I'll have to think on how best to accommodate such a use case. I'm thinking a callback system (call a user-provided function whenever an option is encountered in the args) might do it, but I'll have to check if this would really work: stuff like backtracking for example might render this tough/impossible.