expect

package module
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 5, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

go-expect

A Go library for scripted conversations with interactive programs and duplex streams. It supports literal/regexp matching, exp_continue-style callbacks, reusable password/confirmation/paging rules, cancellation, bounded buffers and explicit secret redaction.

import expect "github.com/gatework/go-expect"

Requires Go 1.26+. The PTY adapter is supported and tested on Linux and macOS. This is an API evolution of Netflix/go-expect; see migration. The original Apache-2.0 license and copyright notices are retained.

Network-device connector

Device authentication, vendor profiles, commands and backups are maintained in go-net-connector. This module owns only matching, streams, PTYs, cancellation and secret filtering. See the device-layer migration for the breaking import change.

Password, confirmation and paging

Start with an already connected io.ReadWriteCloser (for example net.Conn), or create a Console and attach its TTY() to a caller-owned exec.Cmd. A successful NewSession takes exclusive ownership of the stream, including closing it. The stream's Close must interrupt both Read and Write.

s, err := expect.NewSession(conn)
if err != nil {
    return err // conn still belongs to the caller
}
defer s.Close()

// Register before the secret can be sent or echoed.
if err := s.Redact(password); err != nil {
    return err
}
login, err := expect.NewResponder(expect.Rule{
    Pattern: expect.Literal("Password:"),
    Respond: expect.Reply(password + "\r"),
})
if err != nil {
    return err
}
if _, err := login.Expect(ctx, s, []expect.Pattern{
    expect.Literal("device>"),
}); err != nil {
    return err
}

rules, err := expect.NewResponder(
    expect.Rule{
        Pattern: expect.Literal("Apply change? [y/N] "),
        Respond: expect.Reply("y\r"),
        Limit: 2,
    },
    expect.Rule{
        Pattern: expect.Literal("--More--"),
        Respond: expect.Reply(" "),
        Limit: 100,
    },
)
if err != nil {
    return err
}
for _,command := range commands {
    if _, err := s.Send(ctx, command + "\r"); err != nil {
        return err
    }
    if _, err := rules.Expect(ctx, s, []expect.Pattern{
        expect.Literal("device>"),
    }); err != nil {
        return err
    }
}

Use a parent context deadline to bound the entire operation. A returned prompt means the prompt matched; inspect device-specific failure output separately. The generic Session does not establish connections or retry commands. Use go-net-connector/connector for device operations. Neither library accepts unknown host keys or answers arbitrary confirmations automatically.

A complete runnable example, including dynamic regexp replies:

go run ./examples/dialogue

It simulates a local device with net.Pipe; it does not connect to a real device.

Continue matching

result, err := s.Expect(ctx, []expect.Case{
    {
        Pattern: expect.Literal("Password:"),
        Handler: func(ctx context.Context, s *expect.Session, r expect.Result) (expect.Action, error) {
            _, err := s.Send(ctx, password + "\r")
            return expect.ContinueWithoutReset, err
        },
    },
    {Pattern: expect.Literal("device>")},
}, expect.WithTimeout(30*time.Second))

Continue resets the relative timer. ContinueWithoutReset preserves it. Neither extends the parent context deadline. Responder uses the latter. Result contains the final segment only; install WithOutput(writer) to record the filtered receive history across intermediate responses.

PTY and processes

c, err := expect.NewConsole(
    expect.WithSessionOptions(expect.WithDefaultTimeout(10*time.Second)),
)
if err != nil {
    return err
}
defer c.Close()
cmd := exec.CommandContext(ctx, "your-program", "literal-argument")
cmd.Stdin, cmd.Stdout, cmd.Stderr = c.TTY(), c.TTY(), c.TTY()
if err := cmd.Start(); err != nil {
    return err
}
// The child has inherited the slave. Release the parent's copy for EOF.
c.TTY().Close()
// Use c.Session().Expect / Send; always arrange cmd.Wait and failure cleanup.

