# mxsh
`mxsh` is a reusable shell crate with three primary layers:
- `mxsh::ast` and `mxsh::parser` for syntax trees and parsing.
- `mxsh::runtime` for file-descriptor and process abstractions.
- `mxsh`, `mxsh::advanced`, `mxsh::builtin`, and `mxsh::policy`
for embedding, advanced planning, host builtins, and policy.
`mxsh::frontend` is available when you need interactive or CLI-adjacent
frontend adapters.
The `mxsh` binary is now just one frontend over the library.
For a contributor-oriented map of the repository, see [DEVELOPERS.md](DEVELOPERS.md).
## Start Here
- Start with `mxsh::Shell`. It owns the runtime and returns `RunOutcome`
directly from `run` and `run_program`.
- Use `mxsh::ShellBuilder` to assemble a default shell, and keep a
`mxsh::embed::ShellBlueprint` only when you intentionally want reusable
seeded state for advanced sessions.
- Use `Shell::run_cli` when you want stock CLI argument handling on a configured
runtime-owning shell.
- Use `Shell::prepare` only as a convenience escape hatch when you want a
one-off `PreparedProgram` from the runtime-owning API.
- Reach for `mxsh::advanced::Planner` only when you intentionally need the advanced
borrowed-runtime path for plan inspection or separate prepare/execute phases.
- Use `ShellBuilder::history_appender` when an interactive host wants to own
history persistence instead of writing a history file.
- `mxsh::frontend` is optional and is mainly for interactive or CLI-adjacent
integrations.
## Cargo Features
- `parser`: AST and parser only.
- `runtime`: runtime traits, process abstractions, and fd helpers.
- `unix-runtime`: Unix process spawning and terminal control.
- `embed`: embedding, planning, policy, builtins, execution, and diagnostics.
- `frontend`: reusable interactive and CLI-adjacent frontend adapters.
- `cli`: the `mxsh` binary and stock CLI behavior.
- `serde`: serde-backed serialization support used by the embedding and planning layers.
- `test-support`: in-memory and deterministic runtimes plus stdio test helpers.
Examples that use `InMemoryRuntime`, `DeterministicRuntime`, `StringStdioIn`, or
`StringStdioOut` require `test-support`.
The runtime layer is exposed through one trait: `Runtime`.
## Quick Start
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "unix-runtime"))] {
use mxsh::runtime::unix::UnixRuntime;
use mxsh::Shell;
let mut shell = Shell::new(UnixRuntime::new());
let outcome = shell.run("echo hello");
assert_eq!(outcome.status, 0);
# }
```
If you just want to run commands, you do not need a separate session object or
an explicit planning phase.
## Configure One Shell
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "unix-runtime"))] {
use mxsh::embed::StdioConfig;
use mxsh::policy::VariableAttributes;
use mxsh::runtime::unix::UnixRuntime;
use mxsh::{Shell, ShellBuilder};
let mut shell = ShellBuilder::new()
.interactive(true)
.env("GREETING", "hello", VariableAttributes::EXPORT)
.env("PS1", "mxsh> ", VariableAttributes::empty())
.build(UnixRuntime::new())
.expect("shell should build");
let outcome = shell.run("echo \"$GREETING configured shell\"");
assert_eq!(outcome.status, 0);
# }
```
`ShellBuilder` keeps the common path on one runtime-owning `Shell`. Reuse a
`ShellBlueprint` only when you intentionally want `new_session()` cloning.
## Multi-Tenant Embedding
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "test-support"))] {
use mxsh::runtime::testing::InMemoryRuntime;
use mxsh::{Shell, ShellBuilder};
let mut shell = ShellBuilder::new()
.multi_tenant()
.build(InMemoryRuntime::new())
.expect("shell should build");
let result = shell.run("echo hello");
assert_eq!(result.status, 0);
# }
```
`ShellBuilder::multi_tenant()` is a safe preset for shared hosts. It disables
ambient env inheritance, default startup files, implicit file history,
background jobs, and the process-global `.`,
`exec`, `umask`, and `ulimit` builtins. Hosts can still selectively re-enable
behavior with explicit builder settings.
## Language Policy
```rust
# #[cfg(feature = "embed")] {
use mxsh::ast::Program;
use mxsh::parser::ParseOptions;
use mxsh::policy::ShellLanguage;
use mxsh::ShellBuilder;
let language = ShellLanguage::new()
.with_alias_expansion_enabled(false)
.with_function_definitions_enabled(false);
let _program = Program::parse_with(
"echo hello",
&ParseOptions::new()
.alias_expansion_enabled(false)
.function_definitions_enabled(false),
)
.expect("program should parse");
let _blueprint = ShellBuilder::new()
.language(language)
.blueprint()
.expect("blueprint should build");
# }
```
## Rename The Shell
```rust,no_run
# #[cfg(all(
# feature = "embed",
# feature = "frontend",
# feature = "runtime",
# feature = "unix-runtime"
# ))] {
use mxsh::policy::{ShellIdentity, VariableAttributes};
use mxsh::runtime::unix::UnixRuntime;
use mxsh::{Shell, ShellBuilder};
let argv = vec!["toysh".to_string(), "-c".to_string(), "echo $0".to_string()];
let mut shell = ShellBuilder::new()
.identity(
ShellIdentity::named("toysh")
.with_default_history_file(".toysh_history"),
)
.env("PS1", "toysh> ", VariableAttributes::empty())
.build(UnixRuntime::new())
.expect("shell should build");
let outcome = shell.run_cli(&argv);
assert_eq!(outcome.status, 0);
assert_eq!(outcome.exit_code, None);
# }
```
## Register A Host Builtin
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "test-support"))] {
use mxsh::policy::VariableAttributes;
use mxsh::runtime::testing::InMemoryRuntime;
use mxsh::{Shell, ShellBuilder};
let mut shell = ShellBuilder::new()
.register_builtin("set-answer", |ctx, _args| {
ctx.env_set("ANSWER", "42", VariableAttributes::EXPORT);
0
})
.build(InMemoryRuntime::new())
.expect("shell should build");
let result = shell.run("set-answer");
assert_eq!(result.status, 0);
assert_eq!(shell.env_get("ANSWER"), Some("42"));
# }
```
Host builtins receive a narrow context with environment, working-directory, and
stdio helpers:
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "test-support"))] {
use mxsh::runtime::testing::InMemoryRuntime;
use mxsh::{Shell, ShellBuilder};
let mut shell = ShellBuilder::new()
.register_builtin("greet", |ctx, args| {
let name = args.first().map(String::as_str).unwrap_or("world");
let _ = ctx.write_stdout_line(&format!("hello {name}"));
0
})
.build(InMemoryRuntime::new())
.expect("shell should build");
assert_eq!(shell.run("greet hello world").status, 0);
# }
```
`BuiltinHost` intentionally stays narrow. It supports environment access,
working-directory updates, and stdio/output helpers.
## Register A Special Host Builtin
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "test-support"))] {
use mxsh::runtime::testing::InMemoryRuntime;
use mxsh::{Shell, ShellBuilder};
let mut shell = ShellBuilder::new()
.register_special_builtin("remember-prefix", |ctx, _args| {
let seen = ctx.env_get("X").unwrap_or("").to_string();
ctx.env_set("SEEN", seen, Default::default());
0
})
.build(InMemoryRuntime::new())
.expect("shell should build");
let result = shell.run("X=1 remember-prefix");
assert_eq!(result.status, 0);
assert_eq!(shell.env_get("X"), Some("1"));
# }
```
## Command Policy
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "test-support"))] {
use mxsh::policy::CommandOverride;
use mxsh::runtime::testing::InMemoryRuntime;
use mxsh::{Shell, ShellBuilder};
let mut shell = ShellBuilder::new()
.register_command_override(
"echo",
CommandOverride::new(|ctx, _args| {
let _ = ctx.write_stdout_line("override");
0
})
.with_matcher(|argv| argv.len() == 1),
)
.clear_unspecified_utilities()
.add_unspecified_utility("mystery")
.build(InMemoryRuntime::new())
.expect("shell should build");
assert_eq!(shell.run("echo").status, 0);
assert_eq!(shell.run("mystery").status, 1);
# }
```
## Customize Option Schema
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "test-support"))] {
use mxsh::policy::{ShellOptionSchema, ShellOptionSpec, ShellOptions};
use mxsh::runtime::testing::InMemoryRuntime;
use mxsh::{Shell, ShellBuilder};
let mut shell = ShellBuilder::new()
.option_schema(
ShellOptionSchema::empty().with_option(
ShellOptionSpec::new(ShellOptions::ERREXIT)
.with_short_name('E')
.with_long_name("strict"),
),
)
.build(InMemoryRuntime::new())
.expect("shell should build");
assert_eq!(shell.run("set -E").status, 0);
assert!(shell.has_option(ShellOptions::ERREXIT));
# }
```
## Custom Interactive Frontend
```rust,no_run
# #[cfg(all(
# feature = "embed",
# feature = "frontend",
# feature = "runtime",
# feature = "test-support"
# ))] {
use std::collections::VecDeque;
use std::io;
use mxsh::advanced::SessionState;
use mxsh::embed::StdioConfig;
use mxsh::frontend::InteractiveFrontend;
use mxsh::runtime::testing::{InMemoryRuntime, StringStdioOut};
use mxsh::{Shell, ShellBuilder};
struct QueueFrontend {
lines: VecDeque<String>,
}
impl InteractiveFrontend for QueueFrontend {
fn prompt(&mut self, _shell: &SessionState, _continuation: bool) -> io::Result<String> {
Ok(String::new())
}
fn read_line(
&mut self,
_shell: &mut SessionState,
_prompt: &str,
) -> io::Result<Option<String>> {
Ok(self.lines.pop_front())
}
}
let stdout = StringStdioOut::new();
let mut shell = ShellBuilder::new()
.interactive(true)
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.build(InMemoryRuntime::new())
.expect("shell should build");
let mut frontend = QueueFrontend {
lines: VecDeque::from([
"echo hello from frontend".to_string(),
"exit".to_string(),
]),
};
let result = shell
.run_interactive_with_frontend(&mut frontend)
.expect("interactive run should succeed");
assert_eq!(result.exit_code, Some(0));
assert!(stdout.collect().contains("hello from frontend"));
# }
```
## Diagnostics, Trace, and Frontends
- Inspect `RunOutcome::diagnostics` and `RunOutcome::trace` from `Shell::run`
for the default embedding path.
- Use `Shell::run_cli` when you want the stock CLI argument parser on a
configured runtime-owning shell.
- Use `advanced::SessionState::run` when you intentionally need the advanced borrowed-runtime path.
- `RunOutcome` is the supported observability surface for embedders, including
`Shell::run_cli` and `frontend::run_cli`; the public API no longer exposes AST
or exec-plan dump hooks.
- Use `frontend::run_cli` or `frontend::InteractiveFrontend` only when you need
the stock CLI behavior or a custom interactive frontend. These adapters are
optional and require the relevant frontend features.
## Advanced: Reuse One Blueprint Across Sessions
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "unix-runtime"))] {
use mxsh::policy::{ShellOptions, StartupPolicy, VariableAttributes};
use mxsh::runtime::unix::UnixRuntime;
use mxsh::ShellBuilder;
let blueprint = ShellBuilder::new()
.interactive(true)
.startup_policy(StartupPolicy::InteractiveEnvHook)
.options(ShellOptions::MONITOR)
.env("PS1", "mxsh> ", VariableAttributes::empty())
.blueprint()
.expect("blueprint should build");
let mut left = blueprint.new_session();
let mut right = blueprint.new_session();
let mut runtime = UnixRuntime::new();
left.initialize(&mut runtime);
right.initialize(&mut runtime);
# }
```
Blueprints can also preseed aliases, shell functions, positional
parameters, and inherited file descriptors; every `new_session()` gets a fresh
copy of that seeded state.
Use `advanced::SessionState` only when you intentionally want the advanced borrowed-runtime
path, such as reusing one runtime across many sessions or driving planning and
execution in separate phases.
## Advanced: Planning
```rust,no_run
# #[cfg(all(feature = "embed", feature = "runtime", feature = "test-support"))] {
use mxsh::advanced::Planner;
use mxsh::ast::Program;
use mxsh::embed::StdioConfig;
use mxsh::runtime::testing::{InMemoryRuntime, StringStdioOut};
use mxsh::ShellBuilder;
let program = Program::parse("echo hello").expect("program should parse");
let stdout = StringStdioOut::new();
let mut session = ShellBuilder::new()
.stdio(StdioConfig {
stdout: stdout.fd(),
..StdioConfig::default()
})
.new_session()
.expect("session should build");
let mut runtime = InMemoryRuntime::new();
let plan = Planner::new(&mut session, &mut runtime).prepare(&program);
let result = Planner::new(&mut session, &mut runtime).execute_plan(&plan);
assert_eq!(result.status, 0);
assert_eq!(stdout.collect(), "hello\n");
# }
```
## Advanced: Session State
```rust
# #[cfg(feature = "embed")] {
use mxsh::ShellBuilder;
let session = ShellBuilder::new()
.frame(["toolsh", "one", "two"])
.new_session()
.expect("session should build");
let fork = session.fork();
assert_eq!(fork.frame(), ["toolsh".to_string(), "one".to_string(), "two".to_string()]);
assert_eq!(fork.argv0(), Some("toolsh"));
assert_eq!(fork.positional_parameters(), ["one".to_string(), "two".to_string()]);
# }
```
Use `ShellBlueprint` to seed reusable state up front, then rely on
`advanced::SessionState::snapshot`, `restore`, and `fork` when a host needs
explicit reset points or cloned sessions.