2224 lines
44 KiB
Go
2224 lines
44 KiB
Go
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, " "))
|
|
}
|