From coding-standards
This skill should be used when the user asks to "write bash script", "create shell script", "review bash code", "write hook", "create systemd service script", "fix shellcheck warnings", "improve bash script", mentions "bash", "shell", ".sh files", or works with any shell scripting including hooks and CLI tools. Enforces bash purist style with strict mode, die() function, proper variable declarations, assertion comments, result comments, shellcheck compliance, and structured function layout.
How this skill is triggered — by the user, by Claude, or both
Slash command
/coding-standards:dignified-bashThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
**First, evaluate whether Bash is the right choice for the task.** Bash excels at:
First, evaluate whether Bash is the right choice for the task. Bash excels at:
grep, awk, sed, curl) do the heavy liftingConsider Python instead when the task involves:
jq one-liners)Python is available on virtually every Linux system out of the box. Switching from Bash+jq to Python often reduces code size by 2-3x while improving readability and maintainability.
Rule of thumb: If your Bash script has more jq calls than shell commands, or exceeds ~100 lines, reconsider the language choice.
IMPORTANT: Always use Bash, never plain sh. Even for simple scripts, hooks, or one-liners — always use #!/usr/bin/env bash. Plain POSIX sh lacks essential features (arrays, [[ ]], set -o pipefail) and there's no benefit to avoiding bash on modern systems.
All shell scripts must adhere to the Bash Purist style to ensure robustness, readability, and maintainability.
#!/usr/bin/env bash
set -euo pipefail
shellcheck without warnings.
shellcheck is not available, install it first: sudo apt install shellcheck (Debian/Ubuntu) or equivalent for other systems.shellcheck <script.sh> after writing or modifying a script.bash -n: Run bash -n script.sh for quick syntax validation without executing the script. Catches syntax errors that shellcheck might miss. May produce occasional false positives, but still useful as a first-pass check.die() function for fatal errors instead of inline { echo "error"; exit 1; }:
function die {
local -r message="${1:-}"
local -ri code="${2:-1}"
echo "FATAL: ${message}"
exit "$code"
} 1>&2
} 1>&2 instead of >&2 inside the function. This makes all function output go to stderr automatically.# assert: config file exists
[[ -f "$config_file" ]] || die "Config file not found: $config_file"
# assert: source library exists
source "$lib_path" || die "Failed to source: $lib_path" 2
} 1>&2 pattern for any function that should output to stderr:
function log_error {
local -r message="$1"
echo "ERROR: ${message}"
} 1>&2
function log_warn {
local -r message="$1"
echo "WARN: ${message}"
} 1>&2
UPPER_CASE: Read-only constants (declare -r) and environment variables.lower_case: Local variables and function arguments.declare, local, or readonly.declare -i or local -i for integers (e.g., boolean flags 0/1, counters).declare -r or local -r for immutable variables.local -a for indexed arrays inside functions.local -A for associative arrays (dicts) inside functions.# few variables: single line
local network_mode container_name
# many variables: group by domain
local project project_dir project_config
local container_name container_id
local output result
SC2155):
local result
result=$( some_command )
$( <file ) instead of $(cat file) to read file contents (avoids subprocess):
content=$( <"$file_path" )
Command substitution $(...) creates a subshell where:
Be careful with functions that read stdin or modify parent scope — calling them via $(func) will fail silently:
# BROKEN: subshell doesn't get stdin
input=$( read_stdin_func )
Solution: Use nameref (local -n) to assign directly to caller's variable:
function read_stdin {
local -n ref="$1"
IFS= read -r -d '' ref || true
}
read_stdin 'my_data' # pass variable NAME, not value
declare -A / local -A) where they simplify code logic or improve readability.# dicts
local -A config=(
['host']='localhost'
['port']='8080'
['timeout']='30'
)
# dicts: incremental assignment
declare -A restarted_projects
restarted_projects['myproject']=1
(( )) for integer comparisons and boolean checks.# flags
declare -i is_dry_run=0
if (( is_dry_run )); then
...
fi
(( count++ ))
(( total = a + b ))
[[ ]] for conditional expressions instead of the legacy [ (test) command.&& and ||:
# good: single bracket with combined conditions
if [[ -n "$project" && -n "$project_dir" ]]; then
...
fi
# avoid: multiple brackets
if [[ -n "$project" ]] && [[ -n "$project_dir" ]]; then
...
fi
[[ -d "$dir" ]]
(( count > 0 ))
result=$( some_command )
&&/|| chains are NOT if-then-else. The pattern foo && bar || baz does NOT work like if foo; then bar; else baz; fi. If bar returns non-zero, baz will also execute:
# BROKEN: baz runs if bar fails, not just if foo fails
check_something && do_work || handle_error
# SAFE: bar cannot fail (variable assignment)
[[ -f "$file" ]] && found=1 || found=0
# CORRECT: use proper if-then-else for complex logic
if check_something; then
do_work
else
handle_error
fi
Only use &&/|| chains when the middle command cannot fail (e.g., variable assignments, echo, true).read in loops and last line without newline. read returns non-zero at EOF even if it read data. If the last line has no trailing newline, it will be skipped:
# BROKEN: skips last line if file has no trailing newline
while read -r line; do
process "$line"
done < file
# CORRECT: also check if variable has content
while read -r line || [[ -n "$line" ]]; do
process "$line"
done < file
# assert: <description>:
# good: early returns with assert comments
function process_file {
local -r file="$1"
# assert: file exists
[[ -f "$file" ]] || return 1
# assert: file is readable
[[ -r "$file" ]] || return 1
# main logic here, not nested
...
}
# assert: config file exists
[[ -f "$config_file" ]] || die "Config not found: $config_file"
# assert: directory exists
[[ -d "$target_dir" ]] || return 1
# assert: DNS is broken (inverted logic)
check_dns "$container" || return 0
# result: <description>:
function check_container_dns {
# args
local -r container_id="$1"
# vars
local resolv_conf
# code
resolv_conf=$( docker exec "$container_id" cat /etc/resolv.conf 2>/dev/null ) || return 1
# result: true if DNS is broken
[[ "$resolv_conf" == *"$DNS_ERROR_PATTERN"* ]]
}
function is_valid_port {
# args
local -r port="$1"
# result: true if port is in valid range
(( port >= 1 && port <= 65535 ))
}
Functions must follow a strict "Sections" layout:
local -r variables.# consts for local -r# flags for local -i boolean flags (0/1)# arrays for local -a# dicts for local -Alocal for regular variablesExample:
function process_data {
# args
local -r input_dir="$1" output_file="$2"
# consts
local -r max_items=100
# flags
local -i verbose=0
# arrays
local -a items
# dicts
local -A item_counts
# vars
local -i count=0
local item
# code
# assert: input directory exists
[[ -d "$input_dir" ]] || return 1
for item in "${input_dir}"/*; do
...
done
}
die() for failed source:
source "$script_dir/lib.sh" || die "Failed to source: lib.sh"
printf built-ins (%(fmt)T) instead of date to avoid subprocesses.# redirect stdout and stderr to file
command &> file.log # instead of: command > file.log 2>&1
# pipe stdout and stderr
command |& another # instead of: command 2>&1 | another
# discard all output
command &> /dev/null # instead of: command > /dev/null 2>&1
<<<: Use here-string to pass variable content to stdin without spawning a subshell:
# good: no subshell
grep 'pattern' <<< "$content"
jq '.field' <<< "$json"
# avoid: spawns subshell for echo
echo "$content" | grep 'pattern'
# single quotes: literals, fixed strings, empty strings
local fallback=''
local method='GET'
grep --fixed-strings 'error:' "$log_file"
# double quotes: variables, command substitution, interpolation
echo "Processing: $filename"
result="$(some_command)"
# empty line output: just echo without arguments
echo
grep, curl, jq) whenever they are available:
curl --location --insecure --request 'GET' --output 'file.txt' "$url"
"${arr[@]}" expansion. This preserves quoting and handles arguments with spaces correctly:
# arrays
local -a curl_args=(
'--location'
'--silent'
'--max-time' '30'
)
# code
if (( verbose )); then
curl_args+=( '--verbose' )
fi
curl "${curl_args[@]}" "$url"
Array formatting rules:
+=( '--verbose' ) can stay on one lineprintf with fixed-width format specifiers (%-N.Ns). Define the format string once and reuse it for both header and data rows:
# consts
local -r fmt='%-12.12s %-8.8s %-20.20s\n'
# code: header and rows use same format
# format string is a constant defined above, safe to use as variable
# shellcheck disable=SC2059
{
printf "$fmt" 'NAME' 'STATUS' 'MESSAGE'
printf "$fmt" '---' '---' '---'
for item in "${items[@]}"; do
printf "$fmt" "$name" "$status" "$message"
done
}
This is a justified use of shellcheck disable=SC2059 — the format string is a constant, and reusing it ensures consistent column widths across header and data rows.du -h or df -h (1K, 2.5M, 1.2G). Skip this for general-purpose scripts — it's overengineering if not needed:
function humanize_bytes {
# args
local -r bytes="$1"
# code
if (( bytes >= 1073741824 )); then
printf '%.1fG' "$(echo "scale=1; $bytes / 1073741824" | bc)"
elif (( bytes >= 1048576 )); then
printf '%.1fM' "$(echo "scale=1; $bytes / 1048576" | bc)"
elif (( bytes >= 1024 )); then
printf '%.1fK' "$(echo "scale=1; $bytes / 1024" | bc)"
else
printf '%dB' "$bytes"
fi
}
# shellcheck disable=SCxxxx only as a last resort when there is no way to fix or refactor the code. Always try to fix the underlying issue first. If disabling is unavoidable, ALWAYS add a comment on the preceding line explaining why it cannot be fixed:
# nameref intentionally modifies caller's variable, not a bug
# shellcheck disable=SC2034
input_ref="$temp"
npx claudepluginhub j2h4u/oh-my-claude-plugins --plugin coding-standardsGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.