src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/glob.go (about)

     1  package eval
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"strings"
     9  	"unicode"
    10  
    11  	"src.elv.sh/pkg/eval/vals"
    12  	"src.elv.sh/pkg/glob"
    13  	"src.elv.sh/pkg/parse"
    14  )
    15  
    16  // An ephemeral value generated when evaluating tilde and wildcards.
    17  type globPattern struct {
    18  	glob.Pattern
    19  	Flags  globFlag
    20  	Buts   []string
    21  	TypeCb func(os.FileMode) bool
    22  }
    23  
    24  type globFlag uint
    25  
    26  var typeCbMap = map[string]func(os.FileMode) bool{
    27  	"dir":     os.FileMode.IsDir,
    28  	"regular": os.FileMode.IsRegular,
    29  }
    30  
    31  const (
    32  	// noMatchOK indicates that the "nomatch-ok" glob index modifier was
    33  	// present.
    34  	noMatchOK globFlag = 1 << iota
    35  )
    36  
    37  func (f globFlag) Has(g globFlag) bool {
    38  	return (f & g) == g
    39  }
    40  
    41  var _ vals.ErrIndexer = globPattern{}
    42  
    43  var (
    44  	ErrMustFollowWildcard    = errors.New("must follow wildcard")
    45  	ErrModifierMustBeString  = errors.New("modifier must be string")
    46  	ErrWildcardNoMatch       = errors.New("wildcard has no match")
    47  	ErrMultipleTypeModifiers = errors.New("only one type modifier allowed")
    48  	ErrUnknownTypeModifier   = errors.New("unknown type modifier")
    49  )
    50  
    51  var runeMatchers = map[string]func(rune) bool{
    52  	"control": unicode.IsControl,
    53  	"digit":   unicode.IsDigit,
    54  	"graphic": unicode.IsGraphic,
    55  	"letter":  unicode.IsLetter,
    56  	"lower":   unicode.IsDigit,
    57  	"mark":    unicode.IsMark,
    58  	"number":  unicode.IsNumber,
    59  	"print":   unicode.IsPrint,
    60  	"punct":   unicode.IsPunct,
    61  	"space":   unicode.IsSpace,
    62  	"symbol":  unicode.IsSymbol,
    63  	"title":   unicode.IsTitle,
    64  	"upper":   unicode.IsUpper,
    65  }
    66  
    67  func (gp globPattern) Kind() string { return "glob-pattern" }
    68  
    69  func (gp globPattern) Index(k any) (any, error) {
    70  	modifierv, ok := k.(string)
    71  	if !ok {
    72  		return nil, ErrModifierMustBeString
    73  	}
    74  	modifier := modifierv
    75  	switch {
    76  	case modifier == "nomatch-ok":
    77  		gp.Flags |= noMatchOK
    78  	case strings.HasPrefix(modifier, "but:"):
    79  		gp.Buts = append(gp.Buts, modifier[len("but:"):])
    80  	case modifier == "match-hidden":
    81  		lastSeg, err := gp.lastWildSeg()
    82  		if err != nil {
    83  			return nil, err
    84  		}
    85  		gp.Segments[len(gp.Segments)-1] = glob.Wild{
    86  			Type: lastSeg.Type, MatchHidden: true, Matchers: lastSeg.Matchers,
    87  		}
    88  	case strings.HasPrefix(modifier, "type:"):
    89  		if gp.TypeCb != nil {
    90  			return nil, ErrMultipleTypeModifiers
    91  		}
    92  		typeName := modifier[len("type:"):]
    93  		cb, ok := typeCbMap[typeName]
    94  		if !ok {
    95  			return nil, ErrUnknownTypeModifier
    96  		}
    97  		gp.TypeCb = cb
    98  	default:
    99  		var matcher func(rune) bool
   100  		if m, ok := runeMatchers[modifier]; ok {
   101  			matcher = m
   102  		} else if strings.HasPrefix(modifier, "set:") {
   103  			set := modifier[len("set:"):]
   104  			matcher = func(r rune) bool {
   105  				return strings.ContainsRune(set, r)
   106  			}
   107  		} else if strings.HasPrefix(modifier, "range:") {
   108  			rangeExpr := modifier[len("range:"):]
   109  			badRangeExpr := fmt.Errorf("bad range modifier: %s", parse.Quote(rangeExpr))
   110  			runes := []rune(rangeExpr)
   111  			if len(runes) != 3 {
   112  				return nil, badRangeExpr
   113  			}
   114  			from, sep, to := runes[0], runes[1], runes[2]
   115  			switch sep {
   116  			case '-':
   117  				matcher = func(r rune) bool {
   118  					return from <= r && r <= to
   119  				}
   120  			case '~':
   121  				matcher = func(r rune) bool {
   122  					return from <= r && r < to
   123  				}
   124  			default:
   125  				return nil, badRangeExpr
   126  			}
   127  		} else {
   128  			return nil, fmt.Errorf("unknown modifier %s", vals.ReprPlain(modifierv))
   129  		}
   130  		err := gp.addMatcher(matcher)
   131  		return gp, err
   132  	}
   133  	return gp, nil
   134  }
   135  
   136  func (gp globPattern) Concat(v any) (any, error) {
   137  	switch rhs := v.(type) {
   138  	case string:
   139  		var segs []glob.Segment
   140  		segs = append(segs, gp.Segments...)
   141  		segs = append(segs, stringToSegments(rhs)...)
   142  		return globPattern{Pattern: glob.Pattern{Segments: segs}, Flags: gp.Flags,
   143  			Buts: gp.Buts, TypeCb: gp.TypeCb}, nil
   144  	case globPattern:
   145  		// We know rhs contains exactly one segment.
   146  		gp.append(rhs.Segments[0])
   147  		gp.Flags |= rhs.Flags
   148  		gp.Buts = append(gp.Buts, rhs.Buts...)
   149  		// This handles illegal cases such as `**[type:regular]x*[type:directory]`.
   150  		if gp.TypeCb != nil && rhs.TypeCb != nil {
   151  			return nil, ErrMultipleTypeModifiers
   152  		}
   153  		if rhs.TypeCb != nil {
   154  			gp.TypeCb = rhs.TypeCb
   155  		}
   156  		return gp, nil
   157  	}
   158  
   159  	return nil, vals.ErrConcatNotImplemented
   160  }
   161  
   162  func (gp globPattern) RConcat(v any) (any, error) {
   163  	switch lhs := v.(type) {
   164  	case string:
   165  		segs := stringToSegments(lhs)
   166  		// We know gp contains exactly one segment.
   167  		segs = append(segs, gp.Segments[0])
   168  		return globPattern{Pattern: glob.Pattern{Segments: segs}, Flags: gp.Flags,
   169  			Buts: gp.Buts, TypeCb: gp.TypeCb}, nil
   170  	}
   171  
   172  	return nil, vals.ErrConcatNotImplemented
   173  }
   174  
   175  func (gp *globPattern) lastWildSeg() (glob.Wild, error) {
   176  	if len(gp.Segments) == 0 {
   177  		return glob.Wild{}, ErrBadglobPattern
   178  	}
   179  	if !glob.IsWild(gp.Segments[len(gp.Segments)-1]) {
   180  		return glob.Wild{}, ErrMustFollowWildcard
   181  	}
   182  	return gp.Segments[len(gp.Segments)-1].(glob.Wild), nil
   183  }
   184  
   185  func (gp *globPattern) addMatcher(matcher func(rune) bool) error {
   186  	lastSeg, err := gp.lastWildSeg()
   187  	if err != nil {
   188  		return err
   189  	}
   190  	gp.Segments[len(gp.Segments)-1] = glob.Wild{
   191  		Type: lastSeg.Type, MatchHidden: lastSeg.MatchHidden,
   192  		Matchers: append(lastSeg.Matchers, matcher),
   193  	}
   194  	return nil
   195  }
   196  
   197  func (gp *globPattern) append(segs ...glob.Segment) {
   198  	gp.Segments = append(gp.Segments, segs...)
   199  }
   200  
   201  func wildcardToSegment(s string) (glob.Segment, error) {
   202  	switch s {
   203  	case "*":
   204  		return glob.Wild{Type: glob.Star, MatchHidden: false, Matchers: nil}, nil
   205  	case "**":
   206  		return glob.Wild{Type: glob.StarStar, MatchHidden: false, Matchers: nil}, nil
   207  	case "?":
   208  		return glob.Wild{Type: glob.Question, MatchHidden: false, Matchers: nil}, nil
   209  	default:
   210  		return nil, fmt.Errorf("bad wildcard: %q", s)
   211  	}
   212  }
   213  
   214  func stringToSegments(s string) []glob.Segment {
   215  	segs := []glob.Segment{}
   216  	for i := 0; i < len(s); {
   217  		j := i
   218  		for ; j < len(s) && s[j] != '/'; j++ {
   219  		}
   220  		if j > i {
   221  			segs = append(segs, glob.Literal{Data: s[i:j]})
   222  		}
   223  		if j < len(s) {
   224  			for ; j < len(s) && s[j] == '/'; j++ {
   225  			}
   226  			segs = append(segs, glob.Slash{})
   227  			i = j
   228  		} else {
   229  			break
   230  		}
   231  	}
   232  	return segs
   233  }
   234  
   235  func doGlob(ctx context.Context, gp globPattern) ([]any, error) {
   236  	but := make(map[string]struct{})
   237  	for _, s := range gp.Buts {
   238  		but[s] = struct{}{}
   239  	}
   240  
   241  	vs := make([]any, 0)
   242  	if !gp.Glob(func(pathInfo glob.PathInfo) bool {
   243  		select {
   244  		case <-ctx.Done():
   245  			logger.Println("glob aborted")
   246  			return false
   247  		default:
   248  		}
   249  
   250  		if _, ignore := but[pathInfo.Path]; ignore {
   251  			return true
   252  		}
   253  
   254  		if gp.TypeCb == nil || gp.TypeCb(pathInfo.Info.Mode()) {
   255  			vs = append(vs, pathInfo.Path)
   256  		}
   257  		return true
   258  	}) {
   259  		return nil, ErrInterrupted
   260  	}
   261  	if len(vs) == 0 && !gp.Flags.Has(noMatchOK) {
   262  		return nil, ErrWildcardNoMatch
   263  	}
   264  	return vs, nil
   265  }