shelly/lib.go

490 lines
9.4 KiB
Go

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
}