diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-30 16:53:30 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-30 16:53:30 +0300 |
| commit | f2fecaa6ffef505da254b7083116ad840588634b (patch) | |
| tree | 5662004b072ec15a509a45b87a31e811e7180b2f /prompts | |
| parent | cab9ae5285a140fab9f4341a8e20eb24b08adca5 (diff) | |
new
Diffstat (limited to 'prompts')
6 files changed, 625 insertions, 0 deletions
diff --git a/prompts/skills/bash-best-practices/SKILL.md b/prompts/skills/bash-best-practices/SKILL.md new file mode 100644 index 0000000..c59440a --- /dev/null +++ b/prompts/skills/bash-best-practices/SKILL.md @@ -0,0 +1,66 @@ +--- +name: bash-best-practices +description: Bash coding style and conventions derived from foo.zone blog posts. Covers structure, safety, idioms, pipelines, redirection, and common pitfalls. Use when writing, reviewing, or refactoring Bash scripts. +--- + +# Bash Best Practices + +Style and structural conventions drawn from the foo.zone Bash coding style guide and Bash Golf series. Apply when writing, reviewing, or refactoring Bash. + +## When to Use + +- Writing new Bash scripts or functions +- Reviewing or refactoring Bash code +- Aligning code with a strict, readable Bash style +- Resolving style questions (shebang, quoting, pipelines, error handling) + +## Conventions Overview + +Start every script with a portable shebang and strict-mode header: + +```bash +#!/usr/bin/env bash +set -euo pipefail +``` + +Use soft-tabs (spaces), limit lines to ~80 characters, and quote variables whose content is unknown or external. Prefer Bash built-ins for light work and external tools (`sed`, `awk`, `grep`, `bc`) for heavy text processing. + +Key idioms covered in detail below: + +| Topic | File | +|-------|------| +| Shebang, strict mode, indentation, quoting, booleans, `declare`, `local -i` | [`reference/style.md`](reference/style.md) | +| Function naming, namespaces, `::`, private helpers (`_`), assign-then-shift, `case` dispatch | [`reference/functions.md`](reference/functions.md) | +| `set -e`, `set -o pipefail`, `PIPESTATUS`, arithmetic comparisons, restricted bash | [`reference/error-handling.md`](reference/error-handling.md) | +| Process substitution, `while read`, here-docs/here-strings, pipelines, `/dev/tcp`, `mapfile` | [`reference/io-patterns.md`](reference/io-patterns.md) | +| `eval` avoidance, namerefs, dynamic command arrays, atomic overwrite, throttling, `shellcheck` | [`reference/advanced.md`](reference/advanced.md) | + +## Quick Checklist + +- [ ] Shebang is `#!/usr/bin/env bash` +- [ ] Strict mode header (`set -euo pipefail`) at top of script +- [ ] Soft-tabs used, line length around 80 +- [ ] `$(...)` used instead of backticks +- [ ] Variables quoted when content is unknown/external +- [ ] Internal helpers prefixed with `_` +- [ ] `case` used for multi-branch literal string matching +- [ ] Built-ins preferred for light work, external tools for heavy +- [ ] Booleans use `yes`/`no` +- [ ] `eval` avoided; `source` or process substitution used instead +- [ ] `set -e` enabled with localized `set +e` for expected failures +- [ ] `pipefail` used when pipelines must fail on any stage +- [ ] Numeric comparisons use `(( ))` or `-gt`/`-lt`/`-eq` +- [ ] Constants declared with `local -r` or `declare -r` +- [ ] Pipelines broken with backslash and leading `|` +- [ ] `FUNCNAME` used for logging when helpful +- [ ] `declare -n` used for indirection where possible +- [ ] `find -print0 | xargs -0` for file lists with spaces +- [ ] `while read` fed by process substitution (`< <(...)`) for variable survival +- [ ] `IFS='' read -r line` used when exact line preservation matters +- [ ] Here-strings (`<<<`) preferred over `echo | command` for single-line input +- [ ] Commands built dynamically in arrays when arguments are conditional +- [ ] Atomic file overwrite via temp + `diff -q` + `mv` +- [ ] Unit tests and `shellcheck` integrated +- [ ] `local -i` used for integer counters +- [ ] `mapfile` or `$(<file)` used instead of unnecessary `cat` +- [ ] Consistent style throughout the script/project diff --git a/prompts/skills/bash-best-practices/reference/advanced.md b/prompts/skills/bash-best-practices/reference/advanced.md new file mode 100644 index 0000000..07c65c7 --- /dev/null +++ b/prompts/skills/bash-best-practices/reference/advanced.md @@ -0,0 +1,153 @@ +# Advanced Bash Patterns + +## Avoid `eval` + +Avoid `eval`. Prefer sourcing files or process substitution to generate and source code dynamically. + +```bash +# Good: source a file of declarations +source vars.source.sh + +# Good: source generated code from a command +source <(./vars.sh) +``` + +## Namerefs (`declare -n`) + +Use namerefs (Bash 4.3+) for cleaner indirection instead of `eval`. + +```bash +set_value() { + local -n ref="$1" + ref="$2" +} +set_value my_var hello +``` + +You can also construct the target name dynamically: + +```bash +make_var() { + local idx=$1; shift + local name="slot_$idx" + printf -v "$name" '%s' "$*" +} + +get_var() { + local idx=$1 + local -n ref="slot_$idx" + printf '%s\n' "$ref" +} +``` + +## Background-Job Throttling + +When spawning parallel background jobs, cap them to the number of CPU cores and use `wait -n` to pause only until any slot frees up. This avoids process explosions. + +```bash +local -r max_jobs=$(( $(nproc 2>/dev/null || echo 4) )) + +for item in ...; do + while (( $(jobs -rp | wc -l) >= max_jobs )); do + wait -n + done + do_work "$item" & +done +wait +``` + +## Build Commands Dynamically with Arrays + +When constructing commands conditionally, build an array to avoid word-splitting and empty-argument bugs. + +```bash +local -a cmd=("$SOURCE_HIGHLIGHT" "--src-lang=$lang") +if [ -n "$SOURCE_HIGHLIGHT_CSS" ]; then + cmd+=("--style-css-file=$SOURCE_HIGHLIGHT_CSS") +fi +"${cmd[@]}" <<< "$text" +``` + +## Atomic / Safe File Overwrite + +Write to a temporary file, compare with `diff -q`, and `mv` only if content actually changed. This preserves mtime (helpful for downstream skip-logic) and avoids leaving partial files on interrupt. + +```bash +safe_overwrite () { + local -r tmp="$1"; shift + local -r dest="$1"; shift + + if [[ -f "$dest" ]] && diff -q "$tmp" "$dest" >/dev/null 2>&1; then + rm "$tmp" + else + mv "$tmp" "$dest" + fi +} + +# Usage: +echo 'new content' > "$dest.tmp" +safe_overwrite "$dest.tmp" "$dest" +``` + +## Self-Testing and ShellCheck + +Include an `assert` module with helpers like `assert::equals`, `assert::contains`, `assert::not_empty`, and `assert::matches`. Run them as part of a `--test` target. + +Also run `shellcheck` against your scripts as part of the test suite: + +```bash +assert::shellcheck () { + shellcheck \ + --norc \ + --external-sources \ + --check-sourced \ + --exclude=SC2155,SC2010,SC2154,SC1090,SC2012,SC2016,SC1091 \ + ./"$0" +} +``` + +If ShellCheck flags are unavoidable, document the specific `--exclude` reasons in comments. + +## Random Numbers + +Use the special `$RANDOM` variable for quick pseudo-random integers. + +```bash +declare -i delay=$(( RANDOM % 60 )) +sleep $delay +``` + +## Environment Variables for Arguments + +Pass required arguments via environment variables with `${VAR:?message}` for mandatory checks. + +```bash +#!/usr/bin/env bash +declare -r USER=${USER:?Missing the username} +declare -r PASS=${PASS:?Missing the secret password for $USER} +``` + +## Atomic Locking with `mkdir` + +Portable advisory locks can be emulated with `mkdir` because it is atomic: + +```bash +lockdir=/tmp/myjob.lock +if mkdir "$lockdir" 2>/dev/null; then + trap 'rmdir "$lockdir"' EXIT INT TERM + # critical section + do_work +else + echo "Another instance is running" >&2 + exit 1 +fi +``` + +## Smarter globs and faster find-exec + +- Enable extended globs when useful: `shopt -s extglob`; then patterns like `!(tmp|cache)` work. +- Use `-exec ... {} +` to batch many paths in fewer process invocations: + +```bash +find . -name '*.log' -exec gzip -9 {} + +``` diff --git a/prompts/skills/bash-best-practices/reference/error-handling.md b/prompts/skills/bash-best-practices/reference/error-handling.md new file mode 100644 index 0000000..ef06460 --- /dev/null +++ b/prompts/skills/bash-best-practices/reference/error-handling.md @@ -0,0 +1,66 @@ +# Bash Error Handling and Safety + +## Paranoid mode (`set -e`) + +Enable `set -e` so the script exits on any unexpected non-zero status. Temporarily disable it around commands that are allowed to fail. + +```bash +set -e + +some_function () { + # ... critical code ... + + set +e + grep ... || true + local -i ec=$? + set -e + + if (( ec != 0 )); then + : # handle expected non-match + fi +} +``` + +## `pipefail` + +Use `set -o pipefail` so the pipeline returns the status of the last command that exited non-zero, not just the last command. + +```bash +set -o pipefail +command1 | command2 | command3 +``` + +## `PIPESTATUS` + +Capture `PIPESTATUS` into an array immediately after a pipeline to inspect each stage's exit code. + +```bash +tar -cf - ./* | ( cd "$dir" && tar -xf - ) +return_codes=("${PIPESTATUS[@]}") +if (( return_codes[0] != 0 )); then + echo 'tar failed' >&2 +fi +``` + +## Arithmetic and Comparisons + +Use arithmetic evaluation `(( ))` or numeric comparison operators (`-gt`, `-lt`, `-eq`) to avoid unintended lexicographical comparison. + +```bash +# Wrong: lexicographical +if [[ "$my_var" > 3 ]]; then + +# Right: numeric +if (( my_var > 3 )); then +if [[ "$my_var" -gt 3 ]]; then +``` + +## Restricted Bash + +Use `rbash` as a coarse sandbox for highly constrained environments. + +```bash +rbash -c 'echo hi' +``` + +See `man bash` (RESTRICTED SHELL) for details and caveats. diff --git a/prompts/skills/bash-best-practices/reference/functions.md b/prompts/skills/bash-best-practices/reference/functions.md new file mode 100644 index 0000000..083b7a2 --- /dev/null +++ b/prompts/skills/bash-best-practices/reference/functions.md @@ -0,0 +1,77 @@ +# Bash Function Patterns + +## Function Naming and Namespacing + +- Prefer the POSIX-compatible form: `name() { ... }` +- Emulate namespaces with `::`, e.g. `pkg::lang::action`. +- Use `FUNCNAME[0]` for self-aware logging. + +```bash +log() { + local -r callee=${FUNCNAME[1]} + echo "$callee: $*" >&2 +} +``` + +## Private / Internal Functions + +Mark internal helpers with a leading underscore so the public API is obvious: `module::_helper`. This matches the convention used by many Bash projects. + +```bash +# Public +foo::generate () { ... } + +# Internal only +foo::_sort_entries () { ... } +``` + +## Function Arguments: Assign-then-Shift + +Assign function arguments to named `local` variables immediately using `$1`, then `shift`. This makes adding and removing arguments easy without renumbering. + +```bash +some_function () { + local -r param_foo="$1"; shift + local -r param_bar="$1"; shift + local -r param_baz="$1"; shift +} +``` + +## Scope and Functions + +- Functions declared inside other functions are global once defined. +- `export -f function_name` makes a function available in subshells (e.g. `xargs -P`). +- `local` variables have dynamic scope: they are visible down the call stack. + +## Chaining Conditionals + +Functions return exit statuses and can be chained in conditionals. + +```bash +if deploy_check || smoke_test; then + echo "All good." +else + echo "Something failed." >&2 +fi +``` + +## `case` for Multi-Branch String Dispatch + +Replace long `if/elif` chains with `case ... esac` when matching literal string patterns. It is more readable, avoids quoting pitfalls, and performs exact matching. + +```bash +case "$line" in + '* ') + html::make_list_item "$line" + ;; + '# '*) + html::make_heading "$line" 1 + ;; + '## '*) + html::make_heading "$line" 2 + ;; + *) + html::make_paragraph "$line" + ;; +esac +``` diff --git a/prompts/skills/bash-best-practices/reference/io-patterns.md b/prompts/skills/bash-best-practices/reference/io-patterns.md new file mode 100644 index 0000000..ef70e4f --- /dev/null +++ b/prompts/skills/bash-best-practices/reference/io-patterns.md @@ -0,0 +1,132 @@ +# Bash I/O, Pipelines, and Data Processing + +## Built-ins vs External Commands + +- Prefer Bash built-ins for light text processing and arithmetic. +- Use external commands (`sed`, `awk`, `grep`, `cut`, `tr`, `bc`) for heavy or complex text processing. + +```bash +# Prefer built-in +addition=$(( X + Y )) + +# Prefer external for sophisticated transforms +substitution="$(echo "$string" | sed -e 's/^foo/bar/')" +``` + +## Process Substitution + +Use `<(command)` and `>(command)` for treating command output as a file. + +```bash +diff -u <(sort file1) <(sort file2) +tar cjf >(bzip2 -c > file.tar.bz2) foo +``` + +## `while read` with Process Substitution + +When iterating over command output and modifying variables in the parent shell, use process substitution as the input source rather than piping into `while`. A pipe creates a subshell, so variable changes are lost. + +```bash +# Good: changes to $count survive +local -i count=0 +while IFS='' read -r line; do + (( count++ )) +done < <(command) + +# Bad: $count is lost because the while runs in a subshell +local -i count=0 +command | while IFS='' read -r line; do + (( count++ )) +done +``` + +### `IFS='' read -r line` for exact line preservation + +Use `IFS='' read -r line` when reading lines you intend to preserve exactly, including leading and trailing whitespace. Without `IFS=''`, leading/trailing whitespace is stripped; without `-r`, backslashes are interpreted. + +```bash +while IFS='' read -r line; do + echo "$line" +done < file.txt +``` + +## Here-Documents and Here-Strings + +Use here-docs and here-strings for multi-line or inline input. + +```bash +# Here-document with variable interpolation +cat <<EOF +Hello $USER +EOF + +# Literal here-document (no interpolation) +cat <<'EOF' +$USER is not expanded +EOF + +# Here-string +if grep -q foo <<< "$VAR"; then + echo match +fi +``` + +Use `<<-EOF` to strip leading tabs from the body. + +### Prefer here-strings over `echo | command` + +For single-line input, prefer `command <<< "$var"` instead of `echo "$var" | command`. It avoids an extra pipe, subshell, and process spawn. + +```bash +# Good +tr '[:upper:]' '[:lower:]' <<< "$text" + +# Avoid +echo "$text" | tr '[:upper:]' '[:lower:]' +``` + +## Input Placeholders and Redirection + +- `-` as stdin/stdout placeholder for commands like `tar` and `cat`. +- Redirect via file descriptors explicitly (`2>/dev/null`, `1>&2`). +- Remember redirection order matters. + +```bash +echo Foo 2>/dev/null 1>&2 # suppresses everything +``` + +## `/dev/tcp` Networking + +Bash supports TCP via pseudo-files: + +```bash +cat < /dev/tcp/time.nist.gov/13 +exec 5<>/dev/tcp/google.de/80 +``` + +## List Processing: Pipes over Arrays + +For simple list processing, prefer pipelines over arrays. Pass data through stdout to the next stage and use stderr for logging. + +```bash +main () { + filter_lines | + process_lines | + postprocess_lines | + generate_report +} +``` + +## Reading Files and Arrays + +- **Read a whole file into a variable** without spawning `cat`: `cfg=$(<config.ini)` +- **Read lines into an array** safely with `mapfile` (aka `readarray`): `mapfile -t lines < file` +- **Assign formatted strings without a subshell** using `printf -v`: `printf -v msg 'Hello %s' "$USER"` + +## Safe xargs with NULs + +Avoid breaking on spaces/newlines by pairing `find -print0` with `xargs -0`: + +```bash +find . -type f -name '*.log' -print0 | xargs -0 rm -f +``` diff --git a/prompts/skills/bash-best-practices/reference/style.md b/prompts/skills/bash-best-practices/reference/style.md new file mode 100644 index 0000000..6932393 --- /dev/null +++ b/prompts/skills/bash-best-practices/reference/style.md @@ -0,0 +1,131 @@ +# Bash Style and Structure + +## Shebang + +Use `#!/usr/bin/env bash` for portability across Unix-like systems (not all have Bash at `/bin/bash`). + +```bash +#!/usr/bin/env bash +``` + +## Strict Mode Header + +Start every script with a strict-mode header. Combine `set -e`, `set -u`, and `set -o pipefail` so the script aborts on unexpected errors, unset variables, and pipeline failures. + +```bash +set -euo pipefail +``` + +Some projects also add `set -f` (disable pathname expansion). Choose the combination appropriate for your script and apply it consistently. + +If a script sources configuration files that may leave variables unset, initialize those variables to empty strings **before** enabling `set -u`: + +```bash +test -z "$CONFIG_FILE_PATH" && CONFIG_FILE_PATH='' +test -z "$LOG_VERBOSE" && LOG_VERBOSE='' +set -euo pipefail +``` + +Alternatively, access potentially-unset optional variables with `${VAR:-}` or `${VAR:-default}`: + +```bash +if [ -f "${HTML_JS_SCRIPT:-}" ]; then + cp "$HTML_JS_SCRIPT" "$dest" +fi +``` + +## Command Substitution + +Always use `$(...)` instead of backticks. It nests cleanly, is easier to read, and avoids quoting issues. + +```bash +# Good +date_stamp=$(date +%Y%m%d) + +# Bad (backticks) +date_stamp=`date +%Y%m%d` +``` + +## Indentation and Line Length + +- **Indentation**: Use soft-tabs (spaces), not tabs. Two or four spaces are both acceptable; pick one and be consistent within the project. +- **Line length**: Limit to 80 characters where practical. It encourages smaller functions and is friendlier on small screens. + +## Breaking Long Pipelines + +Break long pipelines with a backslash before the pipe and a leading pipe on continuation lines. The leading pipe is a visual eye-catcher. + +```bash +# Good +command1 \ + | command2 \ + | command3 \ + | command4 +``` + +## Quoting Variables + +- Quote variables when the value comes from external input, may contain whitespace, or is unknown. +- In small scripts with simple bare-word values, unquoted variables are acceptable for readability. +- In large or shared scripts, quote consistently to avoid accidents and keep ShellCheck happy. +- Use `${var}` braces only when required (adjacent text, arrays) or when they improve clarity. + +```bash +# Unknown/external input: quote +echo "${greeting} ${name}!" + +# Simple bare words: optional but be consistent +local -r greeting=Hello +local -r name=Paul +echo "$greeting $name!" + +# Braces required +echo "foo${FOO}baz" +``` + +## Boolean Style + +Bash has no native boolean. Use the string literals `yes` and `no`. + +```bash +declare -r SUGAR_FREE=yes +declare -r I_NEED_THE_BUZZ=no +``` + +## Multi-line Comments + +Use a here-doc redirected to the null command for multi-line comments. + +```bash +: <<COMMENT +This is a multi-line comment. +COMMENT +``` + +## `declare` Modifiers + +Use `declare` modifiers for clarity and safety: + +- `-r` for read-only (constants) +- `-i` for integers +- `-a` for indexed arrays +- `-A` for associative arrays (Bash 4+) + +```bash +declare -r MAX_RETRIES=3 +declare -i counter=0 +declare -a fruits=(apple banana cherry) +``` + +## `local -i` for Integer Variables + +Declare integer locals with `local -i` so arithmetic is cleaner and safer. + +```bash +some_function () { + local -i num_files=0 + num_files=$(( num_files + 1 )) + # Alternatively: + (( num_files++ )) +} +``` |
