Created
July 7, 2026 23:43
-
-
Save memotype/7b1538b90368eacb4aacc1a8776c664d to your computer and use it in GitHub Desktop.
ChatGPT Codex bash scripting style-guide
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # GNU Bash Style Guide | |
| Follow this guide when creating or editing Bash scripts in this repository. | |
| ## 1. Target shell | |
| * This is a modern GNU Bash codebase, not a POSIX `sh` codebase. | |
| * Use Bash features where they make code safer or clearer. Bash arrays, associative arrays, `[[ ... ]]`, `mapfile`, process substitution, parameter expansion, and other Bash-specific features are allowed and encouraged. | |
| * Scripts must declare Bash explicitly: | |
| ```bash | |
| #!/usr/bin/env bash | |
| ``` | |
| * Do not try to make code "POSIX compliant" at the cost of clarity, correctness, or safety. | |
| * Do not write code intended for `/bin/sh` unless a file explicitly says otherwise. | |
| ## 2. Naming and variable scope | |
| * Internal script variables must be lowercase. | |
| ```bash | |
| input_file= | |
| temp_dir= | |
| retry_count=0 | |
| ``` | |
| * Use uppercase only for environment variables, shell-control variables, externally documented configuration variables, or conventional exported values. | |
| ```bash | |
| PATH="/usr/local/bin:$PATH" | |
| IFS= read -r line | |
| export API_TOKEN | |
| ``` | |
| * Do not use uppercase names for ordinary local variables. | |
| ```bash | |
| # Good | |
| local response | |
| local file_count | |
| # Bad | |
| local RESPONSE | |
| local FILE_COUNT | |
| ``` | |
| * Prefer `local` inside functions for all function-private variables. | |
| ```bash | |
| parse_config() { | |
| local config_file="$1" | |
| local line | |
| while IFS= read -r line; do | |
| printf '%s\n' "$line" | |
| done < "$config_file" | |
| } | |
| ``` | |
| * Use descriptive names. Avoid single-letter names except for narrowly scoped conventional counters. | |
| ## 3. Quoting and expansion | |
| * Quote every variable expansion unless unquoted expansion is genuinely required. | |
| * Treat unquoted expansion as suspicious. Reconsider it before writing it. | |
| ```bash | |
| # Good | |
| printf '%s\n' "$message" | |
| rm -- "$file" | |
| cp -- "$source" "$destination" | |
| ``` | |
| ```bash | |
| # Bad | |
| printf '%s\n' $message | |
| rm $file | |
| cp $source $destination | |
| ``` | |
| * Simple expansion does not need curly braces. | |
| ```bash | |
| # Preferred | |
| path="$HOME/bin" | |
| message="$prefix$value" | |
| ``` | |
| * Use curly braces when they are required for disambiguation or parameter operations. | |
| ```bash | |
| backup="${file}.bak" | |
| default_value="${value:-fallback}" | |
| required_value="${value:?value must be set}" | |
| trimmed="${path##*/}" | |
| ``` | |
| * Do not use unquoted command substitution. | |
| ```bash | |
| # Good | |
| version="$(git describe --always --dirty)" | |
| ``` | |
| ```bash | |
| # Bad | |
| version=`git describe --always --dirty` | |
| ``` | |
| * Always use `$(...)`, never backticks. | |
| * Keep nested quoting understandable. This is fine: | |
| ```bash | |
| result="$(command "$input")" | |
| ``` | |
| * Avoid deeply nested quote constructions when a small refactor would make the code clearer. | |
| ## 4. Arrays | |
| * Use arrays when data consists of multiple values. Do not encode lists into space-separated strings. | |
| ```bash | |
| files=( | |
| "$config_file" | |
| "$data_file" | |
| "$output_file" | |
| ) | |
| for file in "${files[@]}"; do | |
| process_file "$file" | |
| done | |
| ``` | |
| * Expand arrays with `"${array[@]}"`, not `${array[*]}`. | |
| ```bash | |
| command -- "${arguments[@]}" | |
| ``` | |
| * Do not use command substitution to construct arrays through word splitting. | |
| ```bash | |
| # Bad | |
| files=($(find . -type f)) | |
| ``` | |
| * Prefer `mapfile` when reading line-oriented output into an array. | |
| ```bash | |
| mapfile -t files < <(find "$root" -type f -print) | |
| ``` | |
| * Use associative arrays where keys are meaningful. | |
| ```bash | |
| declare -A settings=( | |
| [host]="localhost" | |
| [port]="8080" | |
| ) | |
| ``` | |
| ## 5. Conditionals and loops | |
| * Prefer Bash's `[[ ... ]]` over `[ ... ]` or `test`. | |
| ```bash | |
| if [[ -f "$config_file" ]]; then | |
| load_config "$config_file" | |
| fi | |
| ``` | |
| * Keep `then` and `do` on the same line as the corresponding `if`, `elif`, `for`, or `while`, separated with `; `. | |
| ```bash | |
| if [[ -n "$value" ]]; then | |
| use_value "$value" | |
| fi | |
| for file in "${files[@]}"; do | |
| process_file "$file" | |
| done | |
| while IFS= read -r line; do | |
| handle_line "$line" | |
| done < "$input_file" | |
| ``` | |
| * If the conditional or loop expression must span multiple lines to stay readable and near 80 columns, put `then` or `do` on its own line after the complete expression. | |
| ```bash | |
| if [[ "$environment" == "production" ]] && | |
| [[ "$confirmation" == "yes" ]] | |
| then | |
| deploy_production | |
| fi | |
| ``` | |
| * Quote normal comparisons. | |
| ```bash | |
| if [[ "$mode" == "debug" ]]; then | |
| enable_debug | |
| fi | |
| ``` | |
| * Leave the right-hand side unquoted only when intentionally using a Bash pattern. | |
| ```bash | |
| if [[ "$filename" == *.log ]]; then | |
| archive_log "$filename" | |
| fi | |
| ``` | |
| * Leave a regex variable unquoted only when intentionally using `=~`. | |
| ```bash | |
| if [[ "$value" =~ $integer_regex ]]; then | |
| process_integer "$value" | |
| fi | |
| ``` | |
| * Add a comment when an intentionally unquoted pattern or regex may not be obvious. | |
| * Use `case` for multi-way dispatch and input classification. | |
| ```bash | |
| case "$command" in | |
| start) | |
| start_service | |
| ;; | |
| stop) | |
| stop_service | |
| ;; | |
| status) | |
| show_status | |
| ;; | |
| *) | |
| die "Unknown command: $command" | |
| ;; | |
| esac | |
| ``` | |
| ## 6. Reading files and streams | |
| * Always use `read -r` unless you explicitly need backslash interpretation. | |
| * Set `IFS=` for line reads that must preserve leading and trailing whitespace. | |
| ```bash | |
| while IFS= read -r line || [[ -n "$line" ]]; do | |
| process_line "$line" | |
| done < "$input_file" | |
| ``` | |
| * Do not use `cat` when redirection is simpler. | |
| ```bash | |
| # Good | |
| grep -- "$pattern" < "$input_file" | |
| while IFS= read -r line; do | |
| process_line "$line" | |
| done < "$input_file" | |
| ``` | |
| ```bash | |
| # Bad | |
| cat "$input_file" | grep -- "$pattern" | |
| cat "$input_file" | while IFS= read -r line; do | |
| process_line "$line" | |
| done | |
| ``` | |
| * Avoid pipelines into `while` loops because Bash commonly runs the loop body in a subshell, which can lose variable changes after the loop. | |
| ```bash | |
| # Good | |
| while IFS= read -r line; do | |
| count=$((count + 1)) | |
| done < <(producer_command) | |
| ``` | |
| * Be careful with process substitution when the producer's exit status matters. Do not hide critical command failures inside `< <(...)`; use an explicit temporary file or a directly checked command when failure handling is important. | |
| ## 7. Never use `eval` | |
| * `eval` is forbidden. | |
| * Do not use it for variable indirection, command construction, argument construction, configuration parsing, dynamic dispatch, or any other purpose. | |
| * Treat any proposed `eval` usage as a design failure that must be redesigned. | |
| ```bash | |
| # Forbidden | |
| eval "$command" | |
| eval "value=\$$name" | |
| ``` | |
| Use safer alternatives: | |
| * Use arrays for command arguments. | |
| * Use associative arrays for keyed data. | |
| * Use `case` for dispatch. | |
| * Use `declare -n` only when controlled Bash nameref behavior is truly the clearest safe option. | |
| * Use explicit functions instead of dynamically assembled shell code. | |
| ```bash | |
| # Good | |
| command=(grep -- "$pattern" "$input_file") | |
| "${command[@]}" | |
| ``` | |
| ## 8. Commands and argument safety | |
| * Use `--` before path-like or user-controlled arguments where the command supports it. | |
| ```bash | |
| rm -- "$file" | |
| mkdir -- "$directory" | |
| grep -- "$pattern" "$input_file" | |
| ``` | |
| * Never parse `ls` output. | |
| * Never use `for file in $(...)`. | |
| * Never rely on whitespace-separated filenames. | |
| * Use null-delimited streams for arbitrary filenames. | |
| ```bash | |
| while IFS= read -r -d '' file; do | |
| process_file "$file" | |
| done < <(find "$root" -type f -print0) | |
| ``` | |
| * Use `find ... -exec ... +` when it is simpler than a manual loop. | |
| ```bash | |
| find "$root" -type f -name '*.log' -exec gzip -- {} + | |
| ``` | |
| * Prefer `printf` over `echo`. | |
| ```bash | |
| printf '%s\n' "$message" | |
| printf 'Error: %s\n' "$error_message" >&2 | |
| ``` | |
| Do not rely on `echo` behavior for strings that may begin with `-` or contain backslashes. | |
| ## 9. Functions and program structure | |
| * Prefer function declarations in this form: | |
| ```bash | |
| process_file() { | |
| local file="$1" | |
| # Work here. | |
| } | |
| ``` | |
| * Do not use the `function` keyword unless there is a compelling repository-specific reason. | |
| * Keep functions small and explicit about inputs, outputs, and exit status. | |
| * Use `main` for standalone executable scripts. | |
| ```bash | |
| main() { | |
| local input_file="$1" | |
| process_file "$input_file" | |
| } | |
| if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then | |
| main "$@" | |
| fi | |
| ``` | |
| * Use `return` inside helper functions and `exit` at top-level program boundaries. | |
| * Do not call `exit` from reusable library code. | |
| ## 10. Error handling | |
| * Do not treat `set -euo pipefail` as a magical safety switch. | |
| * `set -e` has subtle behavior around conditionals, command substitutions, negation, pipelines, and subshells. Do not rely on it as the only form of error handling. | |
| * Use explicit error checks where failure matters. | |
| ```bash | |
| if ! output="$(generate_output "$input_file")"; then | |
| die "Failed to generate output" | |
| fi | |
| ``` | |
| * Use `pipefail` for pipelines whose failure status matters. | |
| ```bash | |
| set -o pipefail | |
| ``` | |
| * Use `set -u` only when the script has been written to handle optional variables and positional parameters deliberately. | |
| ```bash | |
| set -u | |
| input_file="${1:-}" | |
| if [[ -z "$input_file" ]]; then | |
| die "Usage: $0 INPUT_FILE" | |
| fi | |
| ``` | |
| * A reasonable standalone-script baseline is: | |
| ```bash | |
| set -o pipefail | |
| ``` | |
| Then add `set -u` or `set -e` only when the script's control flow has been designed and tested for them. | |
| * Define a clear error helper for standalone programs. | |
| ```bash | |
| die() { | |
| printf 'Error: %s\n' "$*" >&2 | |
| exit 1 | |
| } | |
| ``` | |
| ## 11. Temporary files and cleanup | |
| * Create temporary files and directories with `mktemp`. | |
| * Never invent predictable `/tmp` filenames. | |
| * Clean up with a trap. | |
| ```bash | |
| temp_dir="$(mktemp -d)" | |
| cleanup() { | |
| local status=$? | |
| rm -rf -- "$temp_dir" || true | |
| return "$status" | |
| } | |
| trap cleanup EXIT | |
| ``` | |
| * Keep cleanup logic simple and safe. | |
| * Do not place complex inline shell code inside trap strings when a cleanup function is clearer. | |
| ## 12. Libraries and `source` | |
| * Prefer `source` over `.`. | |
| ```bash | |
| source "$script_dir/lib/common.bash" | |
| ``` | |
| * Source files must be careful not to pollute the caller's namespace. | |
| * Library functions should use a consistent lowercase prefix based on the library name. | |
| ```bash | |
| config_load() { | |
| local config_file="$1" | |
| # ... | |
| } | |
| config_get() { | |
| local key="$1" | |
| # ... | |
| } | |
| ``` | |
| * Library-private variables must be lowercase and should be local whenever possible. | |
| * Avoid setting shell options globally from sourced code. | |
| * Avoid changing `IFS`, `shopt` state, traps, working directory, or exported environment variables from a library unless that behavior is explicitly documented and restored. | |
| * Libraries should not call `exit`. | |
| * Use a lowercase include guard when duplicate sourcing would be harmful. | |
| ```bash | |
| if [[ -n "${config_library_loaded:-}" ]]; then | |
| return 0 | |
| fi | |
| config_library_loaded=1 | |
| ``` | |
| * Derive a script directory safely when needed. | |
| ```bash | |
| script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" | |
| source "$script_dir/lib/common.bash" | |
| ``` | |
| ## 13. Arithmetic and numeric handling | |
| * Use arithmetic contexts for numeric work. | |
| ```bash | |
| count=$((count + 1)) | |
| if ((retry_count >= max_retries)); then | |
| die "Retry limit reached" | |
| fi | |
| ``` | |
| * Do not use `expr`. | |
| * Do not use string comparison operators for numeric comparisons. | |
| ```bash | |
| # Good | |
| if ((count > 10)); then | |
| warn "Too many items" | |
| fi | |
| # Bad | |
| if [[ "$count" > 10 ]]; then | |
| warn "Too many items" | |
| fi | |
| ``` | |
| ## 14. External command checks | |
| * Check for required commands before relying on them. | |
| ```bash | |
| require_command() { | |
| local command_name="$1" | |
| if ! command -v "$command_name" > /dev/null 2>&1; then | |
| die "Required command not found: $command_name" | |
| fi | |
| } | |
| ``` | |
| * Prefer `command -v` over `which`. | |
| ## 15. Output and logging | |
| * Send normal output to stdout and diagnostics to stderr. | |
| * Make machine-readable output predictable and free of unrelated status messages. | |
| * Use consistent helper functions when scripts have multiple logging levels. | |
| ```bash | |
| info() { | |
| printf 'Info: %s\n' "$*" >&2 | |
| } | |
| warn() { | |
| printf 'Warning: %s\n' "$*" >&2 | |
| } | |
| ``` | |
| * Do not print secrets, credentials, tokens, or sensitive command arguments in logs. | |
| ## 16. Formatting | |
| * Indent with two spaces. | |
| * Keep lines near 80 characters when practical, but do not contort code to meet a hard limit. | |
| * Put one command per line unless short grouping is clearly more readable. | |
| * Use blank lines to separate logical stages. | |
| * Prefer readability over clever one-liners. | |
| * Add comments for non-obvious shell behavior, especially quoting, globbing, regex, traps, and intentional shell-option choices. | |
| ## 17. Required review checklist | |
| Before considering Bash code complete, verify: | |
| * The script is explicitly Bash, not accidental POSIX shell. | |
| * Internal variables are lowercase. | |
| * Variable expansions are quoted unless a documented Bash pattern, regex, or arithmetic context requires otherwise. | |
| * No `eval` exists. | |
| * No backticks exist. | |
| * No useless `cat` pipeline exists. | |
| * No `for item in $(...)` exists. | |
| * No `ls` output is being parsed. | |
| * Arrays are used for argument lists and multi-value data. | |
| * File names with spaces, tabs, glob characters, and leading `-` are handled safely. | |
| * `read` uses `IFS= read -r` when reading arbitrary lines. | |
| * Temporary files use `mktemp` and cleanup traps. | |
| * Sourced libraries avoid leaking globals or changing caller behavior unexpectedly. | |
| * Error paths are explicit and tested. | |
| * ShellCheck is run in Bash mode. | |
| * Formatting is checked with `shfmt` when available. | |
| ## 18. Tooling | |
| Use these checks where available: | |
| ```bash | |
| shellcheck --shell=bash script.sh | |
| shfmt -d -i 2 -ci script.sh | |
| ``` | |
| Do not suppress ShellCheck warnings casually. Fix the underlying issue, or add a narrow suppression with a comment explaining why the exception is safe. | |
| ## 19. Preferred template | |
| ```bash | |
| #!/usr/bin/env bash | |
| set -o pipefail | |
| die() { | |
| printf 'Error: %s\n' "$*" >&2 | |
| exit 1 | |
| } | |
| require_command() { | |
| local command_name="$1" | |
| if ! command -v "$command_name" > /dev/null 2>&1; then | |
| die "Required command not found: $command_name" | |
| fi | |
| } | |
| main() { | |
| local input_file="${1:-}" | |
| local line | |
| local line_count=0 | |
| if [[ -z "$input_file" ]]; then | |
| die "Usage: $0 INPUT_FILE" | |
| fi | |
| if [[ ! -f "$input_file" ]]; then | |
| die "File not found: $input_file" | |
| fi | |
| while IFS= read -r line || [[ -n "$line" ]]; do | |
| line_count=$((line_count + 1)) | |
| printf '%s\n' "$line" | |
| done < "$input_file" | |
| printf 'Processed %d lines\n' "$line_count" >&2 | |
| } | |
| if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then | |
| main "$@" | |
| fi | |
| ``` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment