package shelly import ( "errors" "fmt" "log" "os" "os/exec" "path/filepath" "runtime" "runtime/debug" "strings" ) 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" case MODE_TAG: return "MODE_TAG" case MODE_ESCAPE_TAG: return "MODE_ESCAPE_TAG" } return "UNKNOWN" } const ( MODE_NORMAL Mode = iota MODE_STR MODE_TAG MODE_ESCAPE MODE_ESCAPE_STR MODE_ESCAPE_TAG ) type LineType int const ( LINETYPE_SERIAL LineType = iota LINETYPE_PARALLEL LINETYPE_COMMENT LINETYPE_CHANGEWD ) // 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 { Debug bool Trace bool Await bool Wd string 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, MODE_TAG: sb.WriteRune(a) default: return nil, errors.New(fmt.Sprintf("Error at char %d - cant add space here. Mode: %s", i, mode.String())) } case '=': switch mode { case MODE_NORMAL: if sb.Len() > 0 { params = append(params, sb.String()) sb = strings.Builder{} } params = append(params, "=") case MODE_STR, MODE_ESCAPE_STR: sb.WriteRune(a) } 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 } mode = MODE_STR case MODE_TAG: sb.WriteRune(a) params = append(params, sb.String()) sb = strings.Builder{} mode = MODE_NORMAL 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_TAG: mode = MODE_ESCAPE_TAG case MODE_ESCAPE: sb.WriteString("\\") mode = MODE_NORMAL case MODE_ESCAPE_STR: sb.WriteString("\\") mode = MODE_STR } default: switch mode { case MODE_NORMAL, MODE_TAG, 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 case MODE_ESCAPE_TAG: sb.WriteString("\\" + string(a)) mode = MODE_TAG } } } 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, } addLine := func(line Line) { if strings.HasPrefix(line.Tokens[0], "#") { line.LType = LINETYPE_COMMENT } if line.Tokens[0] == "cd" { line.LType = LINETYPE_CHANGEWD } if runtime.GOOS == "windows" && (line.LType == LINETYPE_SERIAL || line.LType == LINETYPE_PARALLEL) { if !strings.HasSuffix(line.Tokens[0], ".exe") { line.Tokens[0] = line.Tokens[0] + ".exe" } } ret = append(ret, line) } for _, tk := range tokens { switch tk { case "\n", ";", "&&": if len(line.Tokens) > 0 { addLine(line) } line = Line{ Tokens: make([]string, 0), } case "&": if len(line.Tokens) > 0 { line.LType = LINETYPE_PARALLEL addLine(line) } line = Line{ Tokens: make([]string, 0), } default: if len(line.Tokens) > 2 && line.Tokens[len(line.Tokens)-1] == "=" { line.Tokens[len(line.Tokens)-2] = line.Tokens[len(line.Tokens)-2] + "=" + tk line.Tokens = line.Tokens[:len(line.Tokens)-1] } else { line.Tokens = append(line.Tokens, tk) } } } if len(line.Tokens) > 0 { addLine(line) } return ret, nil } // Exec will call Lines and execute one by one func Exec(str string, opts ...*Opts) ([]*exec.Cmd, error) { wd, err := os.Getwd() if err != nil { return nil, err } ret := make([]*exec.Cmd, 0) opt := &Opts{ Await: false, Wd: wd, SetupProc: func(cmd *exec.Cmd) { }, } if opts != nil && len(opts) > 0 { opt = opts[0] } if opt.SetupProc == nil { opt.SetupProc = func(cmd *exec.Cmd) { } } if opt.Trace { bs := debug.Stack() log.Printf("Running: %s\n%s", str, string(bs)) } cmdwd := opt.Wd if !filepath.IsAbs(cmdwd) { cmdwd = filepath.Join(wd, cmdwd) } prepCmd := func(l Line) *exec.Cmd { fqncmd, err := exec.LookPath(l.Tokens[0]) if err != nil { fqncmd = filepath.Join(cmdwd, l.Tokens[0]) } if opt.Debug { log.Printf("Will call CMD: %s", fqncmd) } cmd := exec.Command(fqncmd, l.Tokens[1:]...) cmd.Stdout = log.Writer() cmd.Stderr = log.Writer() cmd.Dir = cmdwd opt.SetupProc(cmd) ret = append(ret, cmd) return cmd } lines, err := Lines(str) if err != nil { return ret, err } for _, l := range lines { switch l.LType { case LINETYPE_COMMENT: continue case LINETYPE_CHANGEWD: if filepath.IsAbs(l.Tokens[1]) { cmdwd = l.Tokens[1] } else { cmdwd, err = filepath.Abs(filepath.Join(cmdwd, l.Tokens[1])) if err != nil { return nil, err } } if opt.Debug { log.Printf("CMDWD NOW IS: %s", cmdwd) } continue case LINETYPE_SERIAL: cmd := prepCmd(l) if opt.Debug { log.Printf("Running %s from dir %s with params %s", cmd.Path, cmd.Dir, strings.Join(cmd.Args, ",")) } err = cmd.Run() if opt.Debug { if err != nil { log.Printf("Error running: %s: %s", cmd.Path, err.Error()) } } if err != nil { return ret, err } case LINETYPE_PARALLEL: cmd := prepCmd(l) if opt.Debug { log.Printf("Running %s from dir %s with params %v", cmd.Path, cmd.Dir, cmd.Args) } err = cmd.Start() if opt.Debug { if err != nil { log.Printf("Error running: %s: %s", cmd.Path, err.Error()) } } if err != nil { return nil, err } } } return ret, err } func ExecStr(str string, opts ...*Opts) (string, error) { sb := strings.Builder{} wd, err := os.Getwd() if err != nil { return "", err } ret := make([]*exec.Cmd, 0) opt := &Opts{ Await: false, Wd: wd, SetupProc: func(cmd *exec.Cmd) { }, } if opts != nil && len(opts) > 0 { opt = opts[0] } if opt.SetupProc == nil { opt.SetupProc = func(cmd *exec.Cmd) { } } if opt.Trace { bs := debug.Stack() log.Printf("Running: %s\n%s", str, string(bs)) } cmdwd := opt.Wd prepCmd := func(l Line) (*exec.Cmd, error) { str, err := exec.LookPath(l.Tokens[0]) if err != nil { return nil, err } cmd := exec.Command(str, l.Tokens[1:]...) // cmd.Stdout = log.Writer() // cmd.Stderr = log.Writer() cmd.Dir = cmdwd opt.SetupProc(cmd) ret = append(ret, cmd) return cmd, nil } lines, err := Lines(str) if err != nil { return "", err } for _, l := range lines { switch l.LType { case LINETYPE_COMMENT: continue case LINETYPE_CHANGEWD: if filepath.IsAbs(l.Tokens[1]) { cmdwd = l.Tokens[1] } else { cmdwd, err = filepath.Abs(filepath.Join(wd, l.Tokens[1])) if err != nil { return "", err } } log.Printf("CMDWD NOW IS: %s", cmdwd) continue case LINETYPE_SERIAL, LINETYPE_PARALLEL: cmd, err := prepCmd(l) if err != nil { return "", err } if opt.Debug { log.Printf("Running %s from dir %s with params %v", cmd.Path, cmd.Dir, cmd.Args) } bs, err := cmd.CombinedOutput() sb.Write(bs) sb.WriteString("\n") if opt.Debug { if err != nil { log.Printf("Error running: %s: %s", cmd.Path, err.Error()) } } if err != nil { return "", err } } } return sb.String(), err } // Kill call OS to kill pid func Kill(p int) error { switch runtime.GOOS { case "windows": _, err := Exec(fmt.Sprintf("taskkill /T /F /PID %d", p)) return err default: _, err := Exec(fmt.Sprintf("kill -9 %d", p)) return err } } // Simple Runs a simple command line func Simple(str string) (*exec.Cmd, error) { wd, err := os.Getwd() if err != nil { return nil, err } lines, err := Lines(str) if err != nil { return nil, err } l := lines[0] fqncmd, err := exec.LookPath(l.Tokens[0]) if err != nil { fqncmd = filepath.Join(wd, l.Tokens[0]) } cmd := exec.Command(fqncmd, l.Tokens[1:]...) return cmd, nil }