Skip to content

Instantly share code, notes, and snippets.

@memotype
Created July 7, 2026 23:43
Show Gist options
  • Select an option

  • Save memotype/7b1538b90368eacb4aacc1a8776c664d to your computer and use it in GitHub Desktop.

Select an option

Save memotype/7b1538b90368eacb4aacc1a8776c664d to your computer and use it in GitHub Desktop.
ChatGPT Codex bash scripting style-guide
# 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