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.

229 lines
4.1 KiB
Go

package main
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")
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)),
)
}