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
229 lines
4.1 KiB
Go
package shell
|
|
|
|
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)),
|
|
)
|
|
}
|