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 }