Skip to main content

Crate mxsh

Crate mxsh 

Source
Expand description

§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

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::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

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.