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  }