1// Package filter implements the Elvish filter DSL.
2//
3// The filter DSL is a subset of Elvish's expression syntax, and is useful for
4// filtering a list of items. It is currently used in the listing modes of the
5// interactive editor.
6package filter
7
8import (
9	"errors"
10	"regexp"
11	"strings"
12
13	"src.elv.sh/pkg/diag"
14	"src.elv.sh/pkg/parse"
15	"src.elv.sh/pkg/parse/cmpd"
16)
17
18// Compile parses and compiles a filter.
19func Compile(q string) (Filter, error) {
20	qn, errParse := parseFilter(q)
21	filter, errCompile := compileFilter(qn)
22	return filter, diag.Errors(errParse, errCompile)
23}
24
25func parseFilter(q string) (*parse.Filter, error) {
26	qn := &parse.Filter{}
27	err := parse.ParseAs(parse.Source{Name: "filter", Code: q}, qn, parse.Config{})
28	return qn, err
29}
30
31func compileFilter(qn *parse.Filter) (Filter, error) {
32	if len(qn.Opts) > 0 {
33		return nil, notSupportedError{"option"}
34	}
35	qs, err := compileCompounds(qn.Args)
36	if err != nil {
37		return nil, err
38	}
39	return andFilter{qs}, nil
40}
41
42func compileCompounds(ns []*parse.Compound) ([]Filter, error) {
43	qs := make([]Filter, len(ns))
44	for i, n := range ns {
45		q, err := compileCompound(n)
46		if err != nil {
47			return nil, err
48		}
49		qs[i] = q
50	}
51	return qs, nil
52}
53
54func compileCompound(n *parse.Compound) (Filter, error) {
55	if pn, ok := cmpd.Primary(n); ok {
56		switch pn.Type {
57		case parse.Bareword, parse.SingleQuoted, parse.DoubleQuoted:
58			s := pn.Value
59			ignoreCase := s == strings.ToLower(s)
60			return substringFilter{s, ignoreCase}, nil
61		case parse.List:
62			return compileList(pn.Elements)
63		}
64	}
65	return nil, notSupportedError{cmpd.Shape(n)}
66}
67
68var errEmptySubfilter = errors.New("empty subfilter")
69
70func compileList(elems []*parse.Compound) (Filter, error) {
71	if len(elems) == 0 {
72		return nil, errEmptySubfilter
73	}
74	head, ok := cmpd.StringLiteral(elems[0])
75	if !ok {
76		return nil, notSupportedError{"non-literal subfilter head"}
77	}
78	switch head {
79	case "re":
80		if len(elems) == 1 {
81			return nil, notSupportedError{"re subfilter with no argument"}
82		}
83		if len(elems) > 2 {
84			return nil, notSupportedError{"re subfilter with two or more arguments"}
85		}
86		arg := elems[1]
87		s, ok := cmpd.StringLiteral(arg)
88		if !ok {
89			return nil, notSupportedError{"re subfilter with " + cmpd.Shape(arg)}
90		}
91		p, err := regexp.Compile(s)
92		if err != nil {
93			return nil, err
94		}
95		return regexpFilter{p}, nil
96	case "and":
97		qs, err := compileCompounds(elems[1:])
98		if err != nil {
99			return nil, err
100		}
101		return andFilter{qs}, nil
102	case "or":
103		qs, err := compileCompounds(elems[1:])
104		if err != nil {
105			return nil, err
106		}
107		return orFilter{qs}, nil
108	default:
109		return nil, notSupportedError{"head " + parse.SourceText(elems[0])}
110	}
111}
112
113type notSupportedError struct{ what string }
114
115func (err notSupportedError) Error() string {
116	return err.what + " not supported"
117}
118