go-expect

repository·master·Indexed 19 days ago

https://github.com/netflix/go-expect

A Go package providing an expect-like interface for automating application control via a pseudoterminal (PTY). It focuses on expecting specific output and sending input through a PTY without managing the process lifecycle itself. It includes utilities for matching strings and regular expressions, handling timeouts, and interacting with local commands via os/exec or remote terminals via SSH.

Tokens
4.1K
Snippets
21
Records
24
Agent score
61%

What's inside go-expect

  1. What is go-expect?

    master
    The go-expect package provides an interface similar to the expect utility to automate the control of applications. Unlike the standard expect tool, go-expect does not manage or spawn the process lifecycle itself. Instead, it focuses exclusively on expecting specific output and sending input through a pseudoterminal (PTY).
  2. Monitor Console operations with Observers

    master

    You can hook into the lifecycle of Expect and Send operations using observers.

    ExpectObserver

    Called after each Expect operation.

    • matchers: The list of active matchers when an error occurred, or the matchers that matched buf if err is nil.
    • buf: The captured output that was matched against.
    • err: The error that occurred (can be nil).

    type ExpectObserver func(matchers []Matcher, buf string, err error)

    SendObserver

    Called after each Send operation.

    • msg: The string that was sent.
    • num: The number of bytes actually sent.
    • err: The error that occurred (can be nil).

    type SendObserver func(msg string, num int, err error)

  3. Interact with SSH terminals using golang.org/x/crypto/ssh/terminal

    master

    When working with SSH or other remote terminals, you can use the file descriptor of the go-expect TTY to perform operations like reading passwords. By accessing c.Tty().Fd(), you can pass the underlying file descriptor to functions like terminal.ReadPassword(fd) to simulate or capture interactive terminal behavior.

    package main
    
    import (
    	"fmt"
    
    	"golang.org/x/crypto/ssh/terminal"
    
    	expect "github.com/Netflix/go-expect"
    )
    
    func getPassword(fd int) string {
    	bytePassword, _ := terminal.ReadPassword(fd)
    
    	return string(bytePassword)
    }
    
    func main() {
    	c, _ := expect.NewConsole()
    
    	defer c.Close()
    
    	donec := make(chan struct{})
    	go func() {
    		defer close(donec)
    		c.SendLine("hunter2")
    	}()
    
    	echoText := getPassword(int(c.Tty().Fd()))
    
    	<-donec
    
    	fmt.Printf("\nPassword from stdin: %s", echoText)
    }
  4. Automate applications using os.Exec

    master

    To automate a local command using os/exec, create a new console with expect.NewConsole(). You must then assign the console's TTY (c.Tty()) to the command's Stdin, Stdout, and Stderr. This allows you to interact with the process using c.Send(), c.SendLine(), or by waiting for specific patterns like c.ExpectEOF().

    package main
    
    import (
    	"log"
    	"os"
    	"os/exec"
    	"time"
    
    	expect "github.com/Netflix/go-expect"
    )
    
    func main() {
    	c, err := expect.NewConsole(expect.WithStdout(os.Stdout))
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer c.Close()
    
    	cmd := exec.Command("vi")
    	cmd.Stdin = c.Tty()
    	cmd.Stdout = c.Tty()
    	cmd.Stderr = c.Tty()
    
    	go func() {
    		c.ExpectEOF()
    	}()
    
    	err = cmd.Start()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	time.Sleep(time.Second)
    	c.Send("iHello world\x1b")
    	time.Sleep(time.Second)
    	c.Send("dd")
    	time.Sleep(time.Second)
    	c.SendLine(":q!")
    
    	err = cmd.Wait()
    	if err != nil {
    		log.Fatal(err)
    	}
    }
  5. Access the Console's TTY and File Descriptor

    master

    If you need to interact with the underlying pseudoterminal or the master file descriptor:

    • Tty() *os.File: Returns the slave part of the pty (the pseudoterminal).
    • Fd() uintptr: Returns the file descriptor referencing the master part of the pty.
    • Read(b []byte) (int, error): Reads bytes directly from the Console's tty.
    • Write(b []byte) (int, error): Writes bytes directly to the Console's tty.
  6. Match errors with Error, EOF, and PTSClosed

    master

    Use these functions to expect specific error conditions when reading from the console:

    • Error(errs ...error): Matches if the read operation returns one of the specified errors.
    • EOF(): A helper that expects io.EOF.
    • PTSClosed(): A specialized matcher for Linux systems to detect when a pseudo-terminal (pts) has been closed, specifically matching the os.PathError with Op: "read", Path: "/dev/ptmx", and Err: syscall.Errno(0x5).
    // Expect an EOF
    expect.EOF()
    
    // Expect a specific error
    expect.Error(fmt.Errorf("connection reset"))
    
    // Expect a closed PTS on Linux
    expect.PTSClosed()
  7. Match regular expressions with Regexp and RegexpPattern

    master

    You can match console output using regular expressions in two ways:

    1. Regexp(res ...*regexp.Regexp): Pass pre-compiled *regexp.Regexp objects.
    2. RegexpPattern(ps ...string): Pass raw regex strings. This function will attempt to compile them and returns an error if any pattern is invalid.
    // Using pre-compiled regex
    re := regexp.MustCompile("error: [0-9]+")
    expect.Regexp(re)
    
    // Using raw strings
    expect.RegexpPattern("user: [a-z]+", "id: [0-9]+")
  8. Expect a specific string with ExpectString

    master

    Use ExpectString(s string) to read from the Console's tty until the exact string s is encountered. It returns the buffer read by the console and any error encountered. This is a convenience wrapper around Expect using the String option.

    // s is the exact string you are looking for in the terminal output
    output, err := console.ExpectString("Password: ")
  9. Execute a callback on match with Then

    master

    The Then(f ConsoleCallback) method can be chained to an ExpectOpt to execute a callback function if a match is found. The callback receives a *bytes.Buffer containing the content read from the console at the time of the match.

    expect.String("found data").Then(func(buf *bytes.Buffer) error {
        fmt.Printf("Matched content: %s\n", buf.String())
        return nil
    })
  10. Use Expect for custom matching logic

    master

    The Expect(opts ...ExpectOpt) method is the core primitive for reading from the Console's tty. It reads until a condition specified via ExpectOpt is met or an error occurs.

    Key behaviors:

    • No extra bytes: Once a condition is met, no additional bytes are read. If the program being tested is waiting for input, it will block until the next Expect call.
    • Buffering: Sends are queued in the tty's internal buffer so that subsequent Expect calls can read the remaining bytes (e.g., the rest of a prompt).
    • Timeouts: You can provide a ReadTimeout via options to prevent the call from blocking indefinitely.
    • Matchers: It uses matchers to determine when to stop reading. If a matcher implements the CallbackMatcher interface, its Callback(buf []byte) error method will be executed after a match is found.
    // Example of using Expect with options (assuming String and ReadTimeout are available)
    output, err := console.Expect(
        String("prompt > "),
        ReadTimeout(5 * time.Second),
    )
  11. Configure Expect options with ExpectOpt

    master

    The ExpectOpt type is a functional option used to configure Expect statements. You can use built-in functions to set timeouts, match specific content, or handle errors. Common patterns include using WithTimeout for read timeouts and various matchers like String, Regexp, or Error to define what the test should look for in the console output.

    // Example of using ExpectOpt functions
    expect.WithTimeout(5 * time.Second),
    expect.String("login successful"),
    expect.RegexpPattern("user: [a-z]+"),
  12. Send input to the Console

    master

    To automate input into an interactive application, use the Send or SendLine methods.

    • Send(s string) (int, error): Writes the exact string s to the Console's tty.
    • SendLine(s string) (int, error): Writes the string s followed by a newline character (\n) to the Console's tty.
    // Send a specific command
    _, err := console.Send("ls -la")
    
    // Send a command with a newline
    _, err := console.SendLine("help")