Quick Start

Run a shell script

using Computerwelt.Emulation.Bash;

var bash = Bash.CreateBuilder()
    .WithWorkingDirectory("/home/agent")
    .WithUsername("agent")
    .WithHostname("sandbox")
    .WithLimits(ExecutionLimits.Strict)
    .Build();

var result = await bash.ExecAsync("""
    mkdir -p /home/agent/data
    printf 'banana\napple\ncherry\n' > /home/agent/data/fruit.txt
    sort /home/agent/data/fruit.txt | head -2
    """);

Console.WriteLine(result.Stdout);   // apple\nbanana
Console.WriteLine(result.ExitCode); // 0

The session keeps state between calls the way a terminal does: a cd, an exported variable or a file written by one ExecAsync is there for the next one.

Run a Python program

using Computerwelt.Emulation.Python;

var runner = new PythonRunner();

var result = runner.Run("""
    import json

    data = [{"id": i, "square": i * i} for i in range(5)]
    print(json.dumps(data))
    """);

Console.WriteLine(result.Succeeded);   // True
Console.WriteLine(result.Stdout);

RunResult carries Stdout, Stderr, ExitCode, the globals the program ended with, and, when it raised, the exception and its traceback as Python would have printed them.

Both, over one filesystem

using Computerwelt;
using Computerwelt.Emulation.Bash;

var bash = Bash.CreateBuilder()
    .WithPython()
    .Build();

var result = await bash.ExecAsync("""
    cat > /report.py <<'PY'
    import json, os

    rows = [line.split(',') for line in open('/data.csv').read().splitlines()[1:]]
    print(json.dumps({"rows": len(rows)}))
    PY

    printf 'id,name\n1,a\n2,b\n' > /data.csv
    python /report.py
    """);

Console.WriteLine(result.Stdout);   // {"rows": 2}

The Python program's open, os and os.path are backed by the shell's filesystem, so a file the script wrote is a file the program reads.

Handle failure

var result = await bash.ExecAsync("cat /does/not/exist");

if (result.ExitCode != 0)
{
    Console.Error.WriteLine(result.Stderr);   // cat: /does/not/exist: No such file or directory
}

Ordinary non-zero exits arrive as an ExecResult, not an exception. Exit code 127 means the command was not found, 126 means it was not executable, and 2 means a parse or usage error. Those codes leak into scripts, so they are kept exact.

A limit that is reached, by contrast, raises: a traversal that quietly stopped part-way would report a subset as though it were the whole.

© 2026 Curiosity. All rights reserved.