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 }