Optimize shell scripts for better performance and maintainability in sh files or in RUN instructions in Dockerfiles or in sh instruction in Jenkinsfiles.
Instructions for writing clean, safe, and maintainable shell scripts for bash, sh, zsh, and other shells.
"$var"), use ${var} for clarity, and avoid eval[[ ]], local, arrays) when portability requirements allow; fall back to POSIX constructs only when needed#!/usr/bin/env bash#!/bin/bashprintf over echo for better portability and consistent behaviorbuiltin cd instead of cd, builtin pwd instead of pwd to avoid using customized aliased commandscat << 'EOF' (with quotes) to avoid variable interpolation in heredocs when not neededlocal in functionsexport whenever possible; export is needed only when variables must be passed to child processesAlways enable set -euo pipefail to fail fast on errors, catch unset variables, and surface pipeline failures:
set -e or set -o errexit - Exit immediately when a command returns non-zero statusset -u or set -o nounset - Treat unset variables as errorsset -o pipefail - Return value of pipeline is the last non-zero exit statusset -E or set -o errtrace - Inherit ERR traps in functions and subshellsExit immediately when a command returns a non-zero status. While useful, it has important caveats:
When commands are expected to fail:
Caveat with command substitution:
Caveat with process substitution:
Process substitution launches commands in separate processes, making error detection difficult:
Process substitution is asynchronous - Use wait to capture exit status:
Without pipefail, failure of commands in a pipeline can hide errors:
Ensures ERR traps are inherited by functions, command substitutions, and subshells.
Treats unset variables as errors. Use ${VAR:-default} or ${VAR-default} for optional variables.
Available in Bash 4.4+. Makes command substitution inherit set -e:
trap to clean up temporary resources or handle unexpected exitsreadonly (or declare -r) to prevent accidental reassignmentmktemp to create temporary files or directories safely and ensure they are removed in your cleanup handlerWith set -o pipefail, commands like grep -q or head that exit early can cause exit code 141 (SIGPIPE):
Always encapsulate all script logic inside a main function. This provides several benefits:
#!/usr/bin/env bashlocal in functions to avoid making them globallocal or declare for multiple variables: local var1 var2 var3export readonly, first use readonly then export (cannot combine them)export most of the time; only needed when variables must be passed to child processesUsing default values:
${PARAMETER:-WORD} - Use WORD if PARAMETER is unset or empty${PARAMETER-WORD} - Use WORD only if PARAMETER is unset (not when empty)⚠️ Use the latter syntax (- without colon) for function arguments to allow resetting a value to empty string.
Extraction examples:
Always set default values to prevent dangerous operations:
Always "scope" variables passed by reference to avoid name collisions:
❌ WRONG - Circular reference:
✅ CORRECT - Scoped naming:
Tricky example - Internal variable collision:
Use arrays for complex commands:
Instead of confusing calls like myFunction 0 1 0, use named constants:
Instead of adding arguments with default values, consider using environment variables:
Always assign to variable first (don't use in echo directly):
Keep on same line with ; to ensure status code is from correct command:
Use ${TMPDIR:-/tmp} since TMPDIR variable may not exist:
Always preserve and return the original exit code:
Always use extended regex: sed -E
Avoid grep -P (not supported on Alpine); use -E instead
Use LC_ALL=POSIX to prevent matching accented characters in [A-Za-z]:
Bash and grep regular expressions handle character classes differently based on locale:
[A-Za-z] matches accented characters by default
Set LC_ALL=POSIX to match only ASCII letters:
Consider adding export LC_ALL=POSIX in script headers (can be overridden in subshells)
Generate CSV with millisecond measurements:
Use built-in features over external commands:
echo instead of string concatenation in loops${var//pattern/replacement}) instead of calling sed repeatedlyExample optimization:
sed: 90% faster for simple replacementsecho vs concatenation: Significantly faster for building outputecho -e by using simpler echo when possiblejq for JSON, yq for YAML—or jq on JSON converted via yq) over ad-hoc text processing with grep, awk, or shell string splittingjq/yq are unavailable or not appropriate, choose the next most reliable parser available in your environment, and be explicit about how it should be used safelyjq exit status or using // empty)--raw-output when you need plain stringsset -euo pipefail or test command success before using resultsjq/yq (or alternative tools) are required but not installedUse shellcheck for static analysis when available. If not available, use docker koalaman/shellcheck:stable image.
Continuous improvement from errors found during builds and linting, indicate in the report what needs to be added to fc-optimize-shell-scripts skill next time.