Skip to content

Commit

Permalink
Added support for 'uninterpreted' shell interaction, where the shell …
Browse files Browse the repository at this point in the history
…doesn't attempt to interpret every input line as a command, rather passing all terminator-delineated lines to a single handler function.
  • Loading branch information
zachmu committed May 14, 2019
1 parent 8b8aa74 commit 693241f
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 14 deletions.
6 changes: 3 additions & 3 deletions actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type Actions interface {
ReadPasswordErr() (string, error)
// ReadMultiLinesFunc reads multiple lines from standard input. It passes each read line to
// f and stops reading when f returns false.
ReadMultiLinesFunc(f func(string) bool) string
ReadMultiLinesFunc(f func(string) (keepReading bool)) string
// ReadMultiLines reads multiple lines from standard input. It stops reading when terminator
// is encountered at the end of the line. It returns the lines read including terminator.
// For more control, use ReadMultiLinesFunc.
Expand Down Expand Up @@ -102,13 +102,13 @@ func (s *shellActionsImpl) ReadPasswordErr() (string, error) {
return s.reader.readPasswordErr()
}

func (s *shellActionsImpl) ReadMultiLinesFunc(f func(string) bool) string {
func (s *shellActionsImpl) ReadMultiLinesFunc(f func(string) (keepReading bool)) string {
lines, _ := s.readMultiLinesFunc(f)
return lines
}

func (s *shellActionsImpl) ReadMultiLines(terminator string) string {
return s.ReadMultiLinesFunc(func(line string) bool {
return s.ReadMultiLinesFunc(func(line string) (keepReading bool) {
if strings.HasSuffix(strings.TrimSpace(line), terminator) {
return false
}
Expand Down
8 changes: 3 additions & 5 deletions command_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package ishell_test
package ishell

import (
"testing"

"github.com/abiosoft/ishell"
"github.com/stretchr/testify/assert"
)

func newCmd(name string, help string) *ishell.Cmd {
return &ishell.Cmd{
func newCmd(name string, help string) *Cmd {
return &Cmd{
Name: name,
Help: help,
}
Expand Down
15 changes: 15 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/liquidata-inc/ishell

go 1.12

require (
github.com/abiosoft/ishell v2.0.0+incompatible
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db
github.com/chzyer/logex v1.1.10 // indirect
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
github.com/fatih/color v1.7.0
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/mattn/go-colorable v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/stretchr/testify v1.3.0
)
26 changes: 26 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw=
github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8=
github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
138 changes: 132 additions & 6 deletions ishell.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (

"github.com/abiosoft/readline"
"github.com/fatih/color"
shlex "github.com/flynn-archive/go-shlex"
"github.com/flynn-archive/go-shlex"
)

const (
Expand Down Expand Up @@ -58,10 +58,22 @@ type Shell struct {
progressBar ProgressBar
pager string
pagerArgs []string
isUninterpreted bool
lineTerminator string
quitKeywords []string
contextValues
Actions
}

// UninterpretedConfig configures an uninterpreted shell. See NewUninterpreted().
type UninterpretedConfig struct {
ReadlineConfig *readline.Config
// The line terminator to use
LineTerminator string
// Quit keywords to exit the shell if discovered
QuitKeywords []string
}

// New creates a new shell with default settings. Uses standard output and default prompt ">> ".
func New() *Shell {
return NewWithConfig(&readline.Config{Prompt: defaultPrompt})
Expand All @@ -78,6 +90,19 @@ func NewWithConfig(conf *readline.Config) *Shell {
return NewWithReadline(rl)
}

// NewUninterpreted creates a new uninterpreted shell, one which doesn't attempt to parse out commands and arguments,
// only pull input lines and handle them with a custom handler. This is more appropriate for free-form shells such as
// SQL queries, REPLs, and so on.
func NewUninterpreted(conf *UninterpretedConfig) *Shell {
shell := NewWithConfig(conf.ReadlineConfig)

shell.isUninterpreted = true
shell.lineTerminator = conf.LineTerminator
shell.quitKeywords = conf.QuitKeywords

return shell
}

// NewWithReadline creates a new shell with a custom readline instance.
func NewWithReadline(rl *readline.Instance) *Shell {
shell := &Shell{
Expand Down Expand Up @@ -171,7 +196,12 @@ shell:
var err error
read := make(chan struct{})
go func() {
line, err = s.read()
if s.isUninterpreted {
line = make([]string, 1)
line[0], err = s.readUninterpreted()
} else {
line, err = s.read()
}
read <- struct{}{}
}()
select {
Expand Down Expand Up @@ -208,8 +238,13 @@ shell:
continue
}

err = handleInput(s, line)
if s.isUninterpreted {
err = handleUninterpretedInput(s, line[0])
} else {
err = handleInput(s, line)
}
}

if err != nil {
s.Println("Error:", err)
}
Expand All @@ -228,6 +263,26 @@ func (s *Shell) Process(args ...string) error {
return handleInput(s, args)
}

func handleUninterpretedInput(s *Shell, line string) error {
// Check for any quit words and exit if found. In handleInputs(), the exit case is handled by a command named "exit"
trimmedLine := strings.TrimSpace(line)
trimmedLine = strings.TrimRight(trimmedLine, s.lineTerminator)
for _, keyword := range s.quitKeywords {
if trimmedLine == keyword {
s.Stop()
return nil
}
}

// Generic handler
if s.generic == nil {
return errNoHandler
}
c := newContext(s, nil, []string{line})
s.generic(c)
return c.err
}

func handleInput(s *Shell, line []string) error {
handled, err := s.handleCommand(line)
if handled || err != nil {
Expand Down Expand Up @@ -287,12 +342,70 @@ func (s *Shell) readLine() (line string, err error) {
return ls.line, ls.err
}

func (s *Shell) readUninterpreted() (string, error) {
s.rawArgs = nil
var lines string
var err error

if s.lineTerminator != "" {
firstLine := true
lines, err = s.readMultiLinesFunc(func(line string) (keepReading bool) {
if firstLine {
firstLine = false
for _, keyword := range s.quitKeywords {
if strings.TrimSpace(line) == keyword {
return false
}
}
}

return !strings.HasSuffix(strings.TrimSpace(line), s.lineTerminator)
})

if err != nil {
return "", err
}
} else {
eof := ""
heredoc := false

// heredoc multiline
lines, err = s.readMultiLinesFunc(func(line string) (keepReading bool) {
if !heredoc {
if strings.Contains(line, "<<") {
s := strings.SplitN(line, "<<", 2)
if eof = strings.TrimSpace(s[1]); eof != "" {
heredoc = true
return true
}
}
} else {
return line != eof
}
return strings.HasSuffix(strings.TrimSpace(line), "\\")
})

if err != nil {
return "", err
}

if heredoc {
s := strings.SplitN(lines, "<<", 2)
lines = strings.TrimSuffix(strings.SplitN(s[1], "\n", 2)[1], eof)
}
}

s.rawArgs = []string{lines}
return lines, nil
}

func (s *Shell) read() ([]string, error) {
s.rawArgs = nil
heredoc := false
eof := ""
heredoc := false

// heredoc multiline
lines, err := s.readMultiLinesFunc(func(line string) bool {
lines, err := s.readMultiLinesFunc(func(line string) (keepReading bool) {
if !heredoc {
if strings.Contains(line, "<<") {
s := strings.SplitN(line, "<<", 2)
Expand Down Expand Up @@ -331,7 +444,7 @@ func (s *Shell) read() ([]string, error) {
return args, err
}

func (s *Shell) readMultiLinesFunc(f func(string) bool) (string, error) {
func (s *Shell) readMultiLinesFunc(f func(string) (keepReading bool)) (string, error) {
var lines bytes.Buffer
currentLine := 0
var err error
Expand Down Expand Up @@ -388,6 +501,12 @@ func (s *Shell) DeleteCmd(name string) {
// It is called if the shell input could not be handled by any of the
// added commands.
func (s *Shell) NotFound(f func(*Context)) {
s.Uninterpreted(f)
}

// Uninterpreted adds a generic function for all inputs. It is only called if the shell is configured to handle
// uninterpreted input, and is mutually exclusive with NotFound()
func (s *Shell) Uninterpreted(f func(*Context)) {
s.generic = f
}

Expand Down Expand Up @@ -424,6 +543,13 @@ func (s *Shell) SetHistoryPath(path string) {
s.reader.scanner, _ = readline.NewEx(config)
}

// AddHistory adds the given string to the history file. Useful for handling multi-line input, where the default
// behavior of saving each line as its own history entry is incorrect. In that case, disable auto saving history and
// handle it explicitly in the Uninterpreted() callback.
func (s *Shell) AddHistory(history string) error {
return s.reader.scanner.SaveHistory(history)
}

// SetHomeHistoryPath is a convenience method that sets the history path
// in user's home directory.
func (s *Shell) SetHomeHistoryPath(path string) {
Expand Down

0 comments on commit 693241f

Please sign in to comment.