// Package filters provides helper function to parse and handle command line // filter, used for example in docker ps or docker images commands. package filters import ( "encoding/json" "errors" "fmt" "regexp" "strings" ) // Args stores filter arguments as map key:{map key: bool}. // It contains an aggregation of the map of arguments (which are in the form // of -f 'key=value') based on the key, and stores values for the same key // in a map with string keys and boolean values. // e.g given -f 'label=label1=1' -f 'label=label2=2' -f 'image.name=ubuntu' // the args will be {"image.name":{"ubuntu":true},"label":{"label1=1":true,"label2=2":true}} type Args struct { fields map[string]map[string]bool } // NewArgs initializes a new Args struct. func NewArgs() Args { return Args{fields: map[string]map[string]bool{}} } // ParseFlag parses the argument to the filter flag. Like // // `docker ps -f 'created=today' -f 'image.name=ubuntu*'` // // If prev map is provided, then it is appended to, and returned. By default a new // map is created. func ParseFlag(arg string, prev Args) (Args, error) { filters := prev if len(arg) == 0 { return filters, nil } if !strings.Contains(arg, "=") { return filters, ErrBadFormat } f := strings.SplitN(arg, "=", 2) name := strings.ToLower(strings.TrimSpace(f[0])) value := strings.TrimSpace(f[1]) filters.Add(name, value) return filters, nil } // ErrBadFormat is an error returned in case of bad format for a filter. var ErrBadFormat = errors.New("bad format of filter (expected name=value)") // ToParam packs the Args into a string for easy transport from client to server. func ToParam(a Args) (string, error) { // this way we don't URL encode {}, just empty space if a.Len() == 0 { return "", nil } buf, err := json.Marshal(a.fields) if err != nil { return "", err } return string(buf), nil } // FromParam unpacks the filter Args. func FromParam(p string) (Args, error) { if len(p) == 0 { return NewArgs(), nil } r := strings.NewReader(p) d := json.NewDecoder(r) m := map[string]map[string]bool{} if err := d.Decode(&m); err != nil { r.Seek(0, 0) // Allow parsing old arguments in slice format. // Because other libraries might be sending them in this format. deprecated := map[string][]string{} if deprecatedErr := d.Decode(&deprecated); deprecatedErr == nil { m = deprecatedArgs(deprecated) } else { return NewArgs(), err } } return Args{m}, nil } // Get returns the list of values associates with a field. // It returns a slice of strings to keep backwards compatibility with old code. func (filters Args) Get(field string) []string { values := filters.fields[field] if values == nil { return make([]string, 0) } slice := make([]string, 0, len(values)) for key := range values { slice = append(slice, key) } return slice } // Add adds a new value to a filter field. func (filters Args) Add(name, value string) { if _, ok := filters.fields[name]; ok { filters.fields[name][value] = true } else { filters.fields[name] = map[string]bool{value: true} } } // Del removes a value from a filter field. func (filters Args) Del(name, value string) { if _, ok := filters.fields[name]; ok { delete(filters.fields[name], value) } } // Len returns the number of fields in the arguments. func (filters Args) Len() int { return len(filters.fields) } // MatchKVList returns true if the values for the specified field matches the ones // from the sources. // e.g. given Args are {'label': {'label1=1','label2=1'}, 'image.name', {'ubuntu'}}, // field is 'label' and sources are {'label1': '1', 'label2': '2'} // it returns true. func (filters Args) MatchKVList(field string, sources map[string]string) bool { fieldValues := filters.fields[field] //do not filter if there is no filter set or cannot determine filter if len(fieldValues) == 0 { return true } if sources == nil || len(sources) == 0 { return false } for name2match := range fieldValues { testKV := strings.SplitN(name2match, "=", 2) v, ok := sources[testKV[0]] if !ok { return false } if len(testKV) == 2 && testKV[1] != v { return false } } return true } // Match returns true if the values for the specified field matches the source string // e.g. given Args are {'label': {'label1=1','label2=1'}, 'image.name', {'ubuntu'}}, // field is 'image.name' and source is 'ubuntu' // it returns true. func (filters Args) Match(field, source string) bool { if filters.ExactMatch(field, source) { return true } fieldValues := filters.fields[field] for name2match := range fieldValues { match, err := regexp.MatchString(name2match, source) if err != nil { continue } if match { return true } } return false } // ExactMatch returns true if the source matches exactly one of the filters. func (filters Args) ExactMatch(field, source string) bool { fieldValues, ok := filters.fields[field] //do not filter if there is no filter set or cannot determine filter if !ok || len(fieldValues) == 0 { return true } // try to match full name value to avoid O(N) regular expression matching if fieldValues[source] { return true } return false } // FuzzyMatch returns true if the source matches exactly one of the filters, // or the source has one of the filters as a prefix. func (filters Args) FuzzyMatch(field, source string) bool { if filters.ExactMatch(field, source) { return true } fieldValues := filters.fields[field] for prefix := range fieldValues { if strings.HasPrefix(source, prefix) { return true } } return false } // Include returns true if the name of the field to filter is in the filters. func (filters Args) Include(field string) bool { _, ok := filters.fields[field] return ok } // Validate ensures that all the fields in the filter are valid. // It returns an error as soon as it finds an invalid field. func (filters Args) Validate(accepted map[string]bool) error { for name := range filters.fields { if !accepted[name] { return fmt.Errorf("Invalid filter '%s'", name) } } return nil } // WalkValues iterates over the list of filtered values for a field. // It stops the iteration if it finds an error and it returns that error. func (filters Args) WalkValues(field string, op func(value string) error) error { if _, ok := filters.fields[field]; !ok { return nil } for v := range filters.fields[field] { if err := op(v); err != nil { return err } } return nil } func deprecatedArgs(d map[string][]string) map[string]map[string]bool { m := map[string]map[string]bool{} for k, v := range d { values := map[string]bool{} for _, vv := range v { values[vv] = true } m[k] = values } return m }