Claude Code for pexpect: Automate Interactive CLI Programs in Python — Claude Skills 360 Blog
Blog / AI / Claude Code for pexpect: Automate Interactive CLI Programs in Python
AI

Claude Code for pexpect: Automate Interactive CLI Programs in Python

Published: April 3, 2028
Read time: 5 min read
By: Claude Skills 360

pexpect automates interactive CLI programs by spawning child processes and sending/expecting text. pip install pexpect. Basic: import pexpect; child = pexpect.spawn("ssh user@host"); child.expect("password:"); child.sendline("secret"); child.expect(pexpect.EOF). One-shot: out = pexpect.run("ls -la"). pexpect.run("cmd", timeout=30). Spawn: child = pexpect.spawn("python3", encoding="utf-8"). Send: child.sendline("print('hi')"). Expect: child.expect(">>> ") waits for prompt. Match list: idx = child.expect(["Password:", "denied", pexpect.EOF]). Timeout: child.expect("prompt", timeout=30). pexpect.TIMEOUT. pexpect.EOF. Before: child.before — text before the match. After: child.after — matched text. Logging: child.logfile = sys.stdout. child.logfile_read = fh. FTP: ftp = pexpect.spawn("ftp host"); ftp.expect("Name:"); ftp.sendline(user). SSH key: child.expect("Are you sure"); child.sendline("yes"). pxssh: from pexpect import pxssh; s = pxssh.pxssh(); s.login(host, user, pw); s.sendline("ls"); s.prompt(); print(s.before). s.logout(). Interact: child.interact() — hand off terminal to user. Control: child.sendcontrol("c"). Kill: child.terminate(). child.isalive(). Winsize: child.setwinsize(50, 200). Delimiters: child.expect_exact(">>>") vs regex. child.expect(re.compile(r"\$\s*$")). Claude Code generates pexpect automation scripts for SSH, database CLIs, and interactive program testing.

CLAUDE.md for pexpect

## pexpect Stack
- Version: pexpect >= 4.9 | pip install pexpect
- Spawn: child = pexpect.spawn("cmd", encoding="utf-8", timeout=30)
- Expect: child.expect("pattern") | child.expect(["opt1","opt2",TIMEOUT,EOF])
- Send: child.sendline("text") | child.send("no-newline") | child.sendcontrol("c")
- Match: child.before (text before match) | child.after (matched text)
- pxssh: pxssh.pxssh(); s.login(host, user, pw); s.sendline("cmd"); s.prompt()
- Log: child.logfile = sys.stdout — mirror all I/O for debugging

pexpect Automation Pipeline

# app/automation.py — pexpect spawn, expect/send, SSH via pxssh, and test helpers
from __future__ import annotations

import re
import sys
import time
from pathlib import Path
from typing import Any

import pexpect
from pexpect import EOF, TIMEOUT


# ─────────────────────────────────────────────────────────────────────────────
# 1. Core spawn helpers
# ─────────────────────────────────────────────────────────────────────────────

def spawn(
    command: str,
    args: list[str] | None = None,
    timeout: int = 30,
    encoding: str = "utf-8",
    echo: bool = False,
    log_stdout: bool = False,
) -> pexpect.spawn:
    """
    Spawn a child process.
    log_stdout=True mirrors all output to sys.stdout for debugging.
    """
    if args:
        child = pexpect.spawn(command, args, timeout=timeout, encoding=encoding)
    else:
        child = pexpect.spawn(command, timeout=timeout, encoding=encoding)

    child.echo = echo
    if log_stdout:
        child.logfile_read = sys.stdout

    return child


def run_command(
    command: str,
    timeout: int = 30,
    encoding: str = "utf-8",
    env: dict | None = None,
) -> tuple[str, int]:
    """
    Run a command and return (output, exit_code) using pexpect.run().
    """
    events = {
        r"(?i)password:": lambda m: "secret\n",
        r"\(yes/no\)":    lambda m: "yes\n",
    }
    output, status = pexpect.run(
        command,
        timeout=timeout,
        encoding=encoding,
        events=events,
        env=env,
        withexitstatus=True,
    )
    return output, status


# ─────────────────────────────────────────────────────────────────────────────
# 2. Conversation helpers
# ─────────────────────────────────────────────────────────────────────────────

