| name | bash |
| description | Use when writing shell scripts or working with Bash, Zsh, or POSIX-compatible shells on macOS and Linux. Covers scripting fundamentals, variables, control flow, functions, pipes, process management, and common patterns for automation and developer tooling.
USE FOR: Bash, Zsh, shell scripting, POSIX shell, pipes, redirection, process substitution, shell functions, shell variables, .bashrc, .zshrc, shebang, here documents, command substitution, shell arithmetic
DO NOT USE FOR: PowerShell scripting (use powershell-core), Windows batch files, complex data processing beyond text (consider Python or jq)
|
| license | MIT |
| metadata | {"displayName":"Bash & Shell Scripting","author":"Tyler-R-Kendrick"} |
| compatibility | claude, copilot, cursor |
| references | [{"title":"GNU Bash Reference Manual","url":"https://www.gnu.org/software/bash/manual/bash.html"},{"title":"Zsh Documentation","url":"https://zsh.sourceforge.io/Doc/"},{"title":"POSIX Shell Command Language Specification","url":"https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html"}] |
Bash & Shell Scripting
Overview
Bash is the default shell on most Linux distributions and was the macOS default until Catalina (now Zsh). Shell scripting is the glue that connects CLI tools together and automates repetitive tasks. Most CI/CD pipelines, Docker entrypoints, and deployment scripts are Bash. Understanding shell scripting is a foundational skill for every developer working in Unix-like environments.
Bash vs Zsh vs Fish
| Feature | Bash | Zsh | Fish |
|---|
| Compatibility | POSIX | Mostly POSIX | Not POSIX |
| Default On | Most Linux distros | macOS (Catalina+) | — |
| Plugin Ecosystem | Minimal | Oh My Zsh / Starship | Built-in |
| Auto-complete | Basic | Extensive | Excellent, built-in |
| Scripting | Standard | Bash-compatible + extras | Unique syntax |
Tip: Write portable scripts in Bash (or POSIX sh) for maximum compatibility. Use Zsh/Fish features interactively but avoid them in shared scripts.
Fundamentals
Shebang
Every script should start with a shebang line that tells the OS which interpreter to use:
#!/usr/bin/env bash # Portable — finds bash in PATH
Variables
name="world"
count=42
readonly PI=3.14159
echo "Hello, ${name}"
echo "Count is: $count"
export GLOBAL_VAR="visible to child processes"
local_var="only in this shell"
echo "${MISSING_VAR:-default_value}"
echo "${MISSING_VAR:=default_value}"
Special Variables
| Variable | Meaning |
|---|
$0 | Script name |
$1-$9 | Positional arguments |
${10} | Positional args beyond 9 |
$# | Number of arguments |
$@ | All arguments (as separate words) |
$* | All arguments (as single string) |
$? | Exit code of last command |
$$ | PID of current shell |
$! | PID of last background process |
$_ | Last argument of previous command |
Quoting
name="world"
echo 'Hello, $name'
echo "Hello, $name"
today=$(date +%Y-%m-%d)
today=`date +%Y-%m-%d`
result=$((5 + 3))
echo "Sum: $result"
Control Flow
if / elif / else / fi
if [[ "$name" == "world" ]]; then
echo "Hello, world!"
elif [[ "$name" == "bash" ]]; then
echo "Hello, bash!"
else
echo "Hello, stranger!"
fi
if [[ $count -gt 10 ]]; then
echo "Count is greater than 10"
fi
if [[ -f "$file" ]]; then
echo "File exists"
elif [[ -d "$dir" ]]; then
echo "Directory exists"
fi
Common Test Operators
| Operator | Type | Meaning |
|---|
-f | File | File exists |
-d | File | Directory exists |
-e | File | Path exists |
-r | File | Readable |
-w | File | Writable |
-x | File | Executable |
-s | File | File is non-empty |
-z | String | String is empty |
-n | String | String is non-empty |
== | String | Strings are equal |
!= | String | Strings are not equal |
-eq | Numeric | Equal |
-ne | Numeric | Not equal |
-lt | Numeric | Less than |
-gt | Numeric | Greater than |
-le | Numeric | Less than or equal |
-ge | Numeric | Greater than or equal |
for Loops
for item in apple banana cherry; do
echo "Fruit: $item"
done
for file in *.txt; do
echo "Processing: $file"
done
for ((i = 0; i < 10; i++)); do
echo "Index: $i"
done
for user in $(cut -d: -f1 /etc/passwd); do
echo "User: $user"
done
while / until Loops
count=0
while [[ $count -lt 5 ]]; do
echo "Count: $count"
((count++))
done
until [[ $count -eq 0 ]]; do
echo "Countdown: $count"
((count--))
done
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
case Statements
case "$1" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
status|info)
echo "Checking status..."
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
Functions
greet() {
local name="${1:-World}"
echo "Hello, ${name}!"
}
greet "Bash"
greet
is_even() {
local num=$1
if (( num % 2 == 0 )); then
return 0
else
return 1
fi
}
if is_even 4; then
echo "4 is even"
fi
get_timestamp() {
date +%Y%m%d_%H%M%S
}
ts=$(get_timestamp)
echo "Timestamp: $ts"
Pipes and Redirection
ls -la | grep ".txt" | sort -k5 -n
echo "Hello" > output.txt
echo "World" >> output.txt
command 2> errors.log
command > output.log 2>&1
command &> output.log
command > /dev/null 2>&1
sort < unsorted.txt
cat <<EOF
Hello, $name!
Today is $(date).
EOF
cat <<'EOF'
This is literal: $name
No expansion here.
EOF
grep "pattern" <<< "$variable"
diff <(sort file1.txt) <(sort file2.txt)
Common Patterns
Script Template
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
cleanup() {
echo "Cleaning up..."
rm -f "$tmp_file"
}
trap cleanup EXIT
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
tmp_file=$(mktemp)
main() {
echo "Running ${SCRIPT_NAME} from ${SCRIPT_DIR}"
}
main "$@"
Parsing Command-Line Arguments
usage() {
echo "Usage: $0 [-v] [-o output] [-n count] input_file"
exit 1
}
verbose=false
output=""
count=1
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
verbose=true
shift
;;
-o|--output)
output="$2"
shift 2
;;
-n|--count)
count="$2"
shift 2
;;
-h|--help)
usage
;;
-*)
echo "Unknown option: $1"
usage
;;
*)
input_file="$1"
shift
;;
esac
done
[[ -z "${input_file:-}" ]] && usage
Reading Files Line by Line
while IFS= read -r line; do
echo "Processing: $line"
done < "$input_file"
while IFS= read -r line; do
[[ -z "$line" || "$line" == \#* ]] && continue
echo "$line"
done < config.txt
Finding and Processing Files
find . -name "*.log" -print0 | xargs -0 rm -f
find . -name "*.sh" -exec chmod +x {} \;
find /tmp -type f -name "*.tmp" -mtime +7 -delete
Checking Command Existence
if command -v docker &>/dev/null; then
echo "Docker is installed"
else
echo "Docker is not installed"
exit 1
fi
Conditional Execution
mkdir -p build && cd build
command -v git &>/dev/null || sudo apt install git -y
test -f config.yml && echo "Config found" || echo "Config missing"
Text Processing
grep — Search for Patterns
grep "error" logfile.txt
grep -i "error" logfile.txt
grep -r "TODO" src/
grep -n "function" script.sh
grep -c "error" logfile.txt
grep -v "debug" logfile.txt
grep -E "error|warning" logfile.txt
grep -l "pattern" *.txt
sed — Stream Editor (Find/Replace)
sed 's/old/new/' file.txt
sed 's/old/new/g' file.txt
sed -i 's/old/new/g' file.txt
sed -n '10,20p' file.txt
sed '/^#/d' file.txt
sed -i.bak 's/old/new/g' file.txt
awk — Columnar Data Processing
awk '{print $1}' file.txt
awk '{print $1, $3}' file.txt
awk -F: '{print $1}' /etc/passwd
awk '$3 > 100' data.txt
awk '{sum += $1} END {print sum}'
awk 'NR==1 || $2 > 50' data.txt
Other Text Tools
cut -d',' -f1,3 data.csv
sort file.txt
sort -n file.txt
sort -u file.txt
uniq file.txt
uniq -c file.txt
wc -l file.txt
wc -w file.txt
tr 'a-z' 'A-Z' < file.txt
tr -d '\r' < dos.txt > unix.txt
head -20 file.txt
tail -20 file.txt
tail -f logfile.txt
Process Management
long_running_task &
jobs
fg %1
bg %1
wait
wait $pid
kill $pid
kill -9 $pid
kill %1
nohup long_task &> output.log &
trap 'echo "Caught SIGINT"; exit 1' INT
trap 'cleanup' EXIT TERM
Shell Configuration
File Loading Order
Bash:
- Login shell:
/etc/profile -> ~/.bash_profile -> ~/.bash_login -> ~/.profile
- Interactive non-login:
~/.bashrc
- Non-interactive:
$BASH_ENV
Zsh:
- All:
~/.zshenv
- Login:
~/.zprofile -> ~/.zshrc -> ~/.zlogin
- Interactive:
~/.zshrc
Common Configuration
export PATH="$HOME/.local/bin:$HOME/bin:$PATH"
alias ll='ls -lah'
alias gs='git status'
alias gd='git diff'
alias dc='docker compose'
alias k='kubectl'
mkcd() {
mkdir -p "$1" && cd "$1"
}
export PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups
shopt -s histappend
Shellcheck
ShellCheck is a static analysis tool for shell scripts that catches common bugs and pitfalls.
Installation
brew install shellcheck
sudo apt install shellcheck
sudo pacman -S shellcheck
Usage
shellcheck myscript.sh
shellcheck --shell=bash myscript.sh
shellcheck --exclude=SC2034 myscript.sh
Common ShellCheck Warnings
| Code | Issue | Fix |
|---|
| SC2086 | Double quote to prevent globbing | Use "$var" instead of $var |
| SC2046 | Quote to prevent word splitting | Use "$(command)" |
| SC2034 | Variable appears unused | Remove or export it |
| SC2155 | Declare and assign separately | local var; var=$(cmd) |
| SC2162 | read without -r mangles backslashes | Use read -r |
CI Integration
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
scandir: './scripts'
Best Practices
- Always use
set -euo pipefail — Exit on errors (-e), treat undefined variables as errors (-u), and fail on any command in a pipeline (-o pipefail).
- Quote your variables — Always use
"$var" instead of $var to prevent word splitting and globbing issues.
- Use ShellCheck — Run shellcheck on every script to catch common bugs before they cause problems in production.
- Prefer
[[ ]] over [ ] — Double brackets are a Bash/Zsh built-in with better syntax, regex support, and no word splitting issues.
- Use functions for reusability — Break scripts into functions with
local variables to avoid polluting the global namespace.
- Add
#!/usr/bin/env bash shebang — The env form is more portable across systems where Bash may not be at /bin/bash.
- Trap for cleanup — Use
trap cleanup EXIT to ensure temporary files are removed and resources are released, even on errors.
- Avoid parsing
ls output — Use globs (for f in *.txt) or find instead of parsing ls, which breaks on filenames with spaces or special characters.