src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/edit/filter/compile.go (about)

     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.
     6  package filter
     7  
     8  import (
     9  	"errors"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"src.elv.sh/pkg/errutil"
    14  	"src.elv.sh/pkg/parse"
    15  	"src.elv.sh/pkg/parse/cmpd"
    16  )
    17  
    18  // Compile parses and compiles a filter.
    19  func Compile(q string) (Filter, error) {
    20  	qn, errParse := parseFilter(q)
    21  	filter, errCompile := compileFilter(qn)
    22  	return filter, errutil.Multi(errParse, errCompile)
    23  }
    24  
    25  func 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  
    31  func 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  
    42  func 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  
    54  func 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  
    68  var errEmptySubfilter = errors.New("empty subfilter")
    69  
    70  func 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  
   113  type notSupportedError struct{ what string }
   114  
   115  func (err notSupportedError) Error() string {
   116  	return err.what + " not supported"
   117  }