shelly/lib.go

253 lines
4.8 KiB
Go

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
}