pmsh 0.0.3

A custom shell written in Rust
pmsh-0.0.3 is not a library.

pmsh

Philip Miesbauer SHell (pmsh) — a modern, minimal shell written in Rust.

Codecov

Features

  • Interactive REPL with line editing (rustyline)
  • Command parsing and execution (external commands)
  • Pipelines (e.g., echo hello | wc -c)
  • Builtins: cd, cd -, history, exit, source
  • Persistent command history (~/.pmsh_history, up to 1000 entries)
  • Prompt shows user and current directory, with ~ for HOME
  • Tilde expansion and collapse for paths
  • Deterministic, robust unit and integration tests
  • CI with code quality and coverage reporting

Usage

Build and run:

cargo build --release
./target/release/pmsh

Example session

philip:~$ echo hello
hello
philip:~$ cd /tmp
philip:/tmp$ cd /var
philip:/var$ cd -
/tmp
philip:/tmp$ echo hello | wc -c
6
philip:/tmp$ history
1: echo hello
2: cd /tmp
3: cd /var
4: cd -
5: echo hello | wc -c
6: history
philip:/tmp$ exit
Exiting.

Pipelines

Pipelines allow you to chain commands together, sending the output of one command as input to the next:

philip:~$ echo "hello world" | wc -w
2
philip:~$ cat file.txt | grep pattern | wc -l

Note: Currently, builtins cannot be used inside pipelines (e.g., cd /tmp | echo ok is not supported). This is a planned feature for future releases.

  • cd [dir] — change directory (supports ~ and cd - for previous dir)
  • history — print command history
  • exit — save history and exit

History

  • Commands are saved to ~/.pmsh_history (up to 1000 entries)
  • History is loaded on startup and saved on exit and after each command

Prompt

  • Format: <user>:<cwd>$
  • HOME is shown as ~ (e.g., philip:~$)

Development

Run tests:

cargo test

Run integration test (PTY-based):

cargo test --test integration_repl

Check formatting and lints:

cargo fmt -- --check
cargo clippy -- -D warnings

Generate coverage (requires cargo-tarpaulin):

cargo tarpaulin --out Xml --out Lcov --run-types Tests

Internal Tools Help

This section documents pmsh’s internal modules and how to use them in code and tests.

  • Parser (src/parser.rs)

    • Purpose: Parse a command line into a Command { name, args }.
    • API: Command::parse(line: &str) -> Option<Command>
    • Example:
       use crate::parser::Command;
       if let Some(cmd) = Command::parse("echo hello world") {
       		assert_eq!(cmd.name, "echo");
       		assert_eq!(cmd.args, vec!["hello".into(), "world".into()]);
       }
      
  • Executor (src/executor.rs)

    • Purpose: Execute external programs with arguments.
    • API: Executor::execute(cmd: &Command) -> Result<(), String>
    • Example:
       use crate::executor::Executor;
       use crate::parser::Command;
       let cmd = Command { name: "echo".into(), args: vec!["hi".into()] };
       Executor::execute(&cmd)?;
      
  • History Manager (src/history.rs)

    • Purpose: Persist and manage command history (~/.pmsh_history).
    • APIs:
      • HistoryManager::new() -> Result<Self, String>
      • load(&self) -> Result<Vec<String>, String>
      • save(&self, history: &[String]) -> Result<(), String>
      • add_entry(&self, entry: &str, history: &mut Vec<String>) -> Result<(), String>
    • Example:
       use crate::history::HistoryManager;
       let mgr = HistoryManager::new()?;
       let mut hist = mgr.load()?;
       mgr.add_entry("echo hi", &mut hist)?;
       mgr.save(&hist)?;
      
  • Path Utilities (src/path_utils.rs)

    • Purpose: Home expansion/compaction.
    • APIs:
      • expand_home(path: &str) -> String (turn $HOME/... into ~/... for display)
      • collapse_tilde(path: &str) -> std::path::PathBuf (turn ~/... into absolute path)
    • Example:
       use crate::path_utils::{expand_home, collapse_tilde};
       let shown = expand_home("/home/user/projects");
       let abs = collapse_tilde("~/projects");
      
  • UI (src/ui.rs)

    • Purpose: Prompt formatting.
    • APIs:
      • format_prompt() -> String
      • format_prompt_with(cwd: &str, user: &str) -> String (pure, test helper)
    • Example:
       use crate::ui::format_prompt_with;
       let p = format_prompt_with("/home/user", "alice");
       assert!(p.starts_with("alice:"));
      
  • Builtins (src/builtins.rs)

    • Purpose: Handle internal shell commands.
    • API: handle_builtin(cmd, history_mgr, command_history, oldpwd) -> Result<BuiltinResult, String>
      • Supports cd [dir], cd -, history, exit
      • BuiltinResult::{HandledContinue, HandledExit, NotHandled}
    • Example:
       use crate::builtins::{handle_builtin, BuiltinResult};
       use crate::history::HistoryManager;
       use crate::parser::Command;
       let mgr = HistoryManager::new()?;
       let mut hist = vec![];
       let mut oldpwd = None;
       let cmd = Command { name: "cd".into(), args: vec!["/tmp".into()] };
       match handle_builtin(&cmd, &mgr, &mut hist, &mut oldpwd)? {
       		BuiltinResult::HandledContinue => {}
       		_ => {}
       }
      
  • REPL (src/repl.rs)

    • Purpose: Event loop driving input, builtins, execution, and history.
    • APIs:
      • run_repl(editor, history_mgr, command_history, executor)
      • Traits: LineEditor (readline, add_history_entry), ExecutorTrait (execute)
      • Adapter: RealExecutor bridges to Executor
    • Example (testing with mocks):
       use crate::repl::{run_repl, ReadlineEvent, LineEditor, ExecutorTrait};
       # struct MockEditor; # struct MockExec; # /* see tests for full mocks */
       # impl LineEditor for MockEditor { /* ... */ }
       # impl ExecutorTrait for MockExec { /* ... */ }
       # let mut editor = MockEditor; let exec = MockExec; let mgr = Default::default();
       let mut history = vec![];
       run_repl(&mut editor, &mgr, &mut history, &exec);
      
  • Integration Tests (tests/integration_repl.rs)

    • Purpose: End-to-end validation via a PTY using expectrl.
    • How to run:
       cargo test --test integration_repl
      

Roadmap to POSIX Compliance

This shell is not yet POSIX compliant. Here is a high-level overview of features required to move towards compliance:

  • Pipelines and Redirection:

    • | (pipe)
    • > (redirect stdout)
    • < (redirect stdin)
    • >> (append stdout)
    • 2> (redirect stderr)
  • Special Builtins:

    • . (dot)
    • : (colon)
    • break
    • continue
    • eval
    • exec
    • export
    • readonly
    • return
    • set
    • shift
    • times
    • trap
    • unset
  • Regular Builtins:

    • alias
    • bg
    • command
    • false
    • fc
    • fg
    • getopts
    • jobs
    • kill
    • newgrp
    • pwd
    • read
    • true
    • umask
    • unalias
    • wait
  • Quoting:

    • '...' (single quotes)
    • "..." (double quotes)
    • \ (escape character)
  • Variable Expansion:

    • ${parameter}
    • $parameter
    • $@
    • $*
    • $#
    • $?
    • $$
    • $!
  • Command Substitution:

    • $(command)
    • `command`
  • Conditional Execution:

    • && (AND)
    • || (OR)
  • Background Jobs:

    • & (run in background)
  • Shell Grammar:

    • if/then/elif/else/fi
    • case/esac
    • for loops
    • while loops
    • until loops
    • Functions

Contributing

PRs welcome! Please ensure all tests pass and code is formatted.

License

MIT