Skip to main content
Moru provides full control over command I/O streams: stdin (input), stdout (standard output), and stderr (error output).

Streaming Output

Stream stdout and stderr in real-time:
from moru import Sandbox

sandbox = Sandbox.create()

def on_stdout(data):
    print(f"[OUT] {data}", end="")

def on_stderr(data):
    print(f"[ERR] {data}", end="")

result = sandbox.commands.run(
    """
    echo "Starting..."
    echo "Error message" >&2
    for i in 1 2 3; do
        echo "Count: $i"
        sleep 0.5
    done
    echo "Done!"
    """,
    on_stdout=on_stdout,
    on_stderr=on_stderr,
    timeout=30
)

print(f"\nExit code: {result.exit_code}")

Sending Stdin

Send input to a running command:
# Enable stdin for background command
handle = sandbox.commands.run(
    "cat > /home/user/input.txt",
    background=True,
    stdin=True  # Keep stdin open
)

# Send data
handle.send_stdin("Line 1\n")
handle.send_stdin("Line 2\n")
handle.send_stdin("Line 3\n")

# Close stdin (EOF)
handle.send_stdin("")  # Empty string signals EOF

# Wait for completion
result = handle.wait()

# Verify file was created
content = sandbox.files.read("/home/user/input.txt")
print(content)
# Line 1
# Line 2
# Line 3

Interactive Input/Output

import time

# Start an interactive program
handle = sandbox.commands.run(
    "python3 -c 'while True: print(input().upper())'",
    background=True,
    stdin=True,
    on_stdout=lambda data: print(f"Response: {data}", end="")
)

# Send input and see responses
handle.send_stdin("hello\n")
time.sleep(0.1)  # Response: HELLO

handle.send_stdin("world\n")
time.sleep(0.1)  # Response: WORLD

handle.kill()

Sending Stdin by PID

Send input to any running process:
# Start a process
handle = sandbox.commands.run(
    "read name; echo Hello, $name",
    background=True,
    stdin=True
)

# Send via command handle
handle.send_stdin("Alice\n")

# Or send via commands.send_stdin using PID
sandbox.commands.send_stdin(handle.pid, "additional data\n")

Large Output Handling

For commands that produce large output:
# Stream large output without loading into memory
chunks = []

def collect_output(data):
    chunks.append(data)
    if len(chunks) % 100 == 0:
        print(f"Received {len(chunks)} chunks...")

result = sandbox.commands.run(
    "cat /dev/urandom | head -c 1000000 | base64",
    on_stdout=collect_output,
    timeout=60
)

total_size = sum(len(c) for c in chunks)
print(f"Total output: {total_size} bytes")

Separating Stdout and Stderr

stdout_lines = []
stderr_lines = []

result = sandbox.commands.run(
    """
    echo "Normal output 1"
    echo "Error 1" >&2
    echo "Normal output 2"
    echo "Error 2" >&2
    """,
    on_stdout=lambda data: stdout_lines.extend(data.strip().split("\n")),
    on_stderr=lambda data: stderr_lines.extend(data.strip().split("\n"))
)

print("Stdout:", stdout_lines)
print("Stderr:", stderr_lines)

Progress Tracking

Track progress from command output:
import re

def parse_progress(data):
    # Parse progress from wget output
    match = re.search(r'(\d+)%', data)
    if match:
        print(f"\rDownloading: {match.group(1)}%", end="", flush=True)

result = sandbox.commands.run(
    "wget -q --show-progress https://example.com/large-file.zip",
    on_stderr=parse_progress,  # wget shows progress on stderr
    timeout=300
)

print("\nDownload complete!")

Next Steps