github.com/hattya/go.sh@v0.0.0-20240328132134-f53276d95cc6/interp/expand.go (about)

     1  //
     2  // go.sh/interp :: expand.go
     3  //
     4  //   Copyright (c) 2021-2022 Akinori Hattori <hattya@gmail.com>
     5  //
     6  //   SPDX-License-Identifier: MIT
     7  //
     8  
     9  package interp
    10  
    11  import (
    12  	"fmt"
    13  	"os"
    14  	"os/user"
    15  	"path/filepath"
    16  	"runtime"
    17  	"strconv"
    18  	"strings"
    19  	"unicode"
    20  	"unicode/utf8"
    21  
    22  	"github.com/hattya/go.sh/ast"
    23  	"github.com/hattya/go.sh/pattern"
    24  )
    25  
    26  // ExpMode controls the behavior of word expansions.
    27  type ExpMode uint
    28  
    29  const (
    30  	// Expand a word into a single field, field splitting and pathname
    31  	// expansion will no be performed. The result will be converted the
    32  	// simplest form of parameter expansions into ANSI C style
    33  	// identifiers except for special parameters and positional
    34  	// parameters.
    35  	Arith ExpMode = 1 << iota
    36  
    37  	// Expands multiple tilde-prefixes in a word as if it is in an
    38  	// assignment.
    39  	Assign
    40  
    41  	// Expand a word into a single field, field splitting and pathname
    42  	// expansion will not be performed.
    43  	Literal
    44  
    45  	// Expand a word into a single field, field splitting and pathname
    46  	// expansion will not be performed. The result will be retained if
    47  	// '?', '*', and '[' are quoted.
    48  	Pattern
    49  
    50  	// Expands a word as if it is within double-quotes, field splitting
    51  	// and pathname expansion will not be performed.
    52  	Quote
    53  )
    54  
    55  // Expand expands a word into multiple fields.
    56  func (env *ExecEnv) Expand(word ast.Word, mode ExpMode) ([]string, error) {
    57  	fields, err := env.expand(word, mode)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	var rv []string
    62  	switch {
    63  	case mode&Literal != 0:
    64  		rv = []string{env.join(fields...).unquote()}
    65  	case mode&Pattern != 0:
    66  		rv = []string{env.join(fields...).pattern()}
    67  	default:
    68  		for _, f := range fields {
    69  			switch {
    70  			case mode&(Arith|Quote) != 0:
    71  				rv = append(rv, f.unquote())
    72  			case !f.empty():
    73  				for _, f := range env.split(f) {
    74  					if !f.empty() {
    75  						if env.Opts&NoGlob != 0 {
    76  							rv = append(rv, f.unquote())
    77  						} else {
    78  							rv = append(rv, env.expandPath(f)...)
    79  						}
    80  					}
    81  				}
    82  			}
    83  		}
    84  	}
    85  	return rv, nil
    86  }
    87  
    88  func (env *ExecEnv) expand(word ast.Word, mode ExpMode) (fields []*field, err error) {
    89  	fields = []*field{{}}
    90  	if mode&Quote != 0 {
    91  		fields[0].join("", true)
    92  	}
    93  	for i := 0; i < len(word); i++ {
    94  		switch w := word[i].(type) {
    95  		case *ast.Lit:
    96  			f := fields[len(fields)-1]
    97  			s := w.Value
    98  			if i == 0 {
    99  				off, col := env.expandTilde(f, s, word[i+1:], mode)
   100  				if off != 0 {
   101  					i += off
   102  					s = word[i].(*ast.Lit).Value
   103  				}
   104  				s = s[col:]
   105  			}
   106  			for mode&Assign != 0 {
   107  				// separator
   108  				j := strings.IndexByte(s, os.PathListSeparator)
   109  				if j == -1 {
   110  					break
   111  				}
   112  				f.join(s[:j+1], mode&Quote != 0)
   113  				s = s[j+1:]
   114  				// expansion
   115  				if s == "" && i+1 < len(word) {
   116  					if w, ok := word[i+1].(*ast.Lit); ok {
   117  						i++
   118  						s = w.Value
   119  					}
   120  				}
   121  				off, col := env.expandTilde(f, s, word[i+1:], mode)
   122  				if off != 0 {
   123  					i += off
   124  					s = word[i].(*ast.Lit).Value
   125  				}
   126  				s = s[col:]
   127  			}
   128  			// remaining
   129  			f.join(s, mode&Quote != 0)
   130  		case *ast.Quote:
   131  			switch w.Tok {
   132  			case `\`, `'`:
   133  				var s string
   134  				if len(w.Value) != 0 {
   135  					s = w.Value[0].(*ast.Lit).Value
   136  				}
   137  				fields[len(fields)-1].join(s, true)
   138  			case `"`:
   139  				word, err := env.expand(w.Value, mode&Arith|Quote)
   140  				if err != nil {
   141  					return nil, err
   142  				}
   143  				fields[len(fields)-1].merge(word[0])
   144  				fields = append(fields, word[1:]...)
   145  			}
   146  		case *ast.ParamExp:
   147  			if fields, err = env.expandParam(fields, w, mode); err != nil {
   148  				return
   149  			}
   150  		case *ast.ArithExp:
   151  			word, err := env.expand(w.Expr, Arith)
   152  			if err != nil {
   153  				return nil, err
   154  			}
   155  			expr := env.join(word...).unquote()
   156  			n, err := env.Eval(expr)
   157  			if err != nil {
   158  				err := err.(ArithExprError)
   159  				if expr != "" {
   160  					err.Expr = expr
   161  				} else {
   162  					err.Msg = "arithmetic expression is missing"
   163  				}
   164  				return nil, err
   165  			}
   166  			fields[len(fields)-1].join(strconv.Itoa(n), true)
   167  		}
   168  	}
   169  	return
   170  }
   171  
   172  // expandTilde performs tilde expansion.
   173  func (env *ExecEnv) expandTilde(f *field, s string, word ast.Word, mode ExpMode) (off, col int) {
   174  	if mode&(Arith|Quote) != 0 || !strings.HasPrefix(s, "~") {
   175  		return
   176  	}
   177  	s = s[1:]
   178  	col = 1
   179  	// login name
   180  	var name, sep string
   181  	if mode&Assign != 0 {
   182  		sep = string(os.PathListSeparator) + "/"
   183  	} else {
   184  		sep = "/"
   185  	}
   186  	for {
   187  		if i := strings.IndexAny(s, sep); i != -1 {
   188  			name += s[:i]
   189  			col += i
   190  			break
   191  		}
   192  		name += s
   193  		col += len(s)
   194  		if off >= len(word) {
   195  			break
   196  		} else if w, ok := word[off].(*ast.Lit); ok {
   197  			s = w.Value
   198  			off += 1
   199  			col = 0
   200  		} else {
   201  			if runtime.GOOS == "windows" {
   202  				if w, ok := word[off].(*ast.Quote); ok && w.Tok == `\` && w.Value[0].(*ast.Lit).Value == `\` {
   203  					break
   204  				}
   205  			}
   206  			goto Fail
   207  		}
   208  	}
   209  	// home directory
   210  	if dir := env.homeDir(name); dir != "" {
   211  		f.join(dir, true)
   212  	} else {
   213  		goto Fail
   214  	}
   215  	return
   216  Fail:
   217  	f.join("~"+name, false)
   218  	return
   219  }
   220  
   221  func (env *ExecEnv) homeDir(name string) string {
   222  	var dir string
   223  	if name == "" {
   224  		if v, set := env.Get("HOME"); set {
   225  			dir = v.Value
   226  		} else if runtime.GOOS == "windows" {
   227  			if v, set := env.Get("USERPROFILE"); set {
   228  				dir = v.Value
   229  			}
   230  		}
   231  	} else if u, err := user.Lookup(name); err == nil {
   232  		dir = u.HomeDir
   233  	}
   234  	return filepath.ToSlash(dir)
   235  }
   236  
   237  // expandParam performs parameter expansion.
   238  func (env *ExecEnv) expandParam(fields []*field, pe *ast.ParamExp, mode ExpMode) ([]*field, error) {
   239  	quote := mode&Quote != 0
   240  	var a []string
   241  	var set, null bool
   242  	switch pe.Name.Value {
   243  	case "@":
   244  		set = true
   245  		switch len(env.Args) {
   246  		case 1:
   247  			null = true
   248  		case 2:
   249  			null = env.Args[1] == ""
   250  			fallthrough
   251  		default:
   252  			a = make([]string, len(env.Args)-1)
   253  			copy(a, env.Args[1:])
   254  		}
   255  	case "*":
   256  		set = true
   257  		switch len(env.Args) {
   258  		case 1:
   259  			null = true
   260  		case 2:
   261  			a = []string{env.Args[1]}
   262  			null = env.Args[1] == ""
   263  		default:
   264  			var b strings.Builder
   265  			sep := env.ifs()
   266  			for i, s := range env.Args[1:] {
   267  				if i > 0 {
   268  					b.WriteString(sep)
   269  				}
   270  				b.WriteString(s)
   271  			}
   272  			a = []string{b.String()}
   273  		}
   274  	default:
   275  		var v Var
   276  		if v, set = env.Get(pe.Name.Value); set {
   277  			a = []string{v.Value}
   278  			null = v.Value == ""
   279  		}
   280  	}
   281  	switch {
   282  	case pe.Op == "":
   283  		// simplest form
   284  		switch {
   285  		case mode&Arith != 0 && !(env.isSpParam(pe.Name.Value) || env.isPosParam(pe.Name.Value)):
   286  			fields[len(fields)-1].join(pe.Name.Value, quote)
   287  		case set && !null:
   288  			goto Param
   289  		}
   290  	case pe.Word == nil:
   291  		// string length
   292  		if pe.Op == "#" {
   293  			switch {
   294  			case set:
   295  				var n int
   296  				if pe.Name.Value == "@" {
   297  					n = len(a)
   298  				} else {
   299  					n = utf8.RuneCountInString(a[0])
   300  				}
   301  				fields[len(fields)-1].join(strconv.Itoa(n), quote)
   302  			case !set && env.Opts&NoUnset != 0:
   303  				goto Unset
   304  			}
   305  		}
   306  	default:
   307  		switch pe.Op {
   308  		case ":-", "-":
   309  			// use default values
   310  			switch {
   311  			case set && !null:
   312  				goto Param
   313  			case !set || pe.Op == ":-":
   314  				word, err := env.expand(pe.Word, mode&(Assign|Quote)|Literal)
   315  				if err != nil {
   316  					return nil, err
   317  				}
   318  				fields[len(fields)-1].merge(word[0])
   319  				fields = append(fields, word[1:]...)
   320  			}
   321  		case ":=", "=":
   322  			// assign default values
   323  			switch {
   324  			case set && !null:
   325  				goto Param
   326  			case !set || pe.Op == ":=":
   327  				if env.isSpParam(pe.Name.Value) || env.isPosParam(pe.Name.Value) {
   328  					return nil, ParamExpError{
   329  						ParamExp: pe,
   330  						Msg:      "cannot assign in this way",
   331  					}
   332  				}
   333  				word, err := env.expand(pe.Word, mode&Quote|Literal)
   334  				if err != nil {
   335  					return nil, err
   336  				}
   337  				env.Set(pe.Name.Value, env.join(word...).unquote())
   338  				fields[len(fields)-1].merge(word[0])
   339  				fields = append(fields, word[1:]...)
   340  			}
   341  		case ":?", "?":
   342  			// indicate error if unset or null
   343  			switch {
   344  			case set && !null:
   345  				goto Param
   346  			case !set || pe.Op == ":?":
   347  				var msg string
   348  				if len(pe.Word) == 0 {
   349  					msg = "parameter is unset or null"
   350  				} else {
   351  					word, err := env.expand(pe.Word, mode&Quote|Literal)
   352  					if err != nil {
   353  						return nil, err
   354  					}
   355  					msg = env.join(word...).unquote()
   356  				}
   357  				return nil, ParamExpError{
   358  					ParamExp: pe,
   359  					Msg:      msg,
   360  				}
   361  			}
   362  		case ":+", "+":
   363  			// use alternative values
   364  			if set && (!null || pe.Op == "+") {
   365  				word, err := env.expand(pe.Word, mode&(Assign|Quote)|Literal)
   366  				if err != nil {
   367  					return nil, err
   368  				}
   369  				fields[len(fields)-1].merge(word[0])
   370  				fields = append(fields, word[1:]...)
   371  			}
   372  		case "%", "%%":
   373  			// remove suffix pattern
   374  			switch {
   375  			case set && !null:
   376  				{
   377  					word, err := env.expand(pe.Word, Pattern)
   378  					if err != nil {
   379  						return nil, err
   380  					}
   381  					pats := []string{env.join(word...).pattern()}
   382  					mode := pattern.Suffix
   383  					if pe.Op == "%" {
   384  						mode |= pattern.Smallest
   385  					} else {
   386  						mode |= pattern.Largest
   387  					}
   388  					for i, s := range a {
   389  						m, err := pattern.Match(pats, mode, s)
   390  						if err != nil && err != pattern.NoMatch {
   391  							return nil, err
   392  						}
   393  						if i > 0 {
   394  							fields = append(fields, new(field))
   395  						}
   396  						fields[len(fields)-1].join(s[:len(s)-len(m)], quote)
   397  					}
   398  				}
   399  			case !set && env.Opts&NoUnset != 0:
   400  				goto Unset
   401  			}
   402  		case "#", "##":
   403  			// remove prefix pattern
   404  			switch {
   405  			case set && !null:
   406  				{
   407  					word, err := env.expand(pe.Word, Pattern)
   408  					if err != nil {
   409  						return nil, err
   410  					}
   411  					pats := []string{env.join(word...).pattern()}
   412  					mode := pattern.Prefix
   413  					if pe.Op == "#" {
   414  						mode |= pattern.Smallest
   415  					} else {
   416  						mode |= pattern.Largest
   417  					}
   418  					for i, s := range a {
   419  						m, err := pattern.Match(pats, mode, s)
   420  						if err != nil && err != pattern.NoMatch {
   421  							return nil, err
   422  						}
   423  						if i > 0 {
   424  							fields = append(fields, new(field))
   425  						}
   426  						fields[len(fields)-1].join(s[len(m):], quote)
   427  					}
   428  				}
   429  			case !set && env.Opts&NoUnset != 0:
   430  				goto Unset
   431  			}
   432  		}
   433  	}
   434  	return fields, nil
   435  Param:
   436  	for i, s := range a {
   437  		if i > 0 {
   438  			fields = append(fields, new(field))
   439  		}
   440  		fields[len(fields)-1].join(s, quote)
   441  	}
   442  	return fields, nil
   443  Unset:
   444  	return nil, ParamExpError{
   445  		ParamExp: pe,
   446  		Msg:      "parameter is unset",
   447  	}
   448  }
   449  
   450  // spilt performs field splitting.
   451  func (env *ExecEnv) split(f *field) []*field {
   452  	var ifs string
   453  	if v, set := env.Get("IFS"); set {
   454  		ifs = v.Value
   455  	} else {
   456  		ifs = IFS
   457  	}
   458  
   459  	if ifs == "" {
   460  		return []*field{f}
   461  	}
   462  	fields := []*field{{}}
   463  	ws := true
   464  	for i := 0; i < len(f.b); i++ {
   465  		s := f.b[i]
   466  		if f.quote[i] {
   467  			fields[len(fields)-1].join(s, true)
   468  			ws = false
   469  		} else {
   470  			var i int
   471  			for j, r := range s {
   472  				if strings.ContainsRune(ifs, r) {
   473  					switch {
   474  					case unicode.IsSpace(r):
   475  						// IFS white space
   476  						if ws {
   477  							break
   478  						}
   479  						ws = true
   480  						fallthrough
   481  					case !ws:
   482  						fields[len(fields)-1].join(s[i:j], false)
   483  						fields = append(fields, new(field))
   484  					default:
   485  						ws = false
   486  					}
   487  					i = j + utf8.RuneLen(r)
   488  				} else {
   489  					ws = false
   490  				}
   491  			}
   492  			if i < len(s) {
   493  				fields[len(fields)-1].join(s[i:], false)
   494  			}
   495  		}
   496  	}
   497  	if len(fields[len(fields)-1].b) == 0 && ws {
   498  		fields = fields[:len(fields)-1]
   499  	}
   500  	return fields
   501  }
   502  
   503  // join joins the specified fields into a single field.
   504  func (env *ExecEnv) join(fields ...*field) *field {
   505  	dst := new(field)
   506  	sep := env.ifs()
   507  	for i, f := range fields {
   508  		if i > 0 {
   509  			dst.join(sep, false)
   510  		}
   511  		dst.merge(f)
   512  	}
   513  	return dst
   514  }
   515  
   516  // ifs returns a separator determined by the IFS variable.
   517  func (env *ExecEnv) ifs() string {
   518  	if v, set := env.Get("IFS"); set {
   519  		if v.Value != "" {
   520  			return v.Value[:1]
   521  		}
   522  		return ""
   523  	}
   524  	return " "
   525  }
   526  
   527  // expandPath performs pathname expansion.
   528  func (env *ExecEnv) expandPath(f *field) []string {
   529  	paths, err := pattern.Glob(f.pattern())
   530  	if err != nil || len(paths) == 0 {
   531  		return []string{f.unquote()}
   532  	}
   533  	return paths
   534  }
   535  
   536  // ParamExpError represents an error in parameter expansion.
   537  type ParamExpError struct {
   538  	ParamExp *ast.ParamExp
   539  	Msg      string
   540  }
   541  
   542  func (e ParamExpError) Error() string {
   543  	return fmt.Sprintf("$%s: %s", e.ParamExp.Name.Value, e.Msg)
   544  }
   545  
   546  type field struct {
   547  	b     []string
   548  	quote []bool
   549  }
   550  
   551  func (f *field) empty() bool {
   552  	for i := 0; i < len(f.b); i++ {
   553  		if f.quote[i] || f.b[i] != "" {
   554  			return false
   555  		}
   556  	}
   557  	return true
   558  }
   559  
   560  func (f *field) join(s string, quote bool) {
   561  	f.b = append(f.b, s)
   562  	f.quote = append(f.quote, quote)
   563  }
   564  
   565  func (f *field) merge(t *field) {
   566  	f.b = append(f.b, t.b...)
   567  	f.quote = append(f.quote, t.quote...)
   568  }
   569  
   570  func (f *field) pattern() string {
   571  	var b strings.Builder
   572  	for i := 0; i < len(f.b); i++ {
   573  		s := f.b[i]
   574  		if f.quote[i] {
   575  			for {
   576  				i := strings.IndexAny(s, `?*[\`)
   577  				if i == -1 {
   578  					b.WriteString(s)
   579  					break
   580  				}
   581  				b.WriteString(s[:i])
   582  				b.WriteByte('\\')
   583  				b.WriteByte(s[i])
   584  				s = s[i+1:]
   585  			}
   586  		} else {
   587  			b.WriteString(s)
   588  		}
   589  	}
   590  	return b.String()
   591  }
   592  
   593  func (f *field) unquote() string {
   594  	return strings.Join(f.b, "")
   595  }