bashkit 0.7.2

Awesomely fast virtual sandbox with bash and file system
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# Custom Builtins

Bashkit supports registering custom builtin commands to extend the shell with
domain-specific functionality. Custom builtins have full access to the execution
context including arguments, environment variables, shell variables, and the
virtual filesystem.

**See also:**
- [API Documentation]https://docs.rs/bashkit - Full API reference
- [Clap Builtins]./clap-builtins.md - Derive parser structs for custom builtin args
- [Hooks]./hooks.md - Interceptor hooks for the execution pipeline
- [Compatibility Reference]./compatibility.md - Supported bash features
- [Threat Model]./threat-model.md - Security considerations

## Quick Start

```rust
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};

struct MyCommand;

#[async_trait]
impl Builtin for MyCommand {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        let name = ctx.args.first().map(|s| s.as_str()).unwrap_or("World");
        Ok(ExecResult::ok(format!("Hello, {}!\n", name)))
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut bash = Bash::builder()
        .builtin("greet", Box::new(MyCommand))
        .build();

    let result = bash.exec("greet Alice").await?;
    assert_eq!(result.stdout, "Hello, Alice!\n");
    Ok(())
}
```

## The Builtin Trait

All custom builtins must implement the `Builtin` trait:

```rust,ignore
#[async_trait]
pub trait Builtin: Send + Sync {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> Result<ExecResult>;
}
```

The trait is async-first (via `async_trait`) and requires `Send + Sync` for
thread safety in async contexts.

## Execution Context

The `BuiltinContext` provides access to the execution environment:

```rust,ignore
pub struct BuiltinContext<'a> {
    /// Command arguments (not including the command name)
    pub args: &'a [String],

    /// Environment variables
    pub env: &'a HashMap<String, String>,

    /// Shell variables (mutable)
    pub variables: &'a mut HashMap<String, String>,

    /// Current working directory (mutable)
    pub cwd: &'a mut PathBuf,

    /// Virtual filesystem
    pub fs: Arc<dyn FileSystem>,

    /// Standard input (from pipeline)
    pub stdin: Option<&'a str>,
}
```

### Per-Execution Extensions

For request-scoped data that should not live on the builtin itself, use
`Bash::exec_with_extensions()` (or `exec_streaming_with_extensions()`) and read
the value inside the builtin with `ctx.execution_extension::<T>()`.

```rust,ignore
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, ExecutionExtensions, async_trait};

struct RequestId;

#[async_trait]
impl Builtin for RequestId {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        let req = ctx
            .execution_extension::<String>()
            .cloned()
            .unwrap_or_else(|| "missing".to_string());
        Ok(ExecResult::ok(format!("{req}\n")))
    }
}

let mut bash = Bash::builder()
    .builtin("request-id", Box::new(RequestId))
    .build();

let result = bash
    .exec_with_extensions(
        "request-id",
        ExecutionExtensions::new().with("req-123".to_string()),
    )
    .await?;
assert_eq!(result.stdout, "req-123\n");
```

## Extensions

Use an `Extension` when one capability contributes multiple builtins.

```rust,ignore
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, Extension, async_trait};

struct Hello;

#[async_trait]
impl Builtin for Hello {
    async fn execute(&self, _ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        Ok(ExecResult::ok("hello\n".to_string()))
    }
}

struct MyExtension;

impl Extension for MyExtension {
    fn builtins(&self) -> Vec<(String, Box<dyn Builtin>)> {
        vec![("hello".to_string(), Box::new(Hello))]
    }
}

let mut bash = Bash::builder().extension(MyExtension).build();
let result = bash.exec("hello").await?;
assert_eq!(result.stdout, "hello\n");
```

The same extension can be added to `BashTool::builder().extension(...)`.
`TypeScriptExtension` uses this model to register the embedded
TypeScript/JavaScript builtins.

## BuiltinRegistry — Runtime-Mutable Builtins

`BashBuilder::builtin` and `Extension` are both *build-time*: the set of
builtins is frozen when `Bash::builder().build()` returns. For embedders
that need to register or remove builtins after construction without
rebuilding the interpreter, use `BuiltinRegistry`.

```rust,ignore
use bashkit::{Bash, Builtin, BuiltinContext, BuiltinRegistry, ExecResult, async_trait};
use std::sync::Arc;

struct Greet;

#[async_trait]
impl Builtin for Greet {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        let who = ctx.args.first().map(String::as_str).unwrap_or("world");
        Ok(ExecResult::ok(format!("hello {}\n", who)))
    }
}

// Keep a clone of the registry handle so the embedder can mutate it later.
let registry = BuiltinRegistry::new();
let mut bash = Bash::builder()
    .builtin_registry(registry.clone())
    .build();

// Run something first — VFS state accumulates.
bash.exec("mkdir -p /scratch && echo seed > /scratch/seed.txt").await?;

// Register a builtin after the interpreter is live. No rebuild, no VFS reset.
registry.insert("greet", Arc::new(Greet));
let r = bash.exec("greet Alice").await?;
assert_eq!(r.stdout, "hello Alice\n");

// Pre-existing files are still there.
let r = bash.exec("cat /scratch/seed.txt").await?;
assert_eq!(r.stdout, "seed\n");
```

The registry handle is `Clone`; clones share the same underlying storage,
so mutations made via any clone are visible to all others (including the
interpreter that owns one).

**Resolution order**: shell function → POSIX special builtin → registry
entry → baked-in builtin → `$PATH`. Registry entries can override
baked-in commands (e.g. wrap `cat` with tracing) but shell functions
still win — matching standard bash precedence.

The registry is host-owned: not part of interpreter state, so it survives
`exec()` calls automatically and is not serialized by `Bash::snapshot()`.
Re-attach the handle after restoring from a snapshot.

