aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJP Appel <jeanpierre.appel01@gmail.com>2025-06-26 00:54:29 -0400
committerJP Appel <jeanpierre.appel01@gmail.com>2025-06-26 01:01:43 -0400
commit0232433a2ddd64c270a4d049f5ae9895245ee058 (patch)
treeff2c813f166bc4ea69e556f8e6895d6a6bf7226d
parent173788333e6b14a1b2bdbb127874988bb62bce8d (diff)
Add improved line editing to debug shell
Improved line editing include moveable cursor and command history
-rw-r--r--cmd/atlas.go9
-rw-r--r--go.mod7
-rw-r--r--go.sum4
-rw-r--r--pkg/shell/interpreter.go184
-rw-r--r--pkg/shell/shell.go50
5 files changed, 153 insertions, 101 deletions
diff --git a/cmd/atlas.go b/cmd/atlas.go
index 60da8a2..916e459 100644
--- a/cmd/atlas.go
+++ b/cmd/atlas.go
@@ -208,7 +208,14 @@ func main() {
}
case "shell":
state := make(shell.State)
- interpreter := shell.NewInterpreter(state, os.Stdin, globalFlags.NumWorkers)
+ env := make(map[string]string)
+
+ env["workers"] = fmt.Sprint(globalFlags.NumWorkers)
+ env["db path"] = globalFlags.DBPath
+ env["index root"] = globalFlags.IndexRoot
+ env["version"] = "0.0.1"
+
+ interpreter := shell.NewInterpreter(state, env, globalFlags.NumWorkers)
if err := interpreter.Run(); err != nil && err != io.EOF {
slog.Error("Fatal error occured", slog.String("err", err.Error()))
os.Exit(1)
diff --git a/go.mod b/go.mod
index 23986ed..bef6f2c 100644
--- a/go.mod
+++ b/go.mod
@@ -4,4 +4,9 @@ go 1.24.2
require github.com/mattn/go-sqlite3 v1.14.27
-require github.com/goccy/go-yaml v1.17.1
+require (
+ github.com/goccy/go-yaml v1.17.1
+ golang.org/x/term v0.32.0
+)
+
+require golang.org/x/sys v0.33.0 // indirect
diff --git a/go.sum b/go.sum
index 0b7f4da..1f11a34 100644
--- a/go.sum
+++ b/go.sum
@@ -2,3 +2,7 @@ github.com/goccy/go-yaml v1.17.1 h1:LI34wktB2xEE3ONG/2Ar54+/HJVBriAGJ55PHls4YuY=
github.com/goccy/go-yaml v1.17.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
+golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
diff --git a/pkg/shell/interpreter.go b/pkg/shell/interpreter.go
index 14b2081..3f43f1c 100644
--- a/pkg/shell/interpreter.go
+++ b/pkg/shell/interpreter.go
@@ -1,25 +1,30 @@
package shell
import (
- "bufio"
"errors"
"fmt"
+ "index/suffixarray"
"io"
- "os"
- "os/signal"
"slices"
"strconv"
"strings"
- "syscall"
"unicode"
"github.com/jpappel/atlas/pkg/query"
+ "golang.org/x/term"
)
type Interpreter struct {
State State
- Scanner *bufio.Scanner
Workers uint
+ env map[string]string
+ term *term.Terminal
+ tab struct {
+ inTabMode bool
+ completions []string
+ pos int
+ index *suffixarray.Index
+ }
}
type ITokType int
@@ -38,6 +43,8 @@ const (
// commands
ITOK_CMD_HELP
ITOK_CMD_CLEAR
+ ITOK_CMD_EXIT
+ ITOK_CMD_ENV
ITOK_CMD_LET
ITOK_CMD_DEL
ITOK_CMD_PRINT
@@ -56,19 +63,19 @@ type IToken struct {
Text string
}
-func NewInterpreter(initialState State, inputSource io.Reader, workers uint) *Interpreter {
+func NewInterpreter(initialState State, env map[string]string, workers uint) *Interpreter {
return &Interpreter{
- initialState,
- bufio.NewScanner(inputSource),
- workers,
+ State: initialState,
+ env: env,
+ Workers: workers,
}
}
-func (interpreter *Interpreter) Reset() {
- interpreter.State = make(State)
+func (inter *Interpreter) Reset() {
+ inter.State = make(State)
}
-func (interpreter *Interpreter) Eval(tokens []IToken) (bool, error) {
+func (inter *Interpreter) Eval(w io.Writer, tokens []IToken) (bool, error) {
if len(tokens) == 0 {
return false, nil
}
@@ -94,33 +101,48 @@ out:
t := tokens[i]
switch t.Type {
case ITOK_CMD_HELP:
- printHelp()
+ printHelp(w)
break out
+ case ITOK_CMD_EXIT:
+ return true, nil
case ITOK_CMD_CLEAR:
- fmt.Println("\033[H\033[J")
+ fmt.Fprint(w, "\033[H\033[J")
+ break out
+ case ITOK_CMD_ENV:
+ if t.Text == "" {
+ for k, v := range inter.env {
+ fmt.Fprintln(w, k, ":", v)
+ }
+ } else {
+ v, ok := inter.env[t.Text]
+ if !ok {
+ return false, fmt.Errorf("No env var: %s", t.Text)
+ }
+ fmt.Fprintln(w, t.Text, ":", v)
+ }
break out
case ITOK_CMD_LET:
if variableName != "" {
- interpreter.State[variableName] = carryValue
+ inter.State[variableName] = carryValue
carryValue.Type = VAL_INVALID
}
break out
case ITOK_CMD_DEL:
if len(tokens) == 1 {
- fmt.Println("Deleting all variables")
- interpreter.State = make(State)
+ fmt.Fprintln(w, "Deleting all variables")
+ inter.State = make(State)
} else {
// HACK: variable name is not evaluated correctly so just look at the next token
- delete(interpreter.State, tokens[i+1].Text)
+ delete(inter.State, tokens[i+1].Text)
}
carryValue.Type = VAL_INVALID
break out
case ITOK_CMD_PRINT:
if len(tokens) == 1 {
- fmt.Println("Variables:")
- fmt.Println(interpreter.State)
+ fmt.Fprintln(w, "Variables:")
+ fmt.Fprintln(w, inter.State)
} else {
- carryValue, ok = interpreter.State[tokens[1].Text]
+ carryValue, ok = inter.State[tokens[1].Text]
if !ok {
return false, fmt.Errorf("No variable %s", tokens[1].Text)
}
@@ -149,7 +171,7 @@ out:
}
carryValue.Val = b.String()
case ITOK_CMD_REPATTERN:
- fmt.Println(query.LexRegexPattern)
+ fmt.Fprintln(w, query.LexRegexPattern)
break out
case ITOK_CMD_TOKENIZE:
if carryValue.Type != VAL_STRING {
@@ -188,7 +210,7 @@ out:
return true, errors.New("Type corruption during optimization, expected *query.Clause")
}
- o := query.NewOptimizer(clause, interpreter.Workers)
+ o := query.NewOptimizer(clause, inter.Workers)
switch t.Text {
case "simplify":
o.Simplify()
@@ -196,7 +218,7 @@ out:
o.Tighten()
case "flatten":
o.Flatten()
- case "sortStatements":
+ case "sort":
o.SortStatements()
case "tidy":
o.Tidy()
@@ -227,13 +249,13 @@ out:
return false, err
}
- fmt.Printf("query:\n%s\n--------\nparams:\n%s\n", query, params)
+ fmt.Fprintf(w, "query:\n%s\n--------\nparams:\n%s\n", query, params)
carryValue.Type = VAL_INVALID
break out
case ITOK_VAR_NAME:
// NOTE: very brittle, only allows expansion of a single variable
if i == len(tokens)-1 {
- carryValue, ok = interpreter.State[t.Text]
+ carryValue, ok = inter.State[t.Text]
if !ok {
return false, fmt.Errorf("No variable: %s", t.Text)
}
@@ -278,20 +300,19 @@ out:
default:
return false, fmt.Errorf("Cannot slice argument: %v", cType)
}
- fmt.Println("not implemented yet ;)")
- break out
+ return false, fmt.Errorf("not implemented")
}
}
if carryValue.Type != VAL_INVALID {
- fmt.Println(carryValue)
- interpreter.State["_"] = carryValue
+ fmt.Fprintln(w, carryValue)
+ inter.State["_"] = carryValue
}
return false, nil
}
-func (interpreter Interpreter) Tokenize(line string) []IToken {
+func (inter Interpreter) Tokenize(line string) []IToken {
var prevType ITokType
tokens := make([]IToken, 0, 3)
for word := range strings.SplitSeq(line, " ") {
@@ -306,6 +327,10 @@ func (interpreter Interpreter) Tokenize(line string) []IToken {
if trimmedWord == "help" {
tokens = append(tokens, IToken{Type: ITOK_CMD_HELP})
+ } else if trimmedWord == "exit" {
+ tokens = append(tokens, IToken{Type: ITOK_CMD_EXIT})
+ } else if trimmedWord == "env" {
+ tokens = append(tokens, IToken{Type: ITOK_CMD_ENV})
} else if trimmedWord == "clear" {
tokens = append(tokens, IToken{Type: ITOK_CMD_CLEAR})
} else if trimmedWord == "let" {
@@ -326,6 +351,8 @@ func (interpreter Interpreter) Tokenize(line string) []IToken {
tokens = append(tokens, IToken{Type: ITOK_CMD_TOKENIZE})
} else if trimmedWord == "parse" {
tokens = append(tokens, IToken{Type: ITOK_CMD_PARSE})
+ } else if l := len("env_"); len(trimmedWord) > l && trimmedWord[:l] == "env_" {
+ tokens = append(tokens, IToken{ITOK_CMD_ENV, trimmedWord[l:]})
} else if l := len("opt_"); len(trimmedWord) > l && trimmedWord[:l] == "opt_" {
tokens = append(tokens, IToken{ITOK_CMD_OPTIMIZE, trimmedWord[l:]})
} else if trimmedWord == "compile" {
@@ -367,72 +394,31 @@ func (interpreter Interpreter) Tokenize(line string) []IToken {
return tokens
}
-func (interpreter Interpreter) Run() error {
- signalCh := make(chan os.Signal, 1)
- exitCh := make(chan error, 1)
- lineCh := make(chan string)
- defer close(signalCh)
- defer close(lineCh)
- defer close(exitCh)
-
- signal.Notify(signalCh, syscall.SIGINT)
- go func(output chan<- string, exitCh chan<- error) {
- for {
- if interpreter.Scanner.Scan() {
- output <- interpreter.Scanner.Text()
- } else if err := interpreter.Scanner.Err(); err != nil {
- exitCh <- err
- return
- } else {
- exitCh <- io.EOF
- return
- }
- }
- }(lineCh, exitCh)
-
- for {
- fmt.Print("atlasi> ")
-
- select {
- case <-signalCh:
- fmt.Println("Recieved Ctrl-C, exitting")
- return nil
- case err := <-exitCh:
- return err
- case line := <-lineCh:
- tokens := interpreter.Tokenize(line)
- fatal, err := interpreter.Eval(tokens)
- if fatal {
- return err
- } else if err != nil {
- fmt.Println(err)
- }
- }
- }
-}
-
-func printHelp() {
- fmt.Println("Shitty debug shell for atlas")
- fmt.Println("help - print this help")
- fmt.Println("clear - clear the screen")
- fmt.Println("let name (string|tokens|clause) - save value to a variable")
- fmt.Println("del [name] - delete a variable or all variables")
- fmt.Println("print [name] - print a variable or all variables")
- fmt.Println("slice (string|tokens|name) start stop - slice a string or tokens from start to stop")
- fmt.Println("len (string|tokens|name) - length of a string or token slice")
- fmt.Println("rematch (string|name) - match against regex for querylang spec")
- fmt.Println("repattern - print regex for querylang")
- fmt.Println("tokenize (string|name) - tokenize a string")
- fmt.Println(" ex. tokenize `author:me")
- fmt.Println("parse (tokens|name) - parse tokens into a clause")
- fmt.Println("opt_<subcommand> (clause|name) - optimize clause tree")
- fmt.Println(" sortStatements - sort statements")
- fmt.Println(" flatten - flatten clauses")
- fmt.Println(" compact - compact equivalent statements")
- fmt.Println(" tidy - remove zero statements and `AND` clauses containing any")
- fmt.Println(" contradictions - zero contradicting statements and clauses")
- fmt.Println(" strictEq - zero fuzzy/range statements when an eq is present")
- fmt.Println(" tighten - zero redundant fuzzy/range statements when another mathes the same values")
- fmt.Println("compile (clause|name) - compile clause into query")
- fmt.Println("\nBare commands which return a value assign to an implicit variable _")
+func printHelp(w io.Writer) {
+ fmt.Fprintln(w, "Shitty debug shell for atlas")
+ fmt.Fprintln(w, "help - print this help")
+ fmt.Fprintln(w, "exit - exit interactive mode")
+ fmt.Fprintln(w, "env - print info about environment")
+ fmt.Fprintln(w, " env_<name> - print about specific variable <name>")
+ fmt.Fprintln(w, "clear - clear the screen")
+ fmt.Fprintln(w, "let name (string|tokens|clause) - save value to a variable")
+ fmt.Fprintln(w, "del [name] - delete a variable or all variables")
+ fmt.Fprintln(w, "print [name] - print a variable or all variables")
+ fmt.Fprintln(w, "slice (string|tokens|name) start stop - slice a string or tokens from start to stop")
+ fmt.Fprintln(w, "len (string|tokens|name) - length of a string or token slice")
+ fmt.Fprintln(w, "rematch (string|name) - match against regex for querylang spec")
+ fmt.Fprintln(w, "repattern - print regex for querylang")
+ fmt.Fprintln(w, "tokenize (string|name) - tokenize a string")
+ fmt.Fprintln(w, " ex. tokenize `author:me")
+ fmt.Fprintln(w, "parse (tokens|name) - parse tokens into a clause")
+ fmt.Fprintln(w, "opt_<subcommand> (clause|name) - optimize clause tree")
+ fmt.Fprintln(w, " sort - sort statements")
+ fmt.Fprintln(w, " flatten - flatten clauses")
+ fmt.Fprintln(w, " compact - compact equivalent statements")
+ fmt.Fprintln(w, " tidy - remove zero statements and `AND` clauses containing any")
+ fmt.Fprintln(w, " contradictions - zero contradicting statements and clauses")
+ fmt.Fprintln(w, " strictEq - zero fuzzy/range statements when an eq is present")
+ fmt.Fprintln(w, " tighten - zero redundant fuzzy/range statements when another mathes the same values")
+ fmt.Fprintln(w, "compile (clause|name) - compile clause into query")
+ fmt.Fprintln(w, "\nBare commands which return a value assign to an implicit variable _")
}
diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go
new file mode 100644
index 0000000..19a4223
--- /dev/null
+++ b/pkg/shell/shell.go
@@ -0,0 +1,50 @@
+package shell
+
+import (
+ "fmt"
+ "os"
+
+ "golang.org/x/term"
+)
+
+var commands = []string{
+ "help",
+ "clear",
+ "let",
+ "del",
+ "slice",
+ "rematch",
+ "repattern",
+ "tokenize",
+ "opt_simplify", "opt_tighten", "opt_flatten", "opt_sort", "opt_tidy", "opt_contradictions", "opt_compact", "opt_strictEq",
+ "parse",
+ "compile",
+}
+
+func (inter *Interpreter) Run() error {
+ oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
+ if err != nil {
+ panic(err)
+ }
+ defer term.Restore(int(os.Stdin.Fd()), oldState)
+ inter.term = term.NewTerminal(os.Stdin, "atlasi> ")
+ inter.term.SetPrompt(
+ string(inter.term.Escape.Yellow) + "atlasi> " +
+ string(inter.term.Escape.Reset),
+ )
+
+ for {
+ line, err := inter.term.ReadLine()
+ if err != nil {
+ return err
+ }
+ tokens := inter.Tokenize(line)
+ fatal, err := inter.Eval(inter.term, tokens)
+ if fatal {
+ return err
+ } else if err != nil {
+ fmt.Fprintln(inter.term, string(inter.term.Escape.Red), "Error:",
+ string(inter.term.Escape.Reset), err)
+ }
+ }
+}