expect — expect TCL scripting for interactive programs — spawn, expect, send, interact. pwsh has no native analogue; Pexpect / WSL fallbacks across all 5 shells
Equivalents in every shell
expect -c 'spawn ssh user@host; expect "password:"; send "secret\r"; interact'Tcl-based DSL. Three primary commands: `spawn <program>` (launch the program as a child pty), `expect <pattern>` (block until pattern matches the child's output — defaults are GLOB-style; `-re` switches to regex), `send <text>` (write to the child's stdin — `\r` is the return key). `interact` hands control back to the user (the keyword scripts often use to "log in then let me type"). Inline scripts: `expect -c '<script>'`. File scripts: `#!/usr/bin/expect -f`. Install: `apt install expect`, `dnf install expect`, `pacman -S expect`, `brew install expect`. Powerful for SSH key setup, FTP automation, ssh-copy-id, su / sudo password prompts that BatchMode can't avoid.
expect -c 'spawn ssh user@host; expect "password:"; send "secret\r"; interact'expect -c 'spawn ssh user@host; expect "password:"; send "secret\r"; interact'Same `expect` binary. CAVEAT: fish's quoting rules differ — for one-shot inline scripts, prefer a separate `.exp` file: `echo 'spawn ssh user@host\nexpect "password:"\nsend "secret\r"\ninteract' > script.exp; expect script.exp`. Or use fish heredoc-like form: `expect -c (printf '%s\n' 'spawn ssh user@host' 'expect "password:"' 'send "secret\r"' 'interact')`. The fish-vs-bash quote-rule conflict is the most common source of "expect script that works in bash but breaks under fish".
# pwsh has no native expect. Use WSL or install Cygwin: wsl expect -c "spawn ssh user@host; ..."No pwsh-native expect equivalent. Closest analogues: (1) `Start-Process -RedirectStandardInput` + `-RedirectStandardOutput` for one-shot pipe-fed scenarios (no pattern-match-and-respond capability). (2) `[System.Diagnostics.Process]` with `BeginOutputReadLine` + event handlers — can WATCH child output but writing to child stdin while reading output requires careful threading. (3) The `Pexpect` Python module (`pip install pexpect`) — Pythonic API mirroring expect; run via `python -c "import pexpect; ..."` from pwsh. (4) WSL: `wsl expect -c "..."` from pwsh. For SSH-specific automation, prefer ssh keys + `BatchMode=yes` over scripting password prompts — modern, simpler, secure.
wsl expect -c "spawn ssh user@host; expect password:; send secret\r; interact"No cmd-native expect. WSL is the friction-free path: `wsl expect -c "..."` runs Linux expect from cmd. Legacy alternative: install Cygwin (provides expect.exe via `apt-cyg install expect`) or MSYS2 (`pacman -S expect`). For pure-cmd scripted automation of interactive programs, you're mostly stuck — Windows lacks a pty abstraction; ConPTY (Windows 10 1809+) is the low-level primitive but no high-level scripting tool wraps it cmd-style. Modern Windows answer: PowerShell's SSH client + key auth.
Worked examples
Drive ssh with a password (when keys aren't available)
expect -c 'spawn ssh user@host; expect "password:"; send "secret\r"; interact'expect -c 'spawn ssh user@host; expect "password:"; send "secret\r"; interact'wsl expect -c 'spawn ssh user@host; expect "password:"; send "secret\r"; interact'wsl expect -c "spawn ssh user@host; expect password:; send secret\r; interact"Automate ssh-copy-id with passphrase prompt
expect -c 'spawn ssh-copy-id user@host; expect "password:"; send "pw\r"; expect eof'expect -c 'spawn ssh-copy-id user@host; expect "password:"; send "pw\r"; expect eof'wsl expect -c 'spawn ssh-copy-id user@host; expect "password:"; send "pw\r"; expect eof'wsl expect -c "spawn ssh-copy-id user@host; expect password:; send pw\r; expect eof"Use a `.exp` script file for a complex flow
expect -f setup.expexpect -f setup.expwsl expect -f setup.expwsl expect -f setup.expPattern with regex matching
expect -c 'spawn ftp host; expect -re "Name.*: $"; send "user\r"; expect "Password:"; send "pw\r"; interact'expect -c 'spawn ftp host; expect -re "Name.*: $"; send "user\r"; expect "Password:"; send "pw\r"; interact'wsl expect -c 'spawn ftp host; expect -re "Name.*: $"; send "user\r"; expect "Password:"; send "pw\r"; interact'wsl expect -f ftp.expGotchas
- **`spawn` runs the child under a pty, NOT a pipe — behaves differently from plain shell redirection.** Most interactive programs (ssh, ftp, passwd, su, sudo) check whether stdin is a tty via `isatty(0)`. When stdin is a pipe, they behave non-interactively (or refuse). expect's `spawn` allocates a pseudo-tty, so the child program SEES a tty and stays in interactive mode — which is exactly what you want for automation. The pty abstraction is what makes expect possible. Without expect (or pexpect, or `script(1)`), there's no portable way in shell to feed input to a tty-demanding program; you cannot just do `echo "password" | ssh user@host` — ssh refuses input from a pipe.
- **`\r` vs `\n` — the return key is CR, not LF.** `send "text\n"` sends a LINE FEED, which interactive programs often do NOT recognize as "submit". `send "text\r"` sends a CARRIAGE RETURN — that's what your keyboard sends when you press Enter. ALWAYS use `\r` for "submit", `\n` only when the program literally wants a literal newline (rare). The CR/LF distinction trips up >50% of new expect users. If your script "sends the password but the program doesn't accept it" — try replacing `\n` with `\r`. Same trap exists in Python pexpect (`.sendline(text)` adds `\r` by default).
- **Glob vs regex matching in `expect` patterns.** Default `expect "Password:"` matches a LITERAL substring "Password:" anywhere in the child's output buffer (glob-style — `*` and `?` are wildcards but not regex). For regex: `expect -re "Password.*: $"` (anchored at end with `$`). `-exact` forces literal match (no glob wildcards). The buffer is searched RIGHT-TO-LEFT and the LATEST match wins — useful when prompts repeat (e.g. login fails → password prompt re-appears; you want the LATEST one). For long-running prompts that may not appear immediately, set `set timeout <seconds>` (default 10s, `-1` is wait-forever). The timeout pattern: `expect { "Password:" { send "pw\r" } timeout { puts "timed out"; exit 1 } }`.
- **Storing secrets in expect scripts is dangerous — use stdin or env.** Hardcoding `send "real-password\r"` in a `.exp` file leaves the password in plain text on disk + visible in `ps` output via `cat .../setup.exp`. SAFER patterns: (1) Read from stdin: `expect -c 'stty -echo; expect_user -re "(.*)\n"; send_user "\n"; stty echo; set pw $expect_out(1,string); ...'`. (2) Read from env: `set pw $env(MY_PASSWORD); send "$pw\r"` — script becomes `MY_PASSWORD=secret expect setup.exp`. (3) Use ssh keys + `BatchMode=yes` — the right answer for ssh-specifically, removes the need for expect entirely. expect-with-passwords is a tool for legacy systems (Cisco IOS, old IBM mainframe terminals); for modern Linux/Unix, keys are the answer.
- **`pexpect` (Python port) is more maintainable for complex flows.** Tcl/expect syntax is terse but ages poorly — branching, error handling, JSON I/O get awkward. `pexpect` (`pip install pexpect`) wraps the same pty/spawn primitives in Python: `import pexpect; child = pexpect.spawn("ssh user@host"); child.expect("password:"); child.sendline("pw"); child.interact()`. Cross-platform: works on Linux/macOS/WSL. Windows native: `pip install pexpect winpexpect` or `pexpect.popen_spawn` (no pty — pipe-based; limited). For new automation, default to pexpect over expect; for legacy maintenance, learn enough Tcl/expect to read existing scripts.