Skip to content
Jason on Twitter Jason on GitHub

6 Techniques Hacker News Uses to Create Great Shell Scripts

Note: This is a follow-up to my previous article "6 Techniques I Use to Create a Great User Experience for Shell Scripts". If you haven't read it yet, you might want to check it out first!


After publishing my previous article on how I create a great user experience for shell scripts, the Hacker News community provided valuable feedback and suggestions. I've compiled these insights into six additional techniques to further enhance your shell scripts:

1. Make your script interoperable and pipeline friendly

Ensuring your scripts work well in various environments is crucial. Here are four key practices:

a) Use #!/usr/bin/env bash

#!/usr/bin/env bash uses the user's PATH to find the bash executable, which is helpful when bash might be installed in different locations on different systems.

#!/bin/bash directly specifies the bash location, which might not exist on all systems.

b) Use tput for portable color output

Example:

if command -v tput &>/dev/null && [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
    RED=$(tput setaf 1)
    GREEN=$(tput setaf 2)
    RESET=$(tput sgr0)
else
    RED=""
    GREEN=""
    RESET=""
fi

c) Respect the $NO_COLOR environment variable

As shown above, check for $NO_COLOR before using colors.

d) Output status messages to stderr

Example:

echo "Error: Invalid input" >&2

2. Use printf instead of echo

echo can behave unexpectedly, especially with certain flags or special characters. printf is more consistent across different systems:

printf "Hello, %s!\n" "$name"

3. Use shellcheck to find bash pitfalls

ShellCheck is an excellent tool for identifying common issues in shell scripts. For example, it found this potential problem in my original evaluate.sh script:

# Before
rm -rf $dir/*

# After (ShellCheck suggestion)
rm -rf "$dir"/*

Always run your scripts through ShellCheck before considering them complete.

4. Be aware of double bracket vs single bracket behavior

Double brackets [[]] are a Bash extension and offer more features than single brackets []. They're generally safer and more flexible. Here are key differences:

# Pattern matching fails with single brackets
file="my-special-file.txt"
if [ "$file" = *.txt ]; then    # Always false! Literal comparison
    echo "Single brackets don't do pattern matching"
fi

if [[ $file == *.txt ]]; then   # Works! Pattern matching
    echo "Double brackets support pattern matching"
fi

# Word splitting behavior differs
path="/some path/with spaces"
if [ -d "$path" ]; then         # Must quote variables
    echo "Single brackets need quotes"
fi

if [[ -d $path ]]; then         # Quotes optional (but still recommended)
    echo "Double brackets handle spaces"
fi

# Logical operators are different
if [ "$a" = "1" ] && [ "$b" = "2" ]; then     # Shell operators needed
    echo "Single brackets use shell && ||"
fi

if [[ $a == 1 && $b == 2 ]]; then             # Built-in operators
    echo "Double brackets have logical operators"
fi

# Regular expressions only work in double brackets
if [[ $number =~ ^[0-9]+$ ]]; then            # Regex support
    echo "Double brackets support regex with =~"
fi

Double brackets are more forgiving and feature-rich, but they're Bash-specific. If you need POSIX compatibility (to run on other shells like dash or sh), use single brackets. Otherwise, double brackets are generally safer and more convenient.

Note: Even with double brackets, it's still considered good practice to quote your variables to maintain consistency and prevent surprises when scripts are modified later.

5. Use exit 2 instead of exit 1 for usage errors

It's a convention to use exit code 2 for command line syntax errors:

if [ $# -eq 0 ]; then
    echo "Usage: $0 <argument>" >&2
    exit 2
fi

6. Make use of bash boilerplates

Bash boilerplates can provide a solid foundation for your scripts. One popular option is bash3boilerplate. Here's a simple example:

#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset

__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)"

arg1="${1:-}"

# Your code here

Wrapping Up

If you enjoyed learning these techniques, then check out my previous article, "6 Techniques I Use to Create a Great User Experience for Shell Scripts". It covers: 1) Comprehensive Error Handling and Input Validation, 2) Clear and Colorful Output, 3) Detailed Progress Reporting, 4) Strategic Error Handling with "set -e" and "set +e", 5) Platform-Specific Adaptations, and 6) Timestamped File Outputs for Multiple Runs.

Also, check out the Hacker News discussion of that post which inspired this follow-up post.

Thank you to the Hacker News community for these valuable insights. Happy scripting!