Console does not own the child process. WithRaw() is available for applications requiring unbuffered keys and no terminal echo; canonical mode is the default. See the complete process cleanup and device dialogue in console_test.go.

Contracts and verification

API contracts describe matching, deadlines, error recovery, ownership, thread safety and redaction. Test utilities live in expecttest.

bash script/ci

This runs format checks, module verification, vet, race tests (including local PTY), fuzz smokes, benchmarks, the dialogue example and a disposable source consumer module. CI covers Linux/macOS on Go 1.26 and 1.27. Local tests and CI runs are separate evidence; neither substitutes for real device interoperability.

Automatic releases

Each push to master publishes a release after the complete CI gate passes. Versions start at v0.1.0 and increment the patch number automatically. The publisher reuses the exact CI source artifact, verifies uploaded checksums and bytes, and resumes interrupted drafts without replacing published assets. PRs and feature branches do not publish. See release operations for version policy, retries, queue behavior and local validation commands.

Documentation

Overview

Package expect automates interactive byte streams with bounded waits, continuation callbacks and reusable response rules. Session owns a supplied duplex transport; Console adapts a PTY on Linux and macOS. Callers own process lifecycle, connection establishment, device policy and command success checks.

Matching uses unmodified input. Diagnostics and output copies use explicit streaming secret redaction. Result fields are raw, detached snapshots.

Copyright 2018 Netflix, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy at https://www.apache.org/licenses/LICENSE-2.0

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrClosed          = errors.New("expect: session closed")
	ErrBusy            = errors.New("expect: another expect operation is active")
	ErrTimeout         = errors.New("expect: timeout")
	ErrIdleTimeout     = errors.New("expect: idle timeout")
	ErrBufferLimit     = errors.New("expect: buffer limit reached")
	ErrEmptyMatch      = errors.New("expect: empty match")
	ErrPollUnsupported = errors.New("expect: OS polling unsupported")
)

Functions

This section is empty.

Types

type Action

type Action uint8

Action determines whether a matched Case returns or continues waiting.

const (
	Return Action = iota + 1
	Continue
	ContinueWithoutReset
)

type Capture

type Capture struct {
	Text    string
	Matched bool
}

Capture distinguishes an unmatched optional group from an empty matched group.

func (Capture) GoString

func (c Capture) GoString() string

func (Capture) String

func (Capture) String() string

type Case

type Case struct {
	Pattern Pattern
	Handler Handler
}

Case associates a pattern with an optional handler (nil means Return).

func (Case) GoString

func (c Case) GoString() string

func (Case) String

func (Case) String() string

type Console

type Console struct {
	// contains filtered or unexported fields
}

Console adapts an owned PTY to Session. It does not start, wait for, signal or reap processes. TTY is intended for a caller-owned exec.Cmd or local device.

func NewConsole

func NewConsole(opts ...ConsoleOption) (*Console, error)

NewConsole creates a PTY and a Session. Failure closes every acquired handle.

func (*Console) Close

func (c *Console) Close() error

Close closes the Session and both PTY handles, not any attached process.

func (*Console) Session

func (c *Console) Session() *Session

Session returns the single owner of Console's matching and sending operations.

func (*Console) TTY added in v0.1.1

func (c *Console) TTY() *os.File

TTY returns the slave; the master is intentionally not exposed for competing reads. Close the parent's slave after starting a process when waiting for process EOF.

type ConsoleOption

type ConsoleOption func(*consoleOptions) error

ConsoleOption configures the PTY adapter.

func WithRaw

func WithRaw() ConsoleOption

WithRaw configures the slave in raw mode, including disabling echo.

func WithSessionOptions

func WithSessionOptions(opts ...SessionOption) ConsoleOption

WithSessionOptions configures the Console's underlying Session.

type Event

type Event struct {
	Operation        string
	CaseIndex, Bytes int
	Error            string
}

Event contains metadata only. Error is a classification, never a raw message.

type ExpectOption

type ExpectOption func(*expectOptions) error

ExpectOption configures matching or timing for a single Expect operation.

func WithDeclarationPriority added in v0.1.1

func WithDeclarationPriority() ExpectOption

WithDeclarationPriority selects the first declared matching case, regardless of its byte position. Without this option the earliest byte wins.

func WithIdleTimeout

func WithIdleTimeout(d time.Duration) ExpectOption

WithIdleTimeout limits time without new input; zero disables it.

func WithStreamingFallback added in v0.1.1

func WithStreamingFallback(handler Handler) ExpectOption

WithStreamingFallback consumes the first 32 KiB when 64 KiB are pending and no ordinary case matches. Result.CaseIndex is -1 and Match is the consumed data. Continue and ContinueWithoutReset both preserve all existing timers.

func WithTimeout

func WithTimeout(d time.Duration) ExpectOption

WithTimeout sets the relative timer, including elapsed callback time. Zero disables it. Continue can reset it; context deadlines cannot be reset.

type Handler

type Handler func(context.Context, *Session, Result) (Action, error)

Handler runs after consumption, outside internal locks. It may send or close, but may not call Expect recursively on the same Session.

type Observer

type Observer func(Event)

Observer is invoked once per completed public Expect, Send or SendAll operation. Observers run synchronously outside locks and must not block indefinitely.

type Pattern

type Pattern struct {
	// contains filtered or unexported fields
}

Pattern is an immutable literal, regular expression, EOF or All pattern. Its zero value is invalid. It never contains per-operation match state.

func All

func All(patterns ...Pattern) Pattern

All matches all supplied text patterns, in any order, consuming through the last match. Repeated patterns may refer to the same bytes. Captures are omitted.

func Compile

func Compile(expr string) (Pattern, error)

Compile compiles a Go regular expression. Empty matches are rejected at runtime.

func EOF

func EOF() Pattern

EOF matches a clean end of input, after buffered text has been matched.

func FromRegexp added in v0.1.1

func FromRegexp(re *regexp.Regexp) (Pattern, error)

FromRegexp snapshots a compiled expression, including POSIX/Longest semantics. Later changes to the caller's expression do not affect the Pattern.

func Line added in v0.1.3

func Line(text string) Pattern

Line matches a byte-exact final line with optional space, tab and NUL padding. The match includes its preceding CR/LF delimiter and trailing CR/LF bytes. Empty text and text containing CR or LF are invalid declarations.

func Literal

func Literal(text string) Pattern

Literal matches a nonempty byte string, including arbitrary binary data.

func (Pattern) GoString

func (p Pattern) GoString() string

func (Pattern) IsZero added in v0.1.3

func (p Pattern) IsZero() bool

IsZero distinguishes an unspecified pattern from a malformed declaration.

func (Pattern) String

func (p Pattern) String() string

func (Pattern) ValidateText added in v0.1.3

func (p Pattern) ValidateText() error

ValidateText checks a nonempty text declaration before transport access. EOF and regular expressions matching empty input are not text declarations.

type Redactor added in v0.1.1

type Redactor struct {
	// contains filtered or unexported fields
}

Redactor filters one diagnostic stream independently of any Session. Rules, retained prefixes and replacement runs are scoped to this instance. Methods are concurrency safe. Formatting never exposes secrets or retained data.

func NewRedactor added in v0.1.1

func NewRedactor(secrets []string, replacement string) (*Redactor, error)

NewRedactor copies literal secrets and the nonempty custom replacement.

func (*Redactor) Append added in v0.1.1

func (r *Redactor) Append(data []byte) []byte

Append returns only bytes that cannot begin a future secret match. It replaces each contiguous union of overlapping or adjacent matches once, across chunks.

func (*Redactor) Finish added in v0.1.1

func (r *Redactor) Finish() []byte

Finish ends a stream and conservatively masks any retained secret prefix. Repeated calls are empty; the instance may then filter a fresh stream.

