github.com/theclapp/sh@v2.6.4+incompatible/interp/builtin.go (about) 1 // Copyright (c) 2017, Daniel Martà <mvdan@mvdan.cc> 2 // See LICENSE for licensing information 3 4 package interp 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strconv" 14 "strings" 15 16 "mvdan.cc/sh/expand" 17 "mvdan.cc/sh/syntax" 18 ) 19 20 func isBuiltin(name string) bool { 21 switch name { 22 case "true", ":", "false", "exit", "set", "shift", "unset", 23 "echo", "printf", "break", "continue", "pwd", "cd", 24 "wait", "builtin", "trap", "type", "source", ".", "command", 25 "dirs", "pushd", "popd", "umask", "alias", "unalias", 26 "fg", "bg", "getopts", "eval", "test", "[", "exec", 27 "return", "read", "shopt": 28 return true 29 } 30 return false 31 } 32 33 func oneIf(b bool) int { 34 if b { 35 return 1 36 } 37 return 0 38 } 39 40 // atoi is just a shorthand for strconv.Atoi that ignores the error, 41 // just like shells do. 42 func atoi(s string) int { 43 n, _ := strconv.Atoi(s) 44 return n 45 } 46 47 func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, args []string) int { 48 switch name { 49 case "true", ":": 50 case "false": 51 return 1 52 case "exit": 53 switch len(args) { 54 case 0: 55 case 1: 56 if n, err := strconv.Atoi(args[0]); err != nil { 57 r.errf("invalid exit status code: %q\n", args[0]) 58 r.exit = 2 59 } else { 60 r.exit = n 61 } 62 default: 63 r.errf("exit cannot take multiple arguments\n") 64 r.exit = 1 65 } 66 r.setErr(ShellExitStatus(r.exit)) 67 return 0 // the command's exit status does not matter 68 case "set": 69 if err := Params(args...)(r); err != nil { 70 r.errf("set: %v\n", err) 71 return 2 72 } 73 r.updateExpandOpts() 74 case "shift": 75 n := 1 76 switch len(args) { 77 case 0: 78 case 1: 79 if n2, err := strconv.Atoi(args[0]); err == nil { 80 n = n2 81 break 82 } 83 fallthrough 84 default: 85 r.errf("usage: shift [n]\n") 86 return 2 87 } 88 if n >= len(r.Params) { 89 r.Params = nil 90 } else { 91 r.Params = r.Params[n:] 92 } 93 case "unset": 94 vars := true 95 funcs := true 96 unsetOpts: 97 for i, arg := range args { 98 switch arg { 99 case "-v": 100 funcs = false 101 case "-f": 102 vars = false 103 default: 104 args = args[i:] 105 break unsetOpts 106 } 107 } 108 109 for _, arg := range args { 110 if vr := r.lookupVar(arg); vr.IsSet() && vars { 111 r.delVar(arg) 112 continue 113 } 114 if _, ok := r.Funcs[arg]; ok && funcs { 115 delete(r.Funcs, arg) 116 } 117 } 118 case "echo": 119 newline, doExpand := true, false 120 echoOpts: 121 for len(args) > 0 { 122 switch args[0] { 123 case "-n": 124 newline = false 125 case "-e": 126 doExpand = true 127 case "-E": // default 128 default: 129 break echoOpts 130 } 131 args = args[1:] 132 } 133 for i, arg := range args { 134 if i > 0 { 135 r.out(" ") 136 } 137 if doExpand { 138 arg, _, _ = expand.Format(r.ecfg, arg, nil) 139 } 140 r.out(arg) 141 } 142 if newline { 143 r.out("\n") 144 } 145 case "printf": 146 if len(args) == 0 { 147 r.errf("usage: printf format [arguments]\n") 148 return 2 149 } 150 format, args := args[0], args[1:] 151 for { 152 s, n, err := expand.Format(r.ecfg, format, args) 153 if err != nil { 154 r.errf("%v\n", err) 155 return 1 156 } 157 r.out(s) 158 args = args[n:] 159 if n == 0 || len(args) == 0 { 160 break 161 } 162 } 163 case "break", "continue": 164 if !r.inLoop { 165 r.errf("%s is only useful in a loop", name) 166 break 167 } 168 enclosing := &r.breakEnclosing 169 if name == "continue" { 170 enclosing = &r.contnEnclosing 171 } 172 switch len(args) { 173 case 0: 174 *enclosing = 1 175 case 1: 176 if n, err := strconv.Atoi(args[0]); err == nil { 177 *enclosing = n 178 break 179 } 180 fallthrough 181 default: 182 r.errf("usage: %s [n]\n", name) 183 return 2 184 } 185 case "pwd": 186 r.outf("%s\n", r.envGet("PWD")) 187 case "cd": 188 var path string 189 switch len(args) { 190 case 0: 191 path = r.envGet("HOME") 192 case 1: 193 path = args[0] 194 default: 195 r.errf("usage: cd [dir]\n") 196 return 2 197 } 198 return r.changeDir(path) 199 case "wait": 200 if len(args) > 0 { 201 panic("wait with args not handled yet") 202 } 203 switch err := r.bgShells.Wait().(type) { 204 case nil: 205 case ExitStatus: 206 case ShellExitStatus: 207 default: 208 r.setErr(err) 209 } 210 case "builtin": 211 if len(args) < 1 { 212 break 213 } 214 if !isBuiltin(args[0]) { 215 return 1 216 } 217 return r.builtinCode(ctx, pos, args[0], args[1:]) 218 case "type": 219 anyNotFound := false 220 for _, arg := range args { 221 if _, ok := r.Funcs[arg]; ok { 222 r.outf("%s is a function\n", arg) 223 continue 224 } 225 if isBuiltin(arg) { 226 r.outf("%s is a shell builtin\n", arg) 227 continue 228 } 229 if path, err := exec.LookPath(arg); err == nil { 230 r.outf("%s is %s\n", arg, path) 231 continue 232 } 233 r.errf("type: %s: not found\n", arg) 234 anyNotFound = true 235 } 236 if anyNotFound { 237 return 1 238 } 239 case "eval": 240 src := strings.Join(args, " ") 241 p := syntax.NewParser() 242 file, err := p.Parse(strings.NewReader(src), "") 243 if err != nil { 244 r.errf("eval: %v\n", err) 245 return 1 246 } 247 r.stmts(ctx, file.StmtList) 248 return r.exit 249 case "source", ".": 250 if len(args) < 1 { 251 r.errf("%v: source: need filename\n", pos) 252 return 2 253 } 254 f, err := r.open(ctx, r.relPath(args[0]), os.O_RDONLY, 0, false) 255 if err != nil { 256 r.errf("source: %v\n", err) 257 return 1 258 } 259 defer f.Close() 260 p := syntax.NewParser() 261 file, err := p.Parse(f, args[0]) 262 if err != nil { 263 r.errf("source: %v\n", err) 264 return 1 265 } 266 oldParams := r.Params 267 r.Params = args[1:] 268 oldInSource := r.inSource 269 r.inSource = true 270 r.stmts(ctx, file.StmtList) 271 272 r.Params = oldParams 273 r.inSource = oldInSource 274 if code, ok := r.err.(returnStatus); ok { 275 r.err = nil 276 r.exit = int(code) 277 } 278 return r.exit 279 case "[": 280 if len(args) == 0 || args[len(args)-1] != "]" { 281 r.errf("%v: [: missing matching ]\n", pos) 282 return 2 283 } 284 args = args[:len(args)-1] 285 fallthrough 286 case "test": 287 parseErr := false 288 p := testParser{ 289 rem: args, 290 err: func(err error) { 291 r.errf("%v: %v\n", pos, err) 292 parseErr = true 293 }, 294 } 295 p.next() 296 expr := p.classicTest("[", false) 297 if parseErr { 298 return 2 299 } 300 return oneIf(r.bashTest(ctx, expr, true) == "") 301 case "exec": 302 // TODO: Consider syscall.Exec, i.e. actually replacing 303 // the process. It's in theory what a shell should do, 304 // but in practice it would kill the entire Go process 305 // and it's not available on Windows. 306 if len(args) == 0 { 307 r.keepRedirs = true 308 break 309 } 310 r.exec(ctx, args) 311 r.setErr(ShellExitStatus(r.exit)) 312 return 0 313 case "command": 314 show := false 315 for len(args) > 0 && strings.HasPrefix(args[0], "-") { 316 switch args[0] { 317 case "-v": 318 show = true 319 default: 320 r.errf("command: invalid option %s\n", args[0]) 321 return 2 322 } 323 args = args[1:] 324 } 325 if len(args) == 0 { 326 break 327 } 328 if !show { 329 if isBuiltin(args[0]) { 330 return r.builtinCode(ctx, pos, args[0], args[1:]) 331 } 332 r.exec(ctx, args) 333 return r.exit 334 } 335 last := 0 336 for _, arg := range args { 337 last = 0 338 if r.Funcs[arg] != nil || isBuiltin(arg) { 339 r.outf("%s\n", arg) 340 } else if path, err := exec.LookPath(arg); err == nil { 341 r.outf("%s\n", path) 342 } else { 343 last = 1 344 } 345 } 346 return last 347 case "dirs": 348 for i := len(r.dirStack) - 1; i >= 0; i-- { 349 r.outf("%s", r.dirStack[i]) 350 if i > 0 { 351 r.out(" ") 352 } 353 } 354 r.out("\n") 355 case "pushd": 356 change := true 357 if len(args) > 0 && args[0] == "-n" { 358 change = false 359 args = args[1:] 360 } 361 swap := func() string { 362 oldtop := r.dirStack[len(r.dirStack)-1] 363 top := r.dirStack[len(r.dirStack)-2] 364 r.dirStack[len(r.dirStack)-1] = top 365 r.dirStack[len(r.dirStack)-2] = oldtop 366 return top 367 } 368 switch len(args) { 369 case 0: 370 if !change { 371 break 372 } 373 if len(r.dirStack) < 2 { 374 r.errf("pushd: no other directory\n") 375 return 1 376 } 377 newtop := swap() 378 if code := r.changeDir(newtop); code != 0 { 379 return code 380 } 381 r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) 382 case 1: 383 if change { 384 if code := r.changeDir(args[0]); code != 0 { 385 return code 386 } 387 r.dirStack = append(r.dirStack, r.Dir) 388 } else { 389 r.dirStack = append(r.dirStack, args[0]) 390 swap() 391 } 392 r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) 393 default: 394 r.errf("pushd: too many arguments\n") 395 return 2 396 } 397 case "popd": 398 change := true 399 if len(args) > 0 && args[0] == "-n" { 400 change = false 401 args = args[1:] 402 } 403 switch len(args) { 404 case 0: 405 if len(r.dirStack) < 2 { 406 r.errf("popd: directory stack empty\n") 407 return 1 408 } 409 oldtop := r.dirStack[len(r.dirStack)-1] 410 r.dirStack = r.dirStack[:len(r.dirStack)-1] 411 if change { 412 newtop := r.dirStack[len(r.dirStack)-1] 413 if code := r.changeDir(newtop); code != 0 { 414 return code 415 } 416 } else { 417 r.dirStack[len(r.dirStack)-1] = oldtop 418 } 419 r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) 420 default: 421 r.errf("popd: invalid argument\n") 422 return 2 423 } 424 case "return": 425 if !r.inFunc && !r.inSource { 426 r.errf("return: can only be done from a func or sourced script\n") 427 return 1 428 } 429 code := 0 430 switch len(args) { 431 case 0: 432 case 1: 433 code = atoi(args[0]) 434 default: 435 r.errf("return: too many arguments\n") 436 return 2 437 } 438 r.setErr(returnStatus(code)) 439 case "read": 440 raw := false 441 for len(args) > 0 && strings.HasPrefix(args[0], "-") { 442 switch args[0] { 443 case "-r": 444 raw = true 445 default: 446 r.errf("read: invalid option %q\n", args[0]) 447 return 2 448 } 449 args = args[1:] 450 } 451 452 for _, name := range args { 453 if !syntax.ValidName(name) { 454 r.errf("read: invalid identifier %q\n", name) 455 return 2 456 } 457 } 458 459 line, err := r.readLine(raw) 460 if err != nil { 461 return 1 462 } 463 if len(args) == 0 { 464 args = append(args, "REPLY") 465 } 466 467 values := expand.ReadFields(r.ecfg, string(line), len(args), raw) 468 for i, name := range args { 469 val := "" 470 if i < len(values) { 471 val = values[i] 472 } 473 r.setVar(name, nil, expand.Variable{Value: val}) 474 } 475 476 return 0 477 478 case "getopts": 479 if len(args) < 2 { 480 r.errf("getopts: usage: getopts optstring name [arg]\n") 481 return 2 482 } 483 optind, _ := strconv.Atoi(r.envGet("OPTIND")) 484 if optind-1 != r.optState.argidx { 485 if optind < 1 { 486 optind = 1 487 } 488 r.optState = getopts{argidx: optind - 1} 489 } 490 optstr := args[0] 491 name := args[1] 492 if !syntax.ValidName(name) { 493 r.errf("getopts: invalid identifier: %q\n", name) 494 return 2 495 } 496 args = args[2:] 497 if len(args) == 0 { 498 args = r.Params 499 } 500 diagnostics := !strings.HasPrefix(optstr, ":") 501 502 opt, optarg, done := r.optState.Next(optstr, args) 503 504 r.setVarString(name, string(opt)) 505 r.delVar("OPTARG") 506 switch { 507 case opt == '?' && diagnostics && !done: 508 r.errf("getopts: illegal option -- %q\n", optarg) 509 case opt == ':' && diagnostics: 510 r.errf("getopts: option requires an argument -- %q\n", optarg) 511 default: 512 if optarg != "" { 513 r.setVarString("OPTARG", optarg) 514 } 515 } 516 if optind-1 != r.optState.argidx { 517 r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10)) 518 } 519 520 return oneIf(done) 521 522 case "shopt": 523 mode := "" 524 posixOpts := false 525 for len(args) > 0 && strings.HasPrefix(args[0], "-") { 526 switch args[0] { 527 case "-s", "-u": 528 mode = args[0] 529 case "-o": 530 posixOpts = true 531 case "-p", "-q": 532 panic(fmt.Sprintf("unhandled shopt flag: %s", args[0])) 533 default: 534 r.errf("shopt: invalid option %q\n", args[0]) 535 return 2 536 } 537 args = args[1:] 538 } 539 if len(args) == 0 { 540 if !posixOpts { 541 for i, name := range bashOptsTable { 542 r.printOptLine(name, r.opts[len(shellOptsTable)+i]) 543 } 544 break 545 } 546 for i, opt := range &shellOptsTable { 547 r.printOptLine(opt.name, r.opts[i]) 548 } 549 break 550 } 551 for _, arg := range args { 552 opt := r.optByName(arg, !posixOpts) 553 if opt == nil { 554 r.errf("shopt: invalid option name %q\n", arg) 555 return 1 556 } 557 switch mode { 558 case "-s", "-u": 559 *opt = mode == "-s" 560 default: // "" 561 r.printOptLine(arg, *opt) 562 } 563 } 564 r.updateExpandOpts() 565 566 default: 567 // "trap", "umask", "alias", "unalias", "fg", "bg", 568 panic(fmt.Sprintf("unhandled builtin: %s", name)) 569 } 570 return 0 571 } 572 573 func (r *Runner) printOptLine(name string, enabled bool) { 574 status := "off" 575 if enabled { 576 status = "on" 577 } 578 r.outf("%s\t%s\n", name, status) 579 } 580 581 func (r *Runner) readLine(raw bool) ([]byte, error) { 582 var line []byte 583 esc := false 584 585 for { 586 var buf [1]byte 587 n, err := r.Stdin.Read(buf[:]) 588 if n > 0 { 589 b := buf[0] 590 switch { 591 case !raw && b == '\\': 592 line = append(line, b) 593 esc = !esc 594 case !raw && b == '\n' && esc: 595 // line continuation 596 line = line[len(line)-1:] 597 esc = false 598 case b == '\n': 599 return line, nil 600 default: 601 line = append(line, b) 602 esc = false 603 } 604 } 605 if err == io.EOF && len(line) > 0 { 606 return line, nil 607 } 608 if err != nil { 609 return nil, err 610 } 611 } 612 } 613 614 func (r *Runner) changeDir(path string) int { 615 path = r.relPath(path) 616 info, err := r.stat(path) 617 if err != nil || !info.IsDir() { 618 return 1 619 } 620 if !hasPermissionToDir(info) { 621 return 1 622 } 623 r.Dir = path 624 r.Vars["OLDPWD"] = r.Vars["PWD"] 625 r.Vars["PWD"] = expand.Variable{Value: path} 626 return 0 627 } 628 629 func (r *Runner) relPath(path string) string { 630 if !filepath.IsAbs(path) { 631 path = filepath.Join(r.Dir, path) 632 } 633 return filepath.Clean(path) 634 } 635 636 type getopts struct { 637 argidx int 638 runeidx int 639 } 640 641 func (g *getopts) Next(optstr string, args []string) (opt rune, optarg string, done bool) { 642 if len(args) == 0 || g.argidx >= len(args) { 643 return '?', "", true 644 } 645 arg := []rune(args[g.argidx]) 646 if len(arg) < 2 || arg[0] != '-' || arg[1] == '-' { 647 return '?', "", true 648 } 649 650 opts := arg[1:] 651 opt = opts[g.runeidx] 652 if g.runeidx+1 < len(opts) { 653 g.runeidx++ 654 } else { 655 g.argidx++ 656 g.runeidx = 0 657 } 658 659 i := strings.IndexRune(optstr, opt) 660 if i < 0 { 661 // invalid option 662 return '?', string(opt), false 663 } 664 665 if i+1 < len(optstr) && optstr[i+1] == ':' { 666 if g.argidx >= len(args) { 667 // missing argument 668 return ':', string(opt), false 669 } 670 optarg = args[g.argidx] 671 g.argidx++ 672 g.runeidx = 0 673 } 674 675 return opt, optarg, false 676 }