class Conversation:
    """
    Fluent wrapper around pexpect.spawn for multi-step interactions.

    Usage:
        with Conversation("python3") as c:
            c.wait(">>> ").send("import os").wait(">>> ")
            c.send("print(os.getcwd())").wait(">>> ")
            result = c.child.before
    """

    def __init__(
        self,
        command: str,
        args: list[str] | None = None,
        timeout: int = 30,
        encoding: str = "utf-8",
    ):
        self._command = command
        self._args    = args or []
        self._timeout = timeout
        self._encoding = encoding
        self.child: pexpect.spawn | None = None
        self._log: list[str] = []

    def __enter__(self) -> "Conversation":
        self.child = spawn(
            self._command,
            self._args,
            timeout=self._timeout,
            encoding=self._encoding,
        )
        return self

    def __exit__(self, *_):
        if self.child and self.child.isalive():
            self.child.terminate(force=True)

    def wait(self, pattern: str, timeout: int | None = None) -> "Conversation":
        """Wait for pattern; raises pexpect.TIMEOUT or pexpect.EOF on failure."""
        t = timeout or self._timeout
        self.child.expect(pattern, timeout=t)
        return self

    def wait_any(self, patterns: list[str], timeout: int | None = None) -> int:
        """Wait for any of the patterns. Returns matched index."""
        t = timeout or self._timeout
        return self.child.expect(patterns + [TIMEOUT, EOF], timeout=t)

    def send(self, text: str, newline: bool = True) -> "Conversation":
        """Send text with optional newline."""
        if newline:
            self.child.sendline(text)
        else:
            self.child.send(text)
        self._log.append(f"SEND: {text!r}")
        return self

    def ctrl(self, key: str) -> "Conversation":
        """Send a control character, e.g. ctrl("c") for Ctrl+C."""
        self.child.sendcontrol(key)
        return self

    @property
    def before(self) -> str:
        """Text captured before the most recent match."""
        return self.child.before or ""

    @property
    def log(self) -> list[str]:
        return list(self._log)


# ─────────────────────────────────────────────────────────────────────────────
# 3. SSH via pxssh
# ─────────────────────────────────────────────────────────────────────────────

def ssh_run(
    host: str,
    username: str,
    password: str | None = None,
    commands: list[str] | None = None,
    timeout: int = 30,
    port: int = 22,
) -> list[str]:
    """
    Run commands over SSH using pexpect.pxssh.
    Returns list of command outputs.
    Requires: pip install pexpect

    Usage:
        outputs = ssh_run("host.example.com", "deploy", password="secret",
                          commands=["ls /srv", "systemctl status api"])
    """
    from pexpect import pxssh

    s = pxssh.pxssh(timeout=timeout)
    s.login(host, username, password=password, port=port, auto_prompt_reset=False)

    results = []
    for cmd in (commands or []):
        s.sendline(cmd)
        s.prompt()
        results.append(s.before.strip())

    s.logout()
    return results


def ssh_upload(
    host: str,
    username: str,
    password: str,
    local_path: str,
    remote_path: str,
    timeout: int = 60,
) -> bool:
    """
    Upload a file via scp with pexpect handling the password prompt.
    Returns True on success.
    """
    cmd = f"scp {local_path} {username}@{host}:{remote_path}"
    child = spawn(cmd, timeout=timeout)

    idx = child.expect(["password:", "100%", TIMEOUT, EOF])
    if idx == 0:
        child.sendline(password)
        idx2 = child.expect(["100%", TIMEOUT, EOF])
        return idx2 == 0
    return idx == 1


# ─────────────────────────────────────────────────────────────────────────────
# 4. Database CLI automation
# ─────────────────────────────────────────────────────────────────────────────

def mysql_query(
    host: str,
    user: str,
    password: str,
    database: str,
    sql: str,
    timeout: int = 30,
) -> str:
    """
    Run a SQL query through the mysql CLI and return output.
    """
    cmd = f"mysql -h {host} -u {user} -p{password} {database}"
    child = spawn(cmd, timeout=timeout)
    child.expect(r"mysql\s*>")
    child.sendline(sql)
    child.expect(r"mysql\s*>")
    result = child.before.strip()
    child.sendline("exit")
    child.expect(EOF)
    return result


def psql_query(
    host: str,
    user: str,
    database: str,
    sql: str,
    port: int = 5432,
    password: str | None = None,
    timeout: int = 30,
) -> str:
    """Run a query through psql and return the output."""
    cmd = f"psql -h {host} -p {port} -U {user} -d {database}"
    child = spawn(cmd, timeout=timeout)

    if password:
        idx = child.expect(["Password", r"=# "])
        if idx == 0:
            child.sendline(password)
            child.expect(r"=# ")

    child.sendline(sql)
    child.expect(r"=# ")
    result = child.before.strip()
    child.sendline(r"\q")
    child.expect(EOF)
    return result


# ─────────────────────────────────────────────────────────────────────────────
# 5. Python REPL automation
# ─────────────────────────────────────────────────────────────────────────────

