# command-stream for Rust
[](https://crates.io/crates/command-stream)
[](https://github.com/link-foundation/command-stream/actions/workflows/rust.yml)
[](../LICENSE)
Rust implementation of command-stream: a shell command execution library with
streaming, events, shell parsing, virtual commands, and built-in command support.
## Installation
```bash
cargo add command-stream
```
## Library Usage
```rust
use command_stream::CommandResult;
use command_stream::commands::echo::EchoCommand;
use command_stream::commands::VirtualCommand;
#[tokio::main]
async fn main() {
let command = EchoCommand;
let result: CommandResult = command
.execute(&["hello".to_string(), "from".to_string(), "rust".to_string()], None)
.await
.expect("echo should run");
assert_eq!(result.stdout.trim(), "hello from rust");
// `exit_code()` is an alias for the `code` field, mirroring the
// JavaScript `exitCode` alias.
assert_eq!(result.exit_code(), result.code);
}
```
## Streaming
`StreamingRunner` streams output as it arrives and mirrors the JavaScript
`stream()` async iterator (issue #155):
```rust
use command_stream::{OutputChunk, StreamingRunner};
#[tokio::main]
async fn main() {
// `kill_signal` configures the stop signal (default SIGTERM), just like the
// JavaScript `killSignal` option.
let runner = StreamingRunner::new("sh -c 'while true; do echo tick; sleep 0.1; done'")
.kill_signal("SIGINT");
let mut stream = runner.stream();
let mut count = 0;
while let Some(chunk) = stream.next().await {
match chunk {
OutputChunk::Stdout(data) => {
print!("{}", String::from_utf8_lossy(&data));
count += 1;
if count >= 3 {
stream.kill(); // stop from inside the loop (uses SIGINT)
}
}
OutputChunk::Stderr(_) => {}
// A terminating exit chunk is always delivered (128 + signal => 130).
OutputChunk::Exit(code) => println!("exit: {code}"),
}
}
}
```
Parity guarantees with the JavaScript implementation:
- The stream yields a final `OutputChunk::Exit(code)` when the process exits.
- It never hangs when the process has exited but a grandchild keeps the stdio
pipes open — readers are drained for `exit_pump_grace_ms` (default 100ms) and
then aborted.
- The process can be stopped from inside the loop with `stream.kill()` (configured
signal) or `stream.kill_with(signal)` (explicit override); dropping the stream
(e.g. `break`) stops the process too.
`StreamingRunner::new(command)` interprets a completed command string with the
platform shell. When argument boundaries must be preserved exactly, pass the
executable and arguments separately with `from_argv`:
```rust,no_run
use command_stream::StreamingRunner;
#[tokio::main]
async fn main() -> command_stream::Result<()> {
let runner = StreamingRunner::from_argv(
"my-program",
["argument with spaces", "literal&metacharacters"],
);
let result = runner.collect().await?;
assert!(result.is_success());
Ok(())
}
```
The exact-argv form bypasses `/bin/sh -c` and `cmd.exe /c`, so it does not
require shell-specific quoting. It also accepts OS-native executable and
argument values such as `PathBuf` and `OsString`.
## Command Line
The crate also builds a `command-stream` binary:
```bash
cargo run -- echo hello
```
## TUI Capture
`capture_terminal` runs an interactive program in a real pseudoterminal and
uses `vt100` to retain settled terminal states:
```rust
use command_stream::terminal::{
capture_terminal, TerminalCaptureOptions, TerminalInteraction, TerminalKey,
};
let capture = capture_terminal(TerminalCaptureOptions {
file: "codex".into(),
args: vec!["--no-alt-screen".into()],
interactions: vec![TerminalInteraction {
after_regex: Some("Ready: .+".into()),
idle_duration: std::time::Duration::from_millis(50),
text: Some("Inspect the failing test".into()),
key: Some(TerminalKey::Enter),
..TerminalInteraction::default()
}],
artifact_directory: Some("artifacts/codex".into()),
..TerminalCaptureOptions::default()
})?;
println!("{}", capture.transcript);
# Ok::<(), command_stream::terminal::TerminalCaptureError>(())
```
The capture contains the raw PTY output, consecutive-deduplicated frames, an
ordered unrolled transcript, and asciicast v2 input/output/resize events. The
artifact directory receives `transcript.txt`, `frames.json`, `session.cast`,
`snapshot.svg`, and an animated `recording.svg`; timeout errors retain the
partial capture and those diagnostic files. Use `capture_terminal_async` from
an async application.
### Interactive sessions
`capture_terminal` is batch-only: every interaction is known up front and
`timeout` (30 s by default) kills the child. When the input arrives later and
from elsewhere — an authorization code a human pastes back minutes later, a
chat-ops bridge, a test that interleaves assertions with input — use
`open_terminal`, which keeps the same PTY open until you close it:
```rust,no_run
use command_stream::terminal::{
open_terminal, TerminalCaptureOptions, TerminalInteraction, TerminalKey, TerminalPattern,
};
use std::time::Duration;
let mut session = open_terminal(TerminalCaptureOptions {
file: "my-cli".into(),
args: vec!["login".into()],
..TerminalCaptureOptions::default()
})?;
session.wait_for(
&TerminalPattern::regex(r"https://\S+")?,
Duration::from_millis(50),
None,
)?;
let url = session.transcript();
// ... arbitrary time passes; nothing terminates the child ...
session.send(&TerminalInteraction {
text: Some("ABCD-1234".into()),
key: Some(TerminalKey::Enter),
..TerminalInteraction::default()
})?;
session.wait_for(&TerminalPattern::text("Logged in"), Duration::ZERO, None)?;
let capture = session.close()?;
# let _ = (url, capture);
# Ok::<(), command_stream::terminal::TerminalCaptureError>(())
```
`open_terminal` accepts every `capture_terminal` option and forces
`timeout: None`, so a session runs until the child exits or `close()` is called.
`wait_for` reuses the same readiness semantics as `interactions`, including the
idle wait, and fails when the child exits first or its own timeout elapses.
`send` uses the `TerminalInteraction` vocabulary, `close` stops the child and
returns the usual capture (writing `artifact_directory` artifacts), and `finish`
waits for a child that exits on its own. `capture_terminal` is implemented on
top of the same session, so the two paths cannot drift.
Interactions can wait for literal output with `after`, regex output with
`after_regex`, and output quiescence with `idle_duration`. Named
`TerminalKey` variants cover arrows, Enter, Tab, Escape, Backspace, Ctrl-C, and
Ctrl-D; use `TerminalKey::Raw` for any other escape sequence.
## Features
- Shell parser for pipelines, command lists, logical operators, and redirection.
- Built-in command implementations for file-system and shell utility commands.
- Async execution with `tokio`.
- Virtual command abstractions for embedding command behavior in Rust programs.
- Cross-platform tests covering parser, state, events, streams, and built-ins.
## Development
```bash
cargo fmt --all -- --check
cargo clippy --all-targets --all-features
cargo test --all-features
cargo test --doc --all-features
cargo package --allow-dirty
```
Rust release automation lives in [scripts/](scripts/) and is controlled by
`.github/workflows/rust.yml` from the repository root.