func (*Redactor) GoString added in v0.1.1

func (r *Redactor) GoString() string

func (*Redactor) SetSecrets added in v0.1.1

func (r *Redactor) SetSecrets(secrets []string) ([]byte, error)

SetSecrets ends the old rule scope and returns its safe final bytes. Suspected old secret prefixes are masked before new rules take effect. Invalid rules leave the existing scope untouched. Callers must deliver the returned bytes.

func (*Redactor) String added in v0.1.1

func (*Redactor) String() string

type Responder

type Responder struct {
	// contains filtered or unexported fields
}

Responder is an immutable rule set, safe to reuse across different sessions. Rule counts are local to each call; callback closures remain caller-owned.

func NewResponder

func NewResponder(rules ...Rule) (*Responder, error)

NewResponder validates and snapshots rules without accessing a session.

func (*Responder) Expect

func (r *Responder) Expect(ctx context.Context, s *Session, completion []Pattern, opts ...ExpectOption) (Result, error)

Expect responds to registered prompts while waiting for a completion pattern. Completion wins only same-position ties. Responses preserve the original relative timer. The returned CaseIndex indexes completion, or is -1 on failure.

func (*Responder) GoString

func (r *Responder) GoString() string

func (*Responder) String

func (r *Responder) String() string

type Response

type Response func(context.Context, *Session, Result) error

Response sends a reply or performs a caller-defined action for a matched rule. It must return write errors; actions are never automatically retried.

func Reply

func Reply(text string) Response

Reply returns an action that sends text verbatim, including explicit line ends.

type ResponseError

type ResponseError struct{ RuleIndex int }

ResponseError identifies a response rule that exceeded its per-call limit.

func (*ResponseError) Error

func (e *ResponseError) Error() string

type Result

type Result struct {
	CaseIndex     int
	Before, Match string
	Captures      []Capture
}

Result is a detached snapshot of the final match, not the whole conversation. Before contains pending input on failure; CaseIndex is -1 without a match. Explicit fields contain unredacted bytes; formatting emits only metadata.

func (Result) GoString

func (r Result) GoString() string

func (Result) String

func (r Result) String() string

type Rule

type Rule struct {
	Pattern Pattern
	Respond Response
	Limit   int
}

Rule describes a reusable automatic response. Limit zero means one response per Expect, negative is invalid, positive sets an explicit finite limit.

func (Rule) GoString

func (r Rule) GoString() string

func (Rule) String

func (Rule) String() string

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session owns one duplex transport and one matching buffer. The transport must support concurrent Read/Write and guarantee Close interrupts both. After a successful NewSession, callers must not perform I/O on that transport. Expect and Poll are exclusive. Sends are serialized; Close and Redact are concurrency safe. SendAll may queue bounded input for the next Expect. Do not copy a Session. Callback and output execution is cooperative.

func NewSession

func NewSession(transport io.ReadWriteCloser, opts ...SessionOption) (*Session, error)

NewSession transfers transport ownership only on success. Options are validated before any reads, writes or closes take place.

func (*Session) Close

func (s *Session) Close() error

Close interrupts I/O and joins owned workers. It never waits for application callbacks or closes borrowed output writers. Repeated calls return the same error.

func (*Session) Expect

func (s *Session) Expect(ctx context.Context, cases []Case, opts ...ExpectOption) (Result, error)

Expect consumes input through the earliest currently visible match. Ties use declaration order. Continuations consume their match and rescan the remaining buffer, without rereading or replaying bytes. Context cancellation and timeout leave unmatched input available to a subsequent call.

func (*Session) Poll added in v0.1.1

func (s *Session) Poll(ctx context.Context, cases []Case, opts ...ExpectOption) (Result, error)

Poll matches buffered bytes and performs one OS readiness pass without waiting for future input. No match returns ErrTimeout. A zero Expect timeout instead disables that timer and continues waiting. Requires WithOSPolling.

