shell/run.go

231 lines
5.2 KiB
Go

package shell
import (
"apigo.cc/go/cast"
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"os/signal"
"runtime"
"strings"
"syscall"
"time"
)
// CommandResult 封装命令执行后的输出与错误状态
type CommandResult struct {
Stdout []byte
Stderr []byte
ExitCode int
ProcessErr error
}
// Options 定义执行时的配置,零值即默认行为
type Options struct {
Env []string
Stdin io.Reader
CatchSignal bool
Timeout time.Duration
Dir string
Verbose bool
OnStdout func(data []byte)
OnStderr func(data []byte)
}
// Run 执行命令并返回包含 stdout/stderr 的结果结构体
func Run(name string, args []string, opts *Options) (*CommandResult, error) {
var ctx context.Context
var cancel context.CancelFunc
if opts != nil && opts.Timeout > 0 {
ctx, cancel = context.WithTimeout(context.Background(), opts.Timeout)
defer cancel()
} else {
ctx = context.Background()
}
cmd := exec.CommandContext(ctx, name, args...)
var stdout, stderr bytes.Buffer
// 处理流式实时回调
var stdoutW io.Writer = &stdout
var stderrW io.Writer = &stderr
if opts != nil {
if opts.OnStdout != nil {
stdoutW = io.MultiWriter(stdoutW, writerFunc(opts.OnStdout))
}
if opts.OnStderr != nil {
stderrW = io.MultiWriter(stderrW, writerFunc(opts.OnStderr))
}
}
cmd.Stdout = stdoutW
cmd.Stderr = stderrW
if opts != nil {
if opts.Dir != "" {
cmd.Dir = opts.Dir
}
if opts.Verbose {
fmt.Println(Style(TextCyan, "Exec:", name, strings.Join(args, " ")))
}
if opts.Env != nil {
cmd.Env = append(os.Environ(), opts.Env...)
}
if opts.Stdin != nil {
cmd.Stdin = opts.Stdin
}
if opts.CatchSignal {
setupSignalHandling(cmd)
}
}
if runtime.GOOS != "windows" {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
err := cmd.Run()
res := &CommandResult{
Stdout: stdout.Bytes(),
Stderr: stderr.Bytes(),
ProcessErr: err,
}
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
res.ExitCode = -1
return res, fmt.Errorf("command timed out: %w", err)
} else if exitError, ok := err.(*exec.ExitError); ok {
res.ExitCode = exitError.ExitCode()
} else {
res.ExitCode = -1
}
return res, err
}
return res, nil
}
// InteractiveRun 直接将标准输入、输出和错误流绑定到进程
func InteractiveRun(name string, args []string) error {
cmd := exec.Command(name, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
type writerFunc func([]byte)
func (w writerFunc) Write(p []byte) (n int, err error) {
w(p)
return len(p), nil
}
// Pipeline 执行一组命令,每个命令由 name 和 args 组成
func Pipeline(commands [][]string, opts *Options) (*CommandResult, error) {
if len(commands) == 0 {
return nil, fmt.Errorf("no commands provided")
}
var cmds []*exec.Cmd
for _, cmdParts := range commands {
c := exec.Command(cmdParts[0], cmdParts[1:]...)
if opts != nil && opts.Dir != "" {
c.Dir = opts.Dir
}
cmds = append(cmds, c)
}
for i := 0; i < len(cmds)-1; i++ {
stdout, err := cmds[i].StdoutPipe()
if err != nil {
return nil, err
}
cmds[i+1].Stdin = stdout
}
var lastStdout bytes.Buffer
cmds[len(cmds)-1].Stdout = &lastStdout
for _, c := range cmds {
if err := c.Start(); err != nil {
return nil, err
}
}
for _, c := range cmds {
if err := c.Wait(); err != nil {
return &CommandResult{ExitCode: -1}, err
}
}
return &CommandResult{Stdout: lastStdout.Bytes()}, nil
}
// PipelineCommands 支持命令字符串切片
func PipelineCommands(commands []string, opts *Options) (*CommandResult, error) {
var cmds [][]string
for _, cmdStr := range commands {
cmds = append(cmds, cast.SplitArgs(cmdStr))
}
return Pipeline(cmds, opts)
}
// RunCommand 支持完整命令字符串(如 "ls -la | grep go" 或 "cmd1 && cmd2"),支持 pipe 和 AND 逻辑
func RunCommand(command string, opts *Options) (*CommandResult, error) {
// 处理 && 逻辑
if strings.Contains(command, "&&") {
parts := strings.Split(command, "&&")
var lastResult *CommandResult
var fullStdout []byte
for _, p := range parts {
res, err := RunCommand(strings.TrimSpace(p), opts)
if err != nil {
return res, err
}
lastResult = res
fullStdout = append(fullStdout, res.Stdout...)
}
if lastResult != nil {
lastResult.Stdout = fullStdout
}
return lastResult, nil
}
// 处理 | 逻辑
if strings.Contains(command, "|") {
parts := strings.Split(command, "|")
var cmds []string
for _, p := range parts {
cmds = append(cmds, strings.TrimSpace(p))
}
return PipelineCommands(cmds, opts)
}
args := cast.SplitArgs(command)
if len(args) == 0 {
return &CommandResult{ExitCode: -1}, fmt.Errorf("empty command")
}
return Run(args[0], args[1:], opts)
}
func setupSignalHandling(cmd *exec.Cmd) {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP)
go func() {
sig := <-sigCh
if cmd.Process != nil {
if runtime.GOOS == "windows" {
_ = cmd.Process.Kill()
} else {
pgid, err := syscall.Getpgid(cmd.Process.Pid)
if err == nil {
_ = syscall.Kill(-pgid, sig.(syscall.Signal))
}
}
}
}()
}