Nushell vs Auto-Shell Command Design Architecture
Executive Summary
This document compares the command design philosophy and architecture between Nushell (a production-grade modern shell) and Auto-Shell (our traditional shell implementation).
Key Difference: Nushell is designed as a structured data shell where commands pass typed data, while Auto-Shell is a traditional text shell where commands pass strings.
Command Trait Comparison
Nushell's Command Trait
pub trait Command: Send + Sync + CommandClone {
fn name(&self) -> &str;
fn signature(&self) -> Signature;
fn description(&self) -> &str;
fn extra_description(&self) -> &str { "" }
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData, // ← Structured data!
) -> Result<PipelineData, ShellError>;
fn examples(&self) -> Vec<Example<'_>> { vec![] }
fn search_terms(&self) -> Vec<&str> { vec![] }
fn is_const(&self) -> bool { false }
fn command_type(&self) -> CommandType { CommandType::Builtin }
fn get_dynamic_completion(...) -> Result<Option<Vec<DynamicSuggestion>>, ShellError> {
Ok(None)
}
}
Auto-Shell's Command Trait
pub trait Command {
fn name(&self) -> &str;
fn signature(&self) -> Signature;
fn run(
&self,
args: &ParsedArgs,
input: Option<&str>, // ← String input
shell: &mut Shell,
) -> Result<Option<String>>; // ← String output
}
Key Differences:
- Nushell:
PipelineDatain/out (structured tables, lists, records) - Auto-Shell:
Option<&str>in,Option<String>out (text) - Nushell: Has
EngineStateandStackfor variable scope - Auto-Shell: Has mutable
Shellreference for state - Nushell: Richer metadata (examples, search terms, command types)
- Auto-Shell: Simpler, more focused API
Signature System Comparison
Nushell's Signature
pub struct Signature {
pub name: String,
pub usage: String,
pub extra_usage: String,
pub required_positional: Vec<PositionalArg>,
pub optional_positional: Vec<PositionalArg>,
pub rest_positional: Option<RestPositionalArg>,
pub named: Vec<Flag>,
pub input_output_types: Vec<(Type, Type)>, // Type system!
pub allow_variants: bool,
pub category: Category,
}
pub struct Flag {
pub long: String,
pub short: Option<char>,
pub arg: Option<SyntaxShape>, // Typed flag arguments!
pub required: bool,
pub desc: String,
pub completion: Option<Completion>,
pub var_id: Option<VarId>,
pub default_value: Option<Value>,
}
pub struct PositionalArg {
pub name: String,
pub desc: String,
pub shape: SyntaxShape, // Type validation!
pub completion: Option<Completion>,
pub var_id: Option<VarId>,
pub default_value: Option<Value>,
}
Signature Builder Example:
Signature::build("ls")
.input_output_types(vec![(Type::Nothing, Type::table())])
.rest("pattern", SyntaxShape::GlobPattern, "The glob pattern to use.")
.switch("all", "Show hidden files", Some('a'))
.switch("long", "Get all available columns", Some('l'))
.switch("du", "Display directory size", Some('d'))
.category(Category::FileSystem)
Auto-Shell's Signature
pub struct Signature {
pub name: String,
pub description: String,
pub arguments: Vec<Argument>,
}
pub struct Argument {
pub name: String,
pub description: String,
pub required: bool,
pub is_flag: bool,
pub short: Option<char>,
}
Signature Builder Example:
Signature::new("ls", "List directory contents")
.optional("path", "Path to list")
.flag_with_short("all", 'a', "Show all files")
.flag_with_short("long", 'l', "Long format")
.flag_with_short("human-readable", 'h', "Human-readable sizes")
Key Differences:
- Nushell: Strongly typed with
SyntaxShape(int, string, glob, etc.) - Auto-Shell: Untyped strings only
- Nushell: Type checking at runtime, type coercion
- Auto-Shell: No validation beyond existence/presence
- Nushell: Input/output type declarations for pipeline compatibility
- Auto-Shell: No type information
- Nushell: Categories for organization (FileSystem, Strings, Math, etc.)
- Auto-Shell: No categorization
Data Flow Architecture
Nushell: Structured Data Pipeline
Input PipelineData (Value enum)
↓
Command (processes structured data)
↓
Output PipelineData (Value enum)
↓
Next Command (receives structured data)
Value enum variants:
- String
- Int
- Float
- Bool
- List(Vec<Value>)
- Record(HashMap<String, Value>)
- Table(Vec<Record>)
- Error
- Nothing
Example pipeline:
ls | where type == dir | get name | str upper
Data flow:
ls→ Table[{name, type, size, modified}]where type == dir→ Filtered table (only directories)get name→ List["src", "tests", "target"]str upper→ List["SRC", "TESTS", "TARGET"]
Auto-Shell: String Text Pipeline
Input Option<&str> (raw string)
↓
Command (parses/processes text)
↓
Output Option<String> (formatted string)
↓
Next Command (receives raw string)
String format:
- Plain text
- Tables rendered as strings
- No structured typing
Example pipeline:
ls | grep -E "^d" | awk '{print $NF}' | tr a-z A-Z
Data flow:
ls→ "drwxr-xr-x src/\ndrwxr-xr-x tests/\n-rw-r--r-- Cargo.toml"grep -E "^d"→ "drwxr-xr-x src/\ndrwxr-xr-x tests/"awk '{print $NF}'→ "src/\ntests/"tr a-z A-Z→ "SRC/\nTESTS/"
Type System Comparison
Nushell's SyntaxShape Types
pub enum SyntaxShape {
Any, // Any type
AnyClosing, // Any type (except closure)
Binary, // Binary data
Block, // Code block
Bool, // Boolean
CellPath, // Cell path (e.g., foo.0.bar)
Closure, // Closure/lambda
Compare, // Comparison operator (< > == !=)
DateTime, // Date/time
Duration, // Duration (e.g., 10sec, 5min)
Error, // Error value
Expression, // Expression
Filepath, // File path
Filesize, // File size (1kb, 10mb)
Filter, // Filter expression
Flag, // Flag (true/false)
Float, // Floating point number
Format, // Format pattern
GlobPattern, // Glob pattern (*.rs, **/*.txt)
Int, // Integer
List(Box<SyntaxShape>), // List of type
Math, // Math expression
Nothing, // Nothing value
Number, // Any number
Operator, // Operator
Range, // Range (0..10, 0..=10)
Regex, // Regular expression
Signature, // Command signature
String, // String
Variable, // Variable name
}
Type Validation Example:
fn run(&self, call: &Call, input: PipelineData) -> Result<PipelineData> {
// Engine validates arguments match SyntaxShape
let count: i64 = call.req(engine_state, stack, 0)?; // Must be Int
let pattern: String = call.req(engine_state, stack, 1)?; // Must be String
let all: bool = call.has_flag(engine_state, stack, "all")?; // Boolean flag
// If user passes wrong type, gets compile-time error:
// Error: Type mismatch during runtime
// × expected int, found string
}
Auto-Shell: No Types
pub struct ParsedArgs {
pub positionals: Vec<String>, // Everything is a string!
pub flags: HashMap<String, bool>,
pub named: HashMap<String, String>,
}
// Manual parsing required:
fn run(&self, args: &ParsedArgs, ...) -> Result<Option<String>> {
let count_str = args.positionals.get(0).ok_or("missing count")?;
let count: i64 = count_str.parse().map_err(|e| "invalid integer")?;
let pattern = args.positionals.get(1).ok_or("missing pattern")?;
let all = args.has_flag("all");
// Runtime parsing errors
}
Advanced Features Comparison
1. Completion System
Nushell:
pub enum Completion {
Command(DeclId),
List(NuCow<&'static [&'static str], Vec<String>>),
}
fn get_dynamic_completion(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: DynamicCompletionCallRef,
arg_type: &ArgType,
) -> Result<Option<Vec<DynamicSuggestion>>, ShellError> {
// Return contextual suggestions based on user input
}
Auto-Shell: No completion system
2. Examples System
Nushell:
pub struct Example<'a> {
pub example: &'a str,
pub description: &'a str,
pub result: Option<Value>, // Actual output value!
}
impl Command for Ls {
fn examples(&self) -> Vec<Example<'_>> {
vec![
Example {
description: "List visible files in the current directory",
example: "ls",
result: None, // Side effect, no result
},
Example {
description: "List Rust files",
example: "ls *.rs",
result: None,
},
]
}
}
Users can run help ls and see executable examples with actual output!
Auto-Shell: No example system
3. Command Categories
Nushell:
pub enum Category {
Async,
Bytes,
Chart,
Conversion,
Core,
Custom,
Date,
Debugger,
Default,
Deprecated,
Dev,
Env,
Experimental,
Experimental2,
External,
FileSystem,
Filters,
Format,
Generator,
Hash,
Into,
Math,
Network,
Path,
Platform,
Random,
Shell,
Strings,
System,
Viewers,
}
Commands are organized by category for help category discovery.
Auto-Shell: No categorization
4. Command Types
Nushell:
pub enum CommandType {
Builtin, // Compiled into shell
Custom, // User-defined (def command)
Keyword, // Language keywords (if, for, etc.)
External, // External executables
Alias, // Command aliases
Plugin, // Dynamically loaded plugins
}
Auto-Shell: Only built-in and external
5. Const Evaluation
Nushell:
fn is_const(&self) -> bool {
false // Can this command run at parse time?
}
fn run_const(
&self,
working_set: &StateWorkingSet,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// Run during parsing (e.g., for compile-time constants)
}
Allows certain commands to run at compile-time for optimization.
Auto-Shell: No const evaluation
Error Handling Comparison
Nushell's Rich Error Types
pub enum ShellError {
// Type errors
TypeMismatch { expected: String, found: String, span: Span },
CantConvert { to_type: String, from_type: String, span: Span },
// Runtime errors
IOError { msg: String },
NotFound { span: Span },
PermissionDenied { msg: String },
// Argument errors
MissingParameter { param_name: String, span: Span },
InvalidGlobPattern { pattern: String, msg: String },
// Command errors
CommandNotFound { name: String },
Deprecated { msg: String },
// ... 50+ more error variants
}
impl Diagnostic for ShellError {
fn code(&self) -> Option<ErrorKind> { ... }
fn labels(&self) -> Option<Vec<Label>> { ... }
fn help(&self) -> Option<String> { ... }
}
Error Output:
Error: Type mismatch
× expected int, found string
╭─[script.nu:5:10]
5 │ let x: int = "hello"
· ────┬───
· ╰── this is a string
╰────
help: try removing the type annotation
Auto-Shell's Simple Errors
pub type Result<T> = std::result::Result<T, miette::ErrReport>;
// Usage:
miette::bail!("ls: {}: No such file or directory", path.display());
Error Output:
Error:
× ls: /nonexistent: No such file or directory
Both use miette, but Nushell has structured error types with context.
Variable Scope and State
Nushell's EngineState and Stack
pub struct EngineState {
pub vars: Vec<VarId>, // All variables
pub decls: Vec<Declaration>, // All commands
pub blocks: Vec<Block>, // Code blocks
pub modules: Vec<Module>, // Modules
pub scope: Vec<ScopeFrame>, // Scope stack
pub env_vars: HashMap<String, Value>, // Environment variables
// ... lots more state
}
pub struct Stack {
pub env_vars: HashMap<String, Value>, // Stack-local env
pub vars: Vec<VarId>, // Stack-local vars
pub parent: Option<Box<Stack>>, // Parent stack
}
// Commands can access and modify scope:
fn run(&self, engine_state: &EngineState, stack: &mut Stack, ...) {
// Get variable
let x = stack.get_var(var_id, span)?;
// Set variable
stack.add_var(var_id, value);
// Get environment variable
let path = stack.get_env_var(engine_state, "PATH")?;
}
Benefits:
- Nested scopes (closures, blocks)
- Variable shadowing
- Environment isolation
- Thread-safe variable access
- Type-checked variable assignments
Auto-Shell's Shell State
pub struct Shell {
/// Current working directory
cwd: PathBuf,
/// Environment variables
env: HashMap<String, String>,
/// Command history
history: Vec<String>,
/// Exit code
exit_code: Option<i32>,
/// Command registry
registry: CommandRegistry,
}
// Commands access shell state:
fn run(&self, shell: &mut Shell, ...) {
let cwd = shell.pwd();
shell.set_env("KEY", "value");
}
Benefits:
- Simpler, easier to understand
- Mutable state via &mut Shell
- No variable scoping complexity
- Direct environment manipulation
Plugin System
Nushell Plugins
// Plugins are dynamically loaded shared libraries
pub trait Plugin {
fn signature(&self) -> Signature;
fn run(&self, name: &str, call: &Call, input: &Value) -> Result<Value, ShellError>;
}
// Commands can delegate to plugins:
fn plugin_identity(&self) -> Option<&PluginIdentity> {
Some(&self.identity)
}
Plugins extend Nushell without recompiling the shell.
Auto-Shell: No plugin system (all commands built-in)
Performance Considerations
Nushell Optimizations
- Lazy Evaluation:
PipelineDatastreams values, doesn't collect all - Parallel Processing: Rayon for parallel iteration
- Type Coercion Cache: Avoids repeated type checks
- Span Tracking: Zero-cost in release builds
- Copy-on-Write: Values use Arc for cheap cloning
Auto-Shell Characteristics
- Eager Evaluation: Collects all output before returning
- Single-Threaded: No parallelism
- String Allocations: Many intermediate strings
- Simple Parsing: Fast but untyped
Trade-off: Nushell has higher overhead per operation but enables powerful pipelines. Auto-Shell has lower overhead but limited composability.
Code Organization
Nushell Structure
crates/
├── nu-protocol/ # Core traits, types, engine
│ ├── engine/ # EngineState, Stack, Call
│ ├── signature.rs # Signature, Flag, PositionalArg
│ └── value.rs # Value enum, type system
├── nu-engine/ # Command execution, eval loop
├── nu-command/ # Built-in commands
│ ├── filesystem/ # ls, cd, mv, rm, etc.
│ ├── filters/ # where, select, sort, etc.
│ └── strings/ # str split, str replace, etc.
├── nu-cmd-lang/ # Language keywords (if, for, etc.)
└── nu-parser/ # Parser, lexer, AST
Auto-Shell Structure
auto-shell/src/
├── cmd.rs # Command trait, Signature
├── cmd/
│ ├── commands/ # Built-in commands (ls, cd, etc.)
│ ├── parser.rs # Argument parsing
│ ├── fs.rs # File system utilities
│ ├── builtin.rs # Built-in command routing
│ ├── external.rs # External command execution
│ └── registry.rs # Command registry
└── shell.rs # Shell state, REPL
Both follow similar separation of concerns, but Nushell has more granular module boundaries.
Philosophy Summary
Nushell Philosophy
"Treat everything as structured data"
- Commands output typed data (tables, lists, records)
- Pipeline preserves structure
- Type safety and validation
- Rich metadata for discovery
- Extensible via plugins
- SQL-like data manipulation
Best for:
- Data analysis and transformation
- Complex pipelines
- Type safety requirements
- Interactive data exploration
- Scripting with structured data
Auto-Shell Philosophy
"Traditional Unix shell, modernized"
- Commands output strings
- Text-based pipelines
- Simplicity over structure
- Familiar Unix patterns
- Easy to understand
- Low overhead
Best for:
- Traditional system administration
- Simple automation scripts
- Users familiar with bash/zsh
- Quick one-liners
- Resource-constrained environments
Recommendations for Auto-Shell
Keep What We Have
✅ String-based design - Simpler, more familiar to Unix users ✅ Simple signature API - Easy to learn and implement ✅ Shell state object - Clear state management ✅ Table rendering - Good compromise between text and structure
Consider Adding
🤔 Command examples - Help text with executable examples 🤔 Command categories - Better help organization (FileSystem, Network, etc.) 🤔 Completion system - Basic flag/path completion 🤔 Better error types - Structured errors with context (like ShellError)
Don't Need
❌ Full type system - Adds complexity without proportional benefit ❌ Value enum - Our string approach is simpler ❌ Plugin system - Nice to have, but not essential initially ❌ Complex scoping - Flat shell state is easier to understand
Conclusion
Nushell and Auto-Shell represent two different design philosophies:
-
Nushell: A data shell for structured data manipulation
- Rich type system
- Structured pipelines
- Advanced features (plugins, completions, examples)
- Steeper learning curve
- Powerful for data analysis
-
Auto-Shell: A modern traditional shell with simplicity focus
- Text-based pipelines
- Simple, familiar API
- Easy to learn and extend
- Lower overhead
- Better for system administration
Both approaches are valid for different use cases. Auto-Shell's simplicity is a strength, not a weakness - it makes the code accessible and maintainable while still providing useful features like table formatting, flag aliases, and recursive listing.