shell-scripting
Use this skill when writing bash or zsh scripts, parsing arguments, handling errors, or automating CLI workflows. Triggers on bash scripting, shell scripts, argument parsing, process substitution, here documents, signal trapping, exit codes, and any task requiring portable shell script development.
devtools bashzshshellscriptingcliautomationWhat is shell-scripting?
Use this skill when writing bash or zsh scripts, parsing arguments, handling errors, or automating CLI workflows. Triggers on bash scripting, shell scripts, argument parsing, process substitution, here documents, signal trapping, exit codes, and any task requiring portable shell script development.
shell-scripting
shell-scripting is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex. Writing bash or zsh scripts, parsing arguments, handling errors, or automating CLI workflows.
Quick Facts
| Field | Value |
|---|---|
| Category | devtools |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill shell-scripting- The shell-scripting skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
Shell scripting is the art of automating tasks through the Unix shell - combining built-in commands, control flow, and process management to build reliable CLI tools and automation workflows. This skill covers production-quality bash and zsh scripting: robust error handling, portable argument parsing, safe file operations, and the idioms that separate fragile one-liners from scripts that hold up in production.
Tags
bash zsh shell scripting cli automation
Platforms
- claude-code
- gemini-cli
- openai-codex
Related Skills
Pair shell-scripting with these complementary skills:
Frequently Asked Questions
What is shell-scripting?
Use this skill when writing bash or zsh scripts, parsing arguments, handling errors, or automating CLI workflows. Triggers on bash scripting, shell scripts, argument parsing, process substitution, here documents, signal trapping, exit codes, and any task requiring portable shell script development.
How do I install shell-scripting?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill shell-scripting in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support shell-scripting?
This skill works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Shell Scripting
Shell scripting is the art of automating tasks through the Unix shell - combining built-in commands, control flow, and process management to build reliable CLI tools and automation workflows. This skill covers production-quality bash and zsh scripting: robust error handling, portable argument parsing, safe file operations, and the idioms that separate fragile one-liners from scripts that hold up in production.
When to use this skill
Trigger this skill when the user:
- Asks to write or review a bash or zsh script
- Needs to parse command-line arguments or flags
- Wants to automate a CLI workflow or task runner
- Asks about exit codes, signal trapping, or error handling in shell
- Needs to process files, lines, or streams from the terminal
- Asks about here documents, process substitution, or subshells
- Wants a portable script that works across bash, zsh, and sh
Do NOT trigger this skill for:
- Python or Node.js CLI tools (shell is the wrong tool for complex logic)
- Scripts that require structured data parsing at scale (use a real language instead)
Key principles
Always use
set -euo pipefail- Start every non-trivial script with this.-eexits on error,-utreats unset variables as errors,-o pipefailcatches failures in pipelines. Without this, silent failures hide bugs for weeks.Quote everything - Always double-quote variable expansions:
"$var","$@","${array[@]}". Unquoted variables break on whitespace and glob characters. The only exceptions are intentional word splitting and arithmetic contexts.Check dependencies upfront - Verify required commands exist before the script runs. Fail fast at the top with a clear error, not halfway through a destructive operation.
Use functions for reuse and readability - Extract logic into named functions. Shell functions support local variables (
local), can return exit codes, and make scripts testable. Amain()function at the bottom with a guard is idiomatic.Prefer shell built-ins over external commands -
[[ ]]over[ ],${var##*/}overbasename,${#str}overwc -c. Built-ins are faster, more portable, and avoid spawning subshells. Useprintfoverechofor reliable output formatting.
Core concepts
Exit codes - Every command returns an integer 0-255. 0 means success; any
non-zero value means failure. Use $? to read the last exit code. Use explicit
exit N to return meaningful codes from scripts. The || and && operators
branch on exit code.
File descriptors - 0 = stdin, 1 = stdout, 2 = stderr. Redirect stderr
with 2>file or merge it into stdout with 2>&1. Use >&2 to write errors to
stderr so they don't pollute captured output.
Subshells - Parentheses (cmd) run commands in a child process. Changes to
variables, cd, or set inside a subshell do not affect the parent. Command
substitution $(cmd) also runs in a subshell and captures its stdout.
Variable scoping - All variables are global by default. Use local inside
functions to limit scope. declare -r creates read-only variables. declare -a
declares arrays; declare -A declares associative arrays (bash 4+).
IFS (Internal Field Separator) - Controls how bash splits words and lines.
Default is space/tab/newline. When reading files line by line, set IFS= to
prevent trimming of leading/trailing whitespace: while IFS= read -r line.
Common tasks
Robust script template with trap cleanup
Every production script should start with this foundation:
#!/usr/bin/env bash
set -euo pipefail
# --- constants ---
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly TMP_DIR="$(mktemp -d)"
# --- cleanup ---
cleanup() {
local exit_code=$?
rm -rf "$TMP_DIR"
if [[ $exit_code -ne 0 ]]; then
echo "ERROR: $SCRIPT_NAME failed with exit code $exit_code" >&2
fi
exit "$exit_code"
}
trap cleanup EXIT INT TERM
# --- dependency check ---
require_cmd() {
if ! command -v "$1" &>/dev/null; then
echo "ERROR: required command '$1' not found" >&2
exit 1
fi
}
require_cmd curl
require_cmd jq
# --- main logic ---
main() {
echo "Running $SCRIPT_NAME from $SCRIPT_DIR"
# ... your logic here
}
main "$@"The trap cleanup EXIT fires on any exit - success, error, or signal - ensuring
temp files are always removed. BASH_SOURCE[0] resolves the script's real location
even when called via symlink.
Argument parsing with getopts and long opts
Use getopts for POSIX-portable short flags. For long options, use a while/case
loop with manual shift:
usage() {
cat >&2 <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <input>
Options:
-o, --output <dir> Output directory (default: ./out)
-v, --verbose Enable verbose logging
-h, --help Show this help
EOF
exit "${1:-0}"
}
OUTPUT_DIR="./out"
VERBOSE=false
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output)
[[ -n "${2-}" ]] || { echo "ERROR: --output requires a value" >&2; usage 1; }
OUTPUT_DIR="$2"; shift 2 ;;
-v|--verbose)
VERBOSE=true; shift ;;
-h|--help)
usage 0 ;;
--)
shift; break ;;
-*)
echo "ERROR: unknown option '$1'" >&2; usage 1 ;;
*)
break ;;
esac
done
# remaining positional args available as "$@"
INPUT_FILE="${1-}"
[[ -n "$INPUT_FILE" ]] || { echo "ERROR: input file required" >&2; usage 1; }
}
parse_args "$@"File processing - read, write, and temp files safely
# Read a file line by line without trimming whitespace or interpreting backslashes
while IFS= read -r line; do
echo "Processing: $line"
done < "$input_file"
# Read into an array
mapfile -t lines < "$input_file" # bash 4+; equivalent: readarray -t lines
# Write to a file atomically (avoids partial writes on failure)
write_atomic() {
local target="$1"
local tmp
tmp="$(mktemp "${target}.XXXXXX")"
# write to tmp, then atomically rename
cat > "$tmp"
mv "$tmp" "$target"
}
echo "final content" | write_atomic "/etc/myapp/config"
# Safe temp file with auto-cleanup (cleanup trap handles TMP_DIR removal)
local tmpfile
tmpfile="$(mktemp "$TMP_DIR/work.XXXXXX")"
some_command > "$tmpfile"
process_result "$tmpfile"String manipulation without external tools
# Substring extraction: ${var:offset:length}
str="hello world"
echo "${str:6:5}" # "world"
# Pattern removal (greedy ##, non-greedy #; greedy %%, non-greedy %)
path="/usr/local/bin/myapp"
echo "${path##*/}" # "myapp" (strip longest prefix up to /)
echo "${path%/*}" # "/usr/local/bin" (strip shortest suffix from /)
# Search and replace
filename="report-2024.csv"
echo "${filename/csv/tsv}" # "report-2024.tsv" (first match)
echo "${filename//a/A}" # "report-2024.csv" -> "report-2024.csv" (all matches)
# Case conversion (bash 4+)
lower="${str,,}" # all lowercase
upper="${str^^}" # all uppercase
title="${str^}" # capitalise first character
# String length and emptiness checks
[[ -z "$var" ]] && echo "empty"
[[ -n "$var" ]] && echo "non-empty"
echo "length: ${#str}"
# Check if string starts/ends with a pattern (no grep needed)
[[ "$str" == hello* ]] && echo "starts with hello"
[[ "$str" == *world ]] && echo "ends with world"Parallel execution with xargs and GNU parallel
# xargs: run up to 4 jobs in parallel, one arg per job
find . -name "*.log" -print0 \
| xargs -0 -P4 -I{} gzip "{}"
# xargs with a shell function (must export it first)
process_file() {
local f="$1"
echo "Processing $f"
# ... work ...
}
export -f process_file
find . -name "*.csv" -print0 \
| xargs -0 -P"$(nproc)" -I{} bash -c 'process_file "$@"' _ {}
# GNU parallel (more features: progress, retry, result collection)
# parallel --jobs 4 --bar gzip ::: *.log
# parallel -j4 --results /tmp/out/ ./process.sh ::: file1 file2 file3
# Manual background jobs with wait
pids=()
for host in "${hosts[@]}"; do
ssh "$host" uptime &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid" || echo "WARN: job $pid failed" >&2
donePortable scripts across bash, zsh, and sh
# Detect the running shell
detect_shell() {
if [ -n "${BASH_VERSION-}" ]; then
echo "bash $BASH_VERSION"
elif [ -n "${ZSH_VERSION-}" ]; then
echo "zsh $ZSH_VERSION"
else
echo "sh (POSIX)"
fi
}
# POSIX-safe array alternative (use positional parameters)
set -- alpha beta gamma
for item do # equivalent to: for item in "$@"
echo "$item"
done
# Use $(...) not backticks - both portable, but $() is nestable
result=$(echo "$(date) - $(whoami)")
# Avoid bashisms when targeting /bin/sh:
# [[ ]] -> [ ] (but be careful with quoting)
# local -> still works in most sh implementations (not POSIX but widely supported)
# readonly var=val (POSIX-safe)
# printf not echo -e (echo -e is not portable)
printf '%s\n' "Safe output with no echo flag issues"Interactive prompts and colored output
# Color constants (no-op when not a terminal)
setup_colors() {
if [[ -t 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; BOLD='\033[1m'; RESET='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; BLUE=''; BOLD=''; RESET=''
fi
}
setup_colors
log_info() { printf "${GREEN}[INFO]${RESET} %s\n" "$*"; }
log_warn() { printf "${YELLOW}[WARN]${RESET} %s\n" "$*" >&2; }
log_error() { printf "${RED}[ERROR]${RESET} %s\n" "$*" >&2; }
# Yes/no prompt
confirm() {
local prompt="${1:-Continue?} [y/N] "
local reply
read -r -p "$prompt" reply
[[ "${reply,,}" == y || "${reply,,}" == yes ]]
}
# Prompt with default value
prompt_with_default() {
local prompt="$1" default="$2" value
read -r -p "$prompt [$default]: " value
echo "${value:-$default}"
}
# Spinner for long operations
spin() {
local pid=$1 msg="${2:-Working...}"
local frames=('|' '/' '-' '\')
local i=0
while kill -0 "$pid" 2>/dev/null; do
printf "\r%s %s" "${frames[i++ % 4]}" "$msg"
sleep 0.1
done
printf "\r\033[K" # clear the spinner line
}Gotchas
set -eswallows non-zero exits in conditionals -set -edoes NOT exit on non-zero returns insideif,while,until, or||/&&chains. A command likeif some_command; thenwill not trigger-eifsome_commandfails - this is correct behavior but surprises people who expect-eto be a global safety net.localdoes not isolate errors fromset -e-local var=$(command_that_fails)always returns exit code 0 becauselocalitself succeeds. The subcommand failure is silently swallowed. Declarelocal varon one line, thenvar=$(command_that_fails)on the next soset -ecan catch it.mktempwithout-dcreates a file, not a directory -TMP=$(mktemp)creates a temp file. If you then trymkdir "$TMP/subdir"it fails. Usemktemp -dwhen you need a temp directory.Trap fires on subshell exits too - A
trap cleanup EXITin a parent script also fires when any subshell( ... )in that script exits. If your cleanup function deletes temp directories, a subshell exit mid-script can remove files the parent still needs. Usetrapselectively or test$BASH_SUBSHELLinside the trap function.Word splitting on array expansion without
[@]-"${arr[*]}"expands the array as a single word joined byIFS;"${arr[@]}"expands each element as a separate word. Using*instead of@when passing arrays to functions causes multi-word elements to silently merge.
Anti-patterns
| Anti-pattern | Why it's wrong | What to do instead |
|---|---|---|
Missing set -euo pipefail |
Errors in pipelines and unset variables are silently ignored, causing downstream data corruption | Add set -euo pipefail as the second line of every script |
Unquoted variable: rm -rf $dir |
If $dir is empty or contains spaces, the command destroys unintended paths |
Always quote: rm -rf "$dir" |
Parsing ls output |
ls output is designed for humans; filenames with spaces or newlines break word splitting |
Use find ... -print0 | xargs -0 or a for f in ./* glob |
Using cat file | grep (useless cat) |
Spawns an extra process for no reason | Use input redirection: grep pattern file |
if [ $? -eq 0 ] |
Testing $? after the fact is fragile - any intervening command resets it |
Test the command directly: if some_command; then ... |
| Heredoc with leading whitespace | Indented heredoc content with <<EOF includes the indentation literally |
Use <<-EOF to strip leading tabs (not spaces), or use printf |
References
For detailed reference content, see:
references/bash-cheatsheet.md- Quick reference for bash built-ins, parameter expansion, test operators, and special variables
References
bash-cheatsheet.md
Bash Cheatsheet
Quick reference for writing production bash scripts. Covers the constructs used most often in real automation work.
Special variables
| Variable | Meaning |
|---|---|
$0 |
Script name (path as invoked) |
$1 .. $9 |
Positional arguments |
$@ |
All positional args as separate words (always quote: "$@") |
$* |
All positional args as a single string (rarely useful) |
$# |
Number of positional arguments |
$? |
Exit code of the last command |
$$ |
PID of the current shell |
$! |
PID of the last background job |
$_ |
Last argument of the previous command |
BASH_SOURCE[0] |
Path of the currently executing script file |
LINENO |
Current line number in the script |
FUNCNAME[0] |
Name of the current function |
IFS |
Internal Field Separator (default: space/tab/newline) |
Parameter expansion
Basic
| Syntax | Result |
|---|---|
${var} |
Value of var (braces prevent ambiguity) |
${var:-default} |
Value of var, or default if unset or empty |
${var:=default} |
Value of var; also assigns default if unset or empty |
${var:?message} |
Value of var; exits with message if unset or empty |
${var:+other} |
other if var is set and non-empty; else empty string |
Substring
| Syntax | Result |
|---|---|
${var:offset} |
Substring from offset to end |
${var:offset:length} |
Substring of length starting at offset |
${#var} |
Length of var |
Pattern removal
| Syntax | Result |
|---|---|
${var#pattern} |
Remove shortest prefix matching pattern |
${var##pattern} |
Remove longest prefix matching pattern |
${var%pattern} |
Remove shortest suffix matching pattern |
${var%%pattern} |
Remove longest suffix matching pattern |
Common uses:
"${path##*/}" # basename equivalent
"${path%/*}" # dirname equivalent
"${file%.txt}" # strip .txt extensionSubstitution and case
| Syntax | Result |
|---|---|
${var/pattern/replace} |
Replace first match of pattern with replace |
${var//pattern/replace} |
Replace all matches |
${var/#pattern/replace} |
Replace if pattern matches at start |
${var/%pattern/replace} |
Replace if pattern matches at end |
${var,,} |
Convert all characters to lowercase (bash 4+) |
${var^^} |
Convert all characters to uppercase (bash 4+) |
${var^} |
Capitalise first character (bash 4+) |
Test operators
File tests ([[ -X file ]])
| Operator | True if |
|---|---|
-e file |
File exists (any type) |
-f file |
Regular file exists |
-d file |
Directory exists |
-L file |
Symbolic link exists |
-r file |
File is readable |
-w file |
File is writable |
-x file |
File is executable |
-s file |
File exists and is non-empty |
-z file |
File is zero bytes |
f1 -nt f2 |
f1 is newer than f2 |
f1 -ot f2 |
f1 is older than f2 |
String tests
| Operator | True if |
|---|---|
-z "$s" |
String is empty |
-n "$s" |
String is non-empty |
"$a" == "$b" |
Strings are equal |
"$a" != "$b" |
Strings are not equal |
"$s" == pattern |
String matches glob pattern (no quotes on pattern) |
"$s" =~ regex |
String matches extended regex (bash only) |
Integer tests
| Operator | True if |
|---|---|
$a -eq $b |
Equal |
$a -ne $b |
Not equal |
$a -lt $b |
Less than |
$a -le $b |
Less than or equal |
$a -gt $b |
Greater than |
$a -ge $b |
Greater than or equal |
In (( )) arithmetic context, use ==, !=, <, <=, >, >= directly.
Bash built-ins
| Built-in | Purpose |
|---|---|
echo |
Print text (avoid -e; not portable) |
printf |
Formatted output; portable and reliable |
read |
Read a line from stdin into a variable |
local |
Declare a function-scoped variable |
declare |
Declare variables with attributes (-r, -i, -a, -A) |
readonly |
Mark a variable as immutable |
export |
Make a variable available to child processes |
source / . |
Execute a script in the current shell context |
eval |
Execute a string as a shell command (use with extreme caution) |
mapfile / readarray |
Read lines from stdin into an array (bash 4+) |
typeset |
Alias for declare (also used in zsh) |
trap |
Register a command to run on a signal or exit |
wait |
Wait for background jobs to finish |
jobs |
List background jobs |
disown |
Remove a job from the shell's job table |
getopts |
Parse short option flags |
shift |
Shift positional parameters left by N |
set |
Set shell options or positional parameters |
unset |
Remove a variable or function |
pushd / popd |
Directory stack navigation |
command |
Bypass shell functions; run the external command directly |
type |
Show how a name is interpreted (function, built-in, file) |
compgen |
Generate completions (useful in scripts for listing commands) |
Redirection
| Syntax | Meaning |
|---|---|
cmd > file |
Redirect stdout to file (overwrite) |
cmd >> file |
Redirect stdout to file (append) |
cmd < file |
Read stdin from file |
cmd 2> file |
Redirect stderr to file |
cmd 2>&1 |
Redirect stderr to stdout |
cmd &> file |
Redirect both stdout and stderr to file (bash) |
cmd 2>/dev/null |
Discard stderr |
cmd > /dev/null 2>&1 |
Discard all output (portable) |
cmd1 | cmd2 |
Pipe stdout of cmd1 to stdin of cmd2 |
cmd <<EOF ... EOF |
Here document - feed multi-line string as stdin |
cmd <<-EOF ... EOF |
Here document stripping leading tabs |
cmd <(other) |
Process substitution - treat output of other as a file |
cmd >(other) |
Process substitution - write to stdin of other via a file |
Arrays
# Declare and populate
arr=("alpha" "beta" "gamma")
declare -a arr
# Access
echo "${arr[0]}" # first element
echo "${arr[-1]}" # last element (bash 4.3+)
echo "${arr[@]}" # all elements (always quote)
echo "${#arr[@]}" # number of elements
echo "${!arr[@]}" # all indices
# Append
arr+=("delta")
# Slice: ${arr[@]:offset:length}
echo "${arr[@]:1:2}" # elements 1 and 2
# Delete element
unset 'arr[1]'
# Iterate safely
for item in "${arr[@]}"; do
echo "$item"
done
# Associative array (bash 4+)
declare -A map
map["key"]="value"
echo "${map["key"]}"
echo "${!map[@]}" # all keysArithmetic
# (( )) for integer arithmetic - returns exit code 0/1 for true/false
(( count++ ))
(( total = a + b * c ))
if (( count > 10 )); then echo "too many"; fi
# $(( )) for arithmetic substitution
result=$(( 2 ** 10 )) # 1024
echo $(( RANDOM % 100 )) # random 0-99
# bc for floating point
result=$(echo "scale=2; 22/7" | bc)Common patterns
# Default value for missing first argument
input="${1:-default.txt}"
# Require exactly one argument
[[ $# -eq 1 ]] || { echo "Usage: $0 <file>" >&2; exit 1; }
# Check if running as root
[[ $EUID -eq 0 ]] || { echo "Must run as root" >&2; exit 1; }
# Check if a command exists
command -v docker &>/dev/null || { echo "docker not found" >&2; exit 1; }
# Absolute path of the script's directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Silent background job
some_long_command &>/dev/null &
# Retry a command up to N times
retry() {
local n="$1"; shift
local delay="${1:-2}"; shift
local i
for (( i=1; i<=n; i++ )); do
"$@" && return 0
echo "Attempt $i/$n failed. Retrying in ${delay}s..." >&2
sleep "$delay"
done
return 1
}
retry 3 2 curl -sf https://example.com Frequently Asked Questions
What is shell-scripting?
Use this skill when writing bash or zsh scripts, parsing arguments, handling errors, or automating CLI workflows. Triggers on bash scripting, shell scripts, argument parsing, process substitution, here documents, signal trapping, exit codes, and any task requiring portable shell script development.
How do I install shell-scripting?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill shell-scripting in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support shell-scripting?
shell-scripting works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.