class PythonREPL:
    """
    Automate a Python REPL for testing interactive code.

    Usage:
        with PythonREPL() as repl:
            repl.run("x = 42")
            output = repl.run("print(x)")
            assert "42" in output
    """

    PROMPT = r">>> "
    CONT   = r"\.\.\. "

    def __init__(self, timeout: int = 10):
        self._timeout = timeout
        self.child: pexpect.spawn | None = None

    def __enter__(self) -> "PythonREPL":
        self.child = spawn("python3", timeout=self._timeout)
        self.child.expect(self.PROMPT)
        return self

    def __exit__(self, *_):
        if self.child and self.child.isalive():
            try:
                self.child.sendline("exit()")
                self.child.expect(EOF, timeout=3)
            except Exception:
                self.child.terminate(force=True)

    def run(self, code: str, timeout: int | None = None) -> str:
        """
        Send a line of code to the REPL and return its output.
        """
        t = timeout or self._timeout
        self.child.sendline(code)
        self.child.expect(self.PROMPT, timeout=t)
        return self.child.before.strip()

    def run_block(self, code: str) -> str:
        """
        Send a multi-line block (function / class / if) and return output.
        Automatically sends blank line to close indented blocks.
        """
        lines = code.strip().split("\n")
        for line in lines:
            self.child.sendline(line)
            self.child.expect([self.CONT, self.PROMPT])
        self.child.sendline("")  # end block
        self.child.expect(self.PROMPT)
        return self.child.before.strip()


# ─────────────────────────────────────────────────────────────────────────────
# 6. Expect pattern utils
# ─────────────────────────────────────────────────────────────────────────────

def expect_or_timeout(
    child: pexpect.spawn,
    patterns: list[str],
    timeout: int = 30,
    on_timeout: str | None = None,
) -> int:
    """
    Expect one of patterns; return index or raise RuntimeError on timeout.
    on_timeout: error message to include in RuntimeError.
    """
    idx = child.expect(patterns + [TIMEOUT, EOF], timeout=timeout)
    if idx >= len(patterns):
        msg = on_timeout or f"Timeout waiting for one of {patterns}"
        raise TimeoutError(msg)
    return idx


def capture_until(
    child: pexpect.spawn,
    end_pattern: str,
    timeout: int = 30,
) -> str:
    """Capture all output until end_pattern matches."""
    child.expect(end_pattern, timeout=timeout)
    return child.before or ""


def extract_match(child: pexpect.spawn, pattern: str, group: int = 1) -> str | None:
    """
    Run expect with a regex and extract a capture group.
    Returns None if the pattern doesn't match before timeout.
    """
    try:
        child.expect(re.compile(pattern))
        m = child.match
        return m.group(group) if m else None
    except (TIMEOUT, EOF):
        return None


# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    print("=== pexpect.run ===")
    out = pexpect.run("echo Hello from pexpect", encoding="utf-8").strip()
    print(f"  Output: {out!r}")

    print("\n=== Python REPL automation ===")
    with PythonREPL() as repl:
        repl.run("import math")
        pi_out = repl.run("print(round(math.pi, 4))")
        print(f"  π ≈ {pi_out!r}")
        list_out = repl.run("print([x**2 for x in range(5)])")
        print(f"  Squares: {list_out!r}")

    print("\n=== Conversation wrapper ===")
    with Conversation("python3") as c:
        c.wait(r">>> ").send("x = 'hello world'").wait(r">>> ")
        c.send("print(x.upper())").wait(r">>> ")
        print(f"  Output: {c.before!r}")

    print("\n=== run_command ===")
    output, code = run_command("ls /tmp", timeout=5)
    print(f"  Exit code: {code}")
    print(f"  First line: {output.splitlines()[0]!r}")

For the paramiko alternative — paramiko provides a full SSH2 protocol implementation with SFTP, public-key auth, port forwarding, and SSH agent support; pexpect automates any interactive program (not just SSH) by treating its I/O as a stream, making it the right tool when you need to drive a CLI that has an interactive session model (database CLIs, FTP, legacy apps, test frameworks); use paramiko for production SSH automation, pexpect for anything that’s interactive-but-not-SSH-protocol. For the subprocess alternative — subprocess.run() captures stdout/stderr of non-interactive programs; pexpect handles programs that require interactive back-and-forth (expect a password prompt, send the password, read the result), which subprocess can’t do because the child process detects it’s not a TTY and may disable prompts or buffer differently. The Claude Skills 360 bundle includes pexpect skill sets covering pexpect.spawn() with timeout/encoding, expect()/sendline()/send()/sendcontrol(), before/after match capture, pexpect.run() one-shot execution, pexpect.TIMEOUT/EOF constants, Conversation fluent wrapper, PythonREPL automated REPL testing, ssh_run() via pxssh, ssh_upload() SCP automation, mysql_query()/psql_query() database CLI, expect_or_timeout() safe expect, and capture_until()/extract_match() helpers. Start with the free tier to try interactive CLI automation code generation.

