github.com/sohaha/zlsgo@v1.7.13-0.20240501141223-10dd1a906f76/zshell/shell.go (about) 1 // Package zshell use a simple way to execute shell commands 2 package zshell 3 4 import ( 5 "bufio" 6 "bytes" 7 "context" 8 "errors" 9 "fmt" 10 "io" 11 "os" 12 "os/exec" 13 "strings" 14 "syscall" 15 "time" 16 17 "github.com/sohaha/zlsgo/zstring" 18 ) 19 20 var ( 21 Debug = false 22 Env []string 23 Dir string 24 ) 25 26 type ShellBuffer struct { 27 writer io.Writer 28 buf *bytes.Buffer 29 } 30 31 func newShellStdBuffer(writer io.Writer) *ShellBuffer { 32 return &ShellBuffer{ 33 writer: writer, 34 buf: bytes.NewBuffer([]byte{}), 35 } 36 } 37 38 func (s *ShellBuffer) Write(p []byte) (n int, err error) { 39 n, err = s.buf.Write(p) 40 if s.writer != nil { 41 n, err = s.writer.Write(p) 42 } 43 return n, err 44 } 45 46 func (s *ShellBuffer) String() string { 47 return zstring.Bytes2String(s.buf.Bytes()) 48 } 49 50 func ExecCommandHandle(ctx context.Context, command []string, 51 bef func(cmd *exec.Cmd) error, aft func(cmd *exec.Cmd, err error)) (code int, 52 err error) { 53 var ( 54 isSuccess bool 55 status syscall.WaitStatus 56 ) 57 if len(command) == 0 || (len(command) == 1 && command[0] == "") { 58 return 1, errors.New("no such command") 59 } 60 61 cmd := exec.CommandContext(ctx, command[0], command[1:]...) 62 if Env == nil { 63 cmd.Env = os.Environ() 64 } else { 65 cmd.Env = Env 66 Env = nil 67 } 68 69 if Debug { 70 fmt.Println("[Command]:", strings.Join(command, " ")) 71 } 72 73 err = bef(cmd) 74 if err != nil { 75 return -1, err 76 } 77 78 err = cmd.Start() 79 if Debug { 80 defer func() { 81 var userTime time.Duration 82 if cmd != nil && cmd.ProcessState != nil { 83 userTime = cmd.ProcessState.UserTime() 84 } 85 if isSuccess { 86 fmt.Println("[OK]", status.ExitStatus(), " Used Time:", userTime) 87 } else { 88 fmt.Println("[Fail]", status.ExitStatus(), " Used Time:", userTime) 89 } 90 }() 91 } 92 93 if aft != nil { 94 aft(cmd, err) 95 } 96 97 if err != nil { 98 return -1, err 99 } 100 101 err = cmd.Wait() 102 103 code, isSuccess = cmdResult(cmd) 104 return code, err 105 } 106 107 func cmdResult(cmd *exec.Cmd) (code int, isSuccess bool) { 108 code = cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() 109 isSuccess = cmd.ProcessState.Success() 110 return 111 } 112 113 type pipeWork struct { 114 cmd *exec.Cmd 115 // r *io.PipeReader 116 w *io.PipeWriter 117 } 118 119 func PipeExecCommand(ctx context.Context, commands [][]string) (code int, outStr, errStr string, err error) { 120 var ( 121 cmds []*pipeWork 122 out bytes.Buffer 123 outErr bytes.Buffer 124 set func(r *io.PipeReader) 125 ) 126 set = func(r *io.PipeReader) { 127 if len(commands) == 0 { 128 return 129 } 130 command := commands[0] 131 commands = commands[1:] 132 cmd := exec.CommandContext(ctx, command[0], command[1:]...) 133 if Dir != "" { 134 cmd.Dir = Dir 135 } 136 if r != nil { 137 cmd.Stdin = r 138 } 139 p := &pipeWork{ 140 cmd: cmd, 141 } 142 if len(commands) == 0 { 143 cmd.Stdout = &out 144 cmd.Stderr = &outErr 145 } else { 146 r2, w2 := io.Pipe() 147 cmd.Stdout = w2 148 p.w = w2 149 set(r2) 150 } 151 cmds = append([]*pipeWork{p}, cmds...) 152 } 153 set(nil) 154 155 for _, v := range cmds { 156 err := v.cmd.Start() 157 if err != nil { 158 return 1, "", "", err 159 } 160 } 161 status := 0 162 for _, v := range cmds { 163 err := v.cmd.Wait() 164 if v.w != nil { 165 _ = v.w.Close() 166 } 167 waitStatus, _ := v.cmd.ProcessState.Sys().(syscall.WaitStatus) 168 status = waitStatus.ExitStatus() 169 if err != nil { 170 return status, "", "", err 171 } 172 } 173 174 return status, out.String(), "", nil 175 } 176 177 func ExecCommand(ctx context.Context, command []string, stdIn io.Reader, stdOut io.Writer, 178 stdErr io.Writer) (code int, outStr, errStr string, err error) { 179 stdout := newShellStdBuffer(stdOut) 180 stderr := newShellStdBuffer(stdErr) 181 code, err = ExecCommandHandle(ctx, command, func(cmd *exec.Cmd) error { 182 cmd.Stdout = stdout 183 cmd.Stdin = stdIn 184 cmd.Stderr = stderr 185 if Dir != "" { 186 cmd.Dir = Dir 187 } 188 return nil 189 }, nil) 190 outStr = stdout.String() 191 errStr = stderr.String() 192 return 193 } 194 195 func Run(command string) (code int, outStr, errStr string, err error) { 196 return RunContext(context.Background(), command) 197 } 198 199 func RunContext(ctx context.Context, command string) (code int, outStr, errStr string, err error) { 200 return ExecCommand(ctx, fixCommand(command), nil, nil, nil) 201 } 202 203 func OutRun(command string, stdIn io.Reader, stdOut io.Writer, 204 stdErr io.Writer) (code int, outStr, errStr string, err error) { 205 return ExecCommand(context.Background(), fixCommand(command), stdIn, stdOut, stdErr) 206 } 207 208 func BgRun(command string) (err error) { 209 return BgRunContext(context.Background(), command) 210 } 211 212 func BgRunContext(ctx context.Context, command string) (err error) { 213 if strings.TrimSpace(command) == "" { 214 return errors.New("no such command") 215 } 216 arr := fixCommand(command) 217 cmd := exec.CommandContext(ctx, arr[0], arr[1:]...) 218 err = cmd.Start() 219 if Debug { 220 fmt.Println("[Command]: ", command) 221 if err != nil { 222 fmt.Println("[Fail]", err.Error()) 223 } 224 } 225 go func() { 226 _ = cmd.Wait() 227 }() 228 return err 229 } 230 231 func CallbackRun(command string, callback func(out string, isBasic bool)) (<-chan int, func(string), error) { 232 return CallbackRunContext(context.Background(), command, callback) 233 } 234 235 type Options struct { 236 Dir string 237 Env []string 238 } 239 240 func CallbackRunContext(ctx context.Context, command string, callback func(str string, isStdout bool), opt ...func(option *Options)) (<-chan int, func(string), error) { 241 var ( 242 cmd *exec.Cmd 243 err error 244 code = make(chan int, 1) 245 ) 246 247 var in func(string) 248 read := func(stdout io.ReadCloser, isStdout bool) { 249 scanner := bufio.NewScanner(stdout) 250 for scanner.Scan() { 251 callback(scanner.Text(), isStdout) 252 } 253 } 254 255 _, err = ExecCommandHandle(ctx, fixCommand(command), func(c *exec.Cmd) error { 256 o := Options{} 257 for _, v := range opt { 258 v(&o) 259 } 260 if len(o.Env) > 0 { 261 c.Env = append(c.Env, o.Env...) 262 } 263 if o.Dir != "" { 264 c.Dir = o.Dir 265 } 266 cmd = c 267 stdin, err := c.StdinPipe() 268 if err != nil { 269 return err 270 } 271 in = func(s string) { 272 io.WriteString(stdin, s) 273 } 274 stdout, err := c.StdoutPipe() 275 if err != nil { 276 return err 277 } 278 go read(stdout, true) 279 stderr, err := c.StderrPipe() 280 if err != nil { 281 return err 282 } 283 go read(stderr, false) 284 return errors.New("") 285 }, nil) 286 287 if err.Error() == "" { 288 err = cmd.Start() 289 if err == nil { 290 go func() { 291 _ = cmd.Wait() 292 c, _ := cmdResult(cmd) 293 code <- c 294 }() 295 } else { 296 code <- -1 297 } 298 } 299 300 return code, in, err 301 } 302 303 func fixCommand(command string) (runCommand []string) { 304 var current []rune 305 quoted := false 306 quoteType := '\000' 307 escaped := false 308 for i, c := range command { 309 if escaped { 310 current = append(current, c) 311 escaped = false 312 } else if c == '\\' { 313 escaped = true 314 } else if c == '"' || c == '\'' { 315 if quoted && c == quoteType { 316 quoted = false 317 quoteType = '\000' 318 } else if !quoted { 319 quoted = true 320 quoteType = c 321 } 322 } else if c == ' ' && !quoted { 323 runCommand = append(runCommand, string(current)) 324 current = nil 325 } else { 326 current = append(current, c) 327 } 328 329 if i == len(command)-1 { 330 runCommand = append(runCommand, string(current)) 331 } 332 } 333 return 334 }