func (*Session) Redact

func (s *Session) Redact(secrets ...string) error

Redact adds nonempty literal secrets before sending or receiving them. Registration cannot retract bytes already delivered to output writers.

func (*Session) Send

func (s *Session) Send(ctx context.Context, text string) (n int, err error)

Send writes text once. No partial write is retried. A failure after entering transport.Write closes the Session and returns its progress in WriteError. An invalid transport count is rejected and reported as zero.

func (*Session) SendAll added in v0.1.1

func (s *Session) SendAll(ctx context.Context, text string) (int, error)

SendAll writes only the unsent suffix after positive partial writes. Its context owns the complete deadline, including serialization; WithWriteTimeout applies only to Send. Receive data is queued up to WithBufferLimit while the peer writes before reading. Exceeding that bound fails instead of deadlocking. Every error after transport entry closes the Session and reports total progress.

func (*Session) SendAllPoll added in v0.1.1

func (s *Session) SendAllPoll(ctx context.Context, text string) (int, error)

SendAllPoll uses immediate OS write readiness for every suffix, permitting an initial write without waiting. A blocked suffix returns ErrTimeout with total progress and closes the Session. Requires WithOSPolling.

func (*Session) SendLine

func (s *Session) SendLine(ctx context.Context, text string) (int, error)

SendLine appends the configured line ending (LF by default).

type SessionOption

type SessionOption func(*sessionOptions) error

SessionOption configures a Session before it takes ownership of the transport.

func WithBufferLimit

func WithBufferLimit(n int) SessionOption

WithBufferLimit bounds unconsumed matching input; overflow never discards data.

func WithDefaultTimeout

func WithDefaultTimeout(d time.Duration) SessionOption

WithDefaultTimeout sets the relative Expect timeout; zero disables it.

func WithLineEnding

func WithLineEnding(s string) SessionOption

WithLineEnding selects LF, CR or CRLF for SendLine. Send and Reply are verbatim.

func WithLogger

func WithLogger(l *log.Logger) SessionOption

WithLogger enables metadata-only diagnostics; payloads and causes are omitted.

func WithOSPolling added in v0.1.1

func WithOSPolling() SessionOption

WithOSPolling enables sole-reader OS readiness coordination for Poll and SendAllPoll. The transport must expose syscall.Conn and a nonblocking OS fd. Unsupported transports fail construction before ownership transfers. A working SetReadDeadline(time.Time) error capability selects the runtime poller; otherwise construction selects the bounded native-poll fallback. Wrappers may forward NormalizeReadError(error) error for transport-specific read errors, such as PTY EIO at EOF. Control errors are never normalized.

func WithObserver

func WithObserver(observers ...Observer) SessionOption

WithObserver adds metadata observers in declaration order.

func WithOutput

func WithOutput(writers ...io.Writer) SessionOption

WithOutput copies received input through the streaming secret filter. Writers are borrowed, synchronous and must not reenter Session methods. Failure closes the Session; delivery to earlier writers is not replayed.

func WithWriteTimeout

func WithWriteTimeout(d time.Duration) SessionOption

WithWriteTimeout bounds each Send, including serialization wait; zero disables it.

type WriteError

type WriteError struct {
	Written int
	Cause   error
}

WriteError reports a failed, non-retryable write. Error never prints payloads or the underlying error message; Unwrap exposes the original cause explicitly.

func (*WriteError) Error

func (e *WriteError) Error() string

func (*WriteError) GoString

func (e *WriteError) GoString() string

func (*WriteError) Unwrap

func (e *WriteError) Unwrap() error

Directories

Path Synopsis
examples
dialogue command
Command dialogue runs a local simulated device; no network connection is made.
Command dialogue runs a local simulated device; no network connection is made.
Package expecttest provides test-owned PTY consoles without logging goroutines.
Package expecttest provides test-owned PTY consoles without logging goroutines.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL