NeuroAgent

Key Differences Between sh and Bash Shell Programming

Learn the essential differences between sh and bash shells for better scripting decisions. Understand compatibility, features, and when to use each shell.

Question

What are the key differences between sh and Bash in shell programming?

When writing shell programs, we often use /bin/sh and /bin/bash. I typically use bash, but I’m unclear about the differences between these two shell interpreters.

What are the main differences between Bash and sh?
What should developers be aware of when programming in Bash versus sh?

NeuroAgent

The key differences between sh (Bourne shell) and bash (Bourne Again Shell) lie in their feature sets, compatibility, and intended use. sh is the original Unix shell with minimal functionality, while bash is a feature-rich superset that includes advanced programming capabilities, command-line editing, and job control. When programming, developers should be aware that sh scripts are more portable across Unix systems but lack modern conveniences, whereas bash offers powerful features like arrays, string manipulation, and advanced control structures that make complex scripting easier but may not work on systems without bash installed.

Contents

What is sh?

The sh shell, originally developed by Stephen Bourne at Bell Labs in 1977, represents the foundation of Unix shell programming. As the original Unix shell, sh provides a minimal feature set focused on basic command execution and simple scripting capabilities. When you execute scripts with /bin/sh, you’re typically running either the original Bourne shell or a compatible variant like the Almquist shell (ash) or Debian Almquist shell (dash).

The basic characteristics of sh include:

  • Simple syntax: Basic control structures like if, for, while, and case
  • Limited variable handling: No arrays, basic string operations only
  • Minimal built-in commands: Focus on external command execution
  • POSIX compliance: Adheres to the Portable Operating System Interface standard

The Bourne shell’s design philosophy emphasizes simplicity and reliability, making it ideal for system initialization scripts and environments where minimal resource usage is critical.

What is bash?

Bash, which stands for “Bourne Again Shell,” was created by Brian Fox for the GNU Project in 1989 as a free software replacement for the Bourne shell. As an enhanced version of sh, bash maintains backward compatibility while adding numerous powerful features that modern shell scripting demands.

Key characteristics of bash include:

  • Rich feature set: Arrays, associative arrays, advanced string manipulation
  • Interactive enhancements: Command-line editing, history expansion, tab completion
  • Programming extensions: Functions, arithmetic evaluation, signal handling
  • Job control: Background processes, process management
  • POSIX compliance: Maintains compatibility with sh while adding extensions

Bash has become the default shell on most Linux distributions and macOS systems, making it the de facto standard for shell scripting in modern Unix-like environments.

Key Feature Differences

Variable Handling

sh: Supports only scalar variables with basic assignment and expansion:

bash
name="John Doe"
echo $name

bash: Supports both scalar variables and arrays:

bash
name="John Doe"
ages=(25 30 35)
echo ${ages[1]}  # Output: 30

String Manipulation

sh: Limited to basic parameter expansion:

bash
filename="document.txt"
echo ${filename%.txt}  # Remove .txt suffix

bash: Advanced string operations with ${parameter//pattern/replacement}:

bash
text="hello world"
echo ${text//hello/hello}  # Replace hello with hello

Arithmetic Operations

sh: Requires external command expr:

bash
result=$(expr 5 + 3)

bash: Built-in arithmetic evaluation:

bash
result=$((5 + 3))

Functions and Scope

sh: Basic functions with limited scoping:

bash
func() {
    echo "Hello"
}

bash: Enhanced functions with local variables:

bash
func() {
    local name="John"
    echo "Hello, $name"
}

Control Structures

sh: Basic if statements and loops:

bash
if [ -f "file.txt" ]; then
    echo "File exists"
fi

for i in 1 2 3; do
    echo $i
done

bash: Extended control structures with [[ ]] and advanced loops:

bash
if [[ -f "file.txt" && -r "file.txt" ]]; then
    echo "File exists and is readable"
fi

for ((i=1; i<=3; i++)); do
    echo $i
done

Compatibility and Portability

Portability Considerations

When choosing between sh and bash, portability becomes a critical factor:

  • sh scripts: Run on virtually any Unix-like system, including embedded systems and minimal installations
  • bash scripts: Require bash to be installed, which is common on most systems but not universal

Shebang Line Choices

The choice of shebang line determines which shell interprets your script:

bash
#!/bin/sh    # Most portable, uses system's default sh
#!/bin/bash  # Explicitly uses bash
#!/usr/bin/env bash  # More portable, finds bash in PATH

Compatibility Mode

Bash can operate in POSIX-compatibility mode to behave more like sh:

bash
#!/bin/bash --posix

Compatibility Trade-offs Table:

Feature sh bash Portability Impact
Arrays High - bash scripts won’t run on systems without bash
Advanced strings Medium - basic scripts often work with sh
Arithmetic Limited Built-in Medium - expr works everywhere
Process substitution High - bash-specific feature
Associative arrays High - bash 4.0+ only

When to Use Each Shell

Choose sh When:

  • You need maximum portability across Unix systems
  • Writing system initialization scripts (e.g., /etc/init.d/)
  • Working with embedded systems or minimal Linux distributions
  • Maintaining legacy codebases that must run on older systems
  • Creating scripts for Docker containers where minimal image size matters

Choose bash When:

  • You need advanced features like arrays or complex string manipulation
  • Developing for modern Linux distributions or macOS
  • Creating interactive scripts or command-line tools
  • Working with system administration tasks on standard desktop/server systems
  • Leveraging bash-specific features like process substitution or coprocesses

Hybrid Approach

Many developers use a hybrid strategy:

bash
#!/bin/bash

# Bash-specific features here
declare -A config
config[host]="example.com"

# Fallback to POSIX-compatible features
if [ "$BASH_VERSION" ]; then
    echo "Running bash with version $BASH_VERSION"
else
    echo "Running compatible shell"
fi

Practical Programming Examples

File Processing Example

sh version (limited):

bash
#!/bin/sh

# Process files in current directory
for file in *; do
    if [ -f "$file" ]; then
        echo "Processing: $file"
        # Basic operations only
    fi
done

bash version (enhanced):

bash
#!/bin/bash

# Process files with advanced features
for file in *; do
    if [[ -f "$file" && "$file" == *.txt ]]; then
        echo "Processing text file: $file"
        # Advanced string manipulation
        filename="${file%.*}"
        echo "Base name: $filename"
        
        # Array operations
        files_processed+=("$file")
    fi
done

echo "Total files processed: ${#files_processed[@]}"

Configuration Management

sh version:

bash
#!/bin/sh

# Simple key-value parsing
parse_config() {
    while IFS='=' read -r key value; do
        case "$key" in
            \#*) continue ;;
            *) eval "$key=\"$value\"" ;;
        esac
    done < "$1"
}

bash version:

bash
#!/bin/bash

# Advanced configuration with arrays and validation
declare -A config

parse_config() {
    local line_num=0
    while IFS='=' read -r key value; do
        ((line_num++))
        
        # Skip comments and empty lines
        [[ "$key" =~ ^[[:space:]]*# ]] && continue
        [[ -z "$key" ]] && continue
        
        # Validate key format
        if [[ ! "$key" =~ ^[a-zA-Z_][a-zA-Z0-9_]*$ ]]; then
            echo "Warning: Invalid key '$key' at line $line_num" >&2
            continue
        fi
        
        # Store in associative array
        config["$key"]="$value"
    done < "$1"
}

Best Practices for Shell Scripting

Portability Testing

Always test your scripts with different shells:

bash
#!/bin/bash

# Test script compatibility
test_with_shell() {
    local shell="$1"
    echo "Testing with $shell..."
    
    # Create temporary script
    cat > /tmp/test_script.sh << 'EOF'
#!/bin/sh
echo "Hello from $0"
EOF
    
    chmod +x /tmp/test_script.sh
    $shell /tmp/test_script.sh
    rm /tmp/test_script.sh
}

test_with_shell "sh"
test_with_shell "bash"

Conditional Feature Detection

Use feature detection rather than shell guessing:

bash
#!/bin/bash

# Check for bash features
if [ "${BASH_VERSION}" ]; then
    echo "Bash detected, using advanced features"
    
    # Test for specific features
    if declare -A test_array 2>/dev/null; then
        echo "Associative arrays available"
    fi
else
    echo "Running in compatibility mode"
    # Use POSIX-compatible alternatives
fi

Error Handling

Implement robust error handling:

bash
#!/bin/bash

set -euo pipefail  # Exit on error, undefined vars, pipe failures

error_handler() {
    local exit_code=$?
    echo "Error on line $1: Exit code $exit_code" >&2
    exit $exit_code
}

trap 'error_handler $LINENO' ERR

# Your script code here

Documentation and Comments

Document shell-specific features:

bash
#!/bin/bash

# This script uses bash-specific features:
# - Associative arrays (declare -A)
# - Advanced string manipulation (${var//pattern/replacement})
# - Arithmetic evaluation ((expression))

# For POSIX compatibility, use:
# #!/bin/sh
# # Replace bash-specific features with alternatives

Conclusion

The differences between sh and bash represent a fundamental choice between portability and power in shell programming. sh provides broad compatibility across Unix systems but limits your scripting capabilities, while bash offers powerful features that can significantly enhance your productivity but may not work on all systems.

When developing shell scripts, consider these key takeaways:

  1. Portability vs. Features: Choose sh for maximum compatibility, bash for advanced capabilities
  2. Shebang Matters: Use #!/bin/sh for portability, #!/bin/bash for bash-specific features
  3. Test Compatibility: Always test scripts with different shells when portability is critical
  4. Use Conditional Logic: Implement feature detection rather than shell version checking
  5. Document Dependencies: Clearly indicate which shell-specific features your scripts use

For developers starting new projects, bash is generally recommended for most use cases due to its rich feature set and widespread availability. However, for system initialization scripts, Docker containers, or deployment automation where maximum portability is essential, sh remains the safer choice.

The shell programming landscape continues to evolve, with alternatives like zsh and fish gaining popularity, but understanding the fundamental differences between sh and bash remains essential for any Unix developer working with command-line tools and automation.

Sources

  1. Bash Reference Manual - GNU Project
  2. POSIX Shell and Utilities Standard
  3. Advanced Bash-Scripting Guide
  4. Stack Overflow - Difference between sh and bash
  5. IBM Developer - Shell scripting best practices