TruShell is an interactive shell environment designed to integrate task tracking and time management tools seamlessly with traditional terminal commands. Built in Rust with a custom expression parser, TruShell extends the Unix philosophy by providing a unified interface where productivity features and system commands coexist naturally.
- Overview
- Architecture
- Installation & Building
- Usage
- Language Features
- Module Reference
- Parsing System
- Command Execution
- Examples
- Development
TruShell is not a replacement shell but rather a productivity layer that bridges the gap between system administration and personal task management. It runs as an interactive REPL (Read-Eval-Print Loop) that:
- Accepts and executes ordinary shell commands (
ls,cd,grep, etc.) - Parses and interprets custom expressions for task and time operations
- Maintains compatibility with existing Unix tools through fallback command execution
- Provides a consistent interface for both system-level and productivity-level operations
- Hybrid Parsing: Intelligently distinguishes between shell commands and custom expressions
- Expression Evaluation: Supports arithmetic, comparisons, variables, pipelines, and redirects
- Pipeline Support: Chain operations together using the pipe operator (
|) - Redirect Support: Handle command redirection with
>,>>,<, and&>for both standalone commands and pipeline stages - Task Integration: Foundation for task tracking and time management (future expansion)
- Graceful Fallback: Executes as system commands if parsing fails
TruShell follows a modular, layered architecture typical of interpreted languages:
┌─────────────────────────────────────────┐
│ Interactive REPL (main.rs) │
│ • User Input Loop │
│ • Command/Expression Routing │
│ • Fallback Execution │
└──────────────┬──────────────────────────┘
│
┌───────┴────────┐
│ │
┌──────▼──────┐ ┌──────▼──────────┐
│ Lexer │ │ Parser │
│ (Tokenize) │ │ (AST Build) │
└─────────────┘ └─────────────────┘
│ │
└───────┬────────┘
│
┌──────▼──────────┐
│ AST Nodes │
│ (Expressions) │
└─────────────────┘
│
┌──────▼──────────────────┐
│ Command Execution │
│ • System Calls │
│ • Process Management │
└─────────────────────────┘
- Separation of Concerns: Lexing, parsing, and execution are distinct phases
- Error Resilience: Parse failures trigger fallback to system command execution
- Minimal Dependencies: Uses only
crosstermfor terminal handling - Extensibility: AST-based design allows easy addition of new expression types
- Rust 1.70+ (MSRV: Edition 2021)
- Cargo (Rust's package manager)
git clone https://github.com/TruFoundation/TruShell.git
cd TruShell
cargo build --releaseThe compiled binary will be located at target/release/trushell.
./target/release/trushellOr directly via Cargo:
cargo runUpon startup, TruShell displays a welcome message and enters a prompt loop:
Welcome to TruShell Native Engine
trushell ❯
exit: Gracefully shut down the shellCtrl+D(EOF): Safely terminate the shell
Execute any command available in your PATH:
trushell ❯ ls -la
trushell ❯ pwd
trushell ❯ echo "Hello, World!"
trushell ❯ cd /tmp
trushell ❯ cd ~
The cd command is handled specially to modify TruShell's working directory (not spawned as a subprocess).
Define variables using let:
trushell ❯ let x = 42
trushell ❯ let name = "Alice"
trushell ❯ let flag = true
Perform arithmetic and logical operations:
trushell ❯ let result = 10 + 5
trushell ❯ let product = 3 * 7
trushell ❯ let ratio = 100 / 4
trushell ❯ let is_big = 42 > 10
trushell ❯ let is_equal = 5 == 5
trushell ❯ let not_empty = "text" != ""
Chain operations together using |:
trushell ❯ ls() | filter { $it.size > 1mb }
Redirect command input/output and combine streams:
trushell ❯ echo hello > out.txt
trushell ❯ echo append >> out.txt
trushell ❯ cat < in.txt
trushell ❯ echo hello | cat > out.txt
trushell ❯ echo error &> error.log
TruShell's lexer recognizes the following token categories:
| Token Class | Examples | Purpose |
|---|---|---|
| Keywords | let, true, false |
Language control structures |
| Identifiers | x, $var, _private |
Variable and function names |
| Numbers | 42, 3mb, 100kb |
Numeric literals with optional units |
| Strings | "hello" |
Quoted string literals |
| Flags | -la, --verbose, --help |
Command-line flags (preserved) |
| Operators | +, -, *, /, >, <, ==, != |
Binary operations |
| Delimiters | (), {}, [], ., ,, ; |
Structure and grouping |
| Pipes | | |
Pipeline sequencing |
TruShell supports the following literal types:
pub enum Literal {
Number { value: i64, unit: Option<String> }, // 42, 1mb, 500ms
String(String), // "text"
Boolean(bool), // true, false
}| Operator | Type | Precedence | Example |
|---|---|---|---|
+ |
Addition | Term | 5 + 3 |
- |
Subtraction | Term | 10 - 4 |
* |
Multiplication | Factor | 3 * 4 |
/ |
Division | Factor | 12 / 3 |
> |
Greater Than | Comparison | 5 > 3 |
< |
Less Than | Comparison | 2 < 5 |
>= |
Greater or Equal | Comparison | 5 >= 5 |
<= |
Less or Equal | Comparison | 3 <= 5 |
== |
Equals | Comparison | 5 == 5 |
!= |
Not Equals | Comparison | 3 != 5 |
TruShell follows standard mathematical precedence, evaluated in this order (lowest to highest):
- Comparison Operators (
>,<,>=,<=,==,!=) - Term Operators (
+,-) - Factor Operators (
*,/) - Primary (Literals, Identifiers, Parentheses, Blocks)
Variables are declared with let and referenced with $:
trushell ❯ let count = 10
trushell ❯ let doubled = $count * 2
Variables starting with $ are treated as Variable tokens and can be accessed in expressions.
Code blocks are enclosed in {} and contain semicolon-separated statements:
trushell ❯ let data = { let x = 5; let y = 10; $x + $y }
Purpose: Orchestrates the shell's interactive loop and manages command execution.
- Initializes the shell with a welcome message
- Enters an infinite loop reading user input
- Routes input to the appropriate handler (exit, cd, parse, or fallback)
Key Invariants:
- Reads lines until EOF (
Ctrl+D) or explicitexitcommand - Maintains current working directory for the shell process
- Handles all I/O errors gracefully with user-friendly messages
- Spawns a new process for the given command
- Inherits stdin, stdout, stderr for seamless integration
- Reports execution errors with descriptive messages
Execution Flow:
Command::new(cmd)
└─ .args(args)
└─ .stdin(Stdio::inherit())
└─ .stdout(Stdio::inherit())
└─ .stderr(Stdio::inherit())
└─ .spawn()
└─ .wait()
- Heuristic Detection: Attempts to interpret parsed AST as a CLI invocation
- Use Case: Handles cases where
ls -lais parsed asls - la(subtraction) - Strategy:
- Traverses the AST looking for a chain of subtraction operations
- Extracts the leftmost node as the command name
- Collects remaining identifiers/strings as arguments
- Returns
Some((cmd, args))if the pattern matches, otherwiseNone
Example Transformation:
Input: "ls -la"
Parse: BinaryOp { left: Identifier("ls"), op: Subtract, right: Literal(String("-la")) }
Extract: ("ls", ["-la"])
Execute: ls -la
Purpose: Converts raw user input into an Abstract Syntax Tree (AST) for interpretation.
pub enum Token {
Let, // Variable declaration
Flag(String), // CLI flags (-la, --help)
Identifier(String), // Variable/function names
Number(String), // Numeric literals
StringLiteral(String), // Quoted strings
Boolean(bool), // true/false
Equals, // = assignment
Pipe, // | pipeline
LParen, RParen, // ( )
LBrace, RBrace, // { }
Dot, // . property access
Comma, // , separator
Semicolon, // ; statement terminator
GreaterThan, // >
LessThan, // <
GreaterThanOrEqual, // >=
LessThanOrEqual, // <=
EqualsEquals, // ==
BangEquals, // !=
Plus, // +
Minus, // -
Star, // *
Slash, // /
}pub enum ASTNode {
Let { name: String, value: Box<ASTNode> },
Pipeline { stages: Vec<Box<ASTNode>> },
Command { name: String, args: Vec<ASTNode> },
Redirect { source: Box<ASTNode>, fd: u8, mode: RedirectMode, target: RedirectTarget, merge_stderr: bool },
Block { body: Vec<ASTNode> },
BinaryOp { left: Box<ASTNode>, op: BinaryOperator, right: Box<ASTNode> },
Variable(String),
Literal(Literal),
PropertyAccess { target: Box<ASTNode>, property: String },
Identifier(String),
}Responsibility: Converts a string into a sequence of tokens.
- Whitespace: Skipped entirely
- Identifiers: Start with
a-z,A-Z,_, or$; continue with alphanumerics or_ - Keywords:
let,true,false→ special tokens - Numbers: Digits optionally followed by a unit string (e.g.,
5,100ms,1mb) - Strings: Double-quoted; no escape sequences currently supported
- Flags:
-followed by letters/hyphens (e.g.,-la,--help) - Operators: Single and multi-character (
==,!=,>=,<=)
The lexer recognizes CLI flags intelligently:
if let Some(second) = peek_two_chars_ahead {
if second.is_alphabetic() || second == '-' {
// Lex as a flag (e.g., -la, --verbose)
self.lex_flag()
} else {
// Lex as minus operator (e.g., 5 - 3)
Token::Minus
}
}Effect: ls-la is lexed as a command followed by a flag, not as subtraction.
Responsibility: Converts tokens into an AST using recursive descent parsing.
parse_statement
├─ if let: parse_let_statement
└─ else: parse_pipeline
parse_pipeline
└─ parse_expression (with | separator)
parse_expression
└─ parse_comparison
parse_comparison (>/</>=/<=,==,!=)
└─ parse_term
parse_term (+,-)
└─ parse_factor
parse_factor (*,/)
└─ parse_primary
parse_primary (literals, identifiers, parens, blocks)
└─ parse_identifier_expression (handles ., (, {})
let x = 5
├─ Token::Let
├─ expect identifier → "x"
├─ expect =
└─ parse_expression → Literal(Number(5))
Result: ASTNode::Let {
name: "x",
value: Box::new(Literal(Number(5)))
}ls() | filter { $it.size > 1mb }
├─ parse_expression → Command { name: "ls", args: [] }
├─ Token::Pipe
├─ parse_expression → Command { name: "filter", args: [Block {...}] }
└─ Token::Pipe (none)
Result: ASTNode::Pipeline {
stages: [Command{...}, Command{...}]
}Numbers can include units (e.g., 5mb, 100ms):
pub fn parse_number_literal(raw: &str) -> Result<Literal, ParseError> {
let digits: String = raw.chars().take_while(|ch| ch.is_ascii_digit()).collect();
let unit: String = raw.chars().skip_while(|ch| ch.is_ascii_digit()).collect();
Ok(Literal::Number {
value: digits.parse::<i64>()?,
unit: if unit.is_empty() { None } else { Some(unit) },
})
}Examples:
5→Number { value: 5, unit: None }1mb→Number { value: 1, unit: Some("mb") }100ms→Number { value: 100, unit: Some("ms") }
User Input String
↓
[Lexer::tokenize()]
↓
Token Vector
↓
[Parser::parse_statement()]
↓
ASTNode (Abstract Syntax Tree)
↓
[main.rs execution logic]
↓
Output/Side Effects
Input: "let x = 5 + 3"
Step 1: Tokenization
[Let, Identifier("x"), Equals, Number("5"), Plus, Number("3")]
Step 2: Parsing (Recursive Descent)
parse_statement
└─ parse_let_statement
├─ expect Let → ✓
├─ expect_identifier → "x"
├─ expect Equals → ✓
└─ parse_expression (for "5 + 3")
└─ parse_comparison
└─ parse_term
├─ parse_factor → Literal(5)
├─ detect Plus
├─ parse_factor → Literal(3)
└─ combine: BinaryOp { left: 5, op: Add, right: 3 }
Step 3: AST Result
ASTNode::Let {
name: "x",
value: Box::new(
ASTNode::BinaryOp {
left: Box::new(Literal(Number { value: 5, unit: None })),
op: Add,
right: Box::new(Literal(Number { value: 3, unit: None }))
}
)
}The parser provides descriptive error messages:
pub struct ParseError {
pub message: String,
}Common Errors:
"Expected identifier, found ...""Unexpected token in expression""Unterminated string literal""Unexpected character: '...'"(from lexer)
loop {
1. Read user input
2. Trim whitespace
3. Check for special commands:
├─ "exit" → break loop
└─ "cd ..." → change directory
4. Attempt to parse:
├─ Success → check if it's a probable CLI command
│ ├─ Yes → execute as system command
│ └─ No → print AST (debug mode)
└─ Failure → fallback to system command execution
5. Display output
}
Terminates the shell gracefully:
if trimmed_input == "exit" {
println!("Goodbye!");
break;
}Handled specially without spawning a subprocess:
if trimmed_input.starts_with("cd") {
let parts: Vec<&str> = trimmed_input.split_whitespace().collect();
let new_dir = parts.get(1).copied().unwrap_or(".");
if let Err(e) = std::env::set_current_dir(new_dir) {
eprintln!("trushell: cd: {}: {}", new_dir, e);
}
continue;
}When parsing fails or the AST doesn't match a known pattern, TruShell falls back to executing the input as a system command:
Err(err) => {
eprintln!("Parse error: {}", err);
let parts: Vec<&str> = trimmed_input.split_whitespace().collect();
let command = parts[0];
let args = &parts[1..];
execute_system_command(command, args);
}Result: Most Unix commands work transparently even if parsing fails.
TruShell now supports shell-style redirection for parsed commands, including standalone redirects and redirects on pipeline stages. The execution engine resolves redirect AST nodes before spawning processes and correctly wires command stdin/stdout for pipes:
cmd > filewrites stdout to a filecmd >> fileappends stdout to a filecmd < filereads stdin from a filecmd &> fileredirects stderr to the same target filecmd | other > filepipes data into a redirected final stage
This integration keeps parsed AST behavior aligned with traditional shell semantics while preserving the existing fallback execution model.
Commands are executed with inherited I/O streams:
Command::new(cmd)
.args(args)
.stdin(Stdio::inherit()) // User input reaches subprocess
.stdout(Stdio::inherit()) // Subprocess output visible
.stderr(Stdio::inherit()) // Errors displayed directly
.spawn()
.wait()trushell ❯ let x = 10
Parsed AST: Let { name: "x", value: ... }
trushell ❯ let y = $x * 2
Parsed AST: Let { name: "y", value: ... }
trushell ❯ let z = 100 / 5
Parsed AST: Let { name: "z", value: ... }trushell ❯ let name = "Alice"
trushell ❯ let greeting = "Hello, " + $name
Parsed AST: Let { name: "greeting", value: ... }trushell ❯ let is_adult = 25 > 18
Parsed AST: Let { name: "is_adult", value:
BinaryOp {
left: 25,
op: GreaterThan,
right: 18
}
}
trushell ❯ let in_range = 50 >= 10trushell ❯ ls -la
total 48
drwxr-xr-x 5 user group 160 Jul 5 12:34 .
drwxr-xr-x 10 user group 320 Jul 4 18:22 ..
-rw-r--r-- 1 user group 1234 Jul 05 12:30 README.md
...
trushell ❯ pwd
/home/user/projects/TruShell
trushell ❯ echo "Building..."
Building...trushell ❯ cd /tmp
trushell ❯ pwd
/tmp
trushell ❯ cd -
trushell ❯ pwd
/home/user/projects/TruShelltrushell ❯ ls() | filter { $it.size > 1mb }
Parsed AST: Pipeline {
stages: [
Command { name: "ls", args: [] },
Command { name: "filter", args: [Block { ... }] }
]
}TruShell/
├── Cargo.toml # Rust project manifest
├── Cargo.lock # Dependency lock file
├── src/
│ ├── main.rs # REPL and command execution (4.3 KB)
│ └── parser.rs # Lexer and parser (19.0 KB)
├── target/ # Compiled binaries (excluded from repo)
├── README.md # This file
└── LICENSE.md # Project license
- crossterm (v0.27): Terminal manipulation and event handling
- Standard Library: All core functionality uses
std
# Build in debug mode
cargo build
# Build optimized release binary
cargo build --release
# Run tests
cargo test
# Run with output
cargo run -- --verboseThe parser module includes unit tests:
#[test]
fn tokenize_basic_lets_and_pipeline() { ... }
#[test]
fn parse_let_statement() { ... }
#[test]
fn parse_pipeline_with_function_block() { ... }Run tests with:
cargo test- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit changes with clear messages
- Add tests for new functionality
- Push to your fork
- Submit a pull request
- Task Management API: Commands to create, list, and complete tasks
- Time Tracking:
time start,time stop,time logcommands - Persistence: SQLite backend for task/time storage
- Configuration:
.trushellrcconfiguration file support - Scripting: Multi-line scripts and batching
- Shell Integration:
.bashrc/.zshrcintegration for seamless use - Custom Functions: User-defined functions with parameters
- History & Completion: Command history and tab completion
- Linux Shell Documentation:
man bash,man sh - Rust Book: https://doc.rust-lang.org/book/
- Crossterm Docs: https://docs.rs/crossterm/
- Unix Philosophy: https://en.wikipedia.org/wiki/Unix_philosophy
TruShell is released under the terms specified in LICENSE.md. See that file for full details.
TruFoundation — Empowering productivity through open-source tooling.
Last Updated: July 5, 2026