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?

FeatureShellCheckRash
ModeRead-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

  1. Getting Started: Install Rash and run your first purification
  2. Core Concepts: Learn about determinism, idempotency, and POSIX compliance
  3. Linting: Explore security, determinism, and idempotency rules
  4. Configuration Management: Purify your shell config files
  5. Examples: See real-world use cases
  6. Advanced Topics: Deep dive into AST transformation and testing
  7. 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.

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:

Quick Reference

CommandPurpose
bashrs lint <file>Lint shell script
bashrs config analyze <file>Analyze config file
bashrs config purify <file>Purify config file
bashrs --helpShow 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

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(&quoted_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:

  1. Comments: Variables in comments are ignored
  2. Strings: Variables already in double quotes
  3. Arithmetic: Variables in $((...)) or (( ))
  4. Arrays: Variables used as array indices

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

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

Makefile Overview

Makefile Security

Makefile Best Practices

Bootstrap Installer

Deployment Script

Configuration Management

CI/CD Pipeline

AST-Level Transformation

Property Testing

Mutation Testing

Performance Optimization

CLI Commands

Configuration

Exit Codes

Linter Rules Reference

Development Setup

EXTREME TDD

Toyota Way Principles

Release Process