You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
364 lines
6.6 KiB
Go
364 lines
6.6 KiB
Go
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)),
|
|
)
|
|
}
|