$_ bashkit

Live Mount/Unmount Guide

Bashkit supports attaching and detaching filesystems on a running interpreter without rebuilding it. Shell state — environment variables, working directory, history, aliases — is fully preserved across mount operations.

Motivation

Before live mounts, the only way to add a filesystem after build() was to accumulate mount configs and call reset(), which rebuilds the entire interpreter and loses all in-flight state. Live mounts solve this by exposing the internal [MountableFs] layer that wraps every Bash instance.

Common use cases:

  • Agent workflows: attach a host directory mid-session when a tool needs it
  • Plugin systems: mount/unmount plugin filesystems without restarting
  • Hot-swap deployments: replace a mounted app filesystem with a new version
  • Testing: inject mock data at specific paths during a test

Quick Start

use bashkit::{Bash, FileSystem, InMemoryFs};
use std::path::Path;
use std::sync::Arc;

# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
let mut bash = Bash::new();

// Create and populate a filesystem
let data_fs = Arc::new(InMemoryFs::new());
data_fs.write_file(Path::new("/users.json"), br#"["alice"]"#).await?;

// Mount it live — no rebuild needed
bash.mount("/mnt/data", data_fs)?;

let result = bash.exec("cat /mnt/data/users.json").await?;
assert!(result.stdout.contains("alice"));

// Unmount when done
bash.unmount("/mnt/data")?;
# Ok(())
# }

API

Bash::mount(vfs_path, fs)

Mounts fs at vfs_path. The mount takes effect immediately — subsequent exec() calls see files from the mounted filesystem. If a mount already exists at vfs_path, it is replaced (hot-swap).

# use bashkit::{Bash, FileSystem, InMemoryFs};
# use std::sync::Arc;
# use std::path::Path;
# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
let bash = Bash::new();
let fs = Arc::new(InMemoryFs::new());
bash.mount("/mnt/data", fs)?;
# Ok(())
# }

Errors: Returns Err if vfs_path is not absolute (after normalization).

Bash::unmount(vfs_path)

Removes the mount at vfs_path. Paths that previously resolved to the mounted filesystem fall back to the root filesystem or the next shorter mount prefix.

# use bashkit::{Bash, FileSystem, InMemoryFs};
# use std::sync::Arc;
# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
# let bash = Bash::new();
# let fs = Arc::new(InMemoryFs::new());
# bash.mount("/mnt/data", fs)?;
bash.unmount("/mnt/data")?;
# Ok(())
# }

Errors: Returns Err if nothing is mounted at vfs_path.

How It Works

Every Bash instance wraps its filesystem stack in a [MountableFs] as the outermost layer. This layer uses longest-prefix matching to route path operations to the correct mounted filesystem:

┌──────────────────────────────┐
│  MountableFs (live mounts)   │  ← Bash::mount() / unmount()
├──────────────────────────────┤
│  OverlayFs (text mounts)     │  ← BashBuilder::mount_text()
├──────────────────────────────┤
│  MountableFs (real mounts)   │  ← BashBuilder::mount_real_*_at()
├──────────────────────────────┤
│  Base filesystem             │  ← InMemoryFs or custom
└──────────────────────────────┘

Because the interpreter holds an Arc<dyn FileSystem> pointing to the outermost MountableFs, any mount/unmount operation is visible to the interpreter immediately — no rebuild or state transfer required.

Builder Mounts vs Live Mounts

Builder mountsLive mounts
WhenBefore build()After build()
MethodBashBuilder::mount_text(), mount_real_*()Bash::mount()
StateN/A (no interpreter yet)Fully preserved
Use caseInitial configurationDynamic attachment

Both approaches can be combined: configure initial mounts with the builder, then add/remove mounts at runtime.

Examples

Multiple Mounts

use bashkit::{Bash, FileSystem, InMemoryFs};
use std::path::Path;
use std::sync::Arc;

# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
let mut bash = Bash::new();

let logs = Arc::new(InMemoryFs::new());
logs.write_file(Path::new("/app.log"), b"started\n").await?;

let config = Arc::new(InMemoryFs::new());
config.write_file(Path::new("/app.toml"), b"port = 8080\n").await?;

bash.mount("/var/log", logs)?;
bash.mount("/etc/app", config)?;

let result = bash.exec("cat /var/log/app.log").await?;
assert_eq!(result.stdout, "started\n");

let result = bash.exec("cat /etc/app/app.toml").await?;
assert_eq!(result.stdout, "port = 8080\n");
# Ok(())
# }

Hot-Swap

Re-mounting at the same path replaces the filesystem atomically:

use bashkit::{Bash, FileSystem, InMemoryFs};
use std::path::Path;
use std::sync::Arc;

# #[tokio::main]
# async fn main() -> bashkit::Result<()> {
let mut bash = Bash::new();

let v1 = Arc::new(InMemoryFs::new());
v1.write_file(Path::new("/version"), b"1.0").await?;
bash.mount("/app", v1)?;

let result = bash.exec("cat /app/version").await?;
assert_eq!(result.stdout, "1.0");

// Hot-swap to v2
let v2 = Arc::new(InMemoryFs::new());
v2.write_file(Path::new("/version"), b"2.0").await?;
bash.mount("/app", v2)?;

let result = bash.exec("cat /app/version").await?;
assert_eq!(result.stdout, "2.0");
# Ok(())
# }

See Also

  • [MountableFs] — the underlying mount infrastructure
  • [BashBuilder::mount_text] — pre-build text file mounts
  • [BashBuilder::fs] — custom filesystem injection
  • [Bash::fs] — direct filesystem access
  • VFS specification