shell_cmds.go (28752B)
1 package tui 2 3 import ( 4 "fmt" 5 "math/rand" 6 "path" 7 "regexp" 8 "sort" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/charmbracelet/lipgloss" 14 15 "git.prime8.dev/moncheeta/prime8dev/internal/vfs" 16 ) 17 18 // Commands for the about-page shell. Behaviour follows the web shell 19 // (internal/web/scripts/modules/shell-cmds.js); writes land in the session's 20 // in-memory overlay (vfs.FS) and are gone when the SSH session ends. 21 22 // shellCtx is what a command sees: its argv, stdin, where its output goes, and 23 // whether that is the terminal (styled) or a pipe/file (plain text). 24 type shellCtx struct { 25 s *shellState 26 fs *vfs.FS 27 env shellEnv 28 argv []string 29 stdin string 30 tty bool 31 out func(string) 32 err func(string) 33 res *shellResult 34 } 35 36 func (c *shellCtx) resolve(p string) string { return vfs.Resolve(c.s.home, c.s.cwd, p) } 37 38 func (c *shellCtx) display(abs string) string { return vfs.DisplayPath(c.s.home, abs) } 39 40 type shellCmd func(c *shellCtx) int 41 42 var shellUsage = map[string]string{ 43 "cat": "cat [-n] [file...] print files (or stdin)", 44 "cd": "cd [dir|-] change directory (~ is home, - is the last one)", 45 "clear": "clear clear the screen", 46 "cp": "cp [-r] src... dest copy files and directories", 47 "date": "date print the date and time", 48 "echo": "echo [-n] [-e] [text...] print text", 49 "env": "env print environment variables", 50 "exit": "exit close the session", 51 "export": "export [NAME=value...] set environment variables", 52 "false": "false do nothing, unsuccessfully", 53 "fetch": "fetch system info, the same card as the top of the page", 54 "grep": "grep [-invcrlwFq] pattern [file...] search for a regex", 55 "head": "head [-n N] [file...] first N lines (default 10)", 56 "help": "help list commands", 57 "history": "history [-c] command history (-c clears it)", 58 "hostname": "hostname print the host name", 59 "id": "id print user and group ids", 60 "ls": "ls [-a1d] [path...] list directory contents", 61 "man": "man command show usage for a command", 62 "mkdir": "mkdir [-p] dir... make directories", 63 "mv": "mv src... dest move or rename", 64 "pwd": "pwd print working directory", 65 "restore": "restore drop your changes, back to the original files", 66 "rm": "rm [-rf] path... remove files and directories", 67 "rmdir": "rmdir dir... remove empty directories", 68 "sort": "sort [-rnu] [file...] sort lines", 69 "tail": "tail [-n N] [file...] last N lines (default 10)", 70 "tee": "tee [-a] file... copy stdin to files and stdout", 71 "touch": "touch file... create files", 72 "tree": "tree [-a] [path] draw a directory tree", 73 "true": "true do nothing, successfully", 74 "type": "type name... say what a command is", 75 "uname": "uname [-a] print system information", 76 "uniq": "uniq [-c] [file...] collapse repeated lines", 77 "unset": "unset NAME... remove environment variables", 78 "uptime": "uptime how long this session has been up", 79 "wc": "wc [-lwc] [file...] count lines, words and bytes", 80 "which": "which command... locate a command", 81 "whoami": "whoami print the user name", 82 } 83 84 const shellHelpText = `prime8 shell. what you create here lasts until you disconnect. 85 86 navigate ls cd pwd tree 87 read cat head tail wc grep sort uniq 88 write echo touch mkdir cp mv rm rmdir tee 89 system fetch whoami id hostname uname date uptime env export 90 session clear history man restore exit 91 92 pipes |, redirects > >> < 2>, ; && ||, globs * ? [ ], $VARS and ~ all work. 93 man <cmd> for usage. try: cat about, tree projects, echo hi > notes && cat notes` 94 95 var shellBuiltins = map[string]bool{ 96 "cd": true, "echo": true, "exit": true, "logout": true, "export": true, "history": true, 97 "pwd": true, "type": true, "unset": true, "help": true, "true": true, "false": true, 98 } 99 100 var shellCommandTable map[string]shellCmd 101 102 // shellCommands is every command name, sorted, for completion. 103 var shellCommands []string 104 105 func init() { 106 shellCommandTable = map[string]shellCmd{ 107 "cat": cmdCat, "cd": cmdCd, "clear": cmdClear, "cp": cmdCp, "date": cmdDate, 108 "echo": cmdEcho, "env": cmdEnv, "exit": cmdExit, "logout": cmdExit, "export": cmdExport, 109 "false": func(*shellCtx) int { return 1 }, "fetch": cmdFetch, "grep": cmdGrep, 110 "head": func(c *shellCtx) int { return headTail(c, false) }, "help": cmdHelp, 111 "history": cmdHistory, "hostname": cmdHostname, "id": cmdID, "ls": cmdLs, "man": cmdMan, 112 "mkdir": cmdMkdir, "mv": cmdMv, "pwd": cmdPwd, "restore": cmdRestore, "rm": cmdRm, 113 "rmdir": cmdRmdir, "sort": cmdSort, "sudo": cmdSudo, 114 "tail": func(c *shellCtx) int { return headTail(c, true) }, "tee": cmdTee, 115 "touch": cmdTouch, "tree": cmdTree, "true": func(*shellCtx) int { return 0 }, 116 "type": cmdType, "uname": cmdUname, "uniq": cmdUniq, "unset": cmdUnset, 117 "uptime": cmdUptime, "wc": cmdWc, "which": cmdWhich, "whoami": cmdWhoami, 118 } 119 for name := range shellCommandTable { 120 shellCommands = append(shellCommands, name) 121 } 122 sort.Strings(shellCommands) 123 } 124 125 // --- helpers --- 126 127 // shellFlags splits leading -abc options from operands. Letters in withValue 128 // take the next argument (or the rest of the cluster) as their value. 129 func shellFlags(args []string, withValue string) (map[byte]string, []string) { 130 set := map[byte]string{} 131 var rest []string 132 i := 0 133 for ; i < len(args); i++ { 134 a := args[i] 135 if a == "--" { 136 i++ 137 break 138 } 139 if !strings.HasPrefix(a, "-") || a == "-" || (len(a) > 1 && a[1] >= '0' && a[1] <= '9') { 140 rest = append(rest, a) 141 continue 142 } 143 for j := 1; j < len(a); j++ { 144 f := a[j] 145 if strings.IndexByte(withValue, f) >= 0 { 146 v := a[j+1:] 147 if v == "" && i+1 < len(args) { 148 i++ 149 v = args[i] 150 } 151 set[f] = v 152 break 153 } 154 set[f] = "1" 155 } 156 } 157 rest = append(rest, args[i:]...) 158 return set, rest 159 } 160 161 func has(set map[byte]string, f byte) bool { _, ok := set[f]; return ok } 162 163 func textLines(text string) []string { 164 if text == "" { 165 return nil 166 } 167 ls := strings.Split(text, "\n") 168 if ls[len(ls)-1] == "" { 169 ls = ls[:len(ls)-1] 170 } 171 return ls 172 } 173 174 func joinLines(ls []string) string { 175 if len(ls) == 0 { 176 return "" 177 } 178 return strings.Join(ls, "\n") + "\n" 179 } 180 181 // fileText is a file's content as a text stream: the generated files carry 182 // no trailing newline, so add one like a real text file would have. 183 func fileText(n *vfs.Node) string { 184 if n.Content == "" || strings.HasSuffix(n.Content, "\n") { 185 return n.Content 186 } 187 return n.Content + "\n" 188 } 189 190 type shellInput struct{ name, text string } 191 192 // readInputs yields each file operand's text, or stdin when there are none. 193 func readInputs(c *shellCtx, files []string) []shellInput { 194 if len(files) == 0 { 195 return []shellInput{{"", c.stdin}} 196 } 197 var out []shellInput 198 for _, f := range files { 199 if f == "-" { 200 out = append(out, shellInput{"-", c.stdin}) 201 continue 202 } 203 if c.resolve(f) == "/dev/null" { 204 out = append(out, shellInput{f, ""}) 205 continue 206 } 207 n := c.fs.Get(c.resolve(f)) 208 switch { 209 case n == nil: 210 c.err(c.argv[0] + ": " + f + ": No such file or directory\n") 211 case n.Dir: 212 c.err(c.argv[0] + ": " + f + ": Is a directory\n") 213 default: 214 out = append(out, shellInput{f, fileText(n)}) 215 } 216 } 217 return out 218 } 219 220 func inputsStatus(inputs []shellInput, files []string) int { 221 if len(inputs) == max(len(files), 1) { 222 return 0 223 } 224 return 1 225 } 226 227 func (c *shellCtx) name(n *vfs.Node, label string) string { 228 if c.tty && n != nil && n.Dir { 229 return shellDirStyle.Render(label) 230 } 231 return label 232 } 233 234 func errText(err error) string { 235 if err == nil { 236 return "" 237 } 238 return err.Error() 239 } 240 241 // writeFile creates, replaces or appends to a file through the overlay. 242 func writeFile(c *shellCtx, p, content string, appendTo bool) error { 243 abs := c.resolve(p) 244 if abs == "/dev/null" { 245 return nil 246 } 247 n := c.fs.Get(abs) 248 if n != nil && n.Dir { 249 return vfs.ErrIsDir 250 } 251 if err := c.fs.CheckCreate(abs); err != nil { 252 return err 253 } 254 if appendTo && n != nil { 255 content = n.Content + content 256 } 257 return c.fs.Write(abs, content) 258 } 259 260 func pad2(n int) string { return fmt.Sprintf("%02d", n) } 261 262 // --- commands --- 263 264 func cmdHelp(c *shellCtx) int { 265 c.out(shellHelpText + "\n") 266 return 0 267 } 268 269 func cmdMan(c *shellCtx) int { 270 if len(c.argv) < 2 { 271 c.err("What manual page do you want?\nFor example, try 'man man'.\n") 272 return 1 273 } 274 name := c.argv[1] 275 u, ok := shellUsage[name] 276 if name == "man" { 277 u, ok = shellUsage["man"], true 278 } 279 if !ok { 280 c.err("No manual entry for " + name + "\n") 281 return 16 282 } 283 c.out(strings.ToUpper(name) + "(1)\n\n " + u + "\n") 284 return 0 285 } 286 287 func cmdPwd(c *shellCtx) int { c.out(c.s.cwd + "\n"); return 0 } 288 func cmdWhoami(c *shellCtx) int { c.out(c.s.user + "\n"); return 0 } 289 func cmdHostname(c *shellCtx) int { c.out("prime8\n"); return 0 } 290 291 func cmdID(c *shellCtx) int { 292 u := c.s.user 293 c.out(fmt.Sprintf("uid=1000(%s) gid=1000(%s) groups=1000(%s)\n", u, u, u)) 294 return 0 295 } 296 297 func cmdUname(c *shellCtx) int { 298 set, _ := shellFlags(c.argv[1:], "") 299 if has(set, 'a') { 300 c.out("Linux prime8 6.12.0-prime8 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux\n") 301 } else { 302 c.out("Linux\n") 303 } 304 return 0 305 } 306 307 func cmdFetch(c *shellCtx) int { 308 if c.env.fetch == nil { 309 c.err("fetch: no system info\n") 310 return 1 311 } 312 card := c.env.fetch(c.tty) 313 if strings.TrimSpace(card) == "" { 314 c.err("fetch: no system info\n") 315 return 1 316 } 317 c.out(card + "\n") 318 return 0 319 } 320 321 func shellNow(c *shellCtx) time.Time { 322 if c.env.now.IsZero() { 323 return time.Now() 324 } 325 return c.env.now 326 } 327 328 func cmdDate(c *shellCtx) int { 329 c.out(shellNow(c).Format("Mon Jan _2 15:04:05 MST 2006") + "\n") 330 return 0 331 } 332 333 func cmdUptime(c *shellCtx) int { 334 now := shellNow(c) 335 mins := 0 336 if !c.s.started.IsZero() { 337 mins = int(now.Sub(c.s.started).Minutes()) 338 } 339 up := fmt.Sprintf("%d min", mins) 340 if mins >= 60 { 341 up = fmt.Sprintf("%d:%s", mins/60, pad2(mins%60)) 342 } 343 load := func() string { return fmt.Sprintf("%.2f", rand.Float64()*0.2) } 344 c.out(fmt.Sprintf(" %s up %s, 1 user, load average: %s, %s, %s\n", now.Format("15:04:05"), up, load(), load(), load())) 345 return 0 346 } 347 348 func cmdEcho(c *shellCtx) int { 349 args := c.argv[1:] 350 newline, escapes := true, false 351 for len(args) > 0 && echoFlagRE.MatchString(args[0]) { 352 if strings.Contains(args[0], "n") { 353 newline = false 354 } 355 if strings.Contains(args[0], "e") { 356 escapes = true 357 } 358 if strings.Contains(args[0], "E") { 359 escapes = false 360 } 361 args = args[1:] 362 } 363 s := strings.Join(args, " ") 364 if escapes { 365 s = strings.NewReplacer(`\n`, "\n", `\t`, "\t", `\r`, "\r", `\\`, `\`).Replace(s) 366 } 367 if newline { 368 s += "\n" 369 } 370 c.out(s) 371 return 0 372 } 373 374 func cmdEnv(c *shellCtx) int { 375 env := c.s.envMap() 376 keys := make([]string, 0, len(env)) 377 for k := range env { 378 keys = append(keys, k) 379 } 380 sort.Strings(keys) 381 for _, k := range keys { 382 c.out(k + "=" + env[k] + "\n") 383 } 384 return 0 385 } 386 387 var shellAssignRE = regexp.MustCompile(`^([A-Za-z_][A-Za-z0-9_]*)=(.*)$`) 388 389 func cmdExport(c *shellCtx) int { 390 if len(c.argv) == 1 { 391 return cmdEnv(c) 392 } 393 status := 0 394 for _, a := range c.argv[1:] { 395 if m := shellAssignRE.FindStringSubmatch(a); m != nil { 396 c.s.setVar(m[1], m[2]) 397 } else if !shellAssignRE.MatchString(a + "=") { 398 c.err("export: `" + a + "': not a valid identifier\n") 399 status = 1 400 } 401 } 402 return status 403 } 404 405 func cmdUnset(c *shellCtx) int { 406 for _, a := range c.argv[1:] { 407 delete(c.s.vars, a) 408 } 409 return 0 410 } 411 412 func cmdHistory(c *shellCtx) int { 413 set, _ := shellFlags(c.argv[1:], "") 414 if has(set, 'c') { 415 c.s.cmdHistory = nil 416 return 0 417 } 418 for i, l := range c.s.cmdHistory { 419 c.out(fmt.Sprintf("%5d %s\n", i+1, l)) 420 } 421 return 0 422 } 423 424 func cmdClear(c *shellCtx) int { 425 if c.tty { 426 c.res.clear = true 427 } 428 return 0 429 } 430 431 func cmdExit(c *shellCtx) int { 432 c.res.quit = true 433 return 0 434 } 435 436 func cmdSudo(c *shellCtx) int { 437 c.err(c.s.user + " is not in the sudoers file. This incident will be reported.\n") 438 return 1 439 } 440 441 func cmdRestore(c *shellCtx) int { 442 n := c.s.overlay.Changes() 443 c.s.overlay.Reset() 444 c.out(fmt.Sprintf("restored the original files (%d change%s dropped)\n", n, plural(n, "", "s"))) 445 return 0 446 } 447 448 func plural(n int, one, many string) string { 449 if n == 1 { 450 return one 451 } 452 return many 453 } 454 455 func cmdWhich(c *shellCtx) int { 456 status := 0 457 for _, a := range c.argv[1:] { 458 switch { 459 case shellBuiltins[a]: 460 c.out(a + ": shell built-in command\n") 461 case shellCommandTable[a] != nil: 462 c.out("/usr/bin/" + a + "\n") 463 default: 464 status = 1 465 } 466 } 467 return status 468 } 469 470 func cmdType(c *shellCtx) int { 471 status := 0 472 for _, a := range c.argv[1:] { 473 switch { 474 case shellBuiltins[a]: 475 c.out(a + " is a shell builtin\n") 476 case shellCommandTable[a] != nil: 477 c.out(a + " is /usr/bin/" + a + "\n") 478 default: 479 c.err("type: " + a + ": not found\n") 480 status = 1 481 } 482 } 483 return status 484 } 485 486 func cmdCd(c *shellCtx) int { 487 args := c.argv[1:] 488 if len(args) > 1 { 489 c.err("cd: too many arguments\n") 490 return 1 491 } 492 target := "" 493 if len(args) == 1 { 494 target = args[0] 495 } 496 if target == "-" { 497 if c.s.oldpwd == "" { 498 c.err("cd: OLDPWD not set\n") 499 return 1 500 } 501 target = c.s.oldpwd 502 c.out(target + "\n") 503 } 504 abs := c.resolve(target) 505 n := c.fs.Get(abs) 506 switch { 507 case n == nil: 508 c.err("cd: " + target + ": No such file or directory\n") 509 return 1 510 case !n.Dir: 511 c.err("cd: " + target + ": Not a directory\n") 512 return 1 513 } 514 c.s.oldpwd, c.s.cwd = c.s.cwd, abs 515 return 0 516 } 517 518 func cmdLs(c *shellCtx) int { 519 set, rest := shellFlags(c.argv[1:], "") 520 all := has(set, 'a') || has(set, 'A') 521 perLine := has(set, '1') || !c.tty 522 if len(rest) == 0 { 523 rest = []string{"."} 524 } 525 status := 0 526 show := func(names []string, nodes []*vfs.Node) { 527 if len(names) == 0 { 528 return 529 } 530 parts := make([]string, len(names)) 531 for i := range names { 532 label := names[i] 533 if c.tty && nodes[i].Dir { 534 label += "/" 535 } 536 parts[i] = c.name(nodes[i], label) 537 } 538 if perLine { 539 c.out(strings.Join(parts, "\n") + "\n") 540 } else { 541 c.out(strings.Join(parts, " ") + "\n") 542 } 543 } 544 var fileNames []string 545 var fileNodes []*vfs.Node 546 type dirArg struct { 547 p string 548 n *vfs.Node 549 } 550 var dirs []dirArg 551 for _, p := range rest { 552 n := c.fs.Get(c.resolve(p)) 553 switch { 554 case n == nil: 555 c.err("ls: cannot access '" + p + "': No such file or directory\n") 556 status = 2 557 case n.Dir && !has(set, 'd'): 558 dirs = append(dirs, dirArg{p, n}) 559 default: 560 fileNames = append(fileNames, p) 561 fileNodes = append(fileNodes, n) 562 } 563 } 564 show(fileNames, fileNodes) 565 headers := len(dirs)+len(fileNames) > 1 566 for i, d := range dirs { 567 if i > 0 || len(fileNames) > 0 { 568 c.out("\n") 569 } 570 if headers { 571 c.out(d.p + ":\n") 572 } 573 var names []string 574 var nodes []*vfs.Node 575 for _, k := range d.n.SortedChildren() { 576 if !all && strings.HasPrefix(k.Name, ".") { 577 continue 578 } 579 names = append(names, k.Name) 580 nodes = append(nodes, k) 581 } 582 show(names, nodes) 583 } 584 return status 585 } 586 587 func cmdCat(c *shellCtx) int { 588 set, rest := shellFlags(c.argv[1:], "") 589 inputs := readInputs(c, rest) 590 var b strings.Builder 591 for _, in := range inputs { 592 b.WriteString(in.text) 593 } 594 text := b.String() 595 if has(set, 'n') { 596 ls := textLines(text) 597 for i, l := range ls { 598 ls[i] = fmt.Sprintf("%6d\t%s", i+1, l) 599 } 600 text = joinLines(ls) 601 } 602 c.out(text) 603 return inputsStatus(inputs, rest) 604 } 605 606 func cmdTree(c *shellCtx) int { 607 set, rest := shellFlags(c.argv[1:], "") 608 p := "." 609 if len(rest) > 0 { 610 p = rest[0] 611 } 612 root := c.fs.Get(c.resolve(p)) 613 if root == nil { 614 c.err(p + " [error opening dir]\n\n0 directories, 0 files\n") 615 return 2 616 } 617 dirs, files := 0, 0 618 var b strings.Builder 619 b.WriteString(c.name(root, p) + "\n") 620 var walk func(n *vfs.Node, prefix string) 621 walk = func(n *vfs.Node, prefix string) { 622 var kids []*vfs.Node 623 for _, k := range n.SortedChildren() { 624 if has(set, 'a') || !strings.HasPrefix(k.Name, ".") { 625 kids = append(kids, k) 626 } 627 } 628 for i, k := range kids { 629 last := i == len(kids)-1 630 branch, next := "├── ", "│ " 631 if last { 632 branch, next = "└── ", " " 633 } 634 b.WriteString(prefix + branch + c.name(k, k.Name) + "\n") 635 if k.Dir { 636 dirs++ 637 walk(k, prefix+next) 638 } else { 639 files++ 640 } 641 } 642 } 643 if root.Dir { 644 walk(root, "") 645 } 646 fmt.Fprintf(&b, "\n%d director%s, %d file%s\n", dirs, plural(dirs, "y", "ies"), files, plural(files, "", "s")) 647 c.out(b.String()) 648 return 0 649 } 650 651 func headTail(c *shellCtx, tail bool) int { 652 args := make([]string, len(c.argv)-1) 653 for i, a := range c.argv[1:] { 654 if headCountRE.MatchString(a) { 655 a = "-n" + a[1:] 656 } 657 args[i] = a 658 } 659 set, rest := shellFlags(args, "n") 660 n := 10 661 if v, ok := set['n']; ok { 662 x, err := strconv.Atoi(v) 663 if err != nil || x < 0 { 664 c.err(c.argv[0] + ": invalid number of lines: '" + v + "'\n") 665 return 1 666 } 667 n = x 668 } 669 inputs := readInputs(c, rest) 670 for i, in := range inputs { 671 if len(inputs) > 1 { 672 if i > 0 { 673 c.out("\n") 674 } 675 c.out("==> " + in.name + " <==\n") 676 } 677 ls := textLines(in.text) 678 if tail { 679 ls = ls[max(len(ls)-n, 0):] 680 } else { 681 ls = ls[:min(n, len(ls))] 682 } 683 c.out(joinLines(ls)) 684 } 685 return inputsStatus(inputs, rest) 686 } 687 688 func cmdWc(c *shellCtx) int { 689 set, rest := shellFlags(c.argv[1:], "") 690 pickL, pickW, pickC := has(set, 'l'), has(set, 'w'), has(set, 'c') 691 if !pickL && !pickW && !pickC { 692 pickL, pickW, pickC = true, true, true 693 } 694 inputs := readInputs(c, rest) 695 type row struct { 696 l, w, b int 697 name string 698 } 699 var rows []row 700 for _, in := range inputs { 701 rows = append(rows, row{strings.Count(in.text, "\n"), len(strings.Fields(in.text)), len(in.text), in.name}) 702 } 703 if len(rows) > 1 { 704 t := row{name: "total"} 705 for _, r := range rows { 706 t.l, t.w, t.b = t.l+r.l, t.w+r.w, t.b+r.b 707 } 708 rows = append(rows, t) 709 } 710 cols := 0 711 for _, p := range []bool{pickL, pickW, pickC} { 712 if p { 713 cols++ 714 } 715 } 716 width := 1 717 if len(rest) == 0 && cols > 1 { 718 width = 7 719 } 720 for _, r := range rows { 721 for _, v := range []int{r.l, r.w, r.b} { 722 width = max(width, len(strconv.Itoa(v))) 723 } 724 } 725 for _, r := range rows { 726 var parts []string 727 if pickL { 728 parts = append(parts, fmt.Sprintf("%*d", width, r.l)) 729 } 730 if pickW { 731 parts = append(parts, fmt.Sprintf("%*d", width, r.w)) 732 } 733 if pickC { 734 parts = append(parts, fmt.Sprintf("%*d", width, r.b)) 735 } 736 line := strings.Join(parts, " ") 737 if r.name != "" && r.name != "-" { 738 line += " " + r.name 739 } 740 c.out(line + "\n") 741 } 742 return inputsStatus(inputs, rest) 743 } 744 745 var shellMatchStyle = lipgloss.NewStyle().Foreground(colorAccent).Bold(true) 746 747 func cmdGrep(c *shellCtx) int { 748 set, rest := shellFlags(c.argv[1:], "e") 749 pattern, ok := set['e'] 750 if !ok { 751 if len(rest) == 0 { 752 c.err("Usage: grep [OPTION]... PATTERNS [FILE]...\n") 753 return 2 754 } 755 pattern, rest = rest[0], rest[1:] 756 } 757 src := pattern 758 if has(set, 'F') { 759 src = regexp.QuoteMeta(pattern) 760 } 761 if has(set, 'w') { 762 src = `\b(?:` + src + `)\b` 763 } 764 if has(set, 'i') { 765 src = "(?i)" + src 766 } 767 re, err := regexp.Compile(src) 768 if err != nil { 769 lit := regexp.QuoteMeta(pattern) 770 if has(set, 'i') { 771 lit = "(?i)" + lit 772 } 773 re = regexp.MustCompile(lit) 774 } 775 recursive := has(set, 'r') || has(set, 'R') 776 var inputs []shellInput 777 errs := false 778 if len(rest) == 0 { 779 if recursive { 780 rest = []string{"."} 781 } else { 782 inputs = append(inputs, shellInput{"", c.stdin}) 783 } 784 } 785 var collect func(label string, n *vfs.Node) 786 collect = func(label string, n *vfs.Node) { 787 if !n.Dir { 788 inputs = append(inputs, shellInput{label, fileText(n)}) 789 return 790 } 791 for _, k := range n.SortedChildren() { 792 collect(label+"/"+k.Name, k) 793 } 794 } 795 for _, f := range rest { 796 n := c.fs.Get(c.resolve(f)) 797 switch { 798 case n == nil: 799 if !has(set, 's') { 800 c.err("grep: " + f + ": No such file or directory\n") 801 } 802 errs = true 803 case n.Dir && !recursive: 804 c.err("grep: " + f + ": Is a directory\n") 805 errs = true 806 default: 807 label := strings.TrimRight(f, "/") 808 if label == "" { 809 label = f 810 } 811 collect(label, n) 812 } 813 } 814 status := 1 815 prefix := len(inputs) > 1 || recursive 816 invert := has(set, 'v') 817 for _, in := range inputs { 818 count := 0 819 for i, l := range textLines(in.text) { 820 if re.MatchString(l) == invert { 821 continue 822 } 823 count++ 824 status = 0 825 if has(set, 'l') || has(set, 'c') || has(set, 'q') { 826 continue 827 } 828 head := "" 829 if prefix { 830 head += in.name + ":" 831 } 832 if has(set, 'n') { 833 head += strconv.Itoa(i+1) + ":" 834 } 835 if c.tty && head != "" { 836 head = promptPathStyle.Render(head) 837 } 838 if has(set, 'o') && !invert { 839 for _, m := range re.FindAllString(l, -1) { 840 if m == "" { 841 continue 842 } 843 if c.tty { 844 m = shellMatchStyle.Render(m) 845 } 846 c.out(head + m + "\n") 847 } 848 continue 849 } 850 if c.tty && !invert { 851 l = re.ReplaceAllStringFunc(l, func(m string) string { 852 if m == "" { 853 return m 854 } 855 return shellMatchStyle.Render(m) 856 }) 857 } 858 c.out(head + l + "\n") 859 } 860 if has(set, 'c') { 861 label := "" 862 if prefix { 863 label = in.name + ":" 864 } 865 c.out(label + strconv.Itoa(count) + "\n") 866 } else if has(set, 'l') && count > 0 { 867 c.out(in.name + "\n") 868 } 869 } 870 if errs && status != 0 { 871 return 2 872 } 873 return status 874 } 875 876 func cmdSort(c *shellCtx) int { 877 set, rest := shellFlags(c.argv[1:], "") 878 inputs := readInputs(c, rest) 879 var ls []string 880 for _, in := range inputs { 881 ls = append(ls, textLines(in.text)...) 882 } 883 if has(set, 'n') { 884 num := func(s string) float64 { 885 f, _ := strconv.ParseFloat(strings.Fields(s + " x")[0], 64) 886 return f 887 } 888 sort.SliceStable(ls, func(i, j int) bool { return num(ls[i]) < num(ls[j]) }) 889 } else { 890 sort.Strings(ls) 891 } 892 if has(set, 'r') { 893 for i, j := 0, len(ls)-1; i < j; i, j = i+1, j-1 { 894 ls[i], ls[j] = ls[j], ls[i] 895 } 896 } 897 if has(set, 'u') { 898 var u []string 899 for i, l := range ls { 900 if i == 0 || l != ls[i-1] { 901 u = append(u, l) 902 } 903 } 904 ls = u 905 } 906 c.out(joinLines(ls)) 907 return inputsStatus(inputs, rest) 908 } 909 910 func cmdUniq(c *shellCtx) int { 911 set, rest := shellFlags(c.argv[1:], "") 912 inputs := readInputs(c, rest) 913 var ls []string 914 for _, in := range inputs { 915 ls = append(ls, textLines(in.text)...) 916 } 917 var out []string 918 for i := 0; i < len(ls); { 919 j := i 920 for j < len(ls) && ls[j] == ls[i] { 921 j++ 922 } 923 if has(set, 'c') { 924 out = append(out, fmt.Sprintf("%7d %s", j-i, ls[i])) 925 } else { 926 out = append(out, ls[i]) 927 } 928 i = j 929 } 930 c.out(joinLines(out)) 931 return inputsStatus(inputs, rest) 932 } 933 934 func cmdTee(c *shellCtx) int { 935 set, rest := shellFlags(c.argv[1:], "") 936 status := 0 937 for _, f := range rest { 938 if err := writeFile(c, f, c.stdin, has(set, 'a')); err != nil { 939 c.err("tee: " + f + ": " + errText(err) + "\n") 940 status = 1 941 } 942 } 943 c.out(c.stdin) 944 return status 945 } 946 947 func cmdTouch(c *shellCtx) int { 948 _, rest := shellFlags(c.argv[1:], "") 949 if len(rest) == 0 { 950 c.err("touch: missing file operand\n") 951 return 1 952 } 953 status := 0 954 for _, f := range rest { 955 abs := c.resolve(f) 956 if abs == "/dev/null" { 957 continue 958 } 959 if n := c.fs.Get(abs); n != nil { 960 if !c.fs.Writable(abs) { 961 c.err("touch: cannot touch '" + f + "': Permission denied\n") 962 status = 1 963 } 964 continue 965 } 966 err := c.fs.CheckCreate(abs) 967 if err == nil { 968 err = c.fs.Write(abs, "") 969 } 970 if err != nil { 971 c.err("touch: cannot touch '" + f + "': " + errText(err) + "\n") 972 status = 1 973 } 974 } 975 return status 976 } 977 978 func cmdMkdir(c *shellCtx) int { 979 set, rest := shellFlags(c.argv[1:], "") 980 if len(rest) == 0 { 981 c.err("mkdir: missing operand\n") 982 return 1 983 } 984 status := 0 985 for _, d := range rest { 986 abs := c.resolve(d) 987 fail := func(err error) { 988 c.err("mkdir: cannot create directory '" + d + "': " + errText(err) + "\n") 989 status = 1 990 } 991 if has(set, 'p') { 992 cur := "" 993 for _, part := range strings.Split(strings.Trim(abs, "/"), "/") { 994 if part == "" { 995 continue 996 } 997 cur += "/" + part 998 n := c.fs.Get(cur) 999 if n != nil && n.Dir { 1000 continue 1001 } 1002 var err error 1003 if n != nil { 1004 err = vfs.ErrExist 1005 } else if err = c.fs.CheckCreate(cur); err == nil { 1006 err = c.fs.Mkdir(cur) 1007 } 1008 if err != nil { 1009 fail(err) 1010 break 1011 } 1012 } 1013 continue 1014 } 1015 var err error 1016 if c.fs.Get(abs) != nil { 1017 err = vfs.ErrExist 1018 } else if err = c.fs.CheckCreate(abs); err == nil { 1019 err = c.fs.Mkdir(abs) 1020 } 1021 if err != nil { 1022 fail(err) 1023 } 1024 } 1025 return status 1026 } 1027 1028 func cmdRmdir(c *shellCtx) int { 1029 _, rest := shellFlags(c.argv[1:], "") 1030 if len(rest) == 0 { 1031 c.err("rmdir: missing operand\n") 1032 return 1 1033 } 1034 status := 0 1035 for _, d := range rest { 1036 abs := c.resolve(d) 1037 n := c.fs.Get(abs) 1038 var err error 1039 switch { 1040 case n == nil: 1041 err = vfs.ErrNotExist 1042 case !n.Dir: 1043 err = vfs.ErrNotDir 1044 case len(n.Children) > 0: 1045 err = vfs.ErrNotEmpty 1046 default: 1047 if err = c.fs.CheckCreate(abs); err == nil { 1048 err = c.fs.Remove(abs) 1049 } 1050 } 1051 if err != nil { 1052 c.err("rmdir: failed to remove '" + d + "': " + errText(err) + "\n") 1053 status = 1 1054 } 1055 } 1056 return status 1057 } 1058 1059 var ( 1060 echoFlagRE = regexp.MustCompile(`^-[neE]+$`) 1061 headCountRE = regexp.MustCompile(`^-\d+$`) 1062 ) 1063 1064 var dotPathRE = regexp.MustCompile(`(^|/)\.\.?/*$`) 1065 1066 func cmdRm(c *shellCtx) int { 1067 set, rest := shellFlags(c.argv[1:], "") 1068 recursive := has(set, 'r') || has(set, 'R') 1069 force := has(set, 'f') 1070 if len(rest) == 0 { 1071 if force { 1072 return 0 1073 } 1074 c.err("rm: missing operand\n") 1075 return 1 1076 } 1077 status := 0 1078 for _, p := range rest { 1079 abs := c.resolve(p) 1080 if dotPathRE.MatchString(p) { 1081 c.err("rm: refusing to remove '.' or '..' directory: skipping '" + p + "'\n") 1082 status = 1 1083 continue 1084 } 1085 if abs == "/" && recursive { 1086 c.err("rm: it is dangerous to operate recursively on '/'\nrm: use --no-preserve-root to override this failsafe\n") 1087 status = 1 1088 continue 1089 } 1090 n := c.fs.Get(abs) 1091 if n == nil { 1092 if !force { 1093 c.err("rm: cannot remove '" + p + "': No such file or directory\n") 1094 status = 1 1095 } 1096 continue 1097 } 1098 if n.Dir && !recursive { 1099 c.err("rm: cannot remove '" + p + "': Is a directory\n") 1100 status = 1 1101 continue 1102 } 1103 err := c.fs.CheckCreate(abs) 1104 if err == nil { 1105 err = c.fs.Remove(abs) 1106 } 1107 if err != nil { 1108 c.err("rm: cannot remove '" + p + "': " + errText(err) + "\n") 1109 status = 1 1110 } 1111 } 1112 return status 1113 } 1114 1115 func cmdCp(c *shellCtx) int { return transfer(c, false) } 1116 func cmdMv(c *shellCtx) int { return transfer(c, true) } 1117 1118 func transfer(c *shellCtx, move bool) int { 1119 name := c.argv[0] 1120 set, rest := shellFlags(c.argv[1:], "") 1121 recursive := has(set, 'r') || has(set, 'R') || has(set, 'a') 1122 if len(rest) < 2 { 1123 if len(rest) == 1 { 1124 c.err(name + ": missing destination file operand after '" + rest[0] + "'\n") 1125 } else { 1126 c.err(name + ": missing file operand\n") 1127 } 1128 return 1 1129 } 1130 destArg := rest[len(rest)-1] 1131 srcs := rest[:len(rest)-1] 1132 destAbs := c.resolve(destArg) 1133 destNode := c.fs.Get(destAbs) 1134 destIsDir := destNode != nil && destNode.Dir 1135 if len(srcs) > 1 && !destIsDir { 1136 c.err(name + ": target '" + destArg + "': Not a directory\n") 1137 return 1 1138 } 1139 verb := "create" 1140 if move { 1141 verb = "move" 1142 } 1143 status := 0 1144 for _, src := range srcs { 1145 srcAbs := c.resolve(src) 1146 node := c.fs.Get(srcAbs) 1147 fail := func(msg string) { 1148 c.err(name + ": " + msg + "\n") 1149 status = 1 1150 } 1151 if node == nil { 1152 fail("cannot stat '" + src + "': No such file or directory") 1153 continue 1154 } 1155 if node.Dir && !move && !recursive { 1156 fail("-r not specified; omitting directory '" + src + "'") 1157 continue 1158 } 1159 target := destAbs 1160 if destIsDir { 1161 target = path.Join(destAbs, path.Base(srcAbs)) 1162 } 1163 if target == srcAbs { 1164 fail("'" + src + "' and '" + destArg + "' are the same file") 1165 continue 1166 } 1167 if node.Dir && strings.HasPrefix(target+"/", srcAbs+"/") { 1168 op := "copy" 1169 if move { 1170 op = "move" 1171 } 1172 fail("cannot " + op + " '" + src + "' to a subdirectory of itself, '" + c.display(target) + "'") 1173 continue 1174 } 1175 existing := c.fs.Get(target) 1176 if existing != nil && existing.Dir && !node.Dir { 1177 fail("cannot overwrite directory '" + c.display(target) + "' with non-directory") 1178 continue 1179 } 1180 if existing != nil && !existing.Dir && node.Dir { 1181 fail("cannot overwrite non-directory '" + c.display(target) + "' with directory '" + src + "'") 1182 continue 1183 } 1184 err := c.fs.CheckCreate(target) 1185 if err == nil && move { 1186 err = c.fs.CheckCreate(srcAbs) 1187 } 1188 if err == nil { 1189 err = c.fs.Copy(node, target) 1190 } 1191 if err == nil && move { 1192 err = c.fs.Remove(srcAbs) 1193 } 1194 if err != nil { 1195 fail("cannot " + verb + " '" + src + "': " + errText(err)) 1196 } 1197 } 1198 return status 1199 }