master
Paulo Simão 2021-10-24 18:31:09 -03:00
commit 8ca9e4b50c
5 changed files with 269 additions and 0 deletions

1
.gitignore vendored 100644
View File

@ -0,0 +1 @@
.idea

40
Readme.md 100644
View File

@ -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.

3
go.mod 100644
View File

@ -0,0 +1,3 @@
module go.digitalcircle.com.br/open/shelly
go 1.17

181
lib.go 100644
View File

@ -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
}

44
lib_test.go 100644
View File

@ -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)
}
}