github.com/neugram/ng@v0.0.0-20180309130942-d472ff93d872/syntax/shell/expansion.go (about)

     1  // Copyright 2015 The Neugram Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package shell
     6  
     7  import (
     8  	"fmt"
     9  	"os/user"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  	"unicode"
    14  	"unicode/utf8"
    15  )
    16  
    17  type Params interface {
    18  	Get(name string) string
    19  }
    20  
    21  type paramCollector map[string]bool
    22  
    23  func (p paramCollector) Get(name string) string {
    24  	p[name] = true
    25  	return ""
    26  }
    27  
    28  func Parameters(argv1 []string) ([]string, error) {
    29  	collector := make(paramCollector)
    30  	_, err := expansion(argv1, collector, []expander{braceExpand, paramExpand})
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  	var params []string
    35  	for param := range collector {
    36  		params = append(params, param)
    37  	}
    38  	return params, nil
    39  }
    40  
    41  func Expansion(argv1 []string, params Params) ([]string, error) {
    42  	return expansion(argv1, params, expanders)
    43  }
    44  
    45  func expansion(argv1 []string, params Params, expanders []expander) ([]string, error) {
    46  	var err error
    47  	var argv2 []string
    48  
    49  	for _, expander := range expanders {
    50  		for _, arg := range argv1 {
    51  			if len(arg) == 0 {
    52  				continue
    53  			} else if arg[0] == '\'' || arg[0] == '"' {
    54  				argv2 = append(argv2, arg)
    55  				continue
    56  			}
    57  			argv2, err = expander(argv2, arg, params)
    58  			if err != nil {
    59  				return nil, err
    60  			}
    61  		}
    62  		argv1 = argv2
    63  		argv2 = nil
    64  	}
    65  
    66  	for i, arg := range argv1 {
    67  		if len(arg) == 0 {
    68  			continue
    69  		}
    70  		s, e := arg[0], arg[len(arg)-1]
    71  		if s == '\'' && e == '\'' {
    72  			argv1[i] = arg[1 : len(arg)-1]
    73  		} else if s == '"' && e == '"' {
    74  			v, err := ExpandParams(arg, params)
    75  			if err != nil {
    76  				return nil, err
    77  			}
    78  			v = v[1 : len(v)-1]
    79  			v = quoteUnescaper.Replace(v)
    80  			argv1[i] = v
    81  		} else {
    82  			argv1[i] = unquoteUnescape.ReplaceAllString(arg, "$1")
    83  		}
    84  	}
    85  
    86  	return argv1, nil
    87  }
    88  
    89  var quoteUnescaper = strings.NewReplacer(`\"`, `"`, "\\`", "`")
    90  var unquoteUnescape = regexp.MustCompile(`\\(.)`)
    91  
    92  var expanders = []expander{
    93  	braceExpand,
    94  	tildeExpand,
    95  	paramExpand,
    96  	pathsExpand,
    97  }
    98  
    99  type expander func([]string, string, Params) ([]string, error)
   100  
   101  // brace expansion (for example: "c{d,e}" becomes "cd ce")
   102  func braceExpand(src []string, arg string, _ Params) (res []string, err error) {
   103  	res = src
   104  	var i1 int
   105  	for start := 0; ; {
   106  		i1 = indexUnquoted(arg[start:], '{')
   107  		if i1 == -1 {
   108  			return append(res, arg), nil
   109  		}
   110  		i1 += start
   111  		if i1 == 0 || arg[i1-1] != '$' {
   112  			break
   113  		}
   114  		start = i1 + 1
   115  	}
   116  	i2 := indexUnquoted(arg[i1:], '}')
   117  	if i2 == -1 {
   118  		return append(res, arg), nil
   119  	}
   120  	prefix, suffix := arg[:i1], arg[i1+i2+1:]
   121  	if indexUnquoted(arg, ',') == -1 {
   122  		// Not a {a,b} expansion.
   123  		// Check for {n0..n1} numeric expansion.
   124  		var start, end int
   125  		n, err := fmt.Sscanf(arg[i1:i1+i2+1], "{%d..%d}", &start, &end)
   126  		if err != nil || n != 2 {
   127  			return append(res, arg), nil
   128  		}
   129  		if start > end {
   130  			for i := start; i >= end; i-- {
   131  				res, _ = braceExpand(res, fmt.Sprintf("%s%d%s", prefix, i, suffix), nil)
   132  			}
   133  		} else {
   134  			for i := start; i <= end; i++ {
   135  				res, _ = braceExpand(res, fmt.Sprintf("%s%d%s", prefix, i, suffix), nil)
   136  			}
   137  		}
   138  		return res, nil
   139  	}
   140  	arg = arg[i1+1 : i1+i2]
   141  	for len(arg) > 0 {
   142  		c := indexUnquoted(arg, ',')
   143  		if c == -1 {
   144  			res, _ = braceExpand(res, prefix+arg+suffix, nil)
   145  			break
   146  		}
   147  		res, _ = braceExpand(res, prefix+arg[:c]+suffix, nil)
   148  		arg = arg[c+1:]
   149  	}
   150  	return res, nil
   151  }
   152  
   153  func ExpandTilde(arg string) (res string, err error) {
   154  	if !strings.HasPrefix(arg, "~") {
   155  		return arg, nil
   156  	}
   157  	name := arg[1:]
   158  	for i, r := range name {
   159  		if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
   160  			name = name[:i]
   161  			break
   162  		}
   163  	}
   164  	var u *user.User
   165  	if len(name) == 0 {
   166  		u, err = user.Current()
   167  	} else {
   168  		u, err = user.Lookup(name)
   169  	}
   170  	if err != nil {
   171  		if _, ok := err.(user.UnknownUserError); ok {
   172  			return arg, nil
   173  		}
   174  		return "", fmt.Errorf("expanding %s: %v", arg, err)
   175  	}
   176  	return u.HomeDir + arg[1+len(name):], nil
   177  }
   178  
   179  // tilde expansion (important: cd ~, cd ~/foo, less so: cd ~user1)
   180  func tildeExpand(src []string, arg string, params Params) (res []string, err error) {
   181  	expanded, err := ExpandTilde(arg)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  	return append(src, expanded), nil
   186  }
   187  
   188  // expandBraceParam expands the ${braced param} at the beginning of arg.
   189  func expandBraceParam(arg string, params Params) (string, error) {
   190  	var r rune
   191  	var i2 int
   192  	for i2, r = range arg[1:] {
   193  		if r == '}' {
   194  			i2--
   195  			break
   196  		}
   197  	}
   198  	if i2 == -1 {
   199  		return "", fmt.Errorf("invalid braced parameter expansion: %q", arg)
   200  	}
   201  	// TODO: ${parameter:-word}
   202  	// TODO: ${parameter/pattern/string}
   203  	// TODO: ${parameter[index]}
   204  	// TODO: ${parameter[offset:length]}
   205  	end := 1 + i2 + 1
   206  	name := arg[2:end]
   207  	val := params.Get(name)
   208  	return val + arg[end+1:], nil
   209  }
   210  
   211  // ExpandParams expands $ variables.
   212  func ExpandParams(arg string, params Params) (string, error) {
   213  	skip := 0
   214  	for {
   215  		i1 := indexParam(arg[skip:])
   216  		if i1 == -1 {
   217  			break
   218  		}
   219  		i1 += skip
   220  		i2 := -1
   221  		if len(arg) == i1+1 {
   222  			break
   223  		}
   224  		var name string
   225  		if arg[i1+1] == '{' {
   226  			res, err := expandBraceParam(arg[i1:], params)
   227  			if err != nil {
   228  				return "", err
   229  			}
   230  			arg = arg[:i1] + res
   231  			continue
   232  		} else if r, _ := utf8.DecodeRuneInString(arg[i1+1:]); !unicode.IsLetter(r) && !unicode.IsDigit(r) {
   233  			skip = i1 + 1
   234  			continue
   235  		}
   236  		var r rune
   237  		for i2, r = range arg[i1+1:] {
   238  			if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
   239  				i2--
   240  				break
   241  			}
   242  		}
   243  		if i2 == -1 {
   244  			return "", fmt.Errorf("invalid $ parameter: %q[%d:]", arg, i1)
   245  		}
   246  		end := i1 + 1 + i2 + 1
   247  		name = arg[i1+1 : end]
   248  		val := params.Get(name)
   249  		arg = arg[:i1] + val + arg[end:]
   250  	}
   251  	return arg, nil
   252  }
   253  
   254  // param expansion ($x, $PATH, ${x}, long tail of questionable sh features)
   255  func paramExpand(src []string, arg string, params Params) ([]string, error) {
   256  	expanded, err := ExpandParams(arg, params)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	return append(src, expanded), nil
   261  }
   262  
   263  // paths expansion (*, ?, [)
   264  func pathsExpand(src []string, arg string, params Params) (res []string, err error) {
   265  	res = src
   266  	isGlob := false
   267  	for i := 0; i < len(arg); i++ {
   268  		switch arg[i] {
   269  		case '\\':
   270  			i++
   271  		case '*', '?', '[':
   272  			isGlob = true
   273  		}
   274  	}
   275  	if !isGlob {
   276  		return append(res, arg), nil
   277  	}
   278  	// TODO to support interior quoting (like ab"*".c) this will need a rewrite.
   279  	matches, err := filepath.Glob(arg)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	return append(res, matches...), nil
   284  }
   285  
   286  // indexUnquoted returns the index of the first unquoted Unicode code
   287  // point r, or -1. A code point r is quoted if it is directly preceded
   288  // by a '\' or enclosed in "" or ''.
   289  func indexUnquoted(s string, r rune) int {
   290  	prevSlash := false
   291  	inBlock := rune(-1)
   292  	for i, v := range s {
   293  		if inBlock != -1 {
   294  			if v == inBlock {
   295  				inBlock = -1
   296  			}
   297  			continue
   298  		}
   299  
   300  		if !prevSlash {
   301  			switch v {
   302  			case r:
   303  				return i
   304  			case '\'', '"':
   305  				inBlock = v
   306  			}
   307  		}
   308  
   309  		prevSlash = v == '\\'
   310  	}
   311  
   312  	return -1
   313  }
   314  
   315  // indexParam returns the index of the first $ not quoted with '' or \, or -1.
   316  func indexParam(s string) int {
   317  	prevSlash := false
   318  	inQuote := false
   319  	for i, v := range s {
   320  		if inQuote {
   321  			if v == '\'' {
   322  				inQuote = false
   323  			}
   324  			continue
   325  		}
   326  
   327  		if !prevSlash {
   328  			switch v {
   329  			case '$':
   330  				return i
   331  			case '\'':
   332  				inQuote = true
   333  			}
   334  		}
   335  
   336  		prevSlash = v == '\\'
   337  	}
   338  
   339  	return -1
   340  }