Keep Reading

AI

Claude Code for email.contentmanager: Python Email Content Accessors

Read and write EmailMessage body content with Python's email.contentmanager module and Claude Code — email contentmanager ContentManager for the class that maps content types to get and set handler functions allowing EmailMessage to support get_content and set_content with type-specific behaviour, email contentmanager raw_data_manager for the ContentManager instance that handles raw bytes and str payloads without any conversion, email contentmanager content_manager for the standard ContentManager instance used by email.policy.default that intelligently handles text plain text html multipart and binary content types, email contentmanager get_content_text for the handler that returns the decoded text payload of a text-star message part as a str, email contentmanager get_content_binary for the handler that returns the raw decoded bytes payload of a non-text message part, email contentmanager get_data_manager for the get-handler lookup used by EmailMessage get_content to find the right reader function for the content type, email contentmanager set_content text for the handler that creates and sets a text part correctly choosing charset and transfer encoding, email contentmanager set_content bytes for the handler that creates and sets a binary part with base64 encoding and optional filename Content-Disposition, email contentmanager EmailMessage get_content for the method that reads the message body using the registered content manager handlers, email contentmanager EmailMessage set_content for the method that sets the message body and MIME headers in one call, email contentmanager EmailMessage make_alternative make_mixed make_related for the methods that convert a simple message into a multipart container, email contentmanager EmailMessage add_attachment for the method that attaches a file or bytes to a multipart message, and email contentmanager integration with email.message and email.policy and email.mime and io for building high-level email readers attachment extractors text body accessors HTML readers and policy-aware MIME construction pipelines.

5 min read Feb 12, 2029
AI

Claude Code for email.charset: Python Email Charset Encoding

Control header and body encoding for international email with Python's email.charset module and Claude Code — email charset Charset for the class that wraps a character set name with the encoding rules for header encoding and body encoding describing how to encode text for that charset in email messages, email charset Charset header_encoding for the attribute specifying whether headers using this charset should use QP quoted-printable encoding BASE64 encoding or no encoding, email charset Charset body_encoding for the attribute specifying the Content-Transfer-Encoding to use for message bodies in this charset such as QP or BASE64, email charset Charset output_codec for the attribute giving the Python codec name used to encode the string to bytes for the wire format, email charset Charset input_codec for the attribute giving the Python codec name used to decode incoming bytes to str, email charset Charset get_output_charset for returning the output charset name, email charset Charset header_encode for encoding a header string using the charset's header_encoding method, email charset Charset body_encode for encoding body content using the charset's body_encoding, email charset Charset convert for converting a string from the input_codec to the output_codec, email charset add_charset for registering a new charset with custom encoding rules in the global charset registry, email charset add_alias for adding an alias name that maps to an existing registered charset, email charset add_codec for registering a codec name mapping for use by the charset machinery, and email charset integration with email.message and email.mime and email.policy and email.encoders for building international email senders non-ASCII header encoders Content-Transfer-Encoding selectors charset-aware message constructors and MIME encoding pipelines.

5 min read Feb 11, 2029
AI

Claude Code for email.utils: Python Email Address and Header Utilities

Parse and format RFC 2822 email addresses and dates with Python's email.utils module and Claude Code — email utils parseaddr for splitting a display-name plus angle-bracket address string into a realname and email address tuple, email utils formataddr for combining a realname and address string into a properly quoted RFC 2822 address with angle brackets, email utils getaddresses for parsing a list of raw address header strings each potentially containing multiple comma-separated addresses into a list of realname address tuples, email utils parsedate for parsing an RFC 2822 date string into a nine-tuple compatible with time.mktime, email utils parsedate_tz for parsing an RFC 2822 date string into a ten-tuple that includes the UTC offset timezone in seconds, email utils parsedate_to_datetime for parsing an RFC 2822 date string into an aware datetime object with timezone, email utils formatdate for formatting a POSIX timestamp or the current time as an RFC 2822 date string with optional usegmt and localtime flags, email utils format_datetime for formatting a datetime object as an RFC 2822 date string, email utils make_msgid for generating a globally unique Message-ID string with optional idstring and domain components, email utils decode_rfc2231 for decoding an RFC 2231 encoded parameter value into a tuple of charset language and value, email utils encode_rfc2231 for encoding a string as an RFC 2231 encoded parameter value, email utils collapse_rfc2231_value for collapsing a decoded RFC 2231 tuple to a Unicode string, and email utils integration with email.message and email.headerregistry and datetime and time for building address parsers date formatters message-id generators header extractors and RFC-compliant email construction utilities.

5 min read Feb 10, 2029

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free