Skip to content

Bashisms

As a reminder, this style guide is specific to Bash. When given a choice, ALWAYS prefer Bash built-ins or keywords over external commands or sh(1) syntax.

This section covers common Bashisms, which are idiomatic practices and features specific to the Bash shell (1). By understanding and using these Bash-specific constructs, you can write scripts that are more efficient, readable (2), and robust.

  1. Note on Bashisms: Bashisms refer to features and practices unique to the Bash shell. However, it is worth noting that shells derived from the Bourne Again Shell, such as dash and zsh, also support many of these features.
  2. Readability: While Bashisms generally enhance readability, certain functionalities, such as parameter expansion syntax, can be confusing for beginners. However, the robustness and efficiency of Bashisms make them worth the learning curve.

Conditional Tests

Conditional tests are fundamental to any programming language, enabling decision-making based on evaluating expressions. Bash provides several constructs for these tests, including [ ... ], [[ ... ]], and the test command.

Guidelines

  • Use [[ ... ]]: ALWAYS use the [[ ... ]] construct when performing conditional tests.
  • Avoid [ ... ] and test: Avoid using [ ... ] and the test command for conditional tests.

Advantages of [[ ... ]]

  • Regex Support: Enables direct regex matching within conditional expressions, eliminating the need for external tools like grep.
  • String Comparison: Offers a more consistent and reliable approach to string comparison, especially when dealing with variables containing spaces or special characters.
  • Compound Conditions: Allows combining multiple conditions within a single [[ ... ]] block, simplifying logic and improving readability.
  • Safety: Prevents word splitting and globbing on variables, reducing the risk of unexpected behavior or security vulnerabilities.
Examples

Using [ ... ]:

var="value with spaces"

if [ $var = "value with spaces" ]; then
    echo "The variable matches the value."
else
    echo "This will output because '$var' is treated as multiple arguments."
fi

Potential Issues: Without proper quoting, [ ... ] can cause unexpected behavior due to word splitting, treating the variable as multiple arguments.


Using [[ ... ]]:

var="value with spaces"

if [[ $var == "value with spaces" ]]; then
    echo "The variable matches the value."
else
    echo "This will output because $var is treated as multiple arguments."
fi

Advantage: [[ ... ]] safely handles variables with spaces, whether or not they are quoted.

Using grep for regex matching:

email="hunter@hthompson.dev"

if echo "$email" | grep -qE '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'; then
    echo "Valid email address."
else
    echo "Invalid email address."
fi

Disadvantage: Since grep is an external command, the potential for errors and performance issues increases due to different implementations across systems.


Using [[ ... ]] for regex matching:

email="hunter@hthompson.dev"

if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "Valid email address."
else
    echo "Invalid email address."
fi

Advantage: [[ ... ]] directly supports regex matching, eliminating the need for external commands and enhancing script portability.

Using [ ... ] for compound conditions:

file="example.txt"

touch "$file" && chmod 700 "$file"

if [ -f "$file" ] && [ -r "$file" ] || [ -w "$file" ]; then
    echo "File exists and is readable and/or writable."
else
    echo "File is either missing, unreadable, or unwritable."
fi

Disadvantage: [ ... ] does not support compound conditions directly, requiring additional commands or constructs to achieve the desired behavior.


Using [[ ... ]] for compound conditions:

file="example.txt"

touch "$file" && chmod 700 "$file"

if [[ -f $file && -r $file || -w $file ]]; then
    echo "File exists and is readable and/or writable."
else
    echo "File is either missing, unreadable, or unwritable."
fi

Advantage: [[ ... ]] supports compound conditions, simplifying script logic and improving readability.

Sequence Iteration

Iterating over sequences is a common task in Bash, allowing you to process elements in a range or list. Bash provides built-in mechanisms for sequence iteration, such as brace expansion and C-style for loops.

Guidelines

  • Bash Built-ins: ALWAYS use brace expansion ({start..end}) for fixed ranges and the C-style for loop for variable limits when iterating over sequences.
  • Avoid Using seq: Avoid usingĀ seq for sequence iteration, as it is an external command and not a built-in feature of Bash.

Advantages of Built-in Iteration

  • Simplicity: Built-in mechanisms like brace expansion and C-style for loops are native to Bash, providing a straightforward and efficient way to iterate over sequences.
  • Reduced Dependencies: By utilizing built-in features, you minimize external dependencies, which enhances script reliability and maintainability.
Examples

Using seq:

for i in $(seq 1 5); do
    echo "Number: $i"
done

Using brace expansion:

for i in {1..5}; do
    echo "Number: $i"
done

Using seq:

start=1
end=5

for i in $(seq $start $end); do
    echo "Number: $i"
done

Using C-style for loop:

start=1
end=5

for (( i=start; i<=end; i++ )); do
    echo "Number: $i"
done

Using seq:

for i in $(seq 0 2 100); do
    echo "Number: $i"
done

Using C-style for loop:

for ((i=0; i<=100; i+=2)); do
    echo "Number: $i"
done

Command Substitution

Command substitution allows you to capture the output of commands and use it as part of another command or assignment. Bash provides two syntaxes for command substitution: $(...) and backticks (`...`).

Guidelines

  • Use $(...): ALWAYS use the $(...) syntax for command substitution instead of backticks.
  • Avoid Backticks: Avoid using backticks for command substitution.

Advantages of $(...)

  • Improved Readability: The $(...) format is visually clearer, making scripts easier to read and understand, especially as commands become more complex.
  • Easier Nesting: Facilitates the nesting of multiple commands without the syntactic awkwardness associated with backticks.
  • Enhanced Safety: The $(...) syntax is more robust and less prone to errors, particularly in complex command substitutions.
Examples

Using backticks:

output=`ls -l`
echo "The output is: $output"

Using $(...)

output=$(ls -l)
echo "The output is: $output"

Explanation: The $(...) syntax is clearer and preferred for command substitution.

Using backticks:

date_and_users=`echo "Date: \`date\` - Users: \`who | wc -l\`"`
echo $date_and_users

Using $(...):

date_and_users=$(echo "Date: $(date) - Users: $(who | wc -l)")
echo $date_and_users

Explanation: Nesting commands with $(...) is easier to read and less error-prone compared to using backticks.

Arithmetic Operations

Arithmetic operations allow for mathematical calculations within Bash, enabling you to perform calculations, comparisons, and other numeric operations. Bash supports arithmetic operations using arithmetic expansions $((...)), conditional arithmetic expressions ((...)), and the let command.

Guidelines

  • Use $((...)) and ((...)): ALWAYS use $((...)) for arithmetic expansions and ((...)) for conditional arithmetic expressions.
  • Avoid let: Avoid using let for arithmetic.
  • Reasoning: $((...)) and ((...)) provide a clearer and more intuitive syntax for arithmetic operations in Bash.

Advantages of $((...)) and ((...))

  • Clarity: Both ((...)) and $((...)) clearly delineate arithmetic expressions within scripts, making them easy to identify and understand.
  • Simplicity: These constructs are natively supported by Bash, offering a streamlined and intuitive approach to handling arithmetic.
  • Increased Safety: These methods specifically evaluate arithmetic without the risk of executing other commands, unlike let, which can misinterpret expressions as commands if not carefully handled.
Examples

Using let:

let result=1+2
echo "Result: $result"

Using $((...)):

result=$(( 1 + 2 ))
echo "Result: $result"

Explanation: The$((...)) syntax is clearer and reduces the risk of errors, making it the preferred method for basic arithmetic in Bash.

Using let:

let "a = 5"
let "b = 10"

if let "a < b"; then
  echo "a is less than b"
fi

Using ((...)):

a=5
b=10

if (( a < b )); then
  echo "a is less than b"
fi

Explanation: The ((...)) syntax simplifies conditional arithmetic expressions, enhancing readability and reducing complexity.

Parameter Expansion

Parameter expansion is a powerful feature in Bash that allows you to manipulate variables and perform string operations directly within the shell. Bash provides a wide range of parameter expansion options, including substring extraction, string replacement, and length calculation.

Guidelines

  • Bash Built-ins: Utilize parameter expansion for string manipulations whenever possible. This approach is more efficient and reduces script complexity and dependencies.
  • Avoid External Tools: Avoid using external tools like sed and awk for string manipulation.
    • Reason: While powerful, these tools are often overkill for simple string manipulations that can be efficiently handled by parameter expansion.

Advantages of Parameter Expansion

  • Streamlined Scripting: Keeps string manipulations inline and shell-native, simplifying the script's logic.
  • Enhanced Portability: Improves script portability across different Unix-like systems by avoiding dependency on external tools, which may vary in availability or functionality.
Examples

Using sed

string="Hello, World!"
substring=$(echo "$string" | sed 's/Hello,//')

echo "$substring"

Using Parameter Expansion

string="Hello, World!"
substring=${string/Hello,/}

echo "$substring"

Explanation: Parameter expansion provides a simpler and more efficient method for extracting substrings.

Using awk

filename="document.txt"
basename=$(echo $filename | awk -F. '{print $1}')

echo $basename

Using Parameter Expansion

filename="document.txt"
basename=${filename%.txt}
echo $basename

