Introduction
Welcome to The Rash Book! This guide will teach you how to write safe, deterministic, and idempotent shell scripts using Rash.
What is Rash?
Rash (bashrs) is a shell safety and purification tool that goes beyond traditional linters like ShellCheck. While ShellCheck detects problems, Rash transforms your code to fix them automatically.
Key Features
- Shell Purification: Automatically transform bash scripts to be deterministic and idempotent
- Security Linting: Detect and fix 8 critical security vulnerabilities (SEC001-SEC008)
- Configuration Management: Analyze and purify shell config files like .bashrc and .zshrc
- Makefile Linting: Security and best-practice linting for Makefiles
- POSIX Compliance: All generated shell code passes
shellcheck -s sh
How is Rash Different from ShellCheck?
Feature | ShellCheck | Rash |
---|---|---|
Mode | Read-only (detect) | Read-write (transform) |
Non-determinism | ⚠️ Warns about $RANDOM | ✅ Rewrites to deterministic IDs |
Idempotency | ⚠️ Warns about mkdir | ✅ Transforms to mkdir -p |
Variable Quoting | ⚠️ Suggests quoting | ✅ Automatically adds quotes |
PATH Duplicates | ❌ Not detected | ✅ Detects and removes |
Config Files | ❌ No support | ✅ Analyzes .bashrc, .zshrc |
Makefiles | ❌ No support | ✅ Full linting support |
Example: Before and After
Before (messy, non-deterministic):
!/bin/bash
SESSION_ID=$RANDOM
mkdir /tmp/deploy-$SESSION_ID
cd /tmp/deploy-$SESSION_ID
After (purified, deterministic):
!/bin/sh
Purified by Rash v6.0.0
SESSION_ID="${VERSION:-1.0.0}"
mkdir -p "/tmp/deploy-${SESSION_ID}"
cd "/tmp/deploy-${SESSION_ID}" || exit 1
Design Philosophy: Toyota Way
Rash follows Toyota Way principles:
- 自働化 (Jidoka): Build quality in from the start
- 現地現物 (Genchi Genbutsu): Test with real shells (dash, ash, busybox)
- 反省 (Hansei): Fix bugs before adding features
- 改善 (Kaizen): Continuous improvement through feedback
Who Should Read This Book?
- DevOps engineers who write shell scripts
- Developers maintaining .bashrc/.zshrc files
- System administrators automating tasks
- Anyone who wants safer, more reliable shell scripts
How to Use This Book
- Getting Started: Install Rash and run your first purification
- Core Concepts: Learn about determinism, idempotency, and POSIX compliance
- Linting: Explore security, determinism, and idempotency rules
- Configuration Management: Purify your shell config files
- Examples: See real-world use cases
- Advanced Topics: Deep dive into AST transformation and testing
- Reference: Quick lookup for commands and rules
Prerequisites
- Basic shell scripting knowledge
- Familiarity with command-line tools
- (Optional) Understanding of AST concepts for advanced topics
Conventions
Throughout this book, we use the following conventions:
Shell commands you can run
bashrs --version
// Rust code examples (for advanced topics) fn main() { println!("Hello from Rash!"); }
Note: Important information or tips
⚠️ Warning: Critical information to avoid common mistakes
✅ Best Practice: Recommended approaches
Let's Get Started!
Ready to write safer shell scripts? Let's dive into Installation!
Installation
There are several ways to install Rash depending on your platform and preferences.
Using cargo (Recommended)
The easiest way to install Rash is from crates.io:
cargo install bashrs
This will install both the bashrs
and rash
commands (they are aliases).
Verify Installation
bashrs --version
You should see output like:
bashrs 6.0.0
From Source
If you want the latest development version:
git clone https://github.com/paiml/bashrs.git
cd bashrs
cargo build --release
sudo cp target/release/bashrs /usr/local/bin/
Platform-Specific Installation
macOS
Using Homebrew (coming soon):
brew install bashrs # Not yet available
For now, use cargo install bashrs
.
Linux
Debian/Ubuntu (coming soon)
wget https://github.com/paiml/bashrs/releases/download/v6.0.0/bashrs_6.0.0_amd64.deb
sudo dpkg -i bashrs_6.0.0_amd64.deb
Arch Linux (coming soon)
yay -S bashrs
For now, use cargo install bashrs
.
Windows
Using WSL (Windows Subsystem for Linux):
Inside WSL
cargo install bashrs
Requirements
- Rust 1.70+ (if building from source)
- Linux, macOS, or WSL on Windows
Optional Dependencies
For full functionality, consider installing:
- shellcheck: For POSIX compliance verification
macOS brew install shellcheck Ubuntu/Debian sudo apt-get install shellcheck Arch sudo pacman -S shellcheck
Next Steps
Now that you have Rash installed, let's explore the Quick Start guide!
Quick Start
This chapter will get you up and running with Rash in 5 minutes.
Your First Command
Let's start by checking the version:
bashrs --version
Linting a Shell Script
Create a simple shell script with a security issue:
cat > vulnerable.sh << 'EOF'
!/bin/bash
Vulnerable script - uses eval with user input
read -p "Enter command: " cmd
eval "$cmd"
EOF
Now lint it with Rash:
bashrs lint vulnerable.sh
You'll see output like:
[SEC-001] Use of eval with user input can lead to command injection
→ Line 4: eval "$cmd"
→ Severity: Critical
→ Suggestion: Avoid eval or validate input strictly
Analyzing a Configuration File
Let's analyze a messy .bashrc file:
cat > messy-bashrc << 'EOF'
Messy .bashrc with issues
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="/usr/local/bin:$PATH" # Duplicate!
Unquoted variables (security issue)
cd $HOME/my projects
EOF
Analyze it:
bashrs config analyze messy-bashrc
Output:
Analysis: messy-bashrc
Type: Generic config file
Lines: 7
Complexity: 3/10
Issues Found: 2
[CONFIG-001] Duplicate PATH entry at line 3
→ Path: /usr/local/bin
→ First seen: line 1
→ Suggestion: Remove duplicate entry
[CONFIG-002] Unquoted variable expansion at line 6
→ Variable: $HOME
→ Can cause word splitting and glob expansion
→ Suggestion: Quote the variable: "${HOME}"
Purifying Configuration
Now let's fix these issues automatically:
bashrs config purify messy-bashrc --output clean-bashrc
The purified output (clean-bashrc
):
Messy .bashrc with issues
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
Duplicate! (removed by CONFIG-001)
Unquoted variables (fixed by CONFIG-002)
cd "${HOME}/my projects"
Testing with mdbook
The examples in this book are automatically tested! Here's a Rust test example:
#![allow(unused)] fn main() { // This code is tested when building the book fn purify_example() { let input = "export DIR=$HOME/projects"; let expected = "export DIR=\"${HOME}/projects\""; // This would use the actual Rash library // assert_eq!(rash::config::quote_variables(input), expected); } #[test] fn test_purify_example() { purify_example(); } }
Common Workflows
1. Lint Before Commit
Lint all shell scripts in project
find . -name "*.sh" -exec bashrs lint {} \;
2. CI/CD Integration
# .github/workflows/shellcheck.yml
name: Shell Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install bashrs
run: cargo install bashrs
- name: Lint scripts
run: bashrs lint scripts/*.sh
3. Purify Your dotfiles
Analyze
bashrs config analyze ~/.bashrc
Purify (dry-run by default)
bashrs config purify ~/.bashrc --output ~/.bashrc.purified
Review changes
diff ~/.bashrc ~/.bashrc.purified
Apply changes (creates backup automatically)
bashrs config purify ~/.bashrc --fix
What's Next?
Now that you've seen the basics, let's explore:
- Your First Purification: Step-by-step purification workflow
- Core Concepts: Understand determinism and idempotency
- Security Rules: Deep dive into security linting
Quick Reference
Command | Purpose |
---|---|
bashrs lint <file> | Lint shell script |
bashrs config analyze <file> | Analyze config file |
bashrs config purify <file> | Purify config file |
bashrs --help | Show all commands |
Troubleshooting
"bashrs: command not found"
Ensure ~/.cargo/bin
is in your PATH:
export PATH="$HOME/.cargo/bin:$PATH"
Permission Denied
If you see permission errors:
chmod +x vulnerable.sh
Summary
In this chapter, you learned to:
- ✅ Lint shell scripts for security issues
- ✅ Analyze configuration files
- ✅ Purify config files automatically
- ✅ Integrate Rash into your workflow
Ready for a deeper dive? Continue to Your First Purification!
Your First Purification
What is Purification?
Determinism
Idempotency
POSIX Compliance
Security Rules (SEC001-SEC008)
Determinism Rules (DET001-DET003)
Idempotency Rules (IDEM001-IDEM003)
Writing Custom Rules
Overview
Analyzing Config Files
Purifying .bashrc and .zshrc
CONFIG-001: Deduplicate PATH Entries
Category: Configuration Severity: Warning Since: v6.0.0 Fixable: Yes (automatic)
Problem
Duplicate entries in PATH
cause:
- Slower command lookup
- Confusion about which binary will run
- Maintenance burden
Example Problem
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="/usr/local/bin:$PATH" # Duplicate!
The third line adds /usr/local/bin
again, which was already added in the first line.
Detection
Rash tracks all PATH modifications and detects when the same directory is added multiple times:
bashrs config analyze messy.bashrc
Output:
[CONFIG-001] Duplicate PATH entry
→ Line: 3
→ Path: /usr/local/bin
→ First occurrence: Line 1
→ Suggestion: Remove duplicate entry or use conditional addition
Automatic Fix
Rash can automatically remove duplicates while preserving the first occurrence:
bashrs config purify messy.bashrc --output clean.bashrc
Before:
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
export PATH="/usr/local/bin:$PATH"
After:
export PATH="/usr/local/bin:$PATH"
export PATH="/opt/homebrew/bin:$PATH"
Duplicate removed by CONFIG-001
Implementation Details
The deduplication algorithm:
#![allow(unused)] fn main() { use std::collections::HashSet; /// Deduplicate PATH entries, preserving first occurrence pub fn deduplicate_path_entries(source: &str) -> String { let entries = analyze_path_entries(source); let mut seen_paths = HashSet::new(); let mut result = Vec::new(); for (line_num, line) in source.lines().enumerate() { let line_num = line_num + 1; // Check if this line adds a PATH entry we've seen let mut skip_line = false; for entry in &entries { if entry.line == line_num && seen_paths.contains(&entry.path) { // Duplicate found - skip this line skip_line = true; break; } } if skip_line { continue; } // Track new paths for entry in &entries { if entry.line == line_num { seen_paths.insert(&entry.path); } } result.push(line.to_string()); } result.join("\n") } // Helper function (part of actual implementation) fn analyze_path_entries(source: &str) -> Vec<PathEntry> { // Implementation details... vec![] } struct PathEntry { line: usize, path: String, } }
Testing
The CONFIG-001 rule has comprehensive tests:
#![allow(unused)] fn main() { #[test] fn test_config_001_detect_duplicate_paths() { // ARRANGE let source = r#"export PATH="/usr/local/bin:$PATH" export PATH="/opt/homebrew/bin:$PATH" export PATH="/usr/local/bin:$PATH""#; // ACT let entries = analyze_path_entries(source); let issues = detect_duplicate_paths(&entries); // ASSERT assert_eq!(issues.len(), 1); assert_eq!(issues[0].rule_id, "CONFIG-001"); assert_eq!(issues[0].line, 3); } #[test] fn test_config_001_deduplicate_preserves_first() { // ARRANGE let source = r#"export PATH="/usr/local/bin:$PATH" export PATH="/opt/homebrew/bin:$PATH" export PATH="/usr/local/bin:$PATH""#; // ACT let result = deduplicate_path_entries(source); // ASSERT assert_eq!(result.lines().count(), 2); assert!(result.contains("/usr/local/bin")); assert!(result.contains("/opt/homebrew/bin")); // Should only appear once assert_eq!(result.matches("/usr/local/bin").count(), 1); } }
Real-World Example
Common scenario in .bashrc files:
System default
export PATH="/usr/local/bin:$PATH"
Homebrew
if [ -d "/opt/homebrew/bin" ]; then
export PATH="/opt/homebrew/bin:$PATH"
fi
Custom tools
export PATH="/usr/local/bin:$PATH" # Oops, duplicate!
Python tools
export PATH="$HOME/.local/bin:$PATH"
After purification:
System default
export PATH="/usr/local/bin:$PATH"
Homebrew
if [ -d "/opt/homebrew/bin" ]; then
export PATH="/opt/homebrew/bin:$PATH"
fi
Custom tools - removed duplicate
Python tools
export PATH="$HOME/.local/bin:$PATH"
Configuration
You can configure CONFIG-001 behavior:
Dry-run (default) - show what would change
bashrs config purify ~/.bashrc --dry-run
Apply fixes with backup
bashrs config purify ~/.bashrc --fix
Skip backup (dangerous!)
bashrs config purify ~/.bashrc --fix --no-backup
Related Rules
- CONFIG-002: Quote variable expansions
- CONFIG-004: Remove non-deterministic constructs
FAQ
Q: Why preserve the first occurrence, not the last?
A: The first occurrence is usually the intended primary PATH. Later duplicates are often accidental.
Q: What about conditional PATH additions?
A: Rash preserves conditional logic. Duplicates are only removed if unconditional.
Q: Can I disable this rule?
A: Currently, rules cannot be disabled individually. This feature is planned for v7.1.
See Also
CONFIG-002: Quote Variable Expansions
Category: Security / Reliability Severity: Warning Since: v6.0.0 Fixable: Yes (automatic)
Problem
Unquoted variable expansions can lead to:
- Word splitting: Spaces in values break arguments
- Glob expansion: Wildcards in values expand unexpectedly
- Security vulnerabilities: Injection attacks through unquoted paths
Example Problem
Unquoted variable
export PROJECT_DIR=$HOME/my projects
Causes issues when used
cd $PROJECT_DIR # Fails! Splits into: cd /home/user/my projects
The space in "my projects" causes the shell to interpret this as two arguments.
Detection
Rash analyzes variable usage and detects unquoted expansions:
bashrs config analyze messy.bashrc
Output:
[CONFIG-002] Unquoted variable expansion
→ Line: 1
→ Variable: $HOME
→ Column: 18
→ Can cause word splitting and glob expansion
→ Suggestion: Quote the variable: "${HOME}"
Automatic Fix
Rash automatically adds quotes and converts to brace syntax:
bashrs config purify messy.bashrc --output clean.bashrc
Before:
export PROJECT_DIR=$HOME/my projects
cd $PROJECT_DIR
cp $SOURCE $DEST
After:
export PROJECT_DIR="${HOME}/my projects"
cd "${PROJECT_DIR}"
cp "${SOURCE}" "${DEST}"
Why Quotes Matter
Word Splitting Example
Without quotes
FILE=$HOME/my document.txt
cat $FILE
Error: cat: /home/user/my: No such file or directory
cat: document.txt: No such file or directory
With quotes (correct)
FILE="${HOME}/my document.txt"
cat "${FILE}"
Success!
Glob Expansion Example
Without quotes
PATTERN="*.txt"
echo $PATTERN
Expands to: file1.txt file2.txt file3.txt
With quotes (literal)
PATTERN="*.txt"
echo "${PATTERN}"
Outputs: *.txt
Security Example
Vulnerable
USER_INPUT="file.txt; rm -rf /"
cat $USER_INPUT # DANGER! Executes: cat file.txt; rm -rf /
Safe
USER_INPUT="file.txt; rm -rf /"
cat "${USER_INPUT}" # Safe: cat 'file.txt; rm -rf /'
Implementation
The quoting algorithm:
#![allow(unused)] fn main() { use std::collections::HashMap; /// Quote all unquoted variables in source pub fn quote_variables(source: &str) -> String { let variables = analyze_unquoted_variables(source); if variables.is_empty() { return source.to_string(); } let mut lines_to_fix = HashMap::new(); for var in &variables { lines_to_fix.entry(var.line).or_insert_with(Vec::new).push(var); } let mut result = Vec::new(); for (line_num, line) in source.lines().enumerate() { let line_num = line_num + 1; if lines_to_fix.contains_key(&line_num) { if line.contains('=') { // Assignment: quote RHS result.push(quote_assignment_line(line)); } else { // Command: quote individual variables result.push(quote_command_line(line)); } } else { result.push(line.to_string()); } } result.join("\n") } // Helper functions (part of actual implementation) fn analyze_unquoted_variables(source: &str) -> Vec<UnquotedVariable> { vec![] } fn quote_assignment_line(line: &str) -> String { line.to_string() } fn quote_command_line(line: &str) -> String { line.to_string() } struct UnquotedVariable { line: usize } }
Special Contexts
CONFIG-002 is smart about when NOT to quote:
1. Already Quoted
Already safe - no change
export DIR="${HOME}/projects"
echo "Hello $USER"
2. Arithmetic Context
Arithmetic - no quotes needed
result=$((x + y))
((counter++))
3. Array Indices
Array index - no quotes needed
element="${array[$i]}"
4. Export Without Assignment
Just exporting, not assigning - no change
export PATH
Testing
Comprehensive test coverage for CONFIG-002:
#![allow(unused)] fn main() { #[test] fn test_config_002_quote_simple_variable() { // ARRANGE let source = "export DIR=$HOME/projects"; // ACT let result = quote_variables(source); // ASSERT assert_eq!(result, r#"export DIR="${HOME}/projects""#); } #[test] fn test_config_002_preserve_already_quoted() { // ARRANGE let source = r#"export DIR="${HOME}/projects""#; // ACT let result = quote_variables(source); // ASSERT assert_eq!(result, source, "Should not change already quoted"); } #[test] fn test_config_002_idempotent() { // ARRANGE let source = "export DIR=$HOME/projects"; // ACT let quoted_once = quote_variables(source); let quoted_twice = quote_variables("ed_once); // ASSERT assert_eq!(quoted_once, quoted_twice, "Quoting should be idempotent"); } }
Real-World Example
Common ~/.bashrc scenario:
Before purification
export PROJECT_DIR=$HOME/my projects
export BACKUP_DIR=$HOME/backups
Aliases with unquoted variables
alias proj='cd $PROJECT_DIR'
alias backup='cp $PROJECT_DIR/file.txt $BACKUP_DIR/'
Functions
deploy() {
cd $PROJECT_DIR
./build.sh
cp result.tar.gz $BACKUP_DIR
}
After purification:
After purification
export PROJECT_DIR="${HOME}/my projects"
export BACKUP_DIR="${HOME}/backups"
Aliases with quoted variables
alias proj='cd "${PROJECT_DIR}"'
alias backup='cp "${PROJECT_DIR}/file.txt" "${BACKUP_DIR}/"'
Functions
deploy() {
cd "${PROJECT_DIR}" || return 1
./build.sh
cp result.tar.gz "${BACKUP_DIR}"
}
Configuration
Control CONFIG-002 behavior:
Dry-run to preview changes
bashrs config purify ~/.bashrc --dry-run
Apply with backup (default: ~/.bashrc.backup.TIMESTAMP)
bashrs config purify ~/.bashrc --fix
JSON output for tooling
bashrs config analyze ~/.bashrc --format json
Exceptions
CONFIG-002 intelligently skips:
- Comments: Variables in comments are ignored
- Strings: Variables already in double quotes
- Arithmetic: Variables in
$((...))
or(( ))
- Arrays: Variables used as array indices
Related Rules
- CONFIG-001: PATH deduplication
- CONFIG-007: Validate source paths (security)
- SEC-003: Command injection prevention
Performance
CONFIG-002 is highly optimized:
- Regex-based: O(n) scanning with compiled regex
- Incremental: Only processes lines with variables
- Idempotent: Safe to run multiple times
- Fast: ~1ms for typical .bashrc files
FAQ
Q: Why convert $VAR to ${VAR}?
A: Brace syntax is more explicit and prevents issues like $VARname
ambiguity.
Q: What about single quotes?
A: Variables in single quotes don't expand. CONFIG-002 focuses on double-quote contexts.
Q: Can this break my scripts?
A: Very rarely. Quoting variables is almost always safer. Test with --dry-run
first.
Q: What about $0, $1, $2, etc.?
A: Positional parameters are quoted too: "${1}"
, "${2}"
, etc.
See Also
CONFIG-003: Consolidate Duplicate Aliases
Category: Configuration / Maintainability Severity: Warning Since: v6.1.0 Fixable: Yes (automatic)
Problem
Shell configuration files often accumulate duplicate alias definitions over time as users experiment with different settings. When the same alias is defined multiple times:
- Confusing behavior: Only the last definition takes effect
- Maintenance burden: Harder to track which aliases are active
- Cluttered configs: Unnecessary duplication
- Debugging difficulty: Hard to find which alias definition is "winning"
Example Problem
Early in .bashrc
alias ls='ls --color=auto'
alias ll='ls -la'
... 100 lines later ...
Forgot about the first one!
alias ls='ls -G' # This one wins
alias ll='ls -alh' # This one wins
The second definitions override the first ones, but both remain in the file causing confusion.
Detection
Rash analyzes all alias definitions and detects when the same alias name appears multiple times:
bashrs config analyze messy.bashrc
Output:
[CONFIG-003] Duplicate alias definition: 'ls'
→ Line: 21
→ First occurrence: Line 17
→ Severity: Warning
→ Suggestion: Remove earlier definition or rename alias. Last definition wins in shell.
Automatic Fix
Rash can automatically consolidate duplicates, keeping only the last definition (matching shell behavior):
bashrs config purify messy.bashrc --output clean.bashrc
Before:
alias ll='ls -la'
alias ls='ls --color=auto'
alias ll='ls -alh' # Duplicate
alias grep='grep --color=auto'
alias ll='ls -lAh' # Duplicate
After:
alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias ll='ls -lAh' # Only the last definition kept
Why Last Definition Wins
CONFIG-003 follows shell behavior where later alias definitions override earlier ones:
In shell
$ alias ls='ls --color=auto'
$ alias ls='ls -G'
$ alias ls
alias ls='ls -G' # Only the last one is active
This matches how shells process config files line-by-line.
Implementation
The consolidation algorithm:
use std::collections::HashMap;
use regex::Regex;
/// Consolidate duplicate aliases, keeping only the last definition
pub fn consolidate_aliases(source: &str) -> String {
let aliases = analyze_aliases(source);
if aliases.is_empty() {
return source.to_string();
}
// Build map of alias names to their last definition line
let mut last_definition: HashMap<String, usize> = HashMap::new();
for alias in &aliases {
last_definition.insert(alias.name.clone(), alias.line);
}
// Build set of lines to skip (duplicates)
let mut lines_to_skip = Vec::new();
for alias in &aliases {
if let Some(&last_line) = last_definition.get(&alias.name) {
if alias.line != last_line {
// This is not the last definition - skip it
lines_to_skip.push(alias.line);
}
}
}
// Reconstruct source, skipping duplicate lines
let mut result = Vec::new();
for (line_num, line) in source.lines().enumerate() {
let line_num = line_num + 1;
if lines_to_skip.contains(&line_num) {
continue; // Skip this duplicate
}
result.push(line.to_string());
}
result.join("\n")
}
// Helper types
struct AliasDefinition {
line: usize,
name: String,
value: String,
}
fn analyze_aliases(source: &str) -> Vec<AliasDefinition> {
// Implementation details...
vec![]
}
Testing
CONFIG-003 has comprehensive tests:
#[test]
fn test_config_003_consolidate_simple() {
// ARRANGE
let source = r#"alias ls='ls --color=auto'
alias ls='ls -G'"#;
// ACT
let result = consolidate_aliases(source);
// ASSERT
assert_eq!(result, "alias ls='ls -G'");
}
#[test]
fn test_config_003_consolidate_multiple() {
// ARRANGE
let source = r#"alias ll='ls -la'
alias ls='ls --color=auto'
alias ll='ls -alh'
alias grep='grep --color=auto'
alias ll='ls -lAh'"#;
let expected = r#"alias ls='ls --color=auto'
alias grep='grep --color=auto'
alias ll='ls -lAh'"#;
// ACT
let result = consolidate_aliases(source);
// ASSERT
assert_eq!(result, expected);
}
#[test]
fn test_config_003_idempotent() {
// ARRANGE
let source = r#"alias ls='ls --color=auto'
alias ls='ls -G'"#;
// ACT
let consolidated_once = consolidate_aliases(source);
let consolidated_twice = consolidate_aliases(&consolidated_once);
// ASSERT
assert_eq!(
consolidated_once, consolidated_twice,
"Consolidation should be idempotent"
);
}
Real-World Example
Common scenario in a 5-year-old .bashrc:
Original setup (2019)
alias ll='ls -la'
alias ls='ls --color=auto'
alias grep='grep --color=auto'
Tried new options (2020)
alias ll='ls -lah'
macOS-specific (2021)
alias ls='ls -G'
Final preference (2023)
alias ll='ls -lAh'
After purification:
Consolidated aliases
alias ls='ls -G'
alias grep='grep --color=auto'
alias ll='ls -lAh'
Three duplicate definitions reduced to three clean aliases.
CLI Usage
Analyze for duplicates
bashrs config analyze ~/.bashrc
Lint and exit with error code if found
bashrs config lint ~/.bashrc
Preview what would be fixed
bashrs config purify ~/.bashrc --dry-run
Apply fixes with backup (default: ~/.bashrc.backup.TIMESTAMP)
bashrs config purify ~/.bashrc --fix
Apply without backup (dangerous!)
bashrs config purify ~/.bashrc --fix --no-backup
Output to different file
bashrs config purify ~/.bashrc --output ~/.bashrc.clean
Configuration
You can control CONFIG-003 behavior through CLI flags:
Dry-run to preview changes (default)
bashrs config purify ~/.bashrc --dry-run
Apply with backup
bashrs config purify ~/.bashrc --fix
Skip backup (not recommended)
bashrs config purify ~/.bashrc --fix --no-backup
JSON output for tooling
bashrs config analyze ~/.bashrc --format json
Edge Cases
Comments Between Duplicates
Before
alias ls='ls --color=auto'
This is my preferred ls
alias ls='ls -G'
After
This is my preferred ls
alias ls='ls -G'
Comments and blank lines are preserved.
Mixed Quote Styles
Before
alias ls='ls --color=auto' # Single quotes
alias ls="ls -G" # Double quotes
After
alias ls="ls -G" # Both styles supported
CONFIG-003 handles both single and double quotes.
No Duplicates
If no duplicates exist, the file is unchanged:
alias ll='ls -la'
alias ls='ls --color=auto'
alias grep='grep --color=auto'
No changes needed
Related Rules
- CONFIG-001: PATH deduplication
- CONFIG-002: Quote variable expansions
- CONFIG-004: Remove non-deterministic constructs (coming soon)
Performance
CONFIG-003 is highly optimized:
- Regex-based: O(n) scanning with compiled regex
- Single pass: Analyzes and consolidates in one pass
- Idempotent: Safe to run multiple times
- Fast: ~1ms for typical .bashrc files
FAQ
Q: Why keep the last definition instead of the first?
A: The last definition is what's actually active in your shell. Keeping it matches real behavior and is what users typically intend (later overrides earlier).
Q: What if I have conditional aliases?
A: CONFIG-003 only consolidates identical alias names. Conditional aliases are preserved:
if [ "$OS" = "Darwin" ]; then
alias ls='ls -G'
else
alias ls='ls --color=auto'
fi
Both kept - they're conditional
Q: Can I disable this rule?
A: Currently, rules cannot be disabled individually. This feature is planned for v7.1.
Q: What about functions with the same name as aliases?
A: CONFIG-003 only analyzes alias
definitions. Functions are handled separately.
Best Practices
✅ DO:
- Run
bashrs config analyze
before manual edits - Use
--dry-run
to preview changes first - Keep backups (default behavior)
- Consolidate regularly during config maintenance
❌ DON'T:
- Skip the dry-run step
- Disable backups unless you're certain
- Edit config while shell is sourcing it
- Ignore CONFIG-003 warnings (they indicate confusion)
See Also
- Shell Configuration Overview
- Analyzing Config Files
- Purification Workflow
- CONFIG-001: PATH Deduplication
- CONFIG-002: Quote Variables