Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added run() and exec() functions #237

Merged
merged 4 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
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.8.3-0.20240917004350-924e9d04943f
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.8.3-0.20240917004350-924e9d04943f h1:HDn1oOJCrWswgHqFMK4Mxch2/ypWeCOuqta8NKQAS2Q=
fortio.org/terminal v0.8.3-0.20240917004350-924e9d04943f/go.mod h1:WtVOOMZCaQY2lOV6EIIX3+QKRCwr+jX4OeROc6fN2qc=
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")