shelly/lib.go

313 lines
5.9 KiB
Go

package shelly
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"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"
}
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
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
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:
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,
}
addLine := func(line Line) {
if strings.HasPrefix(line.Tokens[0], "#") {
line.LType = LINETYPE_COMMENT
}
if line.Tokens[0] == "cd" {
line.LType = LINETYPE_CHANGEWD
}
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:
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) {
}
}
cmdwd := opt.Wd
prepCmd := func(l Line) *exec.Cmd {
cmd := exec.Command(l.Tokens[0], 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(wd, l.Tokens[1]))
if err != nil {
return nil, err
}
}
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 %v", cmd.Path, cmd.Dir, 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
}
// 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
}
}