package shelly import ( "errors" "fmt" "log" "os/exec" "strings" "sync" ) type Mode int func (m Mode) String() string { switch m { case MODE_NORMAL: return "NORMAL" case MODE_STR: return "STR" case MODE_ESCAPE: return "ESCAPE" case MODE_ESCAPE_STR: return "ESCAPE_STR" } return "UNKNOWN" } const ( MODE_NORMAL Mode = iota MODE_STR MODE_ESCAPE MODE_ESCAPE_STR ) type LineType int const ( LINETYPE_SERIAL LineType = iota LINETYPE_PARALLEL LINETYPE_COMMENT ) // Opts tells Exec how to behave // Opts.Await will inform whether its call should wait for all parallel started jobs to finish // Opts.SetupProc will customize the processes before execution - Eg.: setting out and err type Opts struct { Await bool SetupProc func(cmd *exec.Cmd) } // Line represents a line to be executed by the shell. // Can be separated by \n our ; - in case of serial execution. & will be used to identify parallel execution type Line struct { Tokens []string LType LineType } // Tokens will break the string into tokens - It can be later organized in command lines and alikes func Tokens(str string) ([]string, error) { params := make([]string, 0) sb := strings.Builder{} mode := MODE_NORMAL for i, a := range str { switch a { case ' ', '\t', '\r': switch mode { case MODE_NORMAL: if sb.Len() > 0 { params = append(params, sb.String()) } sb = strings.Builder{} case MODE_STR: sb.WriteRune(a) default: return nil, errors.New(fmt.Sprintf("Error at char %d - cant add space here. Mode: %s", i, mode.String())) } case '\n': switch mode { case MODE_NORMAL: if sb.Len() > 0 { params = append(params, sb.String()) } params = append(params, "\n") sb = strings.Builder{} case MODE_STR: sb.WriteRune(a) default: return nil, errors.New(fmt.Sprintf("Error at char %d - cant add new line here. Mode: %s", i, mode.String())) } case '"': switch mode { case MODE_NORMAL: if sb.Len() > 0 { params = append(params, sb.String()) } sb = strings.Builder{} mode = MODE_STR case MODE_STR: params = append(params, sb.String()) sb = strings.Builder{} mode = MODE_NORMAL case MODE_ESCAPE: sb.WriteRune(a) mode = MODE_NORMAL case MODE_ESCAPE_STR: sb.WriteRune(a) mode = MODE_STR } case '\\': switch mode { case MODE_NORMAL: mode = MODE_ESCAPE case MODE_STR: mode = MODE_ESCAPE_STR case MODE_ESCAPE: sb.WriteString("\\") mode = MODE_NORMAL case MODE_ESCAPE_STR: sb.WriteString("\\") mode = MODE_STR } default: switch mode { case MODE_NORMAL: sb.WriteRune(a) case MODE_STR: sb.WriteRune(a) case MODE_ESCAPE: sb.WriteString("\\" + string(a)) mode = MODE_NORMAL case MODE_ESCAPE_STR: sb.WriteString("\\" + string(a)) mode = MODE_STR } } } if mode != MODE_NORMAL { return nil, errors.New("Unterminated String") } if sb.Len() > 0 { params = append(params, sb.String()) } return params, nil } // Lines will call Tokens first, and reorg them in executable lines func Lines(str string) ([]Line, error) { ret := make([]Line, 0) tokens, err := Tokens(str) if err != nil { return nil, err } line := Line{ Tokens: make([]string, 0), LType: LINETYPE_SERIAL, } for _, tk := range tokens { switch tk { case "\n", ";": if len(line.Tokens) > 0 { if strings.HasPrefix(line.Tokens[0], "#") { line.LType = LINETYPE_COMMENT } else { line.LType = LINETYPE_SERIAL } ret = append(ret, line) } line = Line{ Tokens: make([]string, 0), } case "&": if len(line.Tokens) > 0 { if strings.HasPrefix(line.Tokens[0], "#") { line.LType = LINETYPE_COMMENT } else { line.LType = LINETYPE_PARALLEL } ret = append(ret, line) } line = Line{ Tokens: make([]string, 0), } default: line.Tokens = append(line.Tokens, tk) } } if len(line.Tokens) > 0 { ret = append(ret, line) } return ret, nil } // Exec will call Lines and execute one by one func Exec(str string, opts ...*Opts) error { var opt *Opts if opts != nil && len(opts) > 0 { opt = opts[0] } if opt.SetupProc == nil { opt.SetupProc = func(cmd *exec.Cmd) { } } prepCmd := func(l Line) *exec.Cmd { cmd := exec.Command(l.Tokens[0], l.Tokens[1:]...) cmd.Stdout = log.Writer() cmd.Stderr = log.Writer() opt.SetupProc(cmd) return cmd } wg := sync.WaitGroup{} lines, err := Lines(str) if err != nil { return err } for _, l := range lines { switch l.LType { case LINETYPE_COMMENT: continue case LINETYPE_SERIAL: cmd := prepCmd(l) err = cmd.Run() if err != nil { return err } case LINETYPE_PARALLEL: go func(l Line) { cmd := prepCmd(l) wg.Add(1) err = cmd.Run() wg.Done() }(l) } } if opt.Await { wg.Wait() } return nil }