github.com/cilki/sh@v2.6.4+incompatible/expand/expand.go (about)

     1  // Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
     2  // See LICENSE for licensing information
     3  
     4  package expand
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"os/user"
    12  	"path/filepath"
    13  	"regexp"
    14  	"runtime"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"mvdan.cc/sh/syntax"
    19  )
    20  
    21  // A Config specifies details about how shell expansion should be performed. The
    22  // zero value is a valid configuration.
    23  type Config struct {
    24  	// Env is used to get and set environment variables when performing
    25  	// shell expansions. Some special parameters are also expanded via this
    26  	// interface, such as:
    27  	//
    28  	//   * "#", "@", "*", "0"-"9" for the shell's parameters
    29  	//   * "?", "$", "PPID" for the shell's status and process
    30  	//   * "HOME foo" to retrieve user foo's home directory (if unset,
    31  	//     os/user.Lookup will be used)
    32  	//
    33  	// If nil, there are no environment variables set. Use
    34  	// ListEnviron(os.Environ()...) to use the system's environment
    35  	// variables.
    36  	Env Environ
    37  
    38  	// TODO(mvdan): consider replacing NoGlob==true with ReadDir==nil.
    39  
    40  	// NoGlob corresponds to the shell option that disables globbing.
    41  	NoGlob bool
    42  	// GlobStar corresponds to the shell option that allows globbing with
    43  	// "**".
    44  	GlobStar bool
    45  
    46  	// CmdSubst expands a command substitution node, writing its standard
    47  	// output to the provided io.Writer.
    48  	//
    49  	// If nil, encountering a command substitution will result in an
    50  	// UnexpectedCommandError.
    51  	CmdSubst func(io.Writer, *syntax.CmdSubst) error
    52  
    53  	// ReadDir is used for file path globbing. If nil, globbing is disabled.
    54  	// Use ioutil.ReadDir to use the filesystem directly.
    55  	ReadDir func(string) ([]os.FileInfo, error)
    56  
    57  	bufferAlloc bytes.Buffer
    58  	fieldAlloc  [4]fieldPart
    59  	fieldsAlloc [4][]fieldPart
    60  
    61  	ifs string
    62  	// A pointer to a parameter expansion node, if we're inside one.
    63  	// Necessary for ${LINENO}.
    64  	curParam *syntax.ParamExp
    65  }
    66  
    67  // UnexpectedCommandError is returned if a command substitution is encountered
    68  // when Config.CmdSubst is nil.
    69  type UnexpectedCommandError struct {
    70  	Node *syntax.CmdSubst
    71  }
    72  
    73  func (u UnexpectedCommandError) Error() string {
    74  	return fmt.Sprintf("unexpected command substitution at %s", u.Node.Pos())
    75  }
    76  
    77  var zeroConfig = &Config{}
    78  
    79  func prepareConfig(cfg *Config) *Config {
    80  	if cfg == nil {
    81  		cfg = zeroConfig
    82  	}
    83  	if cfg.Env == nil {
    84  		cfg.Env = FuncEnviron(func(string) string { return "" })
    85  	}
    86  
    87  	cfg.ifs = " \t\n"
    88  	if vr := cfg.Env.Get("IFS"); vr.IsSet() {
    89  		cfg.ifs = vr.String()
    90  	}
    91  	return cfg
    92  }
    93  
    94  func (cfg *Config) ifsRune(r rune) bool {
    95  	for _, r2 := range cfg.ifs {
    96  		if r == r2 {
    97  			return true
    98  		}
    99  	}
   100  	return false
   101  }
   102  
   103  func (cfg *Config) ifsJoin(strs []string) string {
   104  	sep := ""
   105  	if cfg.ifs != "" {
   106  		sep = cfg.ifs[:1]
   107  	}
   108  	return strings.Join(strs, sep)
   109  }
   110  
   111  func (cfg *Config) strBuilder() *bytes.Buffer {
   112  	b := &cfg.bufferAlloc
   113  	b.Reset()
   114  	return b
   115  }
   116  
   117  func (cfg *Config) envGet(name string) string {
   118  	return cfg.Env.Get(name).String()
   119  }
   120  
   121  func (cfg *Config) envSet(name, value string) {
   122  	wenv, ok := cfg.Env.(WriteEnviron)
   123  	if !ok {
   124  		// TODO: we should probably error here
   125  		return
   126  	}
   127  	wenv.Set(name, Variable{Value: value})
   128  }
   129  
   130  // Literal expands a single shell word. It is similar to Fields, but the result
   131  // is a single string. This is the behavior when a word is used as the value in
   132  // a shell variable assignment, for example.
   133  //
   134  // The config specifies shell expansion options; nil behaves the same as an
   135  // empty config.
   136  func Literal(cfg *Config, word *syntax.Word) (string, error) {
   137  	if word == nil {
   138  		return "", nil
   139  	}
   140  	cfg = prepareConfig(cfg)
   141  	field, err := cfg.wordField(word.Parts, quoteNone)
   142  	if err != nil {
   143  		return "", err
   144  	}
   145  	return cfg.fieldJoin(field), nil
   146  }
   147  
   148  // Document expands a single shell word as if it were within double quotes. It
   149  // is simlar to Literal, but without brace expansion, tilde expansion, and
   150  // globbing.
   151  //
   152  // The config specifies shell expansion options; nil behaves the same as an
   153  // empty config.
   154  func Document(cfg *Config, word *syntax.Word) (string, error) {
   155  	if word == nil {
   156  		return "", nil
   157  	}
   158  	cfg = prepareConfig(cfg)
   159  	field, err := cfg.wordField(word.Parts, quoteDouble)
   160  	if err != nil {
   161  		return "", err
   162  	}
   163  	return cfg.fieldJoin(field), nil
   164  }
   165  
   166  // Pattern expands a single shell word as a pattern, using syntax.QuotePattern
   167  // on any non-quoted parts of the input word. The result can be used on
   168  // syntax.TranslatePattern directly.
   169  //
   170  // The config specifies shell expansion options; nil behaves the same as an
   171  // empty config.
   172  func Pattern(cfg *Config, word *syntax.Word) (string, error) {
   173  	cfg = prepareConfig(cfg)
   174  	field, err := cfg.wordField(word.Parts, quoteNone)
   175  	if err != nil {
   176  		return "", err
   177  	}
   178  	buf := cfg.strBuilder()
   179  	for _, part := range field {
   180  		if part.quote > quoteNone {
   181  			buf.WriteString(syntax.QuotePattern(part.val))
   182  		} else {
   183  			buf.WriteString(part.val)
   184  		}
   185  	}
   186  	return buf.String(), nil
   187  }
   188  
   189  // Format expands a format string with a number of arguments, following the
   190  // shell's format specifications. These include printf(1), among others.
   191  //
   192  // The resulting string is returned, along with the number of arguments used.
   193  //
   194  // The config specifies shell expansion options; nil behaves the same as an
   195  // empty config.
   196  func Format(cfg *Config, format string, args []string) (string, int, error) {
   197  	cfg = prepareConfig(cfg)
   198  	buf := cfg.strBuilder()
   199  	esc := false
   200  	var fmts []rune
   201  	initialArgs := len(args)
   202  
   203  	for _, c := range format {
   204  		switch {
   205  		case esc:
   206  			esc = false
   207  			switch c {
   208  			case 'n':
   209  				buf.WriteRune('\n')
   210  			case 'r':
   211  				buf.WriteRune('\r')
   212  			case 't':
   213  				buf.WriteRune('\t')
   214  			case '\\':
   215  				buf.WriteRune('\\')
   216  			default:
   217  				buf.WriteRune('\\')
   218  				buf.WriteRune(c)
   219  			}
   220  
   221  		case len(fmts) > 0:
   222  			switch c {
   223  			case '%':
   224  				buf.WriteByte('%')
   225  				fmts = nil
   226  			case 'c':
   227  				var b byte
   228  				if len(args) > 0 {
   229  					arg := ""
   230  					arg, args = args[0], args[1:]
   231  					if len(arg) > 0 {
   232  						b = arg[0]
   233  					}
   234  				}
   235  				buf.WriteByte(b)
   236  				fmts = nil
   237  			case '+', '-', ' ':
   238  				if len(fmts) > 1 {
   239  					return "", 0, fmt.Errorf("invalid format char: %c", c)
   240  				}
   241  				fmts = append(fmts, c)
   242  			case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
   243  				fmts = append(fmts, c)
   244  			case 's', 'd', 'i', 'u', 'o', 'x':
   245  				arg := ""
   246  				if len(args) > 0 {
   247  					arg, args = args[0], args[1:]
   248  				}
   249  				var farg interface{} = arg
   250  				if c != 's' {
   251  					n, _ := strconv.ParseInt(arg, 0, 0)
   252  					if c == 'i' || c == 'd' {
   253  						farg = int(n)
   254  					} else {
   255  						farg = uint(n)
   256  					}
   257  					if c == 'i' || c == 'u' {
   258  						c = 'd'
   259  					}
   260  				}
   261  				fmts = append(fmts, c)
   262  				fmt.Fprintf(buf, string(fmts), farg)
   263  				fmts = nil
   264  			default:
   265  				return "", 0, fmt.Errorf("invalid format char: %c", c)
   266  			}
   267  		case c == '\\':
   268  			esc = true
   269  		case args != nil && c == '%':
   270  			// if args == nil, we are not doing format
   271  			// arguments
   272  			fmts = []rune{c}
   273  		default:
   274  			buf.WriteRune(c)
   275  		}
   276  	}
   277  	if len(fmts) > 0 {
   278  		return "", 0, fmt.Errorf("missing format char")
   279  	}
   280  	return buf.String(), initialArgs - len(args), nil
   281  }
   282  
   283  func (cfg *Config) fieldJoin(parts []fieldPart) string {
   284  	switch len(parts) {
   285  	case 0:
   286  		return ""
   287  	case 1: // short-cut without a string copy
   288  		return parts[0].val
   289  	}
   290  	buf := cfg.strBuilder()
   291  	for _, part := range parts {
   292  		buf.WriteString(part.val)
   293  	}
   294  	return buf.String()
   295  }
   296  
   297  func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) {
   298  	buf := cfg.strBuilder()
   299  	for _, part := range parts {
   300  		if part.quote > quoteNone {
   301  			buf.WriteString(syntax.QuotePattern(part.val))
   302  			continue
   303  		}
   304  		buf.WriteString(part.val)
   305  		if syntax.HasPattern(part.val) {
   306  			glob = true
   307  		}
   308  	}
   309  	if glob { // only copy the string if it will be used
   310  		escaped = buf.String()
   311  	}
   312  	return escaped, glob
   313  }
   314  
   315  // Fields expands a number of words as if they were arguments in a shell
   316  // command. This includes brace expansion, tilde expansion, parameter expansion,
   317  // command substitution, arithmetic expansion, and quote removal.
   318  func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
   319  	cfg = prepareConfig(cfg)
   320  	fields := make([]string, 0, len(words))
   321  	dir := cfg.envGet("PWD")
   322  	for _, expWord := range Braces(words...) {
   323  		wfields, err := cfg.wordFields(expWord.Parts)
   324  		if err != nil {
   325  			return nil, err
   326  		}
   327  		for _, field := range wfields {
   328  			path, doGlob := cfg.escapedGlobField(field)
   329  			var matches []string
   330  			abs := filepath.IsAbs(path)
   331  			if doGlob && !cfg.NoGlob {
   332  				base := ""
   333  				if !abs {
   334  					base = dir
   335  				}
   336  				matches, err = cfg.glob(base, path)
   337  				if err != nil {
   338  					return nil, err
   339  				}
   340  			}
   341  			if len(matches) == 0 {
   342  				fields = append(fields, cfg.fieldJoin(field))
   343  				continue
   344  			}
   345  			for _, match := range matches {
   346  				if !abs {
   347  					match = strings.TrimPrefix(match, dir)
   348  				}
   349  				fields = append(fields, match)
   350  			}
   351  		}
   352  	}
   353  	return fields, nil
   354  }
   355  
   356  type fieldPart struct {
   357  	val   string
   358  	quote quoteLevel
   359  }
   360  
   361  type quoteLevel uint
   362  
   363  const (
   364  	quoteNone quoteLevel = iota
   365  	quoteDouble
   366  	quoteSingle
   367  )
   368  
   369  func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) {
   370  	var field []fieldPart
   371  	for i, wp := range wps {
   372  		switch x := wp.(type) {
   373  		case *syntax.Lit:
   374  			s := x.Value
   375  			if i == 0 && ql == quoteNone {
   376  				if prefix, rest := cfg.expandUser(s); prefix != "" {
   377  					// TODO: return two separate fieldParts,
   378  					// like in wordFields?
   379  					s = prefix + rest
   380  				}
   381  			}
   382  			if ql == quoteDouble && strings.Contains(s, "\\") {
   383  				buf := cfg.strBuilder()
   384  				for i := 0; i < len(s); i++ {
   385  					b := s[i]
   386  					if b == '\\' && i+1 < len(s) {
   387  						switch s[i+1] {
   388  						case '\n': // remove \\\n
   389  							i++
   390  							continue
   391  						case '"', '\\', '$', '`': // special chars
   392  							continue
   393  						}
   394  					}
   395  					buf.WriteByte(b)
   396  				}
   397  				s = buf.String()
   398  			}
   399  			field = append(field, fieldPart{val: s})
   400  		case *syntax.SglQuoted:
   401  			fp := fieldPart{quote: quoteSingle, val: x.Value}
   402  			if x.Dollar {
   403  				fp.val, _, _ = Format(cfg, fp.val, nil)
   404  			}
   405  			field = append(field, fp)
   406  		case *syntax.DblQuoted:
   407  			wfield, err := cfg.wordField(x.Parts, quoteDouble)
   408  			if err != nil {
   409  				return nil, err
   410  			}
   411  			for _, part := range wfield {
   412  				part.quote = quoteDouble
   413  				field = append(field, part)
   414  			}
   415  		case *syntax.ParamExp:
   416  			val, err := cfg.paramExp(x)
   417  			if err != nil {
   418  				return nil, err
   419  			}
   420  			field = append(field, fieldPart{val: val})
   421  		case *syntax.CmdSubst:
   422  			val, err := cfg.cmdSubst(x)
   423  			if err != nil {
   424  				return nil, err
   425  			}
   426  			field = append(field, fieldPart{val: val})
   427  		case *syntax.ArithmExp:
   428  			n, err := Arithm(cfg, x.X)
   429  			if err != nil {
   430  				return nil, err
   431  			}
   432  			field = append(field, fieldPart{val: strconv.Itoa(n)})
   433  		default:
   434  			panic(fmt.Sprintf("unhandled word part: %T", x))
   435  		}
   436  	}
   437  	return field, nil
   438  }
   439  
   440  func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) {
   441  	if cfg.CmdSubst == nil {
   442  		return "", UnexpectedCommandError{Node: cs}
   443  	}
   444  	buf := cfg.strBuilder()
   445  	if err := cfg.CmdSubst(buf, cs); err != nil {
   446  		return "", err
   447  	}
   448  	return strings.TrimRight(buf.String(), "\n"), nil
   449  }
   450  
   451  func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
   452  	fields := cfg.fieldsAlloc[:0]
   453  	curField := cfg.fieldAlloc[:0]
   454  	allowEmpty := false
   455  	flush := func() {
   456  		if len(curField) == 0 {
   457  			return
   458  		}
   459  		fields = append(fields, curField)
   460  		curField = nil
   461  	}
   462  	splitAdd := func(val string) {
   463  		for i, field := range strings.FieldsFunc(val, cfg.ifsRune) {
   464  			if i > 0 {
   465  				flush()
   466  			}
   467  			curField = append(curField, fieldPart{val: field})
   468  		}
   469  	}
   470  	for i, wp := range wps {
   471  		switch x := wp.(type) {
   472  		case *syntax.Lit:
   473  			s := x.Value
   474  			if i == 0 {
   475  				prefix, rest := cfg.expandUser(s)
   476  				curField = append(curField, fieldPart{
   477  					quote: quoteSingle,
   478  					val:   prefix,
   479  				})
   480  				s = rest
   481  			}
   482  			if strings.Contains(s, "\\") {
   483  				buf := cfg.strBuilder()
   484  				for i := 0; i < len(s); i++ {
   485  					b := s[i]
   486  					if b == '\\' {
   487  						i++
   488  						b = s[i]
   489  					}
   490  					buf.WriteByte(b)
   491  				}
   492  				s = buf.String()
   493  			}
   494  			curField = append(curField, fieldPart{val: s})
   495  		case *syntax.SglQuoted:
   496  			allowEmpty = true
   497  			fp := fieldPart{quote: quoteSingle, val: x.Value}
   498  			if x.Dollar {
   499  				fp.val, _, _ = Format(cfg, fp.val, nil)
   500  			}
   501  			curField = append(curField, fp)
   502  		case *syntax.DblQuoted:
   503  			allowEmpty = true
   504  			if len(x.Parts) == 1 {
   505  				pe, _ := x.Parts[0].(*syntax.ParamExp)
   506  				if elems := cfg.quotedElems(pe); elems != nil {
   507  					for i, elem := range elems {
   508  						if i > 0 {
   509  							flush()
   510  						}
   511  						curField = append(curField, fieldPart{
   512  							quote: quoteDouble,
   513  							val:   elem,
   514  						})
   515  					}
   516  					continue
   517  				}
   518  			}
   519  			wfield, err := cfg.wordField(x.Parts, quoteDouble)
   520  			if err != nil {
   521  				return nil, err
   522  			}
   523  			for _, part := range wfield {
   524  				part.quote = quoteDouble
   525  				curField = append(curField, part)
   526  			}
   527  		case *syntax.ParamExp:
   528  			val, err := cfg.paramExp(x)
   529  			if err != nil {
   530  				return nil, err
   531  			}
   532  			splitAdd(val)
   533  		case *syntax.CmdSubst:
   534  			val, err := cfg.cmdSubst(x)
   535  			if err != nil {
   536  				return nil, err
   537  			}
   538  			splitAdd(val)
   539  		case *syntax.ArithmExp:
   540  			n, err := Arithm(cfg, x.X)
   541  			if err != nil {
   542  				return nil, err
   543  			}
   544  			curField = append(curField, fieldPart{val: strconv.Itoa(n)})
   545  		default:
   546  			panic(fmt.Sprintf("unhandled word part: %T", x))
   547  		}
   548  	}
   549  	flush()
   550  	if allowEmpty && len(fields) == 0 {
   551  		fields = append(fields, curField)
   552  	}
   553  	return fields, nil
   554  }
   555  
   556  // quotedElems checks if a parameter expansion is exactly ${@} or ${foo[@]}
   557  func (cfg *Config) quotedElems(pe *syntax.ParamExp) []string {
   558  	if pe == nil || pe.Excl || pe.Length || pe.Width {
   559  		return nil
   560  	}
   561  	if pe.Param.Value == "@" {
   562  		return cfg.Env.Get("@").Value.([]string)
   563  	}
   564  	if nodeLit(pe.Index) != "@" {
   565  		return nil
   566  	}
   567  	val := cfg.Env.Get(pe.Param.Value).Value
   568  	if x, ok := val.([]string); ok {
   569  		return x
   570  	}
   571  	return nil
   572  }
   573  
   574  func (cfg *Config) expandUser(field string) (prefix, rest string) {
   575  	if len(field) == 0 || field[0] != '~' {
   576  		return "", field
   577  	}
   578  	name := field[1:]
   579  	if i := strings.Index(name, "/"); i >= 0 {
   580  		rest = name[i:]
   581  		name = name[:i]
   582  	}
   583  	if name == "" {
   584  		return cfg.Env.Get("HOME").String(), rest
   585  	}
   586  	if vr := cfg.Env.Get("HOME " + name); vr.IsSet() {
   587  		return vr.String(), rest
   588  	}
   589  
   590  	u, err := user.Lookup(name)
   591  	if err != nil {
   592  		return "", field
   593  	}
   594  	return u.HomeDir, rest
   595  }
   596  
   597  func findAllIndex(pattern, name string, n int) [][]int {
   598  	expr, err := syntax.TranslatePattern(pattern, true)
   599  	if err != nil {
   600  		return nil
   601  	}
   602  	rx := regexp.MustCompile(expr)
   603  	return rx.FindAllStringIndex(name, n)
   604  }
   605  
   606  // TODO: use this again to optimize globbing; see
   607  // https://github.com/mvdan/sh/issues/213
   608  func hasGlob(path string) bool {
   609  	magicChars := `*?[`
   610  	if runtime.GOOS != "windows" {
   611  		magicChars = `*?[\`
   612  	}
   613  	return strings.ContainsAny(path, magicChars)
   614  }
   615  
   616  var rxGlobStar = regexp.MustCompile(".*")
   617  
   618  // pathJoin2 is a simpler version of filepath.Join without cleaning the result,
   619  // since that's needed for globbing.
   620  func pathJoin2(elem1, elem2 string) string {
   621  	if elem1 == "" {
   622  		return elem2
   623  	}
   624  	if strings.HasSuffix(elem1, string(filepath.Separator)) {
   625  		return elem1 + elem2
   626  	}
   627  	return elem1 + string(filepath.Separator) + elem2
   628  }
   629  
   630  // pathSplit splits a file path into its elements, retaining empty ones. Before
   631  // splitting, slashes are replaced with filepath.Separator, so that splitting
   632  // Unix paths on Windows works as well.
   633  func pathSplit(path string) []string {
   634  	path = filepath.FromSlash(path)
   635  	return strings.Split(path, string(filepath.Separator))
   636  }
   637  
   638  func (cfg *Config) glob(base, pattern string) ([]string, error) {
   639  	parts := pathSplit(pattern)
   640  	matches := []string{""}
   641  	if filepath.IsAbs(pattern) {
   642  		if parts[0] == "" {
   643  			// unix-like
   644  			matches[0] = string(filepath.Separator)
   645  		} else {
   646  			// windows (for some reason it won't work without the
   647  			// trailing separator)
   648  			matches[0] = parts[0] + string(filepath.Separator)
   649  		}
   650  		parts = parts[1:]
   651  	}
   652  	for _, part := range parts {
   653  		switch {
   654  		case part == "", part == ".", part == "..":
   655  			var newMatches []string
   656  			for _, dir := range matches {
   657  				// TODO(mvdan): reuse the previous ReadDir call
   658  				if cfg.ReadDir == nil {
   659  					continue // no globbing
   660  				} else if _, err := cfg.ReadDir(filepath.Join(base, dir)); err != nil {
   661  					continue // not actually a dir
   662  				}
   663  				newMatches = append(newMatches, pathJoin2(dir, part))
   664  			}
   665  			matches = newMatches
   666  			continue
   667  		case part == "**" && cfg.GlobStar:
   668  			for i, match := range matches {
   669  				// "a/**" should match "a/ a/b a/b/cfg ..."; note
   670  				// how the zero-match case has a trailing
   671  				// separator.
   672  				matches[i] = pathJoin2(match, "")
   673  			}
   674  			// expand all the possible levels of **
   675  			latest := matches
   676  			for {
   677  				var newMatches []string
   678  				for _, dir := range latest {
   679  					var err error
   680  					newMatches, err = cfg.globDir(base, dir, rxGlobStar, newMatches)
   681  					if err != nil {
   682  						return nil, err
   683  					}
   684  				}
   685  				if len(newMatches) == 0 {
   686  					// not another level of directories to
   687  					// try; stop
   688  					break
   689  				}
   690  				matches = append(matches, newMatches...)
   691  				latest = newMatches
   692  			}
   693  			continue
   694  		}
   695  		expr, err := syntax.TranslatePattern(part, true)
   696  		if err != nil {
   697  			// If any glob part is not a valid pattern, don't glob.
   698  			return nil, nil
   699  		}
   700  		rx := regexp.MustCompile("^" + expr + "$")
   701  		var newMatches []string
   702  		for _, dir := range matches {
   703  			newMatches, err = cfg.globDir(base, dir, rx, newMatches)
   704  			if err != nil {
   705  				return nil, err
   706  			}
   707  		}
   708  		matches = newMatches
   709  	}
   710  	return matches, nil
   711  }
   712  
   713  func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matches []string) ([]string, error) {
   714  	if cfg.ReadDir == nil {
   715  		// TODO(mvdan): check this at the beginning of a glob?
   716  		return nil, nil
   717  	}
   718  	infos, err := cfg.ReadDir(filepath.Join(base, dir))
   719  	if err != nil {
   720  		// Ignore the error, as this might be a file instead of a
   721  		// directory. v3 refactored globbing to only use one ReadDir
   722  		// call per directory instead of two, so it knows to skip this
   723  		// kind of path at the ReadDir call of its parent.
   724  		// Instead of backporting that complex rewrite into v2, just
   725  		// work around the edge case here. We might ignore other kinds
   726  		// of errors, but at least we don't fail on a correct glob.
   727  		return matches, nil
   728  	}
   729  	for _, info := range infos {
   730  		name := info.Name()
   731  		if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' {
   732  			continue
   733  		}
   734  		if rx.MatchString(name) {
   735  			matches = append(matches, pathJoin2(dir, name))
   736  		}
   737  	}
   738  	return matches, nil
   739  }
   740  
   741  //
   742  // The config specifies shell expansion options; nil behaves the same as an
   743  // empty config.
   744  func ReadFields(cfg *Config, s string, n int, raw bool) []string {
   745  	cfg = prepareConfig(cfg)
   746  	type pos struct {
   747  		start, end int
   748  	}
   749  	var fpos []pos
   750  
   751  	runes := make([]rune, 0, len(s))
   752  	infield := false
   753  	esc := false
   754  	for _, r := range s {
   755  		if infield {
   756  			if cfg.ifsRune(r) && (raw || !esc) {
   757  				fpos[len(fpos)-1].end = len(runes)
   758  				infield = false
   759  			}
   760  		} else {
   761  			if !cfg.ifsRune(r) && (raw || !esc) {
   762  				fpos = append(fpos, pos{start: len(runes), end: -1})
   763  				infield = true
   764  			}
   765  		}
   766  		if r == '\\' {
   767  			if raw || esc {
   768  				runes = append(runes, r)
   769  			}
   770  			esc = !esc
   771  			continue
   772  		}
   773  		runes = append(runes, r)
   774  		esc = false
   775  	}
   776  	if len(fpos) == 0 {
   777  		return nil
   778  	}
   779  	if infield {
   780  		fpos[len(fpos)-1].end = len(runes)
   781  	}
   782  
   783  	switch {
   784  	case n == 1:
   785  		// include heading/trailing IFSs
   786  		fpos[0].start, fpos[0].end = 0, len(runes)
   787  		fpos = fpos[:1]
   788  	case n != -1 && n < len(fpos):
   789  		// combine to max n fields
   790  		fpos[n-1].end = fpos[len(fpos)-1].end
   791  		fpos = fpos[:n]
   792  	}
   793  
   794  	var fields = make([]string, len(fpos))
   795  	for i, p := range fpos {
   796  		fields[i] = string(runes[p.start:p.end])
   797  	}
   798  	return fields
   799  }