Skip to content

Commit

Permalink
Added run() and exec() functions (#237)
Browse files Browse the repository at this point in the history
* Added run() and exec() functions

* Use new Suspend/Resume to run() commands including interactive ones (like vi etc)

* Use the release version of terminal, tweak the help for run()

* update help output
  • Loading branch information
ldemailly authored Sep 17, 2024
1 parent 9a3c5a2 commit a2041f5
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 19 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ See [Open Issues](https://grol.io/grol/issues) for what's left to do
### CLI Usage

```
grol 0.58.0 usage:
grol 0.72.0 usage:
grol [flags] *.gr files to interpret or `-` for stdin without prompt or no arguments for stdin repl...
or 1 of the special arguments
grol {help|envhelp|version|buildinfo}
Expand All @@ -201,9 +201,9 @@ flags:
-history file
history file to use (default "~/.grol_history")
-max-depth int
Maximum interpreter depth (default 194999)
Maximum interpreter depth (default 149999)
-max-duration duration
Maximum duration for a script to run. 0 for unlimited. (default 10s)
Maximum duration for a script to run. 0 for unlimited.
-max-history size
max history size, use 0 to disable. (default 99)
-max-save-len int
Expand All @@ -220,10 +220,11 @@ flags:
show all parenthesis in parse tree (default is to simplify using precedence)
-quiet
Quiet mode, sets loglevel to Error (quietly) to reduces the output
-restrict-io
restrict IOs (safe mode)
-s #! script mode: next argument is a script file to run, rest are args to the script
-shared-state
All files share same interpreter state (default is new state for each)
-unrestricted-io
enable unrestricted io (dangerous)
```
(excluding logger control, see `gorepl help` for all the flags, of note `-logger-no-color` will turn off colors for gorepl too, for development there are also `-profile*` options for pprof, when building without `no_pprof`)

Expand Down
2 changes: 2 additions & 0 deletions eval/eval_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"fortio.org/log"
"fortio.org/terminal"
"grol.io/grol/ast"
"grol.io/grol/lexer"
"grol.io/grol/object"
Expand All @@ -28,6 +29,7 @@ const (
)

type State struct {
Term *terminal.Terminal
Out io.Writer
LogOut io.Writer
macroState *object.Environment
Expand Down
6 changes: 6 additions & 0 deletions eval/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ func (s *State) Errorf(format string, args ...interface{}) object.Error {
return s.NewError(fmt.Sprintf(format, args...))
}

// Errorfp formats and create an *object.Error using given format and args.
func (s *State) Errorfp(format string, args ...interface{}) *object.Error {
e := s.Errorf(format, args...)
return &e
}

// Error converts from a go error to an object.Error.
// If the error is nil, it returns object.NULL instead (no error).
func (s *State) Error(err error) object.Object {
Expand Down
3 changes: 3 additions & 0 deletions extensions/extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ func initInternal(c *Config) error {
createMisc()
createTimeFunctions()
createImageFunctions()
if c.UnrestrictedIOs {
createShellFunctions()
}
return nil
}

Expand Down
87 changes: 87 additions & 0 deletions extensions/shell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package extensions

import (
"bytes"
"context"
"os"
"os/exec"

"fortio.org/log"
"grol.io/grol/eval"
"grol.io/grol/object"
)

func createCmd(s eval.State, args []object.Object) (*exec.Cmd, *object.Error) {
cmdArgs := make([]string, 0, len(args))
for _, arg := range args {
if arg.Type() != object.STRING {
return nil, s.Errorfp("exec: argument %s not a string", arg.Inspect())
}
cmdArgs = append(cmdArgs, arg.(object.String).Value)
}
//nolint:gosec // we do want to run the command given by the user.
return exec.CommandContext(s.Context, cmdArgs[0], cmdArgs[1:]...), nil
}

var (
stdout = object.String{Value: "stdout"}
stderr = object.String{Value: "stderr"}
)

func createShellFunctions() {
shellFn := object.Extension{
Name: "exec",
MinArgs: 1,
MaxArgs: -1,
Help: "executes a command and returns its stdout, stderr and any error",
ArgTypes: []object.Type{object.STRING},
Callback: func(env any, _ string, args []object.Object) object.Object {
s := env.(*eval.State)
cmd, oerr := createCmd(*s, args)
if oerr != nil {
return *oerr
}
log.Infof("Running %#v", cmd)
var sout, serr bytes.Buffer
cmd.Stdout = &sout
cmd.Stderr = &serr
err := cmd.Run()
res := object.MakeQuad(stdout, object.String{Value: sout.String()},
stderr, object.String{Value: serr.String()})
if err != nil {
res = res.Set(eval.ErrorKey, object.String{Value: err.Error()})
} else {
res = res.Set(eval.ErrorKey, object.NULL)
}
return res
},
DontCache: true,
}
MustCreate(shellFn)
shellFn.Name = "run"
shellFn.Help = "runs a command interactively"
shellFn.Callback = func(env any, _ string, args []object.Object) object.Object {
s := env.(*eval.State)
if s.Term != nil {
s.Term.Suspend()
}
s.Context, s.Cancel = context.WithCancel(context.Background()) // no timeout.
cmd, oerr := createCmd(*s, args)
if oerr != nil {
return *oerr
}
log.Infof("Running %#v", cmd)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if s.Term != nil {
s.Context, s.Cancel = s.Term.Resume(context.Background())
}
if err != nil {
return s.Error(err)
}
return object.NULL
}
MustCreate(shellFn)
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
fortio.org/safecast v0.1.1
fortio.org/sets v1.2.0
fortio.org/struct2env v0.4.1
fortio.org/terminal v0.8.2
fortio.org/terminal v0.9.1
fortio.org/testscript v0.3.2 // only for tests
fortio.org/version v1.0.4
github.com/rivo/uniseg v0.4.7
Expand All @@ -18,7 +18,7 @@ require (
require (
fortio.org/term v0.23.0-fortio-6 // indirect
github.com/kortschak/goroutine v1.1.2 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240910204333-9e92970a1eb4 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240916204253-42ee18b96377 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/tools v0.25.0 // indirect
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0=
fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410=
fortio.org/term v0.23.0-fortio-6 h1:pKrUX0tKOxyEhkhLV50oJYucTVx94rzFrXc24lIuLvk=
fortio.org/term v0.23.0-fortio-6/go.mod h1:7buBfn81wEJUGWiVjFNiUE/vxWs5FdM9c7PyZpZRS30=
fortio.org/terminal v0.8.2 h1:kluLHjxsuflyRpkp9HzVM5Df8mbiX1tdDRN9Jdlp2M4=
fortio.org/terminal v0.8.2/go.mod h1:4mFl6U7FmnQ+D/NZuxq05QDX/guBTwCRb2+DxTOj4Tg=
fortio.org/terminal v0.9.1 h1:4MHLoInn+xIpyRxb8z2F25PcUZ96+S0b7z4kYQzvsJc=
fortio.org/terminal v0.9.1/go.mod h1:2cTqXEFMyWSgV2RI2O/iq6O8Wbd/BPiQwk1vRbuEwlc=
fortio.org/testscript v0.3.2 h1:ks5V+Y6H6nmeGqnVlZuLdiFwpqXemDkEnyGgCZa/ZNA=
fortio.org/testscript v0.3.2/go.mod h1:Z2kUvEDHYETV8FLxsdA6zwSZ8sZUiTNJh2Dw5c4a3Pg=
fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U=
Expand All @@ -22,8 +22,8 @@ github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRcl
github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240910204333-9e92970a1eb4 h1:2ET4PwUR2nlFyH11/NrFz+OHyYCrnI1Gz5diQ3ZRi8A=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240910204333-9e92970a1eb4/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240916204253-42ee18b96377 h1:aDWu69N3Si4isYMY1ppnuoGEFypX/E5l4MWA//GPClw=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240916204253-42ee18b96377/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func Main() (retcode int) { //nolint:funlen // we do have quite a lot of flags a
historyFile := flag.String("history", defaultHistoryFile, "history `file` to use")
maxHistory := flag.Int("max-history", terminal.DefaultHistoryCapacity, "max history `size`, use 0 to disable.")
disableLoadSave := flag.Bool("no-load-save", false, "disable load/save of history")
unrestrictedIOs := flag.Bool("unrestricted-io", false, "enable unrestricted io (dangerous)")
restrictIOs := flag.Bool("restrict-io", false, "restrict IOs (safe mode)")
emptyOnly := flag.Bool("empty-only", false, "only allow load()/save() to ./.gr")
noAuto := flag.Bool("no-auto", false, "don't auto load/save the state to ./.gr")
maxDepth := flag.Int("max-depth", eval.DefaultMaxDepth-1, "Maximum interpreter depth")
Expand Down Expand Up @@ -122,7 +122,7 @@ func Main() (retcode int) { //nolint:funlen // we do have quite a lot of flags a
c := extensions.Config{
HasLoad: !*disableLoadSave,
HasSave: !*disableLoadSave,
UnrestrictedIOs: *unrestrictedIOs,
UnrestrictedIOs: !*restrictIOs,
LoadSaveEmptyOnly: *emptyOnly,
}
err := extensions.Init(&c)
Expand Down
8 changes: 4 additions & 4 deletions main_test.txtar
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,17 @@ stderr 'identifier not found: save'
!grol -no-load-save -c 'load("foo.gr")'
stderr 'identifier not found: load'

!grol -c 'save("/tmp/foo.gr"); load("/tmp/foo.gr")'
!grol -restrict-io -c 'save("/tmp/foo.gr"); load("/tmp/foo.gr")'
stderr 'invalid character in filename "/tmp/foo.gr": /'

grol -c 'load("fib_50")'
grol -restrict-io -c 'load("fib_50")'
stdout '^12586269025\n$'
stderr 'Read/evaluated: fib_50.gr'

!grol -c 'load("./fib_50.gr")'
!grol -restrict-io -c 'load("./fib_50.gr")'
stderr 'invalid character in filename "./fib_50.gr": \.'

grol -unrestricted-io -c 'load("./fib_50.gr")'
grol -c 'load("./fib_50.gr")'
stdout '^12586269025\n$'
stderr 'Read/evaluated: ./fib_50.gr'

Expand Down
6 changes: 4 additions & 2 deletions repl/repl.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ func Interactive(options Options) int { //nolint:funlen // we do have quite a fe
s.MaxDepth = options.MaxDepth
s.MaxValueLen = options.MaxValueLen // 0 is unlimited so ok to copy as is.
term, err := terminal.Open(context.Background())
ctx := term.Context
if err != nil {
return log.FErrf("Error creating readline: %v", err)
}
defer term.Close()
s.Term = term
s.Out = term.Out
autoComplete := NewCompletion()
tokInfo := token.Info()
Expand Down Expand Up @@ -252,6 +252,7 @@ func Interactive(options Options) int { //nolint:funlen // we do have quite a fe
}
prev := ""
for {
var ctx context.Context
rd, err := term.ReadLine()
if errors.Is(err, io.EOF) {
log.Infof("EOF, exiting")
Expand All @@ -260,12 +261,13 @@ func Interactive(options Options) int { //nolint:funlen // we do have quite a fe
}
if errors.Is(err, terminal.ErrUserInterrupt) {
log.Debugf("^C from user")
ctx, _ = term.ResetInterrupts(context.Background()) //nolint:fatcontext // we only get a new one after the previous one is done.
term.ResetInterrupts(context.Background()) // will set ctx in term to be used in next loop's eval.
continue
}
if err != nil {
return log.FErrf("Error reading line: %v", err)
}
ctx = term.Context // context can be changed by shell run command through suspend/resume.
log.Debugf("Read: %q", rd)
if idx, ok := extractHistoryNumber(rd); ok {
h := term.History()
Expand Down
9 changes: 9 additions & 0 deletions tests/shell.gr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

run("true")
run("echo", "echoing", "foo")

IsErr("non zero exit", run("false"), "exit status 1")

NoErr("exec captures", exec("false").err, `^exit status 1$`)
NoErr("exec captures", exec("echo", "-n", "foo").stdout, "^foo$")
NoErr("exec captures", exec("ls", "/no/such/file").stderr, "No such file or directory")

0 comments on commit a2041f5

Please sign in to comment.