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)), ) }