package shell import ( "bufio" "fmt" "io" "os" "path/filepath" "regexp" "runtime" "sort" "strconv" "strings" "time" ) var aliases = make(map[string]string) func (s *Shell) initBuiltins() { s.builtins = map[string]func([]string) error{ // Shell builtins "cd": s.builtinCd, "pwd": s.builtinPwd, "echo": s.builtinEcho, "exit": s.builtinExit, "export": s.builtinExport, "source": s.builtinSource, ".": s.builtinSource, "alias": s.builtinAlias, "unalias": s.builtinUnalias, "type": s.builtinType, "test": s.builtinTest, "[": s.builtinTest, "[[": s.builtinDoubleBracket, "read": s.builtinRead, "printf": s.builtinPrintf, "true": s.builtinTrue, "false": s.builtinFalse, "set": s.builtinSet, "unset": s.builtinUnset, "env": s.builtinEnv, "which": s.builtinWhich, "return": s.builtinReturn, "break": s.builtinBreak, "continue": s.builtinContinue, "shift": s.builtinShift, "declare": s.builtinDeclare, "local": s.builtinDeclare, "command": s.builtinCommand, "jobs": s.builtinJobs, "disown": s.builtinDisown, "mktemp": s.builtinMktemp, "uname": s.builtinUname, "whoami": s.builtinWhoami, "hostname": s.builtinHostname, "mapfile": s.builtinMapfile, "readarray": s.builtinMapfile, // Coreutils "ls": s.cmdLs, "cat": s.cmdCat, "grep": s.cmdGrep, "head": s.cmdHead, "tail": s.cmdTail, "sort": s.cmdSort, "wc": s.cmdWc, "find": s.cmdFind, "cp": s.cmdCp, "mv": s.cmdMv, "rm": s.cmdRm, "mkdir": s.cmdMkdir, "touch": s.cmdTouch, "clear": s.cmdClear, "cut": s.cmdCut, "tr": s.cmdTr, "uniq": s.cmdUniq, "tee": s.cmdTee, "date": s.cmdDate, "sleep": s.cmdSleep, "basename": s.cmdBasename, "dirname": s.cmdDirname, "sed": s.cmdSed, "xargs": s.cmdXargs, } } // ─── Shell builtins ─────────────────────────────────────────────────────────── func (s *Shell) builtinCd(args []string) error { path := "" if len(args) > 0 { path = args[0] } else { path = s.GetVar("HOME") if path == "" { path = os.Getenv("USERPROFILE") } } path = s.expandWord(path) if !filepath.IsAbs(path) { pwd, _ := os.Getwd() path = filepath.Join(pwd, path) } if err := os.Chdir(path); err != nil { return fmt.Errorf("cd: %s: %v", path, err) } s.vars["PWD"], _ = os.Getwd() return nil } func (s *Shell) builtinPwd(args []string) error { pwd, err := os.Getwd() if err != nil { return err } fmt.Fprintln(s.Stdout, pwd) return nil } func (s *Shell) builtinEcho(args []string) error { noNewline := false escape := false var parts []string for _, arg := range args { switch arg { case "-n": noNewline = true case "-e": escape = true case "-E": escape = false case "-ne", "-en": noNewline = true escape = true default: if escape { arg = strings.ReplaceAll(arg, `\n`, "\n") arg = strings.ReplaceAll(arg, `\t`, "\t") arg = strings.ReplaceAll(arg, `\r`, "\r") arg = strings.ReplaceAll(arg, `\\`, "\\") arg = strings.ReplaceAll(arg, `\a`, "\a") arg = strings.ReplaceAll(arg, `\b`, "\b") } parts = append(parts, arg) } } line := strings.Join(parts, " ") if noNewline { fmt.Fprint(s.Stdout, line) } else { fmt.Fprintln(s.Stdout, line) } return nil } func (s *Shell) builtinExit(args []string) error { code := 0 if len(args) > 0 { fmt.Sscanf(args[0], "%d", &code) } os.Exit(code) return nil } func (s *Shell) builtinExport(args []string) error { if len(args) == 0 { for _, env := range os.Environ() { fmt.Fprintln(s.Stdout, "export "+env) } return nil } for _, arg := range args { parts := strings.SplitN(arg, "=", 2) if len(parts) == 2 { s.vars[parts[0]] = parts[1] os.Setenv(parts[0], parts[1]) } else if len(parts) == 1 { if val, ok := s.vars[parts[0]]; ok { os.Setenv(parts[0], val) } } } return nil } func (s *Shell) builtinSource(args []string) error { if len(args) == 0 { return fmt.Errorf("source: filename argument required") } data, err := os.ReadFile(args[0]) if err != nil { return fmt.Errorf("source: %v", err) } // Pass remaining args as positional params to sourced script if len(args) > 1 { s.SetArgs(args[1:]) } return s.Execute(string(data)) } func (s *Shell) builtinAlias(args []string) error { if len(args) == 0 { names := make([]string, 0, len(aliases)) for name := range aliases { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Fprintf(s.Stdout, "alias %s='%s'\n", name, aliases[name]) } return nil } for _, arg := range args { parts := strings.SplitN(arg, "=", 2) if len(parts) == 2 { aliases[parts[0]] = strings.Trim(parts[1], "'\"") } else { if val, ok := aliases[parts[0]]; ok { fmt.Fprintf(s.Stdout, "alias %s='%s'\n", parts[0], val) } } } return nil } func (s *Shell) builtinUnalias(args []string) error { for _, arg := range args { if arg == "-a" { aliases = make(map[string]string) return nil } delete(aliases, arg) } return nil } func (s *Shell) builtinType(args []string) error { for _, arg := range args { if _, ok := s.builtins[arg]; ok { fmt.Fprintf(s.Stdout, "%s is a shell builtin\n", arg) } else if val, ok := aliases[arg]; ok { fmt.Fprintf(s.Stdout, "%s is aliased to `%s`\n", arg, val) } else if _, ok := s.funcs[arg]; ok { fmt.Fprintf(s.Stdout, "%s is a function\n", arg) } else if path := findExecutable(arg); path != "" { fmt.Fprintf(s.Stdout, "%s is %s\n", arg, path) } else { fmt.Fprintf(s.Stdout, "%s: not found\n", arg) } } return nil } func (s *Shell) builtinTest(args []string) error { // Strip trailing ] when invoked as [ if len(args) > 0 && args[len(args)-1] == "]" { args = args[:len(args)-1] } if s.evalTest(args) { return nil } return exitCodeErr{1} } func (s *Shell) evalTest(args []string) bool { if len(args) == 0 { return false } // ! negation if args[0] == "!" { return !s.evalTest(args[1:]) } // Compound: -a and -o for i, arg := range args { if arg == "-a" { return s.evalTest(args[:i]) && s.evalTest(args[i+1:]) } if arg == "-o" { return s.evalTest(args[:i]) || s.evalTest(args[i+1:]) } } // Unary if len(args) == 2 { val := args[1] switch args[0] { case "-f": info, err := os.Stat(val) return err == nil && info.Mode().IsRegular() case "-d": info, err := os.Stat(val) return err == nil && info.IsDir() case "-e": _, err := os.Stat(val) return err == nil case "-L", "-h": _, err := os.Lstat(val) return err == nil case "-r": f, err := os.Open(val) if err != nil { return false } f.Close() return true case "-w": f, err := os.OpenFile(val, os.O_WRONLY, 0) if err != nil { return false } f.Close() return true case "-x": info, err := os.Stat(val) return err == nil && (info.Mode()&0111 != 0 || runtime.GOOS == "windows") case "-s": info, err := os.Stat(val) return err == nil && info.Size() > 0 case "-z": return val == "" case "-n": return val != "" } } // Binary if len(args) == 3 { lhs, op, rhs := args[0], args[1], args[2] switch op { case "=", "==": return lhs == rhs case "!=": return lhs != rhs case "<": return lhs < rhs case ">": return lhs > rhs case "-eq": return toInt(lhs) == toInt(rhs) case "-ne": return toInt(lhs) != toInt(rhs) case "-lt": return toInt(lhs) < toInt(rhs) case "-le": return toInt(lhs) <= toInt(rhs) case "-gt": return toInt(lhs) > toInt(rhs) case "-ge": return toInt(lhs) >= toInt(rhs) case "-ef": // Same file i1, e1 := os.Stat(lhs) i2, e2 := os.Stat(rhs) return e1 == nil && e2 == nil && os.SameFile(i1, i2) case "-nt": i1, e1 := os.Stat(lhs) i2, e2 := os.Stat(rhs) return e1 == nil && e2 == nil && i1.ModTime().After(i2.ModTime()) case "-ot": i1, e1 := os.Stat(lhs) i2, e2 := os.Stat(rhs) return e1 == nil && e2 == nil && i1.ModTime().Before(i2.ModTime()) } } // Single arg: true if non-empty if len(args) == 1 { return args[0] != "" } return false } func toInt(s string) int { n, _ := strconv.Atoi(strings.TrimSpace(s)) return n } func (s *Shell) builtinRead(args []string) error { prompt := "" silent := false rawMode := false varNames := []string{"REPLY"} i := 0 for i < len(args) { switch args[i] { case "-r": rawMode = true case "-s": silent = true case "-p": i++ if i < len(args) { prompt = args[i] } default: if !strings.HasPrefix(args[i], "-") { varNames = args[i:] i = len(args) continue } } i++ } _ = silent // terminal raw mode for passwords would need platform code if prompt != "" { fmt.Fprint(s.Stderr, prompt) } reader := bufio.NewReader(s.Stdin) line, err := reader.ReadString('\n') if err != nil && line == "" { return err } line = strings.TrimRight(line, "\r\n") if !rawMode { line = strings.ReplaceAll(line, "\\\n", " ") } if len(varNames) == 1 { s.vars[varNames[0]] = line } else { parts := strings.Fields(line) for j, name := range varNames { if j < len(parts) { if j == len(varNames)-1 { s.vars[name] = strings.Join(parts[j:], " ") } else { s.vars[name] = parts[j] } } else { s.vars[name] = "" } } } return nil } func (s *Shell) builtinPrintf(args []string) error { if len(args) == 0 { return nil } format := args[0] fmtArgs := args[1:] var result strings.Builder argIdx := 0 i := 0 for i < len(format) { if format[i] == '%' && i+1 < len(format) { i++ // Optional width/precision specStart := i for i < len(format) && (format[i] == '-' || format[i] == '0' || (format[i] >= '1' && format[i] <= '9') || format[i] == '.') { i++ } spec := format[specStart:i] if i >= len(format) { break } arg := "" if argIdx < len(fmtArgs) { arg = fmtArgs[argIdx] argIdx++ } switch format[i] { case 's': if spec != "" { result.WriteString(fmt.Sprintf("%-"+spec+"s", arg)) //nolint } else { result.WriteString(arg) } case 'd': n := toInt(arg) if spec != "" { result.WriteString(fmt.Sprintf("%"+spec+"d", n)) } else { result.WriteString(strconv.Itoa(n)) } case 'f': f, _ := strconv.ParseFloat(arg, 64) if spec != "" { result.WriteString(fmt.Sprintf("%"+spec+"f", f)) } else { result.WriteString(fmt.Sprintf("%f", f)) } case 'x': n := toInt(arg) result.WriteString(fmt.Sprintf("%x", n)) case 'o': n := toInt(arg) result.WriteString(fmt.Sprintf("%o", n)) case '%': result.WriteByte('%') argIdx-- // no arg consumed default: result.WriteByte('%') result.WriteByte(format[i]) argIdx-- } i++ } else if format[i] == '\\' && i+1 < len(format) { i++ switch format[i] { case 'n': result.WriteByte('\n') case 't': result.WriteByte('\t') case 'r': result.WriteByte('\r') case '\\': result.WriteByte('\\') case 'a': result.WriteByte('\a') case 'b': result.WriteByte('\b') default: result.WriteByte('\\') result.WriteByte(format[i]) } i++ } else { result.WriteByte(format[i]) i++ } } fmt.Fprint(s.Stdout, result.String()) return nil } func (s *Shell) builtinTrue(_ []string) error { return nil } func (s *Shell) builtinFalse(_ []string) error { return exitCodeErr{1} } func (s *Shell) builtinSet(args []string) error { if len(args) == 0 { keys := make([]string, 0, len(s.vars)) for k := range s.vars { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Fprintf(s.Stdout, "%s=%s\n", k, s.vars[k]) } return nil } i := 0 for i < len(args) { switch args[i] { case "--": s.SetArgs(args[i+1:]) return nil case "-e": s.errexit = true case "+e": s.errexit = false case "-u": s.nounset = true case "+u": s.nounset = false case "-x": // xtrace — ignore case "+x": // xtrace off — ignore case "-o": if i+1 < len(args) { switch args[i+1] { case "pipefail": s.pipefail = true i++ case "errexit": s.errexit = true i++ case "nounset": s.nounset = true i++ } } case "+o": if i+1 < len(args) { i++ // ignore value } default: // Combined flags like -euo if strings.HasPrefix(args[i], "-") { for _, c := range args[i][1:] { switch c { case 'e': s.errexit = true case 'u': s.nounset = true case 'x': // xtrace — ignore } } } } i++ } return nil } func (s *Shell) builtinUnset(args []string) error { for _, arg := range args { delete(s.vars, arg) os.Unsetenv(arg) delete(s.funcs, arg) delete(s.builtins, arg) } return nil } func (s *Shell) builtinEnv(args []string) error { if len(args) == 0 { for _, env := range os.Environ() { fmt.Fprintln(s.Stdout, env) } return nil } // env VAR=val command // Just execute the command with the vars set return s.Execute(strings.Join(args, " ")) } func (s *Shell) builtinWhich(args []string) error { found := false for _, arg := range args { if p := findExecutable(arg); p != "" { fmt.Fprintln(s.Stdout, p) found = true } else if _, ok := s.builtins[arg]; ok { fmt.Fprintf(s.Stdout, "%s: shell builtin\n", arg) found = true } else { fmt.Fprintf(s.Stderr, "%s not found\n", arg) } } if !found { return exitCodeErr{1} } return nil } func (s *Shell) builtinReturn(args []string) error { code := 0 if len(args) > 0 { fmt.Sscanf(args[0], "%d", &code) } else { fmt.Sscanf(s.vars["?"], "%d", &code) } return returnErr{code} } func (s *Shell) builtinBreak(args []string) error { n := 1 if len(args) > 0 { fmt.Sscanf(args[0], "%d", &n) } return breakErr{n} } func (s *Shell) builtinContinue(args []string) error { n := 1 if len(args) > 0 { fmt.Sscanf(args[0], "%d", &n) } return continueErr{n} } func (s *Shell) builtinShift(args []string) error { n := 1 if len(args) > 0 { fmt.Sscanf(args[0], "%d", &n) } if n > len(s.args) { n = len(s.args) } s.SetArgs(s.args[n:]) return nil } func (s *Shell) builtinDeclare(args []string) error { // Parse flags nameref := false isArray := false isExport := false nonFlagStart := 0 for i, arg := range args { if !strings.HasPrefix(arg, "-") { nonFlagStart = i break } nonFlagStart = i + 1 for _, ch := range arg[1:] { switch ch { case 'n': nameref = true case 'a': isArray = true case 'i': // integer — treat as scalar case 'r': // readonly — ignore case 'x': isExport = true case 'g': // global — ignore } } } for _, arg := range args[nonFlagStart:] { if strings.HasPrefix(arg, "-") { continue } if idx := strings.Index(arg, "="); idx > 0 { name := arg[:idx] if !isValidIdentifier(name) { continue } value := s.expandWord(arg[idx+1:]) if nameref { s.namerefs[name] = value } else if isArray { if strings.HasPrefix(value, "(") && strings.HasSuffix(value, ")") { inner := value[1 : len(value)-1] elems := s.tokenize(inner) s.setArray(name, elems) } else { s.setArray(name, []string{value}) } } else { s.vars[name] = value if isExport { os.Setenv(name, value) } } } else { // No = — just declare if !isValidIdentifier(arg) { continue } if isArray { if s.arrays[arg] == nil { s.arrays[arg] = []string{} } } } } return nil } func (s *Shell) builtinCommand(args []string) error { if len(args) > 0 && args[0] == "-v" { found := true for _, name := range args[1:] { if _, ok := s.builtins[name]; ok { fmt.Fprintln(s.Stdout, name) } else if p := findExecutable(name); p != "" { fmt.Fprintln(s.Stdout, p) } else { found = false fmt.Fprintf(s.Stderr, "bash: command not found: %s\n", name) } } if !found { return exitCodeErr{1} } return nil } if len(args) == 0 { return nil } // Skip aliases/functions, go straight to external return s.executeExternal(args[0], args[1:]) } func (s *Shell) builtinJobs(_ []string) error { // Basic stub — full job control would require tracking background pids return nil } func (s *Shell) builtinDisown(_ []string) error { return nil } func (s *Shell) builtinMktemp(args []string) error { template := "" for _, a := range args { if !strings.HasPrefix(a, "-") { template = a break } } // Use Windows temp dir regardless of the /tmp/ prefix in template tmpDir := os.Getenv("TEMP") if tmpDir == "" { tmpDir = os.Getenv("TMP") } if tmpDir == "" { tmpDir = os.TempDir() } // Extract the base pattern (strip directory prefix) base := filepath.Base(template) if base == "" || base == "." { base = "tmp.XXXXXX" } // Replace trailing X's with * for os.CreateTemp xCount := 0 for i := len(base) - 1; i >= 0 && base[i] == 'X'; i-- { xCount++ } goPattern := base if xCount > 0 { goPattern = base[:len(base)-xCount] + "*" } f, err := os.CreateTemp(tmpDir, goPattern) if err != nil { return fmt.Errorf("mktemp: %v", err) } f.Close() fmt.Fprintln(s.Stdout, f.Name()) return nil } func (s *Shell) builtinUname(args []string) error { showSys := len(args) == 0 showRelease := false for _, a := range args { switch a { case "-s": showSys = true case "-r": showRelease = true case "-a": showSys = true showRelease = true } } if showSys { fmt.Fprintln(s.Stdout, "Windows_NT") } if showRelease { kernelRel := os.Getenv("OS_VERSION") if kernelRel == "" { kernelRel = "10.0" } fmt.Fprintln(s.Stdout, kernelRel) } return nil } func (s *Shell) builtinWhoami(_ []string) error { user := os.Getenv("USERNAME") if user == "" { user = os.Getenv("USER") } if user == "" { user = "unknown" } fmt.Fprintln(s.Stdout, user) return nil } func (s *Shell) builtinHostname(_ []string) error { h, err := os.Hostname() if err != nil { h = os.Getenv("COMPUTERNAME") } if h == "" { h = "unknown" } fmt.Fprintln(s.Stdout, h) return nil } func (s *Shell) builtinMapfile(args []string) error { varName := "" trimNewlines := false for _, a := range args { if a == "-t" { trimNewlines = true } else if !strings.HasPrefix(a, "-") { varName = a } } if varName == "" { varName = "MAPFILE" } var lines []string scanner := bufio.NewScanner(s.Stdin) for scanner.Scan() { line := scanner.Text() if !trimNewlines { line += "\n" } lines = append(lines, line) } s.setArray(varName, lines) return nil } // builtinDoubleBracket implements [[ ... ]] func (s *Shell) builtinDoubleBracket(args []string) error { // Strip trailing ]] if present if len(args) > 0 && args[len(args)-1] == "]]" { args = args[:len(args)-1] } if !s.evalDB(args) { return exitCodeErr{1} } return nil } // evalDB evaluates a [[ ... ]] expression. func (s *Shell) evalDB(args []string) bool { if len(args) == 0 { return false } // 1. || — lowest precedence, scan right-to-left for i := len(args) - 1; i >= 0; i-- { if args[i] == "||" { return s.evalDB(args[:i]) || s.evalDB(args[i+1:]) } } // 2. && for i := len(args) - 1; i >= 0; i-- { if args[i] == "&&" { return s.evalDB(args[:i]) && s.evalDB(args[i+1:]) } } // 3. ! prefix if args[0] == "!" { return !s.evalDB(args[1:]) } // 4. Unary flags if len(args) == 2 { val := args[1] switch args[0] { case "-f": info, err := os.Stat(val) return err == nil && info.Mode().IsRegular() case "-e": _, err := os.Stat(val) return err == nil case "-d": info, err := os.Stat(val) return err == nil && info.IsDir() case "-s": info, err := os.Stat(val) return err == nil && info.Size() > 0 case "-r": f, err := os.Open(val) if err != nil { return false } f.Close() return true case "-w": f, err := os.OpenFile(val, os.O_WRONLY, 0) if err != nil { return false } f.Close() return true case "-x": info, err := os.Stat(val) return err == nil && (info.Mode()&0111 != 0 || runtime.GOOS == "windows") case "-n": return val != "" case "-z": return val == "" case "-L": _, err := os.Lstat(val) return err == nil case "-v": _, ok := s.vars[val] if !ok { _, ok = s.arrays[val] } return ok } } // 5. Binary operators if len(args) == 3 { lhs, op, rhs := args[0], args[1], args[2] switch op { case "==": matched, err := filepath.Match(rhs, lhs) if err != nil { return lhs == rhs } return matched case "!=": matched, err := filepath.Match(rhs, lhs) if err != nil { return lhs != rhs } return !matched case "=~": matched, err := regexp.MatchString(rhs, lhs) return err == nil && matched case "<": return lhs < rhs case ">": return lhs > rhs case "-eq": ln, le := strconv.Atoi(lhs) rn, re := strconv.Atoi(rhs) if le != nil || re != nil { return false } return ln == rn case "-ne": ln, le := strconv.Atoi(lhs) rn, re := strconv.Atoi(rhs) if le != nil || re != nil { return false } return ln != rn case "-lt": ln, le := strconv.Atoi(lhs) rn, re := strconv.Atoi(rhs) if le != nil || re != nil { return false } return ln < rn case "-le": ln, le := strconv.Atoi(lhs) rn, re := strconv.Atoi(rhs) if le != nil || re != nil { return false } return ln <= rn case "-gt": ln, le := strconv.Atoi(lhs) rn, re := strconv.Atoi(rhs) if le != nil || re != nil { return false } return ln > rn case "-ge": ln, le := strconv.Atoi(lhs) rn, re := strconv.Atoi(rhs) if le != nil || re != nil { return false } return ln >= rn } } // 6. Single arg truthy test if len(args) == 1 { return args[0] != "" } return false } // ─── Coreutils ──────────────────────────────────────────────────────────────── func (s *Shell) cmdLs(args []string) error { path := "." showAll := false longFormat := false humanReadable := false for _, arg := range args { if strings.HasPrefix(arg, "-") { for _, ch := range arg[1:] { switch ch { case 'a', 'A': showAll = true case 'l': longFormat = true case 'h': humanReadable = true } } } else { path = arg } } entries, err := os.ReadDir(path) if err != nil { return fmt.Errorf("ls: %v", err) } for _, entry := range entries { name := entry.Name() if !showAll && strings.HasPrefix(name, ".") { continue } if longFormat { info, _ := entry.Info() if info != nil { size := info.Size() sizeStr := "" if humanReadable { sizeStr = humanSize(size) } else { sizeStr = fmt.Sprintf("%8d", size) } modTime := info.ModTime().Format("Jan _2 15:04") fmt.Fprintf(s.Stdout, "%s %s %s %s\n", info.Mode(), sizeStr, modTime, name) } } else { fmt.Fprintln(s.Stdout, name) } } return nil } func humanSize(n int64) string { units := []string{"B", "K", "M", "G", "T"} f := float64(n) for _, u := range units { if f < 1024 { return fmt.Sprintf("%5.1f%s", f, u) } f /= 1024 } return fmt.Sprintf("%5.1fP", f) } func (s *Shell) cmdCat(args []string) error { if len(args) == 0 { _, err := io.Copy(s.Stdout, s.Stdin) return err } for _, path := range args { if path == "-" { io.Copy(s.Stdout, s.Stdin) continue } data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("cat: %s: %v", path, err) } fmt.Fprint(s.Stdout, string(data)) } return nil } func (s *Shell) cmdGrep(args []string) error { ignoreCase := false invertMatch := false lineNumber := false countOnly := false quiet := false var patterns []string var files []string i := 0 for i < len(args) { arg := args[i] if strings.HasPrefix(arg, "-") && arg != "-" { for _, ch := range arg[1:] { switch ch { case 'i': ignoreCase = true case 'v': invertMatch = true case 'n': lineNumber = true case 'c': countOnly = true case 'q': quiet = true case 'e': i++ if i < len(args) { patterns = append(patterns, args[i]) } } } } else if len(patterns) == 0 { patterns = append(patterns, arg) } else { files = append(files, arg) } i++ } if len(patterns) == 0 { return fmt.Errorf("grep: no pattern given") } var readers []io.Reader var names []string if len(files) == 0 { readers = append(readers, s.Stdin) names = append(names, "") } else { for _, f := range files { data, err := os.ReadFile(f) if err != nil { fmt.Fprintf(s.Stderr, "grep: %s: %v\n", f, err) continue } readers = append(readers, strings.NewReader(string(data))) names = append(names, f) } } matchCount := 0 for ri, r := range readers { name := names[ri] lineNum := 0 scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() lineNum++ matched := false for _, pat := range patterns { check, p := line, pat if ignoreCase { check = strings.ToLower(check) p = strings.ToLower(p) } if strings.Contains(check, p) { matched = true break } } if invertMatch { matched = !matched } if matched { matchCount++ if quiet { return nil } if !countOnly { prefix := "" if name != "" && len(files) > 1 { prefix = name + ":" } if lineNumber { prefix += fmt.Sprintf("%d:", lineNum) } fmt.Fprintln(s.Stdout, prefix+line) } } } if countOnly { fmt.Fprintln(s.Stdout, matchCount) } } if quiet && matchCount == 0 { return exitCodeErr{1} } if matchCount == 0 && !countOnly { return exitCodeErr{1} } return nil } func (s *Shell) cmdHead(args []string) error { n := 10 var files []string i := 0 for i < len(args) { switch { case args[i] == "-n" && i+1 < len(args): i++ fmt.Sscanf(args[i], "%d", &n) case strings.HasPrefix(args[i], "-n"): fmt.Sscanf(args[i][2:], "%d", &n) case strings.HasPrefix(args[i], "-") && len(args[i]) > 1: fmt.Sscanf(args[i][1:], "%d", &n) default: files = append(files, args[i]) } i++ } readHead := func(r io.Reader, name string) error { scanner := bufio.NewScanner(r) count := 0 for scanner.Scan() { if count >= n { break } fmt.Fprintln(s.Stdout, scanner.Text()) count++ } return nil } if len(files) == 0 { return readHead(s.Stdin, "") } for _, f := range files { if len(files) > 1 { fmt.Fprintf(s.Stdout, "==> %s <==\n", f) } fh, err := os.Open(f) if err != nil { return fmt.Errorf("head: %s: %v", f, err) } readHead(fh, f) fh.Close() } return nil } func (s *Shell) cmdTail(args []string) error { n := 10 var files []string i := 0 for i < len(args) { switch { case args[i] == "-n" && i+1 < len(args): i++ fmt.Sscanf(args[i], "%d", &n) case strings.HasPrefix(args[i], "-n"): fmt.Sscanf(args[i][2:], "%d", &n) case strings.HasPrefix(args[i], "-") && len(args[i]) > 1: fmt.Sscanf(args[i][1:], "%d", &n) default: files = append(files, args[i]) } i++ } readTail := func(r io.Reader) error { scanner := bufio.NewScanner(r) var lines []string for scanner.Scan() { lines = append(lines, scanner.Text()) } start := len(lines) - n if start < 0 { start = 0 } for _, l := range lines[start:] { fmt.Fprintln(s.Stdout, l) } return nil } if len(files) == 0 { return readTail(s.Stdin) } for _, f := range files { if len(files) > 1 { fmt.Fprintf(s.Stdout, "==> %s <==\n", f) } fh, err := os.Open(f) if err != nil { return fmt.Errorf("tail: %s: %v", f, err) } readTail(fh) fh.Close() } return nil } func (s *Shell) cmdSort(args []string) error { reverse := false unique := false numeric := false var files []string for _, arg := range args { if strings.HasPrefix(arg, "-") { for _, ch := range arg[1:] { switch ch { case 'r': reverse = true case 'u': unique = true case 'n': numeric = true } } } else { files = append(files, arg) } } var lines []string readLines := func(r io.Reader) { scanner := bufio.NewScanner(r) for scanner.Scan() { lines = append(lines, scanner.Text()) } } if len(files) == 0 { readLines(s.Stdin) } else { for _, f := range files { fh, err := os.Open(f) if err != nil { fmt.Fprintf(s.Stderr, "sort: %s: %v\n", f, err) continue } readLines(fh) fh.Close() } } if numeric { sort.Slice(lines, func(i, j int) bool { ni := toInt(lines[i]) nj := toInt(lines[j]) if reverse { return ni > nj } return ni < nj }) } else { sort.Strings(lines) if reverse { for l, r := 0, len(lines)-1; l < r; l, r = l+1, r-1 { lines[l], lines[r] = lines[r], lines[l] } } } seen := map[string]bool{} for _, l := range lines { if unique { if seen[l] { continue } seen[l] = true } fmt.Fprintln(s.Stdout, l) } return nil } func (s *Shell) cmdWc(args []string) error { countLines := true countWords := true countBytes := true var files []string for _, arg := range args { if strings.HasPrefix(arg, "-") { countLines = strings.Contains(arg, "l") countWords = strings.Contains(arg, "w") countBytes = strings.Contains(arg, "c") } else { files = append(files, arg) } } doWc := func(r io.Reader, name string) { data, _ := io.ReadAll(r) content := string(data) l := strings.Count(content, "\n") w := len(strings.Fields(content)) c := len(data) out := "" if countLines { out += fmt.Sprintf("%8d", l) } if countWords { out += fmt.Sprintf("%8d", w) } if countBytes { out += fmt.Sprintf("%8d", c) } if name != "" { out += " " + name } fmt.Fprintln(s.Stdout, out) } if len(files) == 0 { doWc(s.Stdin, "") return nil } for _, f := range files { fh, err := os.Open(f) if err != nil { fmt.Fprintf(s.Stderr, "wc: %s: %v\n", f, err) continue } doWc(fh, f) fh.Close() } return nil } func (s *Shell) cmdFind(args []string) error { root := "." name := "" typeFlag := "" maxDepth := -1 for i := 0; i < len(args); i++ { switch args[i] { case "-name": if i+1 < len(args) { i++ name = args[i] } case "-type": if i+1 < len(args) { i++ typeFlag = args[i] } case "-maxdepth": if i+1 < len(args) { i++ fmt.Sscanf(args[i], "%d", &maxDepth) } default: if !strings.HasPrefix(args[i], "-") { root = args[i] } } } rootAbs, _ := filepath.Abs(root) return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } // Check maxdepth if maxDepth >= 0 { rel, _ := filepath.Rel(rootAbs, path) depth := strings.Count(rel, string(os.PathSeparator)) if depth > maxDepth { if info.IsDir() { return filepath.SkipDir } return nil } } if name != "" { matched, _ := filepath.Match(name, info.Name()) if !matched { return nil } } if typeFlag != "" { switch typeFlag { case "f": if !info.Mode().IsRegular() { return nil } case "d": if !info.IsDir() { return nil } case "l": if info.Mode()&os.ModeSymlink == 0 { return nil } } } fmt.Fprintln(s.Stdout, path) return nil }) } func (s *Shell) cmdCp(args []string) error { recursive := false var sources []string dest := "" for _, arg := range args { if arg == "-r" || arg == "-R" || arg == "-rf" { recursive = true } else { sources = append(sources, arg) } } if len(sources) < 2 { return fmt.Errorf("cp: missing destination") } dest = sources[len(sources)-1] sources = sources[:len(sources)-1] for _, src := range sources { info, err := os.Stat(src) if err != nil { return fmt.Errorf("cp: %v", err) } dstPath := dest if dstInfo, err := os.Stat(dest); err == nil && dstInfo.IsDir() { dstPath = filepath.Join(dest, filepath.Base(src)) } if info.IsDir() { if !recursive { return fmt.Errorf("cp: %s: is a directory (use -r)", src) } if err := copyDir(src, dstPath); err != nil { return fmt.Errorf("cp: %v", err) } } else { if err := copyFile(src, dstPath); err != nil { return fmt.Errorf("cp: %v", err) } } } return nil } func copyFile(src, dst string) error { data, err := os.ReadFile(src) if err != nil { return err } info, _ := os.Stat(src) perm := os.FileMode(0644) if info != nil { perm = info.Mode() } return os.WriteFile(dst, data, perm) } func copyDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, _ := filepath.Rel(src, path) target := filepath.Join(dst, rel) if info.IsDir() { return os.MkdirAll(target, info.Mode()) } return copyFile(path, target) }) } func (s *Shell) cmdMv(args []string) error { if len(args) < 2 { return fmt.Errorf("mv: missing destination") } src, dst := args[0], args[1] if dstInfo, err := os.Stat(dst); err == nil && dstInfo.IsDir() { dst = filepath.Join(dst, filepath.Base(src)) } return os.Rename(src, dst) } func (s *Shell) cmdRm(args []string) error { recursive := false force := false var targets []string for _, arg := range args { if strings.HasPrefix(arg, "-") { if strings.Contains(arg, "r") || strings.Contains(arg, "R") { recursive = true } if strings.Contains(arg, "f") { force = true } } else { targets = append(targets, arg) } } for _, t := range targets { var err error if recursive { err = os.RemoveAll(t) } else { err = os.Remove(t) } if err != nil && !force { return fmt.Errorf("rm: %v", err) } } return nil } func (s *Shell) cmdMkdir(args []string) error { parents := false var dirs []string for _, arg := range args { if arg == "-p" { parents = true } else { dirs = append(dirs, arg) } } for _, dir := range dirs { var err error if parents { err = os.MkdirAll(dir, 0755) } else { err = os.Mkdir(dir, 0755) } if err != nil { return fmt.Errorf("mkdir: %v", err) } } return nil } func (s *Shell) cmdTouch(args []string) error { for _, path := range args { if _, err := os.Stat(path); err == nil { now := time.Now() os.Chtimes(path, now, now) } else { f, err := os.Create(path) if err != nil { return fmt.Errorf("touch: %v", err) } f.Close() } } return nil } func (s *Shell) cmdClear(_ []string) error { fmt.Fprint(s.Stdout, "\033[H\033[2J") return nil } func (s *Shell) cmdCut(args []string) error { delimiter := "\t" var fields []int var files []string i := 0 for i < len(args) { switch { case args[i] == "-d" && i+1 < len(args): i++ delimiter = args[i] case strings.HasPrefix(args[i], "-d"): delimiter = args[i][2:] case args[i] == "-f" && i+1 < len(args): i++ for _, part := range strings.Split(args[i], ",") { if n := toInt(part); n > 0 { fields = append(fields, n-1) // 0-indexed } } case strings.HasPrefix(args[i], "-f"): for _, part := range strings.Split(args[i][2:], ",") { if n := toInt(part); n > 0 { fields = append(fields, n-1) } } default: files = append(files, args[i]) } i++ } doCut := func(r io.Reader) { scanner := bufio.NewScanner(r) for scanner.Scan() { line := scanner.Text() parts := strings.Split(line, delimiter) var out []string for _, f := range fields { if f < len(parts) { out = append(out, parts[f]) } } fmt.Fprintln(s.Stdout, strings.Join(out, delimiter)) } } if len(files) == 0 { doCut(s.Stdin) return nil } for _, f := range files { fh, err := os.Open(f) if err != nil { return fmt.Errorf("cut: %s: %v", f, err) } doCut(fh) fh.Close() } return nil } func (s *Shell) cmdTr(args []string) error { if len(args) < 1 { return fmt.Errorf("tr: missing operand") } deleteMode := false squeezeMode := false var rawSet1, rawSet2 string i := 0 for i < len(args) { switch args[i] { case "-d": deleteMode = true case "-s": squeezeMode = true default: if rawSet1 == "" { rawSet1 = args[i] } else { rawSet2 = args[i] } } i++ } set1 := expandTrSet(rawSet1) set2 := expandTrSet(rawSet2) data, _ := io.ReadAll(s.Stdin) result := string(data) if deleteMode { set := map[rune]bool{} for _, ch := range set1 { set[ch] = true } var out strings.Builder for _, ch := range result { if !set[ch] { out.WriteRune(ch) } } result = out.String() } else if len(set2) > 0 { var out strings.Builder prevChar := rune(0) for _, ch := range result { idx := -1 for j, c := range set1 { if c == ch { idx = j break } } if idx >= 0 { newCh := set2[idx] if idx >= len(set2) { newCh = set2[len(set2)-1] // pad with last char } if squeezeMode && newCh == prevChar { continue } out.WriteRune(newCh) prevChar = newCh } else { out.WriteRune(ch) prevChar = ch } } result = out.String() } fmt.Fprint(s.Stdout, result) return nil } // expandTrSet expands a tr character set string, handling a-z ranges and \n, \t etc. func expandTrSet(s string) []rune { var result []rune i := 0 runes := []rune(s) for i < len(runes) { if runes[i] == '\\' && i+1 < len(runes) { i++ switch runes[i] { case 'n': result = append(result, '\n') case 't': result = append(result, '\t') case 'r': result = append(result, '\r') default: result = append(result, runes[i]) } i++ } else if i+2 < len(runes) && runes[i+1] == '-' { // Range like a-z from, to := runes[i], runes[i+2] for c := from; c <= to; c++ { result = append(result, c) } i += 3 } else { result = append(result, runes[i]) i++ } } return result } func (s *Shell) cmdUniq(args []string) error { countMode := false duplicateMode := false uniqueMode := false ignoreCase := false var files []string for _, arg := range args { if strings.HasPrefix(arg, "-") { for _, ch := range arg[1:] { switch ch { case 'c': countMode = true case 'd': duplicateMode = true case 'u': uniqueMode = true case 'i': ignoreCase = true } } } else { files = append(files, arg) } } doUniq := func(r io.Reader) { scanner := bufio.NewScanner(r) var lines []string for scanner.Scan() { lines = append(lines, scanner.Text()) } i := 0 for i < len(lines) { line := lines[i] count := 1 key := line if ignoreCase { key = strings.ToLower(key) } for i+count < len(lines) { nextKey := lines[i+count] if ignoreCase { nextKey = strings.ToLower(nextKey) } if nextKey != key { break } count++ } show := true if duplicateMode && count == 1 { show = false } if uniqueMode && count > 1 { show = false } if show { if countMode { fmt.Fprintf(s.Stdout, "%7d %s\n", count, line) } else { fmt.Fprintln(s.Stdout, line) } } i += count } } if len(files) == 0 { doUniq(s.Stdin) return nil } for _, f := range files { fh, err := os.Open(f) if err != nil { return fmt.Errorf("uniq: %s: %v", f, err) } doUniq(fh) fh.Close() } return nil } func (s *Shell) cmdTee(args []string) error { appendMode := false var files []string for _, arg := range args { if arg == "-a" { appendMode = true } else { files = append(files, arg) } } var writers []io.Writer writers = append(writers, s.Stdout) var toClose []io.Closer for _, f := range files { var fh *os.File var err error if appendMode { fh, err = os.OpenFile(f, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) } else { fh, err = os.Create(f) } if err != nil { fmt.Fprintf(s.Stderr, "tee: %s: %v\n", f, err) continue } writers = append(writers, fh) toClose = append(toClose, fh) } defer func() { for _, c := range toClose { c.Close() } }() mw := io.MultiWriter(writers...) _, err := io.Copy(mw, s.Stdin) return err } func (s *Shell) cmdDate(args []string) error { format := time.RFC3339 now := time.Now() for _, arg := range args { if strings.HasPrefix(arg, "+") { // Convert strftime format to Go format format = convertDateFormat(arg[1:]) } } fmt.Fprintln(s.Stdout, now.Format(format)) return nil } func convertDateFormat(f string) string { replacements := map[string]string{ "%Y": "2006", "%m": "01", "%d": "02", "%H": "15", "%M": "04", "%S": "05", "%A": "Monday", "%a": "Mon", "%B": "January", "%b": "Jan", "%n": "\n", "%t": "\t", "%%": "%", } for k, v := range replacements { f = strings.ReplaceAll(f, k, v) } return f } func (s *Shell) cmdSleep(args []string) error { if len(args) == 0 { return fmt.Errorf("sleep: missing operand") } var total time.Duration for _, arg := range args { if strings.HasSuffix(arg, "m") { n, _ := strconv.ParseFloat(arg[:len(arg)-1], 64) total += time.Duration(n * float64(time.Minute)) } else if strings.HasSuffix(arg, "h") { n, _ := strconv.ParseFloat(arg[:len(arg)-1], 64) total += time.Duration(n * float64(time.Hour)) } else if strings.HasSuffix(arg, "s") { n, _ := strconv.ParseFloat(arg[:len(arg)-1], 64) total += time.Duration(n * float64(time.Second)) } else { n, _ := strconv.ParseFloat(arg, 64) total += time.Duration(n * float64(time.Second)) } } time.Sleep(total) return nil } func (s *Shell) cmdBasename(args []string) error { if len(args) == 0 { return fmt.Errorf("basename: missing operand") } result := filepath.Base(args[0]) if len(args) > 1 { result = strings.TrimSuffix(result, args[1]) } fmt.Fprintln(s.Stdout, result) return nil } func (s *Shell) cmdDirname(args []string) error { if len(args) == 0 { return fmt.Errorf("dirname: missing operand") } fmt.Fprintln(s.Stdout, filepath.Dir(args[0])) return nil } // cmdSed implements a very basic subset of sed: s/pattern/replacement/[g] func (s *Shell) cmdSed(args []string) error { var script string var files []string inPlace := false i := 0 for i < len(args) { switch { case args[i] == "-e" && i+1 < len(args): i++ script = args[i] case args[i] == "-i" || args[i] == "--in-place": inPlace = true case strings.HasPrefix(args[i], "-i"): inPlace = true case !strings.HasPrefix(args[i], "-") && script == "": script = args[i] default: files = append(files, args[i]) } i++ } if script == "" { return fmt.Errorf("sed: no script") } doSed := func(r io.Reader) (string, error) { scanner := bufio.NewScanner(r) var out strings.Builder for scanner.Scan() { line := scanner.Text() line = applySedScript(line, script) out.WriteString(line + "\n") } return out.String(), nil } if len(files) == 0 { result, err := doSed(s.Stdin) if err != nil { return err } fmt.Fprint(s.Stdout, result) return nil } for _, f := range files { fh, err := os.Open(f) if err != nil { return fmt.Errorf("sed: %s: %v", f, err) } result, err := doSed(fh) fh.Close() if err != nil { return err } if inPlace { os.WriteFile(f, []byte(result), 0644) } else { fmt.Fprint(s.Stdout, result) } } return nil } func applySedScript(line, script string) string { // Handle: s/pattern/replacement/ or s/pattern/replacement/g if !strings.HasPrefix(script, "s") { return line } if len(script) < 2 { return line } sep := string(script[1]) parts := strings.SplitN(script[2:], sep, 3) if len(parts) < 2 { return line } pattern := parts[0] replacement := parts[1] flags := "" if len(parts) > 2 { flags = parts[2] } if strings.Contains(flags, "g") { return strings.ReplaceAll(line, pattern, replacement) } return strings.Replace(line, pattern, replacement, 1) } func (s *Shell) cmdXargs(args []string) error { if len(args) == 0 { args = []string{"echo"} } scanner := bufio.NewScanner(s.Stdin) var inputArgs []string for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line != "" { inputArgs = append(inputArgs, strings.Fields(line)...) } } if len(inputArgs) == 0 { return nil } return s.executeCommand(strings.Join(args, " ") + " " + strings.Join(inputArgs, " ")) }