//go:build !windows // +build !windows // Author: NiuJiuRu // Email: niujiuru@qq.com package shell import ( "os" "path/filepath" "strings" "syscall" ) type Executor struct { cwd string // 当前目录 prevCwd string // 上次目录 pg *processGroup // 执行命令的进程组 } type PathError struct { Path string Info string } func (e *PathError) Error() string { return e.Info + ": " + e.Path } func NewExecutor() *Executor { return &Executor{cwd: "/"} } func (e *Executor) Exec(p ExecuteParams) (*ExecuteResult, error) { if e.isCD(p.Cmd) { dir, err := e.resolveCD(e.cwd, p.Cmd) if err != nil { return &ExecuteResult{ Stderr: err.Error(), ExitCode: 1, }, nil } e.prevCwd = e.cwd e.cwd = dir return &ExecuteResult{ExitCode: 0, Cwd: e.cwd}, nil } if strings.TrimSpace(p.Cmd) == "pwd" { return &ExecuteResult{ Stdout: e.cwd + "\n", ExitCode: 0, }, nil } p.Dir = e.cwd defer func() { e.pg = nil // 命令结束 }() result, err := executeInternal(p, func(pg *processGroup) { e.pg = pg }) if result != nil { result.Cwd = e.cwd } return result, err } func (e *Executor) isCD(cmd string) bool { s := strings.TrimSpace(cmd) return s == "cd" || strings.HasPrefix(s, "cd ") } func (e *Executor) resolveCD(cwd, cmd string) (string, error) { fields := strings.Fields(cmd) if len(fields) == 1 { // 只有"cd", 没有参数 return "/", nil } path := fields[1] switch path { case "~": home := os.Getenv("HOME") if home == "" { home = "/" } return home, nil case "-": if e.prevCwd == "" { return cwd, nil } return e.prevCwd, nil } var target string if filepath.IsAbs(path) { // 绝对路径 target = filepath.Clean(path) } else { // 相对路径 target = filepath.Clean(filepath.Join(cwd, path)) } info, err := os.Stat(target) if err != nil { if os.IsNotExist(err) { return "", &PathError{Path: target, Info: "directory not exist"} } return "", err } if !info.IsDir() { return "", &PathError{Path: target, Info: "not a directory"} } return target, nil } func (e *Executor) Interrupt() error { if e.pg == nil { return nil } return syscall.Kill(-e.pg.pgid, syscall.SIGINT) // 等价于 Ctrl+C }