shelly/lib.go

490 lines
9.4 KiB
Go
Raw Permalink Normal View History

2021-10-24 21:31:09 +00:00
package shelly
import (
"errors"
"fmt"
"log"
2021-10-25 21:44:30 +00:00
"os"
2021-10-24 21:31:09 +00:00
"os/exec"
2021-10-25 21:44:30 +00:00
"path/filepath"
2021-10-25 01:08:43 +00:00
"runtime"
2021-10-25 22:05:17 +00:00
"runtime/debug"
2021-10-24 21:31:09 +00:00
"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"
2021-11-03 00:38:03 +00:00
case MODE_TAG:
return "MODE_TAG"
case MODE_ESCAPE_TAG:
return "MODE_ESCAPE_TAG"
2021-10-24 21:31:09 +00:00
}
return "UNKNOWN"
}
const (
MODE_NORMAL Mode = iota
MODE_STR
2021-11-03 00:38:03 +00:00
MODE_TAG
2021-10-24 21:31:09 +00:00
MODE_ESCAPE
MODE_ESCAPE_STR
2021-11-03 00:38:03 +00:00
MODE_ESCAPE_TAG
2021-10-24 21:31:09 +00:00
)
2021-10-25 00:42:20 +00:00
type LineType int
const (
LINETYPE_SERIAL LineType = iota
LINETYPE_PARALLEL
LINETYPE_COMMENT
2021-10-25 21:44:30 +00:00
LINETYPE_CHANGEWD
2021-10-25 00:42:20 +00:00
)
// 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 {
2021-10-25 21:44:30 +00:00
Debug bool
2021-10-25 22:05:17 +00:00
Trace bool
2021-10-25 00:42:20 +00:00
Await bool
2021-10-25 21:44:30 +00:00
Wd string
2021-10-25 00:42:20 +00:00
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
}
2021-10-24 21:31:09 +00:00
// 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{}
2021-11-03 00:38:03 +00:00
case MODE_STR, MODE_TAG:
2021-10-24 21:31:09 +00:00
sb.WriteRune(a)
default:
return nil, errors.New(fmt.Sprintf("Error at char %d - cant add space here. Mode: %s", i, mode.String()))
}
2021-11-04 02:31:34 +00:00
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)
}
2021-10-24 21:31:09 +00:00
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())
2021-11-03 00:38:03 +00:00
sb = strings.Builder{}
mode = MODE_STR
2021-10-24 21:31:09 +00:00
}
2021-11-03 00:47:36 +00:00
mode = MODE_STR
2021-11-03 00:38:03 +00:00
case MODE_TAG:
sb.WriteRune(a)
params = append(params, sb.String())
2021-10-24 21:31:09 +00:00
sb = strings.Builder{}
2021-11-03 00:38:03 +00:00
mode = MODE_NORMAL
2021-10-24 21:31:09 +00:00
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
}
2021-11-03 00:38:03 +00:00
2021-10-24 21:31:09 +00:00
case '\\':
switch mode {
case MODE_NORMAL:
mode = MODE_ESCAPE
case MODE_STR:
mode = MODE_ESCAPE_STR
2021-11-03 00:38:03 +00:00
case MODE_TAG:
mode = MODE_ESCAPE_TAG
2021-10-24 21:31:09 +00:00
case MODE_ESCAPE:
sb.WriteString("\\")
mode = MODE_NORMAL
case MODE_ESCAPE_STR:
sb.WriteString("\\")
mode = MODE_STR
}
default:
switch mode {
2021-11-03 00:38:03 +00:00
case MODE_NORMAL, MODE_TAG, MODE_STR:
2021-10-24 21:31:09 +00:00
sb.WriteRune(a)
2021-11-03 00:38:03 +00:00
2021-10-24 21:31:09 +00:00
case MODE_ESCAPE:
sb.WriteString("\\" + string(a))
mode = MODE_NORMAL
case MODE_ESCAPE_STR:
sb.WriteString("\\" + string(a))
mode = MODE_STR
2021-11-03 00:38:03 +00:00
case MODE_ESCAPE_TAG:
sb.WriteString("\\" + string(a))
mode = MODE_TAG
2021-10-24 21:31:09 +00:00
}
}
}
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
2021-10-25 00:42:20 +00:00
func Lines(str string) ([]Line, error) {
ret := make([]Line, 0)
2021-10-24 21:31:09 +00:00
tokens, err := Tokens(str)
if err != nil {
return nil, err
}
2021-10-25 00:42:20 +00:00
line := Line{
Tokens: make([]string, 0),
LType: LINETYPE_SERIAL,
}
2021-10-25 21:44:30 +00:00
addLine := func(line Line) {
if strings.HasPrefix(line.Tokens[0], "#") {
line.LType = LINETYPE_COMMENT
}
if line.Tokens[0] == "cd" {
line.LType = LINETYPE_CHANGEWD
}
2021-10-30 11:37:31 +00:00
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"
}
}
2021-10-25 21:44:30 +00:00
ret = append(ret, line)
}
2021-10-24 21:31:09 +00:00
for _, tk := range tokens {
switch tk {
2021-11-03 00:38:03 +00:00
2021-10-25 21:44:30 +00:00
case "\n", ";", "&&":
2021-10-25 00:42:20 +00:00
if len(line.Tokens) > 0 {
2021-10-25 21:44:30 +00:00
addLine(line)
2021-10-24 21:31:09 +00:00
}
2021-10-25 00:42:20 +00:00
line = Line{
Tokens: make([]string, 0),
}
case "&":
if len(line.Tokens) > 0 {
2021-10-25 21:44:30 +00:00
line.LType = LINETYPE_PARALLEL
addLine(line)
2021-10-25 00:42:20 +00:00
}
line = Line{
Tokens: make([]string, 0),
}
2021-10-24 21:31:09 +00:00
default:
2021-11-04 02:31:34 +00:00
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)
}
2021-11-03 00:38:03 +00:00
2021-10-24 21:31:09 +00:00
}
}
2021-10-25 00:42:20 +00:00
if len(line.Tokens) > 0 {
2021-10-25 21:44:30 +00:00
addLine(line)
2021-10-24 21:31:09 +00:00
}
return ret, nil
}
// Exec will call Lines and execute one by one
2021-10-25 21:44:30 +00:00
func Exec(str string, opts ...*Opts) ([]*exec.Cmd, error) {
wd, err := os.Getwd()
2021-10-25 00:42:20 +00:00
2021-10-25 21:44:30 +00:00
if err != nil {
return nil, err
}
ret := make([]*exec.Cmd, 0)
opt := &Opts{
Await: false,
Wd: wd,
SetupProc: func(cmd *exec.Cmd) {
},
}
2021-10-25 00:42:20 +00:00
if opts != nil && len(opts) > 0 {
opt = opts[0]
}
if opt.SetupProc == nil {
opt.SetupProc = func(cmd *exec.Cmd) {
}
}
2021-10-25 22:05:17 +00:00
if opt.Trace {
bs := debug.Stack()
log.Printf("Running: %s\n%s", str, string(bs))
}
2021-10-25 21:44:30 +00:00
cmdwd := opt.Wd
2021-10-29 21:18:37 +00:00
if !filepath.IsAbs(cmdwd) {
cmdwd = filepath.Join(wd, cmdwd)
}
2021-10-25 00:42:20 +00:00
prepCmd := func(l Line) *exec.Cmd {
2021-10-29 21:26:21 +00:00
fqncmd, err := exec.LookPath(l.Tokens[0])
if err != nil {
fqncmd = filepath.Join(cmdwd, l.Tokens[0])
}
2021-11-03 01:11:22 +00:00
if opt.Debug {
log.Printf("Will call CMD: %s", fqncmd)
}
2021-10-29 21:26:21 +00:00
cmd := exec.Command(fqncmd, l.Tokens[1:]...)
2021-10-25 00:42:20 +00:00
cmd.Stdout = log.Writer()
cmd.Stderr = log.Writer()
2021-10-25 21:44:30 +00:00
cmd.Dir = cmdwd
2021-10-25 00:42:20 +00:00
opt.SetupProc(cmd)
2021-10-25 21:44:30 +00:00
ret = append(ret, cmd)
2021-10-25 00:42:20 +00:00
return cmd
}
2021-10-24 21:31:09 +00:00
lines, err := Lines(str)
2021-10-25 00:42:20 +00:00
2021-10-24 21:31:09 +00:00
if err != nil {
2021-10-25 21:44:30 +00:00
return ret, err
2021-10-24 21:31:09 +00:00
}
for _, l := range lines {
2021-10-25 00:42:20 +00:00
switch l.LType {
case LINETYPE_COMMENT:
2021-10-24 21:31:09 +00:00
continue
2021-10-25 21:44:30 +00:00
case LINETYPE_CHANGEWD:
if filepath.IsAbs(l.Tokens[1]) {
cmdwd = l.Tokens[1]
} else {
2021-11-04 02:31:34 +00:00
cmdwd, err = filepath.Abs(filepath.Join(cmdwd, l.Tokens[1]))
2021-10-25 21:44:30 +00:00
if err != nil {
return nil, err
}
}
2021-11-04 02:31:34 +00:00
if opt.Debug {
log.Printf("CMDWD NOW IS: %s", cmdwd)
}
2021-10-25 21:44:30 +00:00
continue
2021-10-25 00:42:20 +00:00
case LINETYPE_SERIAL:
cmd := prepCmd(l)
2021-10-25 21:44:30 +00:00
if opt.Debug {
2021-11-04 02:31:34 +00:00
log.Printf("Running %s from dir %s with params %s", cmd.Path, cmd.Dir, strings.Join(cmd.Args, ","))
2021-10-25 21:44:30 +00:00
}
2021-10-25 00:42:20 +00:00
err = cmd.Run()
2021-10-25 21:44:30 +00:00
if opt.Debug {
if err != nil {
log.Printf("Error running: %s: %s", cmd.Path, err.Error())
}
}
2021-10-25 00:42:20 +00:00
if err != nil {
2021-10-25 21:44:30 +00:00
return ret, err
2021-10-25 00:42:20 +00:00
}
case LINETYPE_PARALLEL:
2021-10-25 21:44:30 +00:00
cmd := prepCmd(l)
2021-10-25 00:42:20 +00:00
2021-10-25 21:44:30 +00:00
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
}
2021-10-25 00:42:20 +00:00
2021-10-25 21:44:30 +00:00
}
2021-10-24 21:31:09 +00:00
}
2021-10-25 21:44:30 +00:00
return ret, err
2021-10-24 21:31:09 +00:00
}
2021-10-25 01:08:43 +00:00
2021-10-27 23:37:33 +00:00
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
2022-02-17 00:22:54 +00:00
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:]...)
2022-03-03 14:41:06 +00:00
// cmd.Stdout = log.Writer()
// cmd.Stderr = log.Writer()
2021-10-27 23:37:33 +00:00
cmd.Dir = cmdwd
opt.SetupProc(cmd)
ret = append(ret, cmd)
2022-02-17 00:22:54 +00:00
return cmd, nil
2021-10-27 23:37:33 +00:00
}
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:
2022-02-17 00:22:54 +00:00
cmd, err := prepCmd(l)
if err != nil {
return "", err
}
2021-10-27 23:37:33 +00:00
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
}
2021-10-25 01:08:43 +00:00
// Kill call OS to kill pid
func Kill(p int) error {
switch runtime.GOOS {
case "windows":
2021-10-25 21:44:30 +00:00
_, err := Exec(fmt.Sprintf("taskkill /T /F /PID %d", p))
return err
2021-10-25 01:08:43 +00:00
default:
2021-10-25 21:44:30 +00:00
_, err := Exec(fmt.Sprintf("kill -9 %d", p))
return err
2021-10-25 01:08:43 +00:00
}
}
2021-10-30 11:37:31 +00:00
// 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
}