Expand description
§mxsh
mxsh is a reusable shell crate with three primary layers:
mxsh::astandmxsh::parserfor syntax trees and parsing.mxsh::runtimefor file-descriptor and process abstractions.mxsh,mxsh::advanced,mxsh::builtin, andmxsh::policyfor 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 returnsRunOutcomedirectly fromrunandrun_program. - Use
mxsh::ShellBuilderto assemble a default shell, and keep amxsh::embed::ShellBlueprintonly when you intentionally want reusable seeded state for advanced sessions. - Use
Shell::run_cliwhen you want stock CLI argument handling on a configured runtime-owning shell. - Use
Shell::prepareonly as a convenience escape hatch when you want a one-offPreparedProgramfrom the runtime-owning API. - Reach for
mxsh::advanced::Planneronly when you intentionally need the advanced borrowed-runtime path for plan inspection or separate prepare/execute phases. - Use
ShellBuilder::history_appenderwhen an interactive host wants to own history persistence instead of writing a history file. mxsh::frontendis 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: themxshbinary 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
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
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
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
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
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
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:
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
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
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
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
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::diagnosticsandRunOutcome::tracefromShell::runfor the default embedding path. - Use
Shell::run_cliwhen you want the stock CLI argument parser on a configured runtime-owning shell. - Use
advanced::SessionState::runwhen you intentionally need the advanced borrowed-runtime path. RunOutcomeis the supported observability surface for embedders, includingShell::run_cliandfrontend::run_cli; the public API no longer exposes AST or exec-plan dump hooks.- Use
frontend::run_cliorfrontend::InteractiveFrontendonly 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
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
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
use mxsh::ShellBuilder;
let session = ShellBuilder::new()
.frame(["toolsh", "one", "two"])
.new_session()
.expect("session should build");
let checkpoint = session.detached_checkpoint();
let fork = checkpoint.new_session();
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. Create a fresh session
from the blueprint when a host needs a reset point, or use
advanced::SessionState::detached_checkpoint when it needs an explicit
detached-session checkpoint for separate execution.
Re-exports§
pub use crate::embed::PreparedProgram;pub use crate::embed::Shell;pub use crate::embed::ShellBuilder;
Modules§
- advanced
- Advanced borrowed-runtime embedding and planning APIs.
- ast
- builtin
- Host builtin registration and execution APIs.
- embed
- Default embedding API centered on a runtime-owning shell.
- frontend
- Frontend adapters for interactive and CLI use.
- parser
- policy
- Policy-oriented configuration types for embedding.
- runtime
- Runtime abstractions for embedding mxsh.