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.
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
# #[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
# #[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
# #[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
# #[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
# #[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
# #[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:
# #[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
# #[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
# #[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
# #[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
# #[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
# #[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
# #[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
# #[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.