Advantage: Parameter expansion is simpler and more efficient for removing a suffix, reducing the need for external tools.

Using wc

string="Hello, World!"
length=$(echo -n $string | wc -c)
echo $length

Using Parameter Expansion

string="Hello, World!"
length=${#string}
echo $length

Advantage: Parameter expansion can directly compute the length of a string without invoking an external command, making it a more efficient choice.

Avoiding Parsing ls

While parsing the output of ls may seem like a convenient way to list files and directories, this approach is generally discouraged due to the potential for errors and unexpected behavior. Instead, Bash provides more reliable and secure methods for file and directory parsing, such as Bash globbing patterns (*).

Guidelines

  • Bash Built-ins: ALWAYS use Bash globbing patterns like * or tools like find for file and directory parsing.
  • Avoid Using ls: Avoid using ls in loops or where accurate filename interpretation is critical.

Risks of Parsing ls

  • Word Splitting: Filenames with spaces or special characters can cause word splitting issues when parsing ls output.
  • Command Interpretation: Filenames starting with a hyphen (-) can be misinterpreted as options by commands like ls, leading to unintended behavior.
  • Potential Vulnerabilities: Although parsing ls output may not directly cause security vulnerabilities like code injection, improper handling of filenames can introduce bugs or unexpected behavior, which may lead to security issues in more complex scenarios.

Alternatives to Parsing ls

  • Bash Globing: Use Bash globbing patterns like * for listing files and directories, ensuring reliable and secure file parsing.
  • find Command: For more complex file operations, consider using the find command, which provides extensive options for file and directory traversal.
  • read Command: When processing files or directories, use the read command to safely parse inputs into variables, avoiding the need for external commands like ls.
  • stat Command: For detailed file information, use the stat command, which provides extensive metadata about files and directories.
  • file Command: To determine file types, use the file command, which can identify the type of a file based on its contents.
Examples

Using ls in a loop:

for file in $(ls /path/to/dir); do
    echo "Processing $file"
done

Using Bash Globing:

for file in /path/to/dir/*; do
    echo "Processing $(basename "$file")"
done

Explanation: Bash globbing handles filenames with spaces or special characters correctly, making it more reliable than parsing ls output.

Using ls with wildcards:

for file in $(ls /path/to/dir/*.txt); do
    echo "Processing $file"
done

Using find:

find /path/to/dir -type f -name "*.txt" -exec echo "Processing {}" \;

Explanation: The find command is powerful and versatile, allowing you to search for files by various criteria and execute commands on the results, making it suitable for more complex file operations.

Using ls with while loop:

ls /path/to/dir > filelist.txt
while read -r file; do
    echo "Processing $file"
done < filelist.txt

Using find and read:

find /path/to/dir -type f > filelist.txt
while IFS= read -r file; do
    echo "Processing $file"
done < filelist.txt

Explanation: Using read with IFS (Internal Field Separator) set to handle filenames correctly ensures that the script processes each file safely, even if the filenames contain spaces or special characters.

Using ls -l:

ls -l /path/to/file

Using stat:

stat /path/to/file

Explanation: The stat command provides comprehensive metadata about a file, including size, permissions, and modification times, offering more detailed information than ls -l.

Using ls with file extension check:

if [[ $(ls /path/to/file) == *.txt ]]; then
    echo "This is a text file"
fi

Using file:

file_type=$(file --mime-type -b /path/to/file)
if [[ $file_type == "text/plain" ]]; then
    echo "This is a text file"
fi

Explanation: The file command determines the file type based on its content, rather than just its extension, providing a more accurate and reliable method for file type detection.

Element Collections

Element collections are a common feature in Bash scripting, allowing you to manage groups of items such as filenames, user inputs, or configuration values. Bash provides three primary methods for handling collections: arrays, associative arrays, and space-separated strings.

Guidelines

  • Arrays for Collections: ALWAYS use arrays when managing collections of elements.
  • Avoid Space-Separated Strings: Avoid using space-separated strings for collections.

Advantages of Arrays

  • Clarity and Safety: Arrays prevent errors related to word splitting and glob expansion that can occur with space-separated strings.
  • Flexibility: Arrays allow for straightforward manipulation and access to individual elements, as well as simpler expansion in commands that accept multiple arguments.
  • Ease of Maintenance: Code utilizing arrays is generally clearer and easier to maintain, particularly as script complexity increases.
Examples

Using Space-Separated Strings

items="apple orange banana"
for item in $items; do
  echo "Item: $item"
done

Using Arrays

items=("apple" "orange" "banana")
for item in "${items[@]}"; do
  echo "Item: $item"
done

Advantage: Arrays handle items with spaces or special characters correctly, preventing unintended word splitting.

Using Space-Separated Strings

files="file1.txt file2.txt file3.txt"
cp $files /destination/

Using Arrays

files=("file1.txt" "file2.txt" "file3.txt")
cp "${files[@]}" /destination/

Advantage: Arrays simplify command syntax and ensure all arguments are correctly passed, even if filenames contain spaces.

Using Space-Separated Strings

string="one two three"
first=$(echo $string | cut -d' ' -f1)
echo "First element: $first"

Using Arrays

array=("one" "two" "three")
first=${array[0]}
echo "First element: $first"

Advantage: Arrays allow direct access to individual elements without additional parsing.

//// Adding Elements to a Collection

Using Space-Separated Strings

string="apple orange"
string="$string banana"
echo $string

Using Arrays

array=("apple" "orange")
array+=("banana")
echo "${array[@]}"

Advantage: Arrays provide straightforward syntax for adding elements, improving readability and maintainability.

////

Parsing Input into Variables

Parsing input into variables is a common task in Bash scripting, allowing you to extract and process data from user inputs, files, or other sources. Bash provides the read command for these tasks, offering a more efficient and secure alternative to external commands like awk, sed, or cut.

Guidelines

  • Bash Built-ins: Use read to safely and efficiently parse user inputs and other data directly into variables.
  • Customize with IFS: Adjust the Internal Field Separator (IFS) as needed when using read to ensure that inputs are split according to your specific requirements, enhancing the flexibility and accuracy of data parsing.
  • Avoid External Commands: Minimize the use of external commands like awk, sed, or cut for parsing strings.

Advantages of read

  • Efficiency: read operates within the shell, eliminating the need for slower, resource-intensive external commands.
  • Controlled Parsing: read allows for controlled and direct parsing of inputs into variables within the shell, reducing the complexity and potential risks associated with using external commands for input processing.
  • Simplicity: Offers an easy-to-understand syntax for directly parsing complex data structures.
  • Portability: read is a Bash builtin, ensuring consistent behavior across different systems and environments.
Examples

Using awk

string="one two three"
first=$(echo $string | awk '{print $1}')
second=$(echo $string | awk '{print $2}')
third=$(echo $string | awk '{print $3}')
echo "First: $first, Second: $second, Third: $third"

Using read

string="one two three"
read -r first second third <<< "$string"
echo "First: $first, Second: $second, Third: $third"

Advantage: read provides a simpler and more efficient way to parse space-separated strings directly into variables.

Using sed

input="username:password"
username=$(echo $input | sed 's/:.*//')
password=$(echo $input | sed 's/.*://')
echo "Username: $username, Password: $password"

Using read

input="username:password"
IFS=':' read -r username password <<< "$input"
echo "Username: $username, Password: $password"

Advantage: read directly parses the input into variables, reducing complexity and improving readability.

Efficient Array Population

The mapfile command, also known as readarray, is a Bash built-in that provides an efficient way to populate an array with lines from a file or the output of a command. This command simplifies the process of reading and storing multiline data, making it particularly useful when working with large datasets or when you need to process input line by line.

Guidelines

  • Use mapfile for Multiline Input: When you need to read and store multiline data into an array, prefer mapfile over looping constructs.
  • Specify a Delimiter with -d: Use the -d option to specify a custom delimiter if the input data is not newline-separated.
  • Avoid Excessive Loops: mapfile can efficiently replace complex loops that read and store data line by line, improving script readability and performance.

Advantages of mapfile

  • Efficiency: mapfile reads an entire stream into an array at once, making it faster and more efficient than reading lines individually in a loop.
  • Simplicity: The command reduces the amount of code required to populate arrays, leading to cleaner and more maintainable scripts.
  • Flexibility: With options like -t (to remove trailing newlines) and -d (to specify a delimiter), mapfile offers flexibility in handling various input formats.
Examples

Using a loop:

lines=()
while IFS= read -r line; do
    lines+=("$line")
done < input.txt

Using mapfile:

mapfile -t lines < input.txt

Advantage: mapfile simplifies the process of reading a file into an array, reducing the code and improving efficiency.

Using a loop:

declare -a commands
while IFS= read -r command; do
    commands+=("$command")
done < <(ls /usr/bin)

Using mapfile:

mapfile -t commands < <(ls /usr/bin)

Advantage: mapfile efficiently reads the output of a command into an array without the need for an explicit loop.

Example: Reading data separated by colons (:):

input="one:two:three:four"
mapfile -d ':' -t items <<< "$input"
echo "${items[@]}"

Explanation: The -d option allows mapfile to split the input based on a custom delimiter, which can be useful for parsing structured data.