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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ $ 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`.

## Language features

Functional int, float, string and boolean expressions
Expand Down Expand Up @@ -167,7 +169,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,6 +183,10 @@ 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
Expand Down
10 changes: 6 additions & 4 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/terminal v0.5.1
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-4 // 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-4 h1:vLK1/UYCLW1kznWcZb0Zcgmb6rC3Kw0mDhDCoI63boY=
fortio.org/term v0.23.0-fortio-4/go.mod h1:7buBfn81wEJUGWiVjFNiUE/vxWs5FdM9c7PyZpZRS30=
fortio.org/terminal v0.5.1 h1:d73dxJkFIONw+zh0wZa4OB+iLUiMJ8TvPc4fUJlvqpc=
fortio.org/terminal v0.5.1/go.mod h1:douGcICmZmwNOEeP+hb3BZUkZ3VbTFnGkn28RHiqfmU=
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=
24 changes: 17 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"

"fortio.org/cli"
"fortio.org/log"
Expand All @@ -28,24 +29,34 @@ 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)")
configDir, err := os.UserConfigDir() // TODO: this is ugly on a mac... allow homedir...
historyFileDefault := filepath.Join(configDir, ".grol_history")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
historyFileDefault := filepath.Join(configDir, ".grol_history")
historyFileDefault := filepath.Join(configDir, "grol", "history")

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and now I have to mkdir all etc...? isn't it enough to use the config dir and put files there

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's dirty. I would prefer if you added a .grol_history file at the root of home folder than creating a file at the root of config folder.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok so I switched back to homedir as default but added GROL_HISTORY_FILE env var for people who don't like it so they don't have to set it in the flag either, does that work?

if err != nil {
log.Warnf("Couldn't get user config dir: %v", err)
historyFileDefault = ""
}
historyFile := flag.String("history", historyFileDefault, "history `file` to use")
maxHistory := flag.Int("max-history", 99, "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()
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: *historyFile,
MaxHistory: *maxHistory,
}
if hookBefore != nil {
ret := hookBefore()
if ret != 0 {
return ret
}
}
err := extensions.Init()
err = extensions.Init()
if err != nil {
return log.FErrf("Error initializing extensions: %v", err)
}
Expand All @@ -58,8 +69,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
89 changes: 70 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,74 @@ 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.LoggerSetup()
term.SetPrompt(PROMPT)
options.Compact = true // because terminal doesn't 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