The big work in progress nothing works commit
parent
cf1667f0bf
commit
ba1ad8f13a
@ -0,0 +1,9 @@
|
||||
package commands
|
||||
|
||||
const fakePath = "/bin:/usr/bin"
|
||||
const fakePipeBuf = "4096"
|
||||
const trampSuccess = "tramp_exit_status 0\n"
|
||||
const trampFailure = "tramp_exit_status 1\n"
|
||||
const lsExecutable = "/bin/ls"
|
||||
const testExecutable = "/bin/test"
|
||||
const statExecutable = "/bin/stat"
|
@ -0,0 +1,45 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EchoCommand struct{}
|
||||
|
||||
func (c *EchoCommand) Execute(shell *Shell, args []string) error {
|
||||
if len(args) > 1 {
|
||||
if "\"`uname -sr`\"" == args[0]+" "+args[1] {
|
||||
log.Println("Tramp requested uname, returning a fake one: " + fakeUname)
|
||||
_, err := shell.WriteOutput(
|
||||
[]byte(
|
||||
fmt.Sprintf(
|
||||
"\"%s\"\n%s\n",
|
||||
fakeUname,
|
||||
"tramp_exit_status 0",
|
||||
),
|
||||
),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if "\"`tty`\"" == args[0] {
|
||||
log.Println("Telling tramp that it's dealing with a tty")
|
||||
_, err := shell.WriteOutputString("\"/dev/pts/0\"\n" + trampSuccess)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(args) > 0 {
|
||||
if args[0] == "~root" {
|
||||
log.Println("Telling tramp root's home directory")
|
||||
_, err := shell.WriteOutputString("/root")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_, err := shell.WriteOutput([]byte(strings.Join(args, " ") + "\n"))
|
||||
return err
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package commands
|
||||
|
||||
import "strings"
|
||||
|
||||
func AssignmentsToMap(args []string) map[string]string {
|
||||
var assignments = make(map[string]string)
|
||||
for _, arg := range args {
|
||||
split := strings.Split(arg, "=")
|
||||
if len(split) > 1 {
|
||||
assignments[split[0]] = split[1]
|
||||
}
|
||||
}
|
||||
|
||||
return assignments
|
||||
}
|
||||
|
||||
func ApplyEnvIfPresent(shell *Shell, comm *Command) {
|
||||
var args []string
|
||||
args = append(args, comm.Name)
|
||||
args = append(args, comm.Arguments...)
|
||||
|
||||
assignments := AssignmentsToMap(args)
|
||||
if ps1, ok := assignments["PS1"]; ok {
|
||||
shell.SetPrompt(ps1)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package commands
|
||||
|
||||
type GetConfCommand struct{}
|
||||
|
||||
func (c *GetConfCommand) Execute(shell *Shell, args []string) error {
|
||||
if len(args) > 0 {
|
||||
switch args[0] {
|
||||
case "PATH":
|
||||
shell.WriteOutputString(fakePath + "\n" + trampSuccess)
|
||||
case "PIPE_BUF":
|
||||
shell.WriteOutputString(fakePipeBuf + "\n" + trampSuccess)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"git.snorba.art/hugo/nssh/storage"
|
||||
)
|
||||
|
||||
type CommandHandler struct {
|
||||
shell *Shell
|
||||
filestore storage.Filestore
|
||||
}
|
||||
|
||||
func NewCommandHandler(shell *Shell, filestore storage.Filestore) *CommandHandler {
|
||||
return &CommandHandler{
|
||||
shell: shell,
|
||||
filestore: filestore,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CommandHandler) Handle(comm *Command) error {
|
||||
if strings.Contains(comm.Name, "=") {
|
||||
ApplyEnvIfPresent(h.shell, comm)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch comm.Name {
|
||||
case "exec":
|
||||
ApplyEnvIfPresent(h.shell, comm)
|
||||
case "(cd":
|
||||
h.shell.WriteOutput([]byte("tramp_exit_status 0\n"))
|
||||
case "echo":
|
||||
(&EchoCommand{}).Execute(h.shell, comm.Arguments)
|
||||
case "(echo":
|
||||
if strings.Join(comm.Arguments, " ") == "foo ; echo bar)" {
|
||||
log.Println("Handling tramp's foobar test")
|
||||
h.shell.WriteOutputString("foo\nbar\n")
|
||||
}
|
||||
case "stty":
|
||||
return (&SttyCommand{}).Execute(h.shell, comm.Arguments)
|
||||
case "set":
|
||||
log.Println("Ignoring \"set\" command")
|
||||
case "locale":
|
||||
if len(comm.Arguments) > 0 {
|
||||
if comm.Arguments[0] == "-a" {
|
||||
locales := []string{"C", "C.UTF-8", "POSIX", "en_US.utf8"}
|
||||
log.Println("Tramp requested locale, returning fake values: ", locales)
|
||||
h.shell.WriteOutputString(
|
||||
strings.Join(locales, "\n") + "\n",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Ignoring \"locale\" command with unsupported parameters: ", comm.Arguments)
|
||||
case "getconf":
|
||||
return (&GetConfCommand{}).Execute(h.shell, comm.Arguments)
|
||||
case "test", "/bin/test":
|
||||
return (&TestCommand{filestore: h.filestore}).Execute(h.shell, comm.Arguments)
|
||||
case "while":
|
||||
if len(comm.Arguments) > 6 {
|
||||
var err error
|
||||
|
||||
log.Printf("Pointing tramp to executable for \"%s\"", comm.Arguments[6])
|
||||
|
||||
switch comm.Arguments[6] {
|
||||
case "$d/ls":
|
||||
_, err = h.shell.WriteOutputString("tramp_executable " + lsExecutable)
|
||||
case "$d/test":
|
||||
_, err = h.shell.WriteOutputString("tramp_executable " + testExecutable)
|
||||
case "$d/stat":
|
||||
_, err = h.shell.WriteOutputString("tramp_executable " + statExecutable)
|
||||
}
|
||||
return err
|
||||
}
|
||||
case "ls", "/bin/ls":
|
||||
if len(comm.Arguments) > 2 &&
|
||||
comm.Arguments[1] == "-al" &&
|
||||
comm.Arguments[2] == "/dev/null" {
|
||||
_, err := h.shell.WriteOutputString(
|
||||
fmt.Sprintf(
|
||||
"crw-rw-rw- 1 root root 1, 3 May 5 00:05 /dev/null\n%s",
|
||||
trampSuccess,
|
||||
),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
if len(comm.Arguments) > 1 && comm.Arguments[0] == "-lnd" {
|
||||
_, err := h.shell.WriteOutputString(
|
||||
fmt.Sprintf(
|
||||
"drwxrwxr-x 3 1000 1000 28 Apr 28 14:04 %s\n%s",
|
||||
comm.Arguments[1],
|
||||
trampSuccess,
|
||||
),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// should handle "--color=never --help 2>&1 | grep -iq busybox" so that we
|
||||
// can pretend to have busybox ls. The assumption is that this is easier
|
||||
// to emulate than the --dired flag of ls
|
||||
if len(comm.Arguments) > 7 &&
|
||||
comm.Arguments[4] == "grep" &&
|
||||
comm.Arguments[6] == "busybox" {
|
||||
_, err := h.shell.WriteOutputString(trampSuccess)
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := h.shell.WriteOutputString(trampFailure)
|
||||
return err
|
||||
case "mkdir":
|
||||
return (&MkdirCommand{filestore: h.filestore}).Execute(comm.Arguments)
|
||||
default:
|
||||
log.Printf("Error: Received unexpected command %s", comm.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.snorba.art/hugo/nssh/storage"
|
||||
)
|
||||
|
||||
type MkdirCommand struct {
|
||||
filestore storage.Filestore
|
||||
}
|
||||
|
||||
func (c *MkdirCommand) Execute(arguments []string) error {
|
||||
for _, dir := range arguments {
|
||||
if len(dir) > 0 && dir[0] != '-' {
|
||||
var currentPath string
|
||||
for _, part := range strings.Split(dir, "/") {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
currentPath += "/" + part
|
||||
if exists, _ := c.filestore.DirectoryExists(currentPath); !exists {
|
||||
err := c.filestore.MakeDirectory(currentPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,363 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/google/shlex"
|
||||
"github.com/pkg/term/termios"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type Shell struct {
|
||||
pty *os.File
|
||||
tty *os.File
|
||||
prompt []byte
|
||||
|
||||
readData []byte
|
||||
readIndex int
|
||||
winsize *Winsize
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Name string
|
||||
Arguments []string
|
||||
}
|
||||
|
||||
func (s *Shell) GetPty() *os.File {
|
||||
return s.pty
|
||||
}
|
||||
|
||||
func (s *Shell) Attach(conn io.ReadWriteCloser) error {
|
||||
pty, tty, err := termios.Pty()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.pty = pty
|
||||
s.tty = tty
|
||||
|
||||
if s.winsize != nil {
|
||||
SetWinsize(s.pty.Fd(), s.winsize)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer s.pty.Close()
|
||||
io.Copy(s.pty, conn)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
io.Copy(conn, s.pty)
|
||||
}()
|
||||
|
||||
_, err = s.tty.Write(s.prompt)
|
||||
return err
|
||||
}
|
||||
|
||||
func NewShell(prompt string) *Shell {
|
||||
return &Shell{
|
||||
prompt: []byte(prompt),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) SetWinsize(w, h uint32) {
|
||||
s.winsize = &Winsize{Width: uint16(w), Height: uint16(h)}
|
||||
|
||||
if s.pty != nil {
|
||||
SetWinsize(s.pty.Fd(), s.winsize)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) Close() (error, error) {
|
||||
return s.pty.Close(), s.tty.Close()
|
||||
}
|
||||
|
||||
var ErrEmptyCommand error = errors.New("Empty command line")
|
||||
|
||||
type State struct {
|
||||
Variables map[string]string
|
||||
Commands map[string]CommandExecutor
|
||||
}
|
||||
|
||||
type Statement interface {
|
||||
Words() []Word
|
||||
Statements() []Statement
|
||||
AddStatement(Statement)
|
||||
Evaluate(state State, output io.Writer) (exitcode uint8)
|
||||
}
|
||||
|
||||
type BaseStatement struct {
|
||||
words []Word
|
||||
statements []Statement
|
||||
}
|
||||
|
||||
func (s *BaseStatement) Words() []Word {
|
||||
return s.words
|
||||
}
|
||||
|
||||
func (s *BaseStatement) Statements() []Statement {
|
||||
return s.statements
|
||||
}
|
||||
|
||||
func (s *BaseStatement) AddStatement(statement Statement) {
|
||||
s.statements = append(s.statements, statement)
|
||||
}
|
||||
|
||||
func (s *BaseStatement) AddWord(word Word) {
|
||||
s.words = append(s.words, word)
|
||||
}
|
||||
|
||||
type Word string
|
||||
|
||||
func (w Word) Statements() []Statement {
|
||||
return []Statement{}
|
||||
}
|
||||
|
||||
func (w Word) AddStatement(statement Statement) {}
|
||||
|
||||
func (w Word) AddWord(word Word) {}
|
||||
|
||||
type ParserParameters interface {
|
||||
MakeStatement() Statement
|
||||
ShouldLeave(Word, rune) bool
|
||||
Supports(rune) bool
|
||||
SubParsers() []Parser
|
||||
}
|
||||
|
||||
type Parser interface {
|
||||
Parse(*bufio.Reader) (Statement, error)
|
||||
Parameters() ParserParameters
|
||||
}
|
||||
|
||||
type BaseParser struct {
|
||||
parameters ParserParameters
|
||||
}
|
||||
|
||||
func (p *BaseParser) Parameters() ParserParameters {
|
||||
return p.parameters
|
||||
}
|
||||
|
||||
type BaseParameters struct {
|
||||
EntryMarkers []rune
|
||||
subParsers []Parser
|
||||
}
|
||||
|
||||
func (p *BaseParameters) SubParsers() []Parser {
|
||||
return p.subParsers
|
||||
}
|
||||
|
||||
func (p *BaseParameters) Supports(r rune) bool {
|
||||
for _, char := range p.EntryMarkers {
|
||||
if char == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *BaseParameters) MakeStatement() Statement {
|
||||
return &BaseStatement{}
|
||||
}
|
||||
|
||||
func (p *BaseParameters) ShouldLeave(w Word, r rune) bool {
|
||||
if r == '\n' || r == ';' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BaseParser) Parse(r *bufio.Reader) (Statement, error) {
|
||||
var currentWord Word = ""
|
||||
statement := p.Parameters().MakeStatement()
|
||||
for {
|
||||
char, _, err := r.ReadRune()
|
||||
if err != nil {
|
||||
return statement, err
|
||||
}
|
||||
|
||||
if char == ' ' || char == '\t' {
|
||||
statement.AddWord(currentWord)
|
||||
currentWord = ""
|
||||
}
|
||||
|
||||
if p.Parameters().ShouldLeave(currentWord, char) {
|
||||
return statement, nil
|
||||
}
|
||||
|
||||
var matchedParser bool
|
||||
|
||||
for _, parser := range p.Parameters().SubParsers() {
|
||||
if parser.Parameters().Supports(char) {
|
||||
matchedParser = true
|
||||
nestedStatement, err := parser.Parse(r)
|
||||
|
||||
if word, ok := nestedStatement.(Word); ok {
|
||||
statement.AddWord(word)
|
||||
} else {
|
||||
statement.AddStatement(nestedStatement)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return statement, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !matchedParser {
|
||||
currentWord += Word(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Shell) ReadCommand() (*Command, error) {
|
||||
var currentLine string
|
||||
var backslash bool
|
||||
var insideDoubleQuote bool
|
||||
var insideSingleQuote bool
|
||||
var maybeHeredoc bool
|
||||
var insideHeredoc bool
|
||||
var heredocMarker string
|
||||
var heredocMarkerComplete bool
|
||||
var heredocCurrentWord string
|
||||
|
||||
reader := bufio.NewReader(s.tty)
|
||||
|
||||
GatherCommand:
|
||||
for {
|
||||
char, _, err := reader.ReadRune()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if insideHeredoc {
|
||||
currentLine += string(char)
|
||||
|
||||
if !heredocMarkerComplete && char == '<' {
|
||||
insideHeredoc = false
|
||||
continue
|
||||
}
|
||||
|
||||
if char == '\n' {
|
||||
log.Printf("Heredoc current word: \"%s\"", heredocCurrentWord)
|
||||
if heredocCurrentWord == heredocMarker {
|
||||
log.Println("exiting heredoc")
|
||||
insideHeredoc = false
|
||||
heredocMarkerComplete = false
|
||||
break GatherCommand
|
||||
}
|
||||
|
||||
if !heredocMarkerComplete {
|
||||
log.Println("Encountered heredoc marker: " + heredocCurrentWord)
|
||||
heredocMarker = strings.Trim(heredocCurrentWord, "'")
|
||||
heredocMarkerComplete = true
|
||||
}
|
||||
|
||||
heredocCurrentWord = ""
|
||||
continue
|
||||
}
|
||||
|
||||
heredocCurrentWord += string(char)
|
||||
continue
|
||||
}
|
||||
|
||||
if backslash {
|
||||
if !insideDoubleQuote && char == '\n' {
|
||||
currentLine += string(" ")
|
||||
continue
|
||||
}
|
||||
|
||||
currentLine += "\\" + string(char)
|
||||
backslash = false
|
||||
continue
|
||||
}
|
||||
|
||||
switch rune(char) {
|
||||
case '<':
|
||||
currentLine += string(char)
|
||||
if maybeHeredoc {
|
||||
maybeHeredoc = false
|
||||
insideHeredoc = true
|
||||
continue
|
||||
}
|
||||
|
||||
maybeHeredoc = true
|
||||
continue
|
||||
case '\\':
|
||||
backslash = true
|
||||
continue
|
||||
case '"':
|
||||
insideDoubleQuote = !insideDoubleQuote
|
||||
currentLine += string(char)
|
||||
case '\'':
|
||||
insideSingleQuote = !insideSingleQuote
|
||||
currentLine += string(char)
|
||||
case '\n':
|
||||
if insideSingleQuote || insideDoubleQuote {
|
||||
currentLine += string(char)
|
||||
continue
|
||||
}
|
||||
|
||||
break GatherCommand
|
||||
default:
|
||||
currentLine += string(char)
|
||||
}
|
||||
}
|
||||
|
||||
commandLine, err := shlex.Split(currentLine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(commandLine) == 0 {
|
||||
return nil, ErrEmptyCommand
|
||||
}
|
||||
|
||||
comm := &Command{Name: commandLine[0]}
|
||||
if len(commandLine) > 1 {
|
||||
comm.Arguments = commandLine[1:]
|
||||
}
|
||||
|
||||
return comm, nil
|
||||
}
|
||||
|
||||
func (s *Shell) SetPrompt(prompt string) {
|
||||
log.Printf("Changing prompt to \"%s\"", prompt)
|
||||
s.prompt = []byte(prompt)
|
||||
}
|
||||
|
||||
func (s *Shell) WritePrompt() error {
|
||||
_, err := s.tty.Write(s.prompt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Shell) WriteOutput(output []byte) (int, error) {
|
||||
return s.tty.Write(output)
|
||||
}
|
||||
|
||||
func (s *Shell) WriteOutputString(output string) (int, error) {
|
||||
return s.WriteOutput([]byte(output))
|
||||
}
|
||||
|
||||
// Winsize stores the Height and Width of a terminal.
|
||||
type Winsize struct {
|
||||
Height uint16
|
||||
Width uint16
|
||||
x uint16 // unused
|
||||
y uint16 // unused
|
||||
}
|
||||
|
||||
// SetWinsize sets the size of the given pty.
|
||||
func SetWinsize(fd uintptr, winsize *Winsize) {
|
||||
log.Printf("window resize %dx%d", winsize.Width, winsize.Height)
|
||||
unix.Syscall(
|
||||
unix.SYS_IOCTL,
|
||||
fd,
|
||||
uintptr(unix.TIOCSWINSZ), uintptr(unsafe.Pointer(winsize)),
|
||||
)
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package shell
|
||||
|
||||
type Block struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
type DollarBlock struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
type BlockParameters struct{}
|
||||
type DollarBlockParameters struct {
|
||||
BlockParameters
|
||||
}
|
||||
|
||||
func (p *BlockParameters) Enter(i *CharIterator) error {
|
||||
return i.Next()
|
||||
}
|
||||
|
||||
func (p *DollarBlockParameters) SubParsers() []Parser {
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: &WordParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DollarBlockParameters) MakeToken() Token {
|
||||
return &DollarBlock{}
|
||||
}
|
||||
|
||||
func (p *BlockParameters) SubParsers() []Parser {
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: &StatementParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BlockParameters) Supports(charsBefore []rune, r rune) bool {
|
||||
if r == '{' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *BlockParameters) ShouldLeave(charsBefore []rune, r rune) bool {
|
||||
if r == '}' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *BlockParameters) MakeToken() Token {
|
||||
return &Block{}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Echo struct{}
|
||||
|
||||
func (e *Echo) Name() string {
|
||||
return "echo"
|
||||
}
|
||||
|
||||
func (e *Echo) Execute(arguments []string, _ io.Reader, stdout io.Writer, stderr io.Writer) uint8 {
|
||||
_, err := stdout.Write([]byte(strings.Join(arguments, " ")))
|
||||
if err != nil {
|
||||
stderr.Write([]byte(err.Error()))
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package shell
|
||||
|
||||
import "io"
|
||||
|
||||
type CommandExecutor interface {
|
||||
Name() string
|
||||
Execute(arguments []string, stdin io.Reader, stdout io.Writer, stderr io.Writer) (exitcode uint8)
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
type DollarParameters struct {
|
||||
ignoreParams *IgnoreFirstCharParameters
|
||||
}
|
||||
|
||||
func (p *DollarParameters) SubParsers() []Parser {
|
||||
if p.ignoreParams == nil {
|
||||
p.ignoreParams = &IgnoreFirstCharParameters{}
|
||||
}
|
||||
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: p.ignoreParams,
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &WordParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &SubshellParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &DollarBlockParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *DollarParameters) Enter(_ *CharIterator) error {
|
||||
if p.ignoreParams != nil {
|
||||
p.ignoreParams.Reset()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *DollarParameters) Supports(charsBefore []rune, r rune) bool {
|
||||
if r == '$' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *DollarParameters) ShouldLeave(charsBefore []rune, r rune) bool {
|
||||
if isWhitespace(r) || r == ';' {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *DollarParameters) Leave(i *CharIterator) error {
|
||||
return i.Previous()
|
||||
}
|
||||
|
||||
func (p *DollarParameters) MakeToken() Token {
|
||||
return &Dollar{}
|
||||
}
|
||||
|
||||
type Dollar struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
func (d *Dollar) Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) {
|
||||
if len(d.tokens) > 1 {
|
||||
logstr := "Unexpected tokens after dollar sign: " + d.tokens[1].String()
|
||||
log.Println(logstr)
|
||||
stderr.Write([]byte(logstr))
|
||||
return []string{""}, 2
|
||||
}
|
||||
|
||||
if word, ok := d.tokens[0].(*Word); ok {
|
||||
return []string{state.Variable(word.String())}, 0
|
||||
} else if block, ok := d.tokens[0].(*DollarBlock); ok && len(block.Tokens()) > 0 {
|
||||
|
||||
if word, ok := block.Tokens()[0].(*Word); ok {
|
||||
return []string{state.Variable(word.String())}, 0
|
||||
} else {
|
||||
log.Println("Whaat")
|
||||
}
|
||||
} else {
|
||||
logstr := "Unexpected tokens after dollar sign: " + d.String()
|
||||
log.Println(logstr)
|
||||
stderr.Write([]byte(logstr))
|
||||
return []string{""}, 2
|
||||
}
|
||||
|
||||
return []string{""}, 1
|
||||
}
|
||||
|
||||
type IgnoreFirstCharParameters struct {
|
||||
entered bool
|
||||
}
|
||||
|
||||
func (p *IgnoreFirstCharParameters) Reset() {
|
||||
p.entered = false
|
||||
}
|
||||
|
||||
func (p *IgnoreFirstCharParameters) Enter(_ *CharIterator) error {
|
||||
p.entered = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IgnoreFirstCharParameters) SubParsers() []Parser {
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: &CharParserParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *IgnoreFirstCharParameters) MakeToken() Token {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IgnoreFirstCharParameters) Supports(_ []rune, r rune) bool {
|
||||
if !p.entered {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *IgnoreFirstCharParameters) ShouldLeave(_ []rune, r rune) bool {
|
||||
return true
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type DollarTest struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (t *DollarTest) TestParse() {
|
||||
parser := &BaseParser{&DollarParameters{}}
|
||||
reader := bufio.NewReader(strings.NewReader("$variable"))
|
||||
|
||||
iterator, err := NewCharIterator(reader)
|
||||
t.NoError(err)
|
||||
|
||||
token, err := parser.Parse(iterator, &CharCollection{})
|
||||
t.ErrorIs(err, io.EOF)
|
||||
|
||||
t.IsType(&Dollar{}, token)
|
||||
t.IsType(&Word{}, token.Tokens()[0])
|
||||
|
||||
t.Equal(token.Tokens()[0].String(), "variable")
|
||||
}
|
||||
|
||||
func (t *DollarTest) TestExpand() {
|
||||
parser := &BaseParser{&DollarParameters{}}
|
||||
reader := bufio.NewReader(strings.NewReader("$variable $hey"))
|
||||
|
||||
iterator, err := NewCharIterator(reader)
|
||||
t.NoError(err)
|
||||
|
||||
token, err := parser.Parse(iterator, &CharCollection{})
|
||||
|
||||
t.IsType(&Dollar{}, token)
|
||||
t.IsType(&Word{}, token.Tokens()[0])
|
||||
|
||||
state := NewShellState()
|
||||
state.SetVariable("variable", "Test")
|
||||
|
||||
expanded, exit := token.(Expandable).Expand(state, nil, nil)
|
||||
t.Equal(uint8(0), exit)
|
||||
t.Equal(expanded[0], "Test")
|
||||
}
|
||||
|
||||
func TestDollarTest(t *testing.T) {
|
||||
suite.Run(t, new(DollarTest))
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type LineParameters struct{}
|
||||
|
||||
func (p *LineParameters) SubParsers() []Parser {
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: &StatementParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *LineParameters) Supports(charsBefore []rune, r rune) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *LineParameters) ShouldLeave(charsBefore []rune, r rune) bool {
|
||||
if r == '\n' {
|
||||
return countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *LineParameters) MakeToken() Token {
|
||||
return &Line{}
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
func (l *Line) Evaluate(state State, stdin io.Reader, stdout io.Writer, stderr io.Writer) uint8 {
|
||||
var retval uint8 = 0
|
||||
|
||||
for _, token := range l.Tokens() {
|
||||
if eval, ok := token.(Evalable); ok {
|
||||
retval = eval.Evaluate(state, stdin, stdout, stderr)
|
||||
} else {
|
||||
stderr.Write([]byte(
|
||||
fmt.Sprintf(
|
||||
"shell: Syntax error near unexpected token: %s",
|
||||
token.String(),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return retval
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Parser interface {
|
||||
Parse(*CharIterator, *CharCollection) (Token, error)
|
||||
Parameters() ParserParameters
|
||||
}
|
||||
|
||||
type BaseParser struct {
|
||||
parameters ParserParameters
|
||||
}
|
||||
|
||||
func (p *BaseParser) Parameters() ParserParameters {
|
||||
return p.parameters
|
||||
}
|
||||
|
||||
type CharIterator struct {
|
||||
reader *bufio.Reader
|
||||
currentChar rune
|
||||
lastChar rune
|
||||
}
|
||||
|
||||
func NewCharIterator(reader *bufio.Reader) (*CharIterator, error) {
|
||||
char, _, err := reader.ReadRune()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CharIterator{
|
||||
reader: reader,
|
||||
currentChar: char,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (i *CharIterator) Next() error {
|
||||
var err error
|
||||
|
||||
i.lastChar = i.currentChar
|
||||
i.currentChar, _, err = i.reader.ReadRune()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *CharIterator) Previous() error {
|
||||
if i.lastChar == -1 {
|
||||
return errors.New("Chariterator can only go back once")
|
||||
}
|
||||
|
||||
err := i.reader.UnreadRune()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i.currentChar = i.lastChar
|
||||
i.lastChar = -1
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type CharCollection struct {
|
||||
chars []rune
|
||||
}
|
||||
|
||||
func (c *CharCollection) Append(r rune) {
|
||||
c.chars = append(c.chars, r)
|
||||
}
|
||||
|
||||
func (c *CharCollection) Chars() []rune {
|
||||
return c.chars
|
||||
}
|
||||
|
||||
func (i CharIterator) Current() rune {
|
||||
return i.currentChar
|
||||
}
|
||||
|
||||
func (p *BaseParser) Parse(i *CharIterator, charsBefore *CharCollection) (Token, error) {
|
||||
token := p.Parameters().MakeToken()
|
||||
if enter, ok := p.parameters.(Enterable); ok {
|
||||
err := enter.Enter(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
parsers := p.Parameters().SubParsers()
|
||||
|
||||
ParseLoop:
|
||||
for {
|
||||
char := i.Current()
|
||||
|
||||
if p.Parameters().ShouldLeave(charsBefore.Chars(), char) {
|
||||
if leave, ok := p.Parameters().(Leavable); ok {
|
||||
err := leave.Leave(i)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
var matchedParser bool
|
||||
for _, parser := range parsers {
|
||||
if parser.Parameters().Supports(charsBefore.Chars(), char) {
|
||||
matchedParser = true
|
||||
|
||||
nestedToken, err := parser.Parse(i, charsBefore)
|
||||
|
||||
if token != nil {
|
||||
token.AddToken(nestedToken)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
charsBefore.Append(char)
|
||||
err = i.Next()
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
continue ParseLoop
|
||||
}
|
||||
}
|
||||
|
||||
if !matchedParser {
|
||||
return token, fmt.Errorf(
|
||||
"Parser encountered unsupported token or char. Parser: %s full text: %s -\n Char: %s",
|
||||
reflect.TypeOf(*&p.parameters).String(),
|
||||
string(charsBefore.Chars()),
|
||||
string(char),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package shell
|
||||
|
||||
type ParserParameters interface {
|
||||
MakeToken() Token
|
||||
ShouldLeave(charsBefore []rune, r rune) bool
|
||||
Supports(charsBefore []rune, r rune) bool
|
||||
SubParsers() []Parser
|
||||
}
|
||||
|
||||
type Enterable interface {
|
||||
Enter(*CharIterator) error
|
||||
}
|
||||
|
||||
type Resetable interface {
|
||||
Reset()
|
||||
}
|
||||
|
||||
type Leavable interface {
|
||||
Leave(*CharIterator) error
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type CharIteratorTest struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (t *CharIteratorTest) TestNextAndBacktrack() {
|
||||
charIterator, err := NewCharIterator(bufio.NewReader(strings.NewReader("abcdefg")))
|
||||
t.NoError(err)
|
||||
|
||||
t.Equal(string('a'), string(charIterator.Current()))
|
||||
|
||||
err = charIterator.Next()
|
||||
t.NoError(err)
|
||||
|
||||
t.Equal(string('b'), string(charIterator.Current()))
|
||||
|
||||
err = charIterator.Previous()
|
||||
t.NoError(err)
|
||||
|
||||
t.Equal(string('a'), string(charIterator.Current()))
|
||||
|
||||
err = charIterator.Previous()
|
||||
t.Error(err)
|
||||
t.Equal("Chariterator can only go back once", err.Error())
|
||||
|
||||
err = charIterator.Next()
|
||||
t.NoError(err)
|
||||
|
||||
t.Equal(string('b'), string(charIterator.Current()))
|
||||
|
||||
}
|
||||
|
||||
func TestCharIteratorTest(t *testing.T) {
|
||||
suite.Run(t, new(CharIteratorTest))
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package shell
|
||||
|
||||
type Pipe struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
type PipeParameters struct {
|
||||
StatementParameters
|
||||
}
|
||||
|
||||
func (p *PipeParameters) SubParsers() []Parser {
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: &StatementParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PipeParameters) Supports(charsBefore []rune, r rune) bool {
|
||||
if r == '|' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *PipeParameters) MakeToken() Token {
|
||||
return &Pipe{}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type QuoteParameters struct{}
|
||||
|
||||
func (p *QuoteParameters) SubParsers() []Parser {
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: &DollarParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &CharParserParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *QuoteParameters) Supports(charsBefore []rune, r rune) bool {
|
||||
if r == '"' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *QuoteParameters) ShouldLeave(charsBefore []rune, r rune) bool {
|
||||
if r == '"' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *QuoteParameters) MakeToken() Token {
|
||||
return &Quote{}
|
||||
}
|
||||
|
||||
type Quote struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
func (q *Quote) Expand(state State, stdin io.Reader, stderr io.Writer) ([]string, uint8) {
|
||||
var retcode uint8
|
||||
var str string
|
||||
|
||||
for _, token := range q.Tokens() {
|
||||
if expand, ok := token.(Expandable); ok {
|
||||
log.Println(expand)
|
||||
var expansion []string
|
||||
expansion, retcode = expand.Expand(state, stdin, stderr)
|
||||
|
||||
str += strings.Join(expansion, " ")
|
||||
continue
|
||||
}
|
||||
|
||||
str += token.String()
|
||||
}
|
||||
|
||||
return []string{strings.Trim(str, "'")}, retcode
|
||||
}
|
||||
|
||||
type SingleQuoteParameters struct {
|
||||
QuoteParameters
|
||||
}
|
||||
|
||||
type SingleQuote struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
func (p *SingleQuoteParameters) Supports(charsBefore []rune, r rune) bool {
|
||||
if r == '\'' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *SingleQuoteParameters) ShouldLeave(charsBefore []rune, r rune) bool {
|
||||
if r == '\'' {
|
||||
return len(charsBefore) == 0 ||
|
||||
countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.snorba.art/hugo/nssh/commands/shell/builtins"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type QuotesTest struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (t *QuotesTest) TestParse() {
|
||||
expectedContents := fmt.Sprintf("'%s'", "word1 word2 word3\n word4")
|
||||
|
||||
parser := &BaseParser{&QuoteParameters{}}
|
||||
reader := bufio.NewReader(strings.NewReader(expectedContents))
|
||||
|
||||
iterator, err := NewCharIterator(reader)
|
||||
t.NoError(err)
|
||||
|
||||
token, err := parser.Parse(iterator, &CharCollection{})
|
||||
t.ErrorIs(err, io.EOF)
|
||||
|
||||
t.Equal(expectedContents, token.String())
|
||||
|
||||
expanded, _ := token.(Expandable).Expand(nil, nil, nil)
|
||||
t.Equal("word1 word2 word3\n word4", strings.Join(expanded, " "))
|
||||
}
|
||||
|
||||
func (t *QuotesTest) TestExpandVariables() {
|
||||
parser := &BaseParser{&QuoteParameters{}}
|
||||
reader := bufio.NewReader(
|
||||
strings.NewReader("This is a $variable ${variable2} $variable"),
|
||||
)
|
||||
|
||||
state := NewShellState()
|
||||
state.SetVariable("variable", "supergood")
|
||||
state.SetVariable("variable2", "thing")
|
||||
|
||||
echo := &builtins.Echo{}
|
||||
state.SetCommand(echo.Name(), echo)
|
||||
|
||||
iterator, err := NewCharIterator(reader)
|
||||
t.NoError(err)
|
||||
|
||||
token, err := parser.Parse(iterator, &CharCollection{})
|
||||
t.ErrorIs(err, io.EOF)
|
||||
|
||||
expanded, _ := token.(Expandable).Expand(state, nil, nil)
|
||||
t.Equal("This is a supergood thing supergood", expanded[0])
|
||||
}
|
||||
|
||||
func TestQuotesTest(t *testing.T) {
|
||||
suite.Run(t, new(QuotesTest))
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package shell
|
||||
|
||||
import (
|
||||
"bufio"
|
@ -0,0 +1,112 @@
|
||||
package shell
|
||||
|
||||
type State interface {
|
||||
Variable(string) string
|
||||
Command(string) CommandExecutor
|
||||
Env(string) string
|
||||
|
||||
Pushd(string) error
|
||||
Popd() error
|
||||
Cwd() string
|
||||
|
||||
SetVariable(string, string)
|
||||
SetCommand(string, CommandExecutor)
|
||||
SetEnv(string, string)
|
||||
|
||||
Clone() State
|
||||
}
|
||||
|
||||
func NewShellState() *ShellState {
|
||||
return &ShellState{
|
||||
variables: map[string]string{},
|
||||
commands: map[string]CommandExecutor{},
|
||||
env: map[string]string{},
|
||||
dirstack: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
type ShellState struct {
|
||||
variables map[string]string
|
||||
commands map[string]CommandExecutor
|
||||
env map[string]string
|
||||
dirstack []string
|
||||
}
|
||||
|
||||
func (s *ShellState) Variable(n string) string {
|
||||
if v, ok := s.variables[n]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *ShellState) Command(n string) CommandExecutor {
|
||||
if v, ok := s.commands[n]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShellState) Env(n string) string {
|
||||
if v, ok := s.env[n]; ok {
|
||||
return v
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *ShellState) SetVariable(n string, v string) {
|
||||
s.variables[n] = v
|
||||
}
|
||||
|
||||
func (s *ShellState) SetCommand(n string, e CommandExecutor) {
|
||||
s.commands[n] = e
|
||||
}
|
||||
|
||||
func (s *ShellState) SetEnv(n string, v string) {
|
||||
s.env[n] = v
|
||||
}
|
||||
|
||||
func (s *ShellState) Pushd(dir string) error {
|
||||
s.dirstack = append([]string{dir}, s.dirstack...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShellState) Popd() error {
|
||||
if len(s.dirstack) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.dirstack = s.dirstack[1:]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ShellState) Cwd() string {
|
||||
if len(s.dirstack) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return s.dirstack[0]
|
||||
}
|
||||
|
||||
func (s *ShellState) Clone() State {
|
||||
state := NewShellState()
|
||||
|
||||
for k, v := range s.commands {
|
||||
state.SetCommand(k, v)
|
||||
}
|
||||
|
||||
for k, v := range s.variables {
|
||||
state.SetVariable(k, v)
|
||||
}
|
||||
|
||||
for k, v := range s.env {
|
||||
state.SetEnv(k, v)
|
||||
}
|
||||
|
||||
state.dirstack = make([]string, len(s.dirstack))
|
||||
copy(s.dirstack, state.dirstack)
|
||||
|
||||
return state
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package shell
|
||||
|
||||
type Statement struct {
|
||||
BaseToken
|
||||
}
|
||||
|
||||
type StatementParameters struct{}
|
||||
|
||||
func (p *StatementParameters) SubParsers() []Parser {
|
||||
return []Parser{
|
||||
&BaseParser{
|
||||
parameters: &QuoteParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &SingleQuoteParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &WordParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &DollarParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &PipeParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &SubshellParameters{},
|
||||
},
|
||||
&BaseParser{
|
||||
parameters: &BlockParameters{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StatementParameters) Supports(charsBefore []rune, r rune) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *StatementParameters) MakeToken() Token {
|
||||
return &Statement{}
|
||||
}
|
||||
|
||||
func (p *StatementParameters) ShouldLeave(charsBefore []rune, r rune) bool {
|
||||
if r == '\n' || r == ';' {
|
||||
return countBackslashSuffixes(charsBefore)%2 == 0
|
||||
}
|
||||