commit 8ca9e4b50c4073d5ef66ce1a1ea19217a97dfe7e Author: Paulo Simão Date: Sun Oct 24 18:31:09 2021 -0300 1st ver diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..ef5b9e1 --- /dev/null +++ b/Readme.md @@ -0,0 +1,40 @@ +# Shelly + +An abstraction for shell directly in GO + +## Goal + +Simplify process spawning and execution on top of exec.Command function. + +The proposal is: use a string based approach for launching other process - as if you were calling a shell, but instead, +its go directly executing your commands. + +Example: + +```go + +package main + +import "go.digitalcircle.com.br/open/shelly" +import "os" +import "os/exec" + +func main(){ + err := shelly.Exec(` + ls -larth + pwd + whoami + date`, &shelly.Opts{ + SetupProc: func(cmd *exec.Cmd) { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + }, + }) + if err != nil { + panic(err) + } +} +``` + +Please note: Commands may be connected by `;` and `\n`. In any case they will be executed sequentially. +`&` will come later (pending implementation), and will allow processes to run in parallel. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12fc29f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module go.digitalcircle.com.br/open/shelly + +go 1.17 diff --git a/lib.go b/lib.go new file mode 100644 index 0000000..3621175 --- /dev/null +++ b/lib.go @@ -0,0 +1,181 @@ +package shelly + +import ( + "errors" + "fmt" + "log" + "os/exec" + "strings" +) + +type Mode int + +type Opts struct { + SetupProc func(cmd *exec.Cmd) +} + +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 +) + +// 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) ([][]string, error) { + ret := make([][]string, 0) + tokens, err := Tokens(str) + if err != nil { + return nil, err + } + line := make([]string, 0) + for _, tk := range tokens { + switch tk { + case "\n", "&&", ";": + if len(line) > 0 { + ret = append(ret, line) + } + line = make([]string, 0) + default: + line = append(line, tk) + } + } + if len(line) > 0 { + ret = append(ret, line) + } + return ret, nil +} + +// Exec will call Lines and execute one by one +func Exec(str string, opts ...*Opts) error { + lines, err := Lines(str) + if err != nil { + return err + } + for _, l := range lines { + if len(l) < 1 { + continue + } + if strings.HasPrefix(l[0], "#") { + continue + } + cmd := exec.Command(l[0], l[1:]...) + if opts != nil && len(opts) > 0 { + opts[0].SetupProc(cmd) + } + cmd.Stdout = log.Writer() + cmd.Stderr = log.Writer() + err = cmd.Run() + if err != nil { + return err + } + } + + return nil + +} diff --git a/lib_test.go b/lib_test.go new file mode 100644 index 0000000..a975ec1 --- /dev/null +++ b/lib_test.go @@ -0,0 +1,44 @@ +package shelly + +import ( + "log" + "os" + "os/exec" + "testing" +) + +func TestLines(t *testing.T) { + lines, err := Lines("; \";\" ;;") + //Lines("a\nb\\\\nc") + // Lines(` + //"A STR" 1 2 "a b" 1.23 \n & + //"STR \\ with escape \n chars \t" 1 234 true + //ls -larth + //pwd + //whoami + //date`) + if err != nil { + t.Fatal(err) + } + + for _, l := range lines { + log.Printf("%#v", l) + } +} + +func TestExec(t *testing.T) { + err := Exec(` + ls -larth + pwd + whoami + date`, &Opts{ + SetupProc: func(cmd *exec.Cmd) { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + }, + }) + if err != nil { + t.Fatal(err) + } + +}