3 unstable releases
| 0.1.1 | Feb 23, 2026 |
|---|---|
| 0.0.3 | Feb 22, 2026 |
| 0.0.1 | Feb 22, 2026 |
#181 in Testing
140KB
3.5K
SLoC
pmsh
Philip Miesbauer SHell (pmsh) — a modern, minimal shell written in Rust.
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~andcd -for previous dir)history— print command historyexit— 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()]); }
- Purpose: Parse a command line into a
-
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)?;
- Purpose: Persist and manage command history (
-
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() -> Stringformat_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}
- Supports
- 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:
RealExecutorbridges toExecutor
- 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
- Purpose: End-to-end validation via a PTY using
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)breakcontinueevalexecexportreadonlyreturnsetshifttimestrapunset
-
Regular Builtins:
aliasbgcommandfalsefcfggetoptsjobskillnewgrppwdreadtrueumaskunaliaswait
-
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/ficase/esacforloopswhileloopsuntilloops- Functions
Contributing
PRs welcome! Please ensure all tests pass and code is formatted.
License
MIT
Dependencies
~5–16MB
~161K SLoC