### Arguments

Arguments are passed as a slice of strings, excluding the command name itself:

```rust,ignore
// For "mycommand arg1 arg2", ctx.args = ["arg1", "arg2"]
let first_arg = ctx.args.first().map(|s| s.as_str()).unwrap_or("default");
```

For custom builtins with richer flags, options, or subcommands, implement
`ClapBuiltin` with a `#[derive(clap::Parser)]` argument type. See the
`clap_builtins_guide` rustdoc module for tested examples.

### Environment Variables

Read-only access to environment variables set via `BashBuilder::env()` or `export`:

```rust,ignore
let home = ctx.env.get("HOME").map(|s| s.as_str()).unwrap_or("/");
```

### Shell Variables

Mutable access to shell variables allows builtins to set variables:

```rust,ignore
ctx.variables.insert("RESULT".to_string(), "computed_value".to_string());
```

### Filesystem Access

The virtual filesystem supports all standard operations:

```rust,ignore
// Read a file
let content = ctx.fs.read_file(Path::new("/data/input.txt")).await?;

// Write a file
ctx.fs.write_file(Path::new("/output/result.txt"), b"output").await?;

// Check existence
if ctx.fs.exists(Path::new("/config")).await? {
    // ...
}
```

### Standard Input

When the builtin is invoked in a pipeline, stdin contains the output from the
previous command:

```rust,ignore
// echo "hello" | mycommand
let input = ctx.stdin.unwrap_or("");
let processed = input.to_uppercase();
```

## Return Values

Builtins return `Result<ExecResult>`:

```rust,ignore
pub struct ExecResult {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: i32,
}
```

Helper constructors:

```rust
# use bashkit::ExecResult;
// Success with output
ExecResult::ok("output\n".to_string());

// Error with message and exit code
ExecResult::err("error message\n".to_string(), 1);
```

## Examples

### Database Query Builtin

```rust,ignore
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};
use sqlx::PgPool;
use std::sync::Arc;

struct Psql {
    pool: Arc<PgPool>,
}

#[async_trait]
impl Builtin for Psql {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        // Parse -c "query" argument
        let query = match ctx.args.iter().position(|a| a == "-c") {
            Some(i) => ctx.args.get(i + 1).map(|s| s.as_str()).unwrap_or(""),
            None => return Ok(ExecResult::err("Usage: psql -c 'query'\n".into(), 1)),
        };

        // Execute query (simplified - real impl would format results)
        match sqlx::query(query).fetch_all(&*self.pool).await {
            Ok(rows) => Ok(ExecResult::ok(format!("{} rows\n", rows.len()))),
            Err(e) => Ok(ExecResult::err(format!("ERROR: {}\n", e), 1)),
        }
    }
}

// Usage
let pool = Arc::new(PgPool::connect("postgres://...").await?);
let mut bash = Bash::builder()
    .builtin("psql", Box::new(Psql { pool }))
    .build();

bash.exec("psql -c 'SELECT * FROM users'").await?;
```

### HTTP Client Builtin

```rust,ignore
struct HttpGet {
    client: reqwest::Client,
}

#[async_trait]
impl Builtin for HttpGet {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        let url = match ctx.args.first() {
            Some(url) => url,
            None => return Ok(ExecResult::err("Usage: httpget <url>\n".into(), 1)),
        };

        match self.client.get(url).send().await {
            Ok(resp) => {
                let body = resp.text().await.unwrap_or_default();
                Ok(ExecResult::ok(body))
            }
            Err(e) => Ok(ExecResult::err(format!("Error: {}\n", e), 1)),
        }
    }
}
```

### Overriding Default Builtins

Custom builtins can override default builtins by using the same name:

```rust,no_run
use bashkit::{Bash, Builtin, BuiltinContext, ExecResult, async_trait};

struct SecureEcho;

#[async_trait]
impl Builtin for SecureEcho {
    async fn execute(&self, ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        // Redact sensitive patterns
        let output: Vec<_> = ctx.args.iter()
            .map(|s| if s.contains("password") { "[REDACTED]" } else { s.as_str() })
            .collect();
        Ok(ExecResult::ok(format!("{}\n", output.join(" "))))
    }
}

# fn main() {
let bash = Bash::builder()
    .builtin("echo", Box::new(SecureEcho))  // Overrides default echo
    .build();
# }
```

## Best Practices

1. **Return proper exit codes**: Use 0 for success, non-zero for errors
2. **Include newlines**: Output should end with `\n` for proper formatting
3. **Handle missing args gracefully**: Provide usage messages for incorrect invocations
4. **Use stderr for errors**: Write error messages to `ExecResult::stderr`
5. **Keep builtins stateless when possible**: Use `Arc` for shared state that needs mutation

## Thread Safety

The `Builtin` trait requires `Send + Sync`. For builtins with mutable state, use
appropriate synchronization:

```rust
use bashkit::{Builtin, BuiltinContext, ExecResult, async_trait};
use std::sync::Arc;

struct Counter {
    count: Arc<std::sync::atomic::AtomicU64>,
}

#[async_trait]
impl Builtin for Counter {
    async fn execute(&self, _ctx: BuiltinContext<'_>) -> bashkit::Result<ExecResult> {
        let n = self.count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
        Ok(ExecResult::ok(format!("{}\n", n)))
    }
}
```

## Integration with Scripts

Custom builtins integrate seamlessly with bash scripting:

```bash
# Variables work
NAME="Alice"
greet $NAME

# Pipelines work
echo "hello world" | upper | head -1

# Conditionals work
if mycheck; then
    echo "passed"
else
    echo "failed"
fi

# Loops work
for item in a b c; do
    process $item
done
```