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 }