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

Initial integration with fortio.org/terminal #120

Merged
merged 11 commits into from
Aug 10, 2024
Merged
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ $ info.keywords
== Eval ==> ["else","error","false","first","func","if","len","log","macro","print","println","quote","rest","return","true","unquote"]
```

There is also in interactive repl mode: `history`, `!23` to repeat the 23rd statement for instance and `help`.
And full edit and history navigation with arrow keys etc...

## Language features

Functional int, float, string and boolean expressions
Expand Down Expand Up @@ -167,7 +170,7 @@ See [Open Issues](https://grol.io/grol/issues) for what's left to do
### CLI Usage

```
grol 0.29.0 usage:
grol 0.38.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
Expand All @@ -181,9 +184,15 @@ flags:
show eval results (default true)
-format
don't execute, just parse and re format the input
-history string
history file to use (default "~/.grol_history")
-max-history size
max history size, use 0 to disable. (default 99)
-parse
show parse tree
-shared-state
All files share same interpreter state (default is new state for each)
```
(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`)

If you don't want to pass a flag and want to permanently change the `grol` history file location from your HOME directory, set GROL_HISTORY_FILE in the environment.
12 changes: 7 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ require (
fortio.org/cli v1.8.0
fortio.org/log v1.16.0
fortio.org/sets v1.2.0
fortio.org/struct2env v0.4.1
fortio.org/terminal v0.7.2
fortio.org/testscript v0.3.1 // only for tests
fortio.org/version v1.0.4
)

require (
fortio.org/struct2env v0.4.1 // indirect
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-20240626151235-a6a393ffd658 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/tools v0.23.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3 // indirect
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/tools v0.24.0 // indirect
)
20 changes: 12 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ fortio.org/sets v1.2.0 h1:FBfC7R2xrOJtkcioUbY6WqEzdujuBoZRbSdp1fYF4Kk=
fortio.org/sets v1.2.0/go.mod h1:J2BwIxNOLWsSU7IMZUg541kh3Au4JEKHrghVwXs68tE=
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.7.2 h1:Bfpw6ORqrpaTVtlP0NxSS2VyfESR17DvT9qqz75ixJU=
fortio.org/terminal v0.7.2/go.mod h1:Z/dydQSo8hCwiUGOt2pJiR8OsNAFn6pTt3pbHDsdtSM=
fortio.org/testscript v0.3.1 h1:MmRO64AsmzaU1KlYMzAbotJIMKRGxD1XXssJnBRiMGQ=
fortio.org/testscript v0.3.1/go.mod h1:7OJ+U4avooRNqc7p/VHKJadYgj9fA6+N0SbGU8FVWGs=
fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U=
fortio.org/version v1.0.4/go.mod h1:2JQp9Ax+tm6QKiGuzR5nJY63kFeANcgrZ0osoQFDVm0=
github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRclWjck=
github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658 h1:i7K6wQLN/0oxF7FT3tKkfMCstxoT4VGG36YIB9ZKLzI=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240626151235-a6a393ffd658/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3 h1:oWb21rU9Q9XrRwXLB7jHc1rbp6EiiimZZv5MLxpu4T0=
golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
51 changes: 44 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import (
"fmt"
"io"
"os"
"path/filepath"

"fortio.org/cli"
"fortio.org/log"
"fortio.org/struct2env"
"fortio.org/terminal"
"grol.io/grol/eval"
"grol.io/grol/extensions" // register extensions
"grol.io/grol/object"
Expand All @@ -19,6 +22,19 @@ func main() {
os.Exit(Main())
}

type Config struct {
HistoryFile string
}

var config = Config{}

func EnvHelp(w io.Writer) {
res, _ := struct2env.StructToEnvVars(config)
str := struct2env.ToShellWithPrefix("GROL_", res, true)
fmt.Fprintln(w, "# Grol environment variables:")
fmt.Fprint(w, str)
}

var hookBefore, hookAfter func() int

func Main() int {
Expand All @@ -28,16 +44,38 @@ func Main() int {
compact := flag.Bool("compact", false, "When printing code, use no indentation and most compact form")
showEval := flag.Bool("eval", true, "show eval results")
sharedState := flag.Bool("shared-state", false, "All files share same interpreter state (default is new state for each)")

const historyDefault = "~/.grol_history" // virtual/token filename, will be replaced by actual home dir if not changed.
cli.EnvHelpFuncs = append(cli.EnvHelpFuncs, EnvHelp)
defaultHistoryFile := historyDefault
errs := struct2env.SetFromEnv("GROL_", &config)
if len(errs) > 0 {
log.Errf("Error setting config from env: %v", errs)
}
if config.HistoryFile != "" {
defaultHistoryFile = config.HistoryFile
}
historyFile := flag.String("history", defaultHistoryFile, "history `file` to use")
maxHistory := flag.Int("max-history", terminal.DefaultHistoryCapacity, "max history `size`, use 0 to disable.")
cli.ArgsHelp = "*.gr files to interpret or `-` for stdin without prompt or no arguments for stdin repl..."
cli.MaxArgs = -1
cli.Main()
histFile := *historyFile
if histFile == historyDefault {
homeDir, err := os.UserHomeDir()
histFile = filepath.Join(homeDir, ".grol_history")
if err != nil {
log.Warnf("Couldn't get user home dir: %v", err)
histFile = ""
}
}
log.Infof("grol %s - welcome!", cli.LongVersion)
options := repl.Options{
ShowParse: *showParse,
ShowEval: *showEval,
FormatOnly: *format,
Compact: *compact,
ShowParse: *showParse,
ShowEval: *showEval,
FormatOnly: *format,
Compact: *compact,
HistoryFile: histFile,
MaxHistory: *maxHistory,
}
if hookBefore != nil {
ret := hookBefore()
Expand All @@ -58,8 +96,7 @@ func Main() int {
return len(errs)
}
if len(flag.Args()) == 0 {
repl.Interactive(os.Stdin, os.Stdout, options)
return 0
return repl.Interactive(options)
}
options.All = true
s := eval.NewState()
Expand Down
88 changes: 69 additions & 19 deletions repl/repl.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package repl

import (
"bufio"
"errors"
"fmt"
"io"
"regexp"
"slices"
"strconv"
"strings"

"fortio.org/log"
"fortio.org/terminal"
"grol.io/grol/ast"
"grol.io/grol/eval"
"grol.io/grol/lexer"
Expand All @@ -33,13 +37,15 @@ func logParserErrors(p *parser.Parser) bool {
}

type Options struct {
ShowParse bool
ShowEval bool
All bool
NoColor bool // color controlled by log package, unless this is set to true.
FormatOnly bool
Compact bool
NilAndErr bool // Show nil and errors in normal output.
ShowParse bool
ShowEval bool
All bool
NoColor bool // color controlled by log package, unless this is set to true.
FormatOnly bool
Compact bool
NilAndErr bool // Show nil and errors in normal output.
HistoryFile string
MaxHistory int
}

func EvalAll(s *eval.State, macroState *object.Environment, in io.Reader, out io.Writer, options Options) []string {
Expand Down Expand Up @@ -78,29 +84,73 @@ func EvalString(what string) (res string, errs []string, formatted string) {
return
}

func Interactive(in io.Reader, out io.Writer, options Options) {
func Interactive(options Options) int {
options.NilAndErr = true
s := eval.NewState()
macroState := object.NewMacroEnvironment()

scanner := bufio.NewScanner(in)
prev := ""
prompt := PROMPT

term, err := terminal.Open()
if err != nil {
return log.FErrf("Error creating readline: %v", err)
}
defer term.Close()
term.SetPrompt(PROMPT)
options.Compact = true // because terminal doesn't (yet) do well will multi-line commands.
term.NewHistory(options.MaxHistory)
_ = term.SetHistoryFile(options.HistoryFile)
// Regular expression for "!nn" to run history command nn.
historyRegex := regexp.MustCompile(`^!(\d+)$`)
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
for {
fmt.Fprint(out, prompt)
scanned := scanner.Scan()
if !scanned {
return
rd, err := term.ReadLine()
if errors.Is(err, io.EOF) {
log.Infof("Exit requested") // Don't say EOF as ^C comes through as EOF as well.
return 0
}
if err != nil {
return log.FErrf("Error reading line: %v", err)
}
log.Debugf("Read: %q", rd)
l := prev + rd
if historyRegex.MatchString(l) {
h := term.History()
slices.Reverse(h)
idxStr := l[1:]
idx, _ := strconv.Atoi(idxStr)
if idx < 1 || idx > len(h) {
log.Errf("Invalid history index %d", idx)
continue
}
l = h[idx-1]
fmt.Fprintf(term.Out, "Repeating history %d: %s\n", idx, l)
term.ReplaceLatest(l)
}
switch {
case l == "history":
h := term.History()
slices.Reverse(h)
for i, v := range h {
fmt.Fprintf(term.Out, "%02d: %s\n", i+1, v)
}
continue
case l == "help":
fmt.Fprintln(term.Out, "Type 'history' to see history, '!n' to repeat history n, 'info' for language builtins")
continue
}
l := prev + scanner.Text()
// errors are already logged and this is the only case that can get contNeeded (EOL instead of EOF mode)
contNeeded, _, _ := EvalOne(s, macroState, l, out, options)
contNeeded, _, formatted := EvalOne(s, macroState, l, term.Out, options)
if contNeeded {
prev = l + "\n"
prompt = CONTINUATION
term.SetPrompt(CONTINUATION)
} else {
if prev != "" {
// In addition to raw lines, we also add the single line version to history.
log.LogVf("Adding to history: %q", formatted)
term.AddToHistory(formatted)
}
prev = ""
prompt = PROMPT
term.SetPrompt(PROMPT)
}
}
}
Expand Down