
Bash Script Validator
- 407 installs
- 286 repo stars
- Updated July 26, 2026
- akin-ozer/cc-devops-skills
bash-script-validator is an agent skill that validates bash scripts for syntax errors, common pitfalls, and unsafe patterns for developers who need shell automation to pass review before merge or deployment.
About
bash-script-validator is an agent skill from akin-ozer/cc-devops-skills that reviews bash shell scripts before they merge or deploy. The skill checks syntax validity, flags common bash pitfalls such as unquoted variables and fragile command substitutions, and surfaces unsafe patterns that cause production failures in CI/CD or server automation. Developers reach for bash-script-validator when shell scripts power deploy hooks, cron jobs, or pipeline steps and need an early review gate without running every script on a live host. It fits the ship-phase review step so shell automation fails in pull request review instead of during a production deployment.
- Catch bash syntax errors before runtime
- Flag unsafe or non-portable shell patterns
- Improve reliability of deploy scripts
- Speed up shell code review
- Reduce production automation surprises
Bash Script Validator by the numbers
- 407 all-time installs (skills.sh)
- Ranked #136 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/akin-ozer/cc-devops-skills --skill bash-script-validatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 407 |
|---|---|
| repo stars | ★ 286 |
| Last updated | July 26, 2026 |
| Repository | akin-ozer/cc-devops-skills ↗ |
How do you validate bash scripts before deployment?
Validate bash scripts for syntax, common pitfalls, and unsafe patterns before merge or deployment so shell automation fails early in review.
Who is it for?
DevOps engineers and backend developers who maintain bash deploy scripts, hooks, or CI shell steps and want pre-merge validation.
Skip if: Teams that have fully migrated pipelines to Python, Go, or Makefile tasks with no bash automation to review.
When should I use this skill?
A bash script is added or changed in a deploy hook, cron job, or CI step and needs syntax and safety review before merge.
What you get
Validation report listing syntax errors, pitfall warnings, and unsafe pattern findings
- validation report
- pitfall warnings
Files
Bash Script Validator
Overview
This skill validates Bash and POSIX shell scripts with layered checks:
1. Syntax validation (bash -n or sh -n) 2. ShellCheck static analysis (system binary or wrapper fallback) 3. Custom security, portability, and optimization checks
Use the default flow below, then branch to fallbacks only when the environment is constrained.
Trigger Guidance
Use this skill when the request includes script quality, linting, syntax checking, or shell portability work.
Trigger Phrases
- "Validate this bash script"
- "Lint this
.shfile" - "Find security issues in this shell script"
- "Why does this script fail ShellCheck?"
- "Make this script POSIX compliant"
- "Review this shell script before CI"
Non-Trigger Examples
- General Linux command questions with no script file
- Kubernetes, Terraform, or pipeline validation tasks that do not involve shell scripts
- Pure prose editing tasks
Deterministic Execution Model
Run commands from this skill directory:
cd devops-skills-plugin/skills/bash-script-validatorStep 1: Preflight
1. Confirm target path exists and is readable. 2. Confirm bash is available. 3. Determine whether fixes can be applied directly (write access) or only suggested (read-only).
Step 2: Run Baseline Validation (Default Path)
bash scripts/validate.sh <script-path>For deterministic stage behavior, set the ShellCheck provider explicitly:
# Modes: auto (default), system, wrapper, disabled
VALIDATOR_SHELLCHECK_MODE=system bash scripts/validate.sh <script-path>Record:
- Detected shell type
- Exit code (
0clean,1warnings,2errors) - All reported issue lines and ShellCheck codes (
SC####) when present
Step 3: Load Only Needed References
Progressive disclosure by issue type:
- ShellCheck code explanations:
docs/shellcheck-reference.md - General fix patterns and security mistakes:
docs/common-mistakes.md - Bash-only behavior:
docs/bash-reference.md - POSIX portability or bashism fixes:
docs/shell-reference.md - Text-processing optimization issues:
docs/grep-reference.md,docs/awk-reference.md,docs/sed-reference.md,docs/regex-reference.md(only when directly relevant)
Step 4: Provide or Apply Fixes
For each issue, include:
1. Exact location from validator output (line number and snippet) 2. Root cause 3. Corrected code 4. Why the change is safer or more portable 5. Subsection-level citation (format below)
If the request includes patching files and write access is available, apply fixes in small batches grouped by issue type.
Step 5: Rerun Policy (Mandatory After Changes)
After each batch of edits, rerun the validator:
bash scripts/validate.sh <script-path>Rerun loop rules:
1. Continue until no new errors are introduced. 2. If warnings remain by design, document why they are intentionally accepted. 3. If constraints prevent full resolution, report unresolved items with a clear next action. 4. Always report the latest rerun exit code and remaining issue count.
Fallback Behavior
Use these branches only when the default flow cannot run as-is.
| Constraint | Fallback action | Reporting requirement |
|---|---|---|
shellcheck missing, wrapper available | Let scripts/validate.sh use scripts/shellcheck_wrapper.sh --cache automatically | State that wrapper mode was used |
shellcheck and wrapper unavailable | Run syntax + custom checks only (validator does this) | Explicitly call out reduced coverage and missing ShellCheck analysis |
| Python unavailable for wrapper | Skip wrapper path, keep syntax + custom checks | State why ShellCheck could not run |
| Target file is read-only | Provide precise patch suggestions without editing | Mark response as "advisory only" |
| Target file missing or unreadable | Stop and request a valid file path | Do not fabricate results |
| Binary/non-text input | Stop validation | Report unsupported input type |
Citation Guidance for Fixes
Use subsection-level citations for every non-trivial fix.
Required citation format:
Reference: docs/<file>.md -> <Section> -> <Subsection>Examples:
Reference: docs/common-mistakes.md -> 1. Unquoted Variables -> SolutionReference: docs/shellcheck-reference.md -> SC2164: Use || exit After cdReference: docs/shell-reference.md -> POSIX Best Practices -> 5. Avoid Bashisms
Citation rules:
1. Cite the most specific section that justifies the fix. 2. For ShellCheck findings, include both the SC#### code and the matching section. 3. If no exact subsection exists, cite the closest section and state that the fix is inferred from that guidance.
Response Template
Use this structure for deterministic output:
Validation ResultsCommand:bash scripts/validate.sh <script-path>Detected shell:<shell>Exit code:<code>Summary:<errors> errors, <warnings> warnings, <info> infoIssue:<short label> (Line <n>)Problem:
<problematic snippet>Fix:
<corrected snippet>Why:<short explanation>Reference:docs/<file>.md -> <Section> -> <Subsection>Rerun command:bash scripts/validate.sh <script-path>Exit code after fixes:<code>Remaining issues:<count or none>
Example Flows
Fully Automated Environment
# 1) Baseline validation
bash scripts/validate.sh examples/bad-bash.sh
# 2) Apply fixes to target script
# 3) Rerun validation
bash scripts/validate.sh examples/bad-bash.shExpected behavior: full syntax + ShellCheck + custom-check coverage, with iterative reruns until stable.
Deterministic CI Gate
# Requires a system shellcheck binary.
bash scripts/run_ci_checks.shThis runner enforces VALIDATOR_REQUIRE_SHELLCHECK=1 and VALIDATOR_SHELLCHECK_MODE=system so CI fails if the ShellCheck stage is skipped or unavailable.
Constrained Environment (No ShellCheck Runtime)
# shellcheck unavailable and wrapper cannot run
bash scripts/validate.sh examples/bad-shell.shExpected behavior: syntax + custom checks still run. Report reduced coverage and list what must be revalidated once ShellCheck is available.
Validator Script Details
Scripts
scripts/validate.sh: primary validator entrypointscripts/shellcheck_wrapper.sh: optional ShellCheck fallback using a cached Python virtual environment
Detection and Ordering
Validation order in scripts/validate.sh:
1. File checks (exists/readable/text) 2. Shebang-based shell detection 3. Syntax check 4. ShellCheck (or fallback/skip behavior) 5. Custom checks 6. Summary with exit code
Exit Codes
0: no issues found1: warnings found2: errors found
References
Load only what is needed:
docs/bash-reference.mddocs/shell-reference.mddocs/shellcheck-reference.mddocs/common-mistakes.mddocs/grep-reference.mddocs/awk-reference.mddocs/sed-reference.mddocs/regex-reference.md
Done Criteria
This skill update is complete when all are true:
1. Trigger guidance is explicit (positive and non-trigger examples). 2. Default workflow is deterministic and ordered. 3. Fallback behavior is explicit for missing tooling and constrained environments. 4. Fix explanations include subsection-level citations. 5. Post-fix rerun policy is mandatory and reported with exit codes. 6. Documentation supports both fully automated and constrained execution paths.
GNU AWK (gawk) Reference Guide
Overview
AWK is a powerful text processing language designed for pattern scanning and processing. It's particularly useful for field-based data manipulation.
Official Manual: https://www.gnu.org/software/gawk/manual/ Man Page: man awk
Basic Syntax
awk 'pattern { action }' file
awk -F delimiter 'pattern { action }' file
awk -f script.awk fileStructure
BEGIN { # Executed before processing }
pattern { # Executed for matching lines }
END { # Executed after processing }Built-in Variables
Field Variables
$0 # Entire line
$1 # First field
$2 # Second field
$NF # Last field
$(NF-1) # Second to last fieldControl Variables
NF # Number of fields in current record
NR # Current record number (line number)
FNR # Record number in current file
FS # Input field separator (default: whitespace)
OFS # Output field separator (default: space)
RS # Input record separator (default: newline)
ORS # Output record separator (default: newline)
FILENAME # Current filenameCommon Usage Patterns
Basic Field Processing
# Print specific fields
awk '{print $1}' file.txt # First field
awk '{print $1, $3}' file.txt # First and third fields
awk '{print $NF}' file.txt # Last field
# Print with custom separator
awk '{print $1 ":" $2}' file.txt # Custom separator
awk -v OFS='\t' '{print $1, $2}' file # Tab-separated
# Print entire line with line number
awk '{print NR, $0}' file.txtCustom Field Separator
# Use comma as separator
awk -F',' '{print $1}' file.csv
# Use colon as separator (like /etc/passwd)
awk -F':' '{print $1}' /etc/passwd
# Multiple character separator
awk -F'::' '{print $2}' file.txt
# Regex as separator
awk -F'[,:]' '{print $1}' file.txt # Comma or colonPattern Matching
# Match lines containing pattern
awk '/pattern/ {print}' file.txt
awk '/error/ {print $0}' logfile.txt
# Case-insensitive match
awk 'tolower($0) ~ /pattern/' file.txt
# Regex on specific field
awk '$2 ~ /pattern/' file.txt
awk '$2 !~ /pattern/' file.txt # Not matching
# Exact match
awk '$1 == "value"' file.txt
awk '$2 != "value"' file.txtNumeric Comparisons
# Greater than
awk '$3 > 100' file.txt
# Less than or equal
awk '$2 <= 50' file.txt
# Range
awk '$1 >= 10 && $1 <= 20' file.txt
# Complex conditions
awk '$1 > 100 && $2 == "active"' file.txt
awk '$1 > 100 || $2 > 200' file.txtBEGIN and END Blocks
# Header and footer
awk 'BEGIN {print "Name\tAge"} {print $1, $2} END {print "---"}' file
# Initialize variables
awk 'BEGIN {count=0} {count++} END {print count}' file
# Set field separator in BEGIN
awk 'BEGIN {FS=","} {print $1}' file.csvCalculations
# Sum a column
awk '{sum += $1} END {print sum}' file.txt
# Average
awk '{sum += $1; count++} END {print sum/count}' file
# Count records
awk 'END {print NR}' file.txt
# Max value
awk 'BEGIN {max=0} {if ($1 > max) max=$1} END {print max}' file
# Count occurrences
awk '{count[$1]++} END {for (key in count) print key, count[key]}' fileConditional Processing
# If-else
awk '{if ($1 > 100) print "High"; else print "Low"}' file
# Ternary operator
awk '{print ($1 > 100) ? "High" : "Low"}' file
# Multiple conditions
awk '{
if ($1 > 100) print "High"
else if ($1 > 50) print "Medium"
else print "Low"
}' fileArrays
Associative Arrays
# Count occurrences
awk '{count[$1]++} END {for (key in count) print key, count[key]}' file
# Group by key
awk '{sum[$1] += $2} END {for (key in sum) print key, sum[key]}' file
# Check if key exists
awk '{if ($1 in array) print "Duplicate"}' fileArray Examples
# Count unique values
awk '{a[$1]++} END {print length(a)}' file
# Find duplicates
awk '{count[$1]++} END {for (k in count) if (count[k] > 1) print k}' file
# Store and print in order
awk '{lines[NR] = $0} END {for (i=1; i<=NR; i++) print lines[i]}' fileFunctions
Built-in String Functions
length(string) # String length
substr(string, start, len) # Substring
index(string, substring) # Find substring position
split(string, array, sep) # Split string into array
sub(regex, replacement, string) # Replace first match
gsub(regex, replacement, string) # Replace all matches
tolower(string) # Convert to lowercase
toupper(string) # Convert to uppercase
match(string, regex) # Test regex matchString Function Examples
# String length
awk '{print length($1)}' file
# Substring
awk '{print substr($1, 1, 3)}' file # First 3 characters
# Replace
awk '{gsub(/old/, "new"); print}' file
# Convert case
awk '{print toupper($1)}' file
# Split and process
awk '{split($0, a, ":"); print a[1]}' /etc/passwdMath Functions
int(x) # Integer part
sqrt(x) # Square root
sin(x) # Sine
cos(x) # Cosine
atan2(y,x) # Arctangent
log(x) # Natural logarithm
exp(x) # Exponential
rand() # Random number [0,1)
srand() # Seed random number generatorPractical Examples for Shell Scripts
Log File Analysis
# Count HTTP status codes
awk '{print $9}' access.log | sort | uniq -c
# Sum response times
awk '{sum += $10; count++} END {print sum/count}' access.log
# Filter by time range
awk '$4 > "[01/Jan/2025:10:00:00" && $4 < "[01/Jan/2025:11:00:00"' access.log
# Extract and count IPs
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -10CSV Processing
# Print specific columns from CSV
awk -F',' '{print $1, $3}' file.csv
# Skip header
awk -F',' 'NR > 1 {print $1, $2}' file.csv
# Convert CSV to tab-separated
awk -F',' -v OFS='\t' '{print $1, $2, $3}' file.csv
# Filter rows
awk -F',' '$3 > 100 {print $0}' file.csvSystem Administration
# Parse /etc/passwd
awk -F':' '{print $1, $6}' /etc/passwd # username, home directory
awk -F':' '$3 >= 1000 {print $1}' /etc/passwd # Regular users
# Disk usage analysis
df -h | awk '$5 > 80 {print $0}' # > 80% full
# Process monitoring
ps aux | awk '$3 > 50 {print $2, $11}' # High CPU processes
# Network stats
netstat -an | awk '/ESTABLISHED/ {print $5}' | cut -d: -f1 | sort | uniq -cData Transformation
# Swap columns
awk '{print $2, $1}' file
# Add line numbers
awk '{print NR ":", $0}' file
# Remove duplicates (keeping first occurrence)
awk '!seen[$0]++' file
# Join lines
awk '{printf "%s ", $0} END {print ""}' file
# Transpose rows to columns
awk '{for (i=1; i<=NF; i++) a[NR,i]=$i; max=(NF>max?NF:max)}
END {for (i=1; i<=max; i++) {
for (j=1; j<=NR; j++) printf "%s ", a[j,i]
print ""
}}' fileMulti-line AWK Scripts
In Shell Script
awk '
BEGIN {
FS = ","
print "Processing..."
}
{
sum += $2
count++
}
END {
print "Average:", sum/count
}
' file.csvExternal AWK File
# script.awk
BEGIN {
FS = ","
}
{
sum += $2
}
END {
print "Total:", sum
}
# Execute
awk -f script.awk file.csvCommon Patterns
Skip Empty Lines
awk 'NF > 0' file
awk '/./' filePrint Line Range
awk 'NR>=10 && NR<=20' file # Lines 10-20Print Unique Lines
awk '!seen[$0]++' fileCount Pattern Occurrences
awk '/pattern/ {count++} END {print count}' fileFormat Output
# Fixed-width columns
awk '{printf "%-20s %10s\n", $1, $2}' file
# Align numbers
awk '{printf "%s: %8.2f\n", $1, $2}' filePerformance Tips
1. Use Built-in Variables
# Faster
awk '{print $NF}' file
# Slower
awk '{print $length($0)}' file2. Avoid Unnecessary Operations
# Good
awk '$1 > 100' file
# Wasteful
awk '{if ($1 > 100) print $0}' file3. Use Regex Efficiently
# Compile regex once
awk 'BEGIN {pattern = /error/} $0 ~ pattern' fileCommon Pitfalls in Shell Scripts
1. Not Quoting AWK Scripts
# Wrong - shell expands $1
awk {print $1} file
# Right
awk '{print $1}' file2. Division by Zero
# Dangerous
awk '{print $1/$2}' file
# Safe
awk '{if ($2 != 0) print $1/$2; else print "N/A"}' file3. Floating Point Comparison
# Problematic
awk '$1 == 0.1' file
# Better
awk 'function abs(x){return x<0?-x:x} abs($1 - 0.1) < 0.001' file4. Not Handling Missing Fields
# Check field existence
awk 'NF >= 3 {print $3}' fileCombining with Other Tools
# awk with grep
grep "error" log | awk '{print $1, $NF}'
# awk with sort
awk '{print $2}' file | sort -n
# Pipeline
cat file | awk '$1 > 100' | sort | uniqResources
Bash Reference Guide
Overview
Bash (Bourne Again SHell) is a Unix shell and command language. This guide covers bash-specific features, syntax, and best practices.
Official Documentation: https://www.gnu.org/software/bash/manual/
Bash vs POSIX Shell (sh)
Bash is a superset of POSIX sh with many extensions. Not all bash scripts are POSIX-compliant.
Bash-Specific Features (NOT in POSIX sh)
1. Arrays
# Bash only
array=(one two three)
echo "${array[0]}"
declare -a indexed_array
declare -A associative_array2. [[ ]] Test Construct
# Bash only - more powerful than [ ]
if [[ "$var" == pattern* ]]; then
echo "Matches pattern"
fi
# POSIX sh - use [ ]
if [ "$var" = "exact" ]; then
echo "Exact match"
fi3. Process Substitution
# Bash only
diff <(ls dir1) <(ls dir2)4. Brace Expansion
# Bash only
echo {1..10} # Outputs: 1 2 3 4 5 6 7 8 9 10
mv file.{txt,bak} # Renames file.txt to file.bak5. Function Keyword
# Bash style (function keyword optional)
function myfunction {
echo "Hello"
}
# POSIX sh style (no function keyword)
myfunction() {
echo "Hello"
}6. Local Variables
# Bash only
function myfunc {
local var="value"
echo "$var"
}7. Extended Pattern Matching
shopt -s extglob
# Bash only
?(pattern-list) # Matches zero or one occurrence
*(pattern-list) # Matches zero or more occurrences
+(pattern-list) # Matches one or more occurrences
@(pattern-list) # Matches one occurrence
!(pattern-list) # Matches anything except pattern8. Advanced Parameter Expansion
# Bash supports more advanced parameter expansion
${var,,} # Lowercase
${var^^} # Uppercase
${var:0:5} # Substring
${var/pattern/replacement} # Replace first
${var//pattern/replacement} # Replace all9. Source vs Dot
source script.sh # Bash (also works: . script.sh)
. script.sh # POSIX sh10. Bash Built-in Variables
$RANDOM # Random number
$SECONDS # Seconds since script started
$BASH_VERSION
$BASH_SOURCE
$FUNCNAME
$DIRSTACKCore Bash Syntax
Variables
# Assignment (no spaces around =)
var="value"
readonly CONST="constant"
declare -i integer=42
declare -r readonly_var="const"
declare -x export_var="exported"
# Reading variables
echo "$var"
echo "${var}" # Preferred for clarity
# Command substitution
result=$(command)
result=`command` # Deprecated, use $() instead
# Arithmetic
result=$((5 + 3))
((var++))
((var += 5))Quoting Rules
# Double quotes: Preserve literal value except $, `, \, and !
echo "Value: $var"
# Single quotes: Preserve literal value of all characters
echo 'Value: $var' # Outputs: Value: $var
# No quotes: Word splitting and pathname expansion
files=$var # Dangerous if var contains spaces
# Always quote variable expansions unless you need word splitting
cp "$file" "$destination"Control Structures
# If statement
if [[ condition ]]; then
# commands
elif [[ condition ]]; then
# commands
else
# commands
fi
# Case statement
case "$var" in
pattern1)
# commands
;;
pattern2|pattern3)
# commands
;;
*)
# default
;;
esac
# For loops
for item in list; do
echo "$item"
done
for ((i=0; i<10; i++)); do
echo "$i"
done
# While loop
while [[ condition ]]; do
# commands
done
# Until loop
until [[ condition ]]; do
# commands
doneFunctions
# Function definition
function_name() {
local local_var="value"
echo "$1" # First argument
return 0 # Exit status
}
# Call function
function_name arg1 arg2
# Function with return value (via stdout)
get_value() {
echo "returned value"
}
result=$(get_value)Error Handling
# Exit on error
set -e
set -o errexit
# Exit on undefined variable
set -u
set -o nounset
# Pipe failure detection
set -o pipefail
# Combining options
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO"' ERR
# Trap exit
trap cleanup EXIT
cleanup() {
# Cleanup code
rm -f "$temp_file"
}Input/Output Redirection
# Redirect stdout
command > file
# Redirect stderr
command 2> errors.txt
# Redirect both
command &> output.txt
command > output.txt 2>&1
# Append
command >> file
# Here document
cat <<EOF
multiple
lines
of text
EOF
# Here string
grep pattern <<< "$variable"Best Practices
1. Use SheBang
#!/usr/bin/env bash
# Portable shebang that finds bash in PATH2. Enable Strict Mode
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'3. Quote Variables
# Good
cp "$source" "$destination"
# Bad (fails with spaces)
cp $source $destination4. Use $() Instead of Backticks
# Good
result=$(command)
# Bad
result=`command`5. Check Command Existence
if command -v shellcheck &>/dev/null; then
echo "ShellCheck is installed"
fi6. Use [[ ]] for Tests
# Preferred in bash
if [[ "$var" == "value" ]]; then
# ...
fi
# Use [ ] only for POSIX compliance
if [ "$var" = "value" ]; then
# ...
fi7. Handle Errors Appropriately
if ! command; then
echo "Command failed" >&2
exit 1
fi
command || { echo "Failed" >&2; exit 1; }8. Use Meaningful Variable Names
# Good
user_count=10
max_retries=3
# Bad
n=10
x=39. Add Comments
# Explain complex logic
# Document function parameters
# Clarify non-obvious behavior10. Use Functions for Reusability
log_error() {
echo "[ERROR] $*" >&2
}
log_info() {
echo "[INFO] $*"
}Common Pitfalls
1. Unquoted Variables
# Wrong
file=/path/with spaces/file.txt
cat $file # Fails!
# Right
file="/path/with spaces/file.txt"
cat "$file"2. Not Checking Return Codes
# Wrong
cd /some/directory
rm -rf * # Dangerous if cd fails!
# Right
cd /some/directory || exit 1
rm -rf *3. Using [ ] with Bash Features
# Wrong
if [ "$var" == pattern* ]; then # == not in POSIX
# Right (bash)
if [[ "$var" == pattern* ]]; then
# Right (POSIX)
if [ "$var" = "exact" ]; then4. Word Splitting Issues
# Wrong
files=$(ls *.txt)
for file in $files; do # Breaks on spaces
echo "$file"
done
# Right
for file in *.txt; do
echo "$file"
done5. Not Using Local in Functions
# Wrong - pollutes global scope
function bad {
var="value"
}
# Right
function good {
local var="value"
}Parameter Expansion Reference
${var} # Value of var
${var:-default} # Use default if var is unset
${var:=default} # Assign default if var is unset
${var:?error} # Error if var is unset
${var:+alternate} # Use alternate if var is set
${#var} # Length of var
${var:offset:length} # Substring
${var#pattern} # Remove shortest match from beginning
${var##pattern} # Remove longest match from beginning
${var%pattern} # Remove shortest match from end
${var%%pattern} # Remove longest match from end
${var/pattern/replacement} # Replace first match
${var//pattern/replacement} # Replace all matches
${var^} # Uppercase first character
${var^^} # Uppercase all
${var,} # Lowercase first character
${var,,} # Lowercase allSpecial Variables
$0 # Script name
$1-$9 # Positional parameters
${10} # 10th parameter (braces required)
$# # Number of positional parameters
$* # All positional parameters (as single word)
$@ # All positional parameters (as separate words)
$$ # Process ID of shell
$! # PID of last background command
$? # Exit status of last command
$_ # Last argument of previous commandResources
Common Shell Scripting Mistakes
This guide covers frequent mistakes made in bash and shell scripts, their consequences, and how to fix them.
1. Unquoted Variables
Problem
# Wrong
file=/path/with spaces/file.txt
cat $file # Breaks into multiple argumentsConsequence
Word splitting and glob expansion cause unexpected behavior.
Solution
# Right
file="/path/with spaces/file.txt"
cat "$file"Rule
Always quote variable expansions unless you explicitly need word splitting.
---
2. Not Checking Command Success
Problem
# Wrong
cd /some/directory
rm -rf * # DANGEROUS if cd fails!Consequence
Commands execute even if previous commands fail, potentially catastrophic.
Solution
# Right
cd /some/directory || exit 1
rm -rf *
# Or use set -e
set -e
cd /some/directory
rm -rf *
# Or check explicitly
if ! cd /some/directory; then
echo "Failed to change directory" >&2
exit 1
fi
rm -rf *---
3. Using [ ] with Bash Features
Problem
# Wrong (== not POSIX, may fail in sh)
if [ "$var" == "value" ]; then
echo "match"
fiSolution
# POSIX sh - use single =
if [ "$var" = "value" ]; then
echo "match"
fi
# Or use bash [[ ]] (bash only)
if [[ "$var" == "value" ]]; then
echo "match"
fi---
4. Useless Use of cat (UUOC)
Problem
# Wrong - unnecessary cat
cat file.txt | grep pattern
cat file.txt | awk '{print $1}'Consequence
Wastes a process, less efficient.
Solution
# Right
grep pattern file.txt
awk '{print $1}' file.txt
# Or use redirection
< file.txt grep pattern---
5. Not Using -r with read
Problem
# Wrong
while read line; do
echo "$line"
done < fileConsequence
Backslashes are interpreted, leading character may be removed.
Solution
# Right
while IFS= read -r line; do
echo "$line"
done < file-rprevents backslash interpretationIFS=prevents leading/trailing whitespace trimming
---
6. Testing $? After Multiple Commands
Problem
# Wrong
command1
command2
if [ $? -eq 0 ]; then # Tests command2, not command1!
echo "Success"
fiSolution
# Right - test immediately
command1
if [ $? -eq 0 ]; then
echo "command1 succeeded"
fi
# Better - test directly
if command1; then
echo "Success"
fi---
7. Arrays in POSIX sh Scripts
Problem
#!/bin/sh
# Wrong - arrays not in POSIX sh
array=(one two three)
echo "${array[0]}"Solution
# Use bash
#!/bin/bash
array=(one two three)
echo "${array[0]}"
# Or use POSIX alternatives
set -- one two three
echo "$1"---
8. Not Declaring Functions Before Use
Problem
# Wrong - function not defined yet
my_function
my_function() {
echo "Hello"
}Solution
# Right - define first
my_function() {
echo "Hello"
}
my_function---
9. Using eval Unsafely
Problem
# DANGEROUS
user_input="$1"
eval "$user_input" # Command injection risk!Consequence
Security vulnerability - arbitrary code execution.
Solution
# Avoid eval when possible
# If necessary, sanitize input thoroughly
# Or use safer alternatives
# Example: dynamic variable names
var_name="my_var"
# Don't: eval "echo \$$var_name"
# Do: Use indirect expansion (bash)
echo "${!var_name}"---
10. Forgetting set -u
Problem
# Wrong - typo goes unnoticed
nmae="John" # Typo
echo "Hello, $name" # Prints "Hello, " (empty)Solution
# Right - use set -u
set -u
nmae="John" # Typo
echo "Hello, $name" # Error: name: unbound variable---
11. Incorrect String Comparison
Problem
# Wrong - numeric comparison on strings
if [ "$version" -gt "2.0" ]; then
echo "New version"
fiSolution
# Right - string comparison
if [ "$version" = "2.0" ]; then
echo "Exact match"
fi
# Or use proper version comparison
if [[ "$version" > "2.0" ]]; then
echo "Greater"
fi---
12. Not Handling Spaces in Filenames
Problem
# Wrong
for file in $(ls *.txt); do
echo "$file"
doneConsequence
Files with spaces break into multiple items.
Solution
# Right - use glob directly
for file in *.txt; do
echo "$file"
done
# Or use find with -print0
while IFS= read -r -d '' file; do
echo "$file"
done < <(find . -name "*.txt" -print0)---
13. Backticks Instead of $()
Problem
# Deprecated
result=`command arg1 arg2`Solution
# Modern
result=$(command arg1 arg2)Why
- Better nesting:
$(cmd1 $(cmd2)) - Better readability
- Fewer escaping issues
---
14. Using = Instead of == in [[ ]]
Not really a mistake, but inconsistent:
# Both work in [[ ]]
[[ "$var" = "value" ]] # POSIX style (works)
[[ "$var" == "value" ]] # Bash style (also works)
# Only = works in [ ]
[ "$var" = "value" ] # Works
[ "$var" == "value" ] # May fail in POSIX shRecommendation: Use = for portability, or stick to == in bash with [[ ]].
---
15. Not Quoting $@
Problem
# Wrong
script.sh "$@" # Right
command $@ # Wrong if args have spacesSolution
# Right
command "$@" # Preserves argument boundaries---
16. Using ls to Process Files
Problem
# Wrong
files=$(ls *.txt)
for file in $files; do
process "$file"
doneIssues
- Breaks on spaces
- Breaks on newlines in filenames
- Breaks on glob characters
Solution
# Right
for file in *.txt; do
process "$file"
done
# Or with find
find . -name "*.txt" -exec process {} \;---
17. Incorrect Exit Codes
Problem
# Wrong
function check_file() {
if [ -f "$1" ]; then
echo "File exists"
return 1 # Success should be 0!
fi
return 0
}Solution
# Right - 0 is success, non-zero is failure
function check_file() {
if [ -f "$1" ]; then
echo "File exists"
return 0
fi
return 1
}---
18. Using -a and -o in [ ]
Problem
# Deprecated and error-prone
[ "$a" = "x" -a "$b" = "y" ]
[ "$a" = "x" -o "$b" = "y" ]Solution
# Right - use && and ||
[ "$a" = "x" ] && [ "$b" = "y" ]
[ "$a" = "x" ] || [ "$b" = "y" ]
# Or use [[ ]] in bash
[[ "$a" = "x" && "$b" = "y" ]]
[[ "$a" = "x" || "$b" = "y" ]]---
19. Not Making Scripts Executable
Problem
# Wrong
bash script.sh # Works but not idealSolution
# Right
chmod +x script.sh
./script.shAnd include proper shebang:
#!/usr/bin/env bash---
20. Forgetting Final Newline
Problem
Some tools expect files to end with a newline.
Solution
Ensure your editor adds a final newline, or:
echo "" >> file---
21. Using grep -q Without Knowing Implications
Problem
# Potentially inefficient
if [ "$(grep pattern file)" ]; then
echo "Found"
fiSolution
# Better - grep -q exits on first match
if grep -q pattern file; then
echo "Found"
fi---
22. Incorrect glob Pattern
Problem
# Wrong - doesn't match hidden files
for file in *; do
process "$file"
doneSolution
# Include hidden files (bash)
shopt -s dotglob
for file in *; do
process "$file"
done
shopt -u dotglob
# Or explicitly
for file in * .[!.]* ..?*; do
[ -e "$file" ] && process "$file"
done---
23. Not Handling Empty Globs
Problem
# Fails if no .txt files
for file in *.txt; do
process "$file" # Processes literal "*.txt"
doneSolution
# Bash - fail gracefully
shopt -s nullglob
for file in *.txt; do
process "$file"
done
shopt -u nullglob
# POSIX - check existence
for file in *.txt; do
[ -e "$file" ] || continue
process "$file"
done---
24. Not Sanitizing Input
Problem
# Dangerous
rm -rf "/$1" # What if $1 is empty or manipulated?Solution
# Safer
if [ -z "$1" ]; then
echo "Error: No argument provided" >&2
exit 1
fi
# Validate
case "$1" in
/*)
echo "Error: Absolute paths not allowed" >&2
exit 1
;;
esac
rm -rf "$1"---
25. Using -e for File Existence
Not a mistake, but be specific:
[ -e "$file" ] # Exists (any type)
[ -f "$file" ] # Regular file
[ -d "$file" ] # Directory
[ -L "$file" ] # Symbolic link
[ -r "$file" ] # Readable
[ -w "$file" ] # Writable
[ -x "$file" ] # Executable---
Quick Checklist
Before running a script, verify:
- [ ] Proper shebang (#!/bin/bash or #!/bin/sh)
- [ ] set -euo pipefail (strict mode)
- [ ] All variables quoted
- [ ] Error handling for critical commands
- [ ] Using $() not backticks
- [ ] Not using ls for file processing
- [ ] Functions defined before use
- [ ] Proper exit codes (0 = success)
- [ ] Input validation
- [ ] ShellCheck passes
---
Resources
- ShellCheck - Catches most of these
- Bash Pitfalls
- POSIX Shell
GNU grep Reference Guide
Overview
grep (Global Regular Expression Print) searches for patterns in text files. It's one of the most commonly used Unix tools.
Official Manual: https://www.gnu.org/software/grep/manual/ Man Page: man grep
Basic Syntax
grep [OPTIONS] PATTERN [FILE...]
grep [OPTIONS] -e PATTERN ... [FILE...]
grep [OPTIONS] -f PATTERN_FILE ... [FILE...]Common Options
Basic Options
-i, --ignore-case # Case-insensitive search
-v, --invert-match # Invert match (select non-matching lines)
-w, --word-regexp # Match whole words only
-x, --line-regexp # Match whole lines only
-c, --count # Count matching lines
-n, --line-number # Show line numbers
-H, --with-filename # Print filename with matches
-h, --no-filename # Suppress filename output
-l, --files-with-matches # Print only filenames with matches
-L, --files-without-match # Print only filenames without matchesContext Options
-A NUM, --after-context=NUM # Print NUM lines after match
-B NUM, --before-context=NUM # Print NUM lines before match
-C NUM, --context=NUM # Print NUM lines before and afterRegular Expression Options
-E, --extended-regexp # Use Extended Regular Expressions (ERE)
-F, --fixed-strings # Treat PATTERN as fixed strings, not regex
-G, --basic-regexp # Use Basic Regular Expressions (BRE) - default
-P, --perl-regexp # Use Perl-compatible regex (PCRE)Output Options
-o, --only-matching # Print only matched parts
-q, --quiet, --silent # Suppress output, just return exit code
--color[=WHEN] # Colorize output (auto, always, never)
-s, --no-messages # Suppress error messagesFile Selection
-r, --recursive # Recursive search
-R, --dereference-recursive # Recursive, following symlinks
--include=PATTERN # Search only files matching PATTERN
--exclude=PATTERN # Skip files matching PATTERN
--exclude-dir=PATTERN # Skip directories matching PATTERNCommon Usage Patterns
Basic Searches
# Simple string search
grep "error" logfile.txt
# Case-insensitive search
grep -i "error" logfile.txt
# Search for whole word
grep -w "error" logfile.txt
# Count matches
grep -c "error" logfile.txt
# Show line numbers
grep -n "error" logfile.txtMultiple Files
# Search in multiple files
grep "pattern" file1.txt file2.txt
# Search in all txt files
grep "pattern" *.txt
# Recursive search
grep -r "pattern" /path/to/dir
# Recursive with file pattern
grep -r --include="*.log" "error" /var/logContext Display
# Show 3 lines after match
grep -A 3 "error" logfile.txt
# Show 3 lines before match
grep -B 3 "error" logfile.txt
# Show 3 lines before and after
grep -C 3 "error" logfile.txtInvert Match
# Show lines that DON'T contain pattern
grep -v "debug" logfile.txt
# Exclude multiple patterns
grep -v "debug\|info" logfile.txtMultiple Patterns
# Match any pattern (OR)
grep -e "error" -e "warning" file.txt
grep "error\|warning" file.txt
# Match all patterns (AND) - requires pipeline
grep "error" file.txt | grep "critical"File Selection
# List files containing match
grep -l "pattern" *.txt
# List files NOT containing match
grep -L "pattern" *.txt
# Recursive with excludes
grep -r --exclude-dir=".git" "pattern" .
grep -r --exclude="*.min.js" "pattern" .Regular Expressions in grep
Basic Regular Expressions (BRE) - Default
# BRE Metacharacters (no escaping needed)
. # Any single character
^ # Start of line
$ # End of line
[...] # Character class
[^...] # Negated character class
* # Zero or more of previous
# BRE Metacharacters (MUST be escaped)
\+ # One or more (requires \)
\? # Zero or one (requires \)
\{m,n\} # Between m and n occurrences (requires \)
\(...\) # Group (requires \)
\| # Alternation (requires \)BRE Examples
# Match lines starting with "Error"
grep "^Error" file.txt
# Match lines ending with "failed"
grep "failed$" file.txt
# Match any character
grep "a.c" file.txt # Matches abc, aXc, a5c
# Character class
grep "[0-9]" file.txt # Match any digit
grep "[A-Za-z]" file.txt # Match any letter
# One or more (escaped)
grep "a\+b" file.txt # Matches ab, aab, aaab
# Groups and alternation (escaped)
grep "\(error\|warning\)" file.txtExtended Regular Expressions (ERE) - grep -E
# ERE - No escaping needed for +, ?, |, (), {}
+ # One or more
? # Zero or one
{m,n} # Between m and n occurrences
(...) # Group
| # AlternationERE Examples
# One or more (no escape)
grep -E "a+b" file.txt
# Zero or one
grep -E "colou?r" file.txt # Matches color or colour
# Alternation
grep -E "(error|warning)" file.txt
# Quantifiers
grep -E "[0-9]{3}-[0-9]{4}" file.txt # Phone: 123-4567
grep -E "[0-9]{1,3}" file.txt # 1 to 3 digits
# Groups
grep -E "(http|https)://[^ ]+" file.txt # URLsCharacter Classes
POSIX Character Classes
[:alnum:] # Alphanumeric [A-Za-z0-9]
[:alpha:] # Alphabetic [A-Za-z]
[:digit:] # Digits [0-9]
[:lower:] # Lowercase [a-z]
[:upper:] # Uppercase [A-Z]
[:space:] # Whitespace [ \t\n\r\f\v]
[:blank:] # Space and tab [ \t]
[:punct:] # Punctuation
[:xdigit:] # Hex digits [0-9A-Fa-f]
[:word:] # Word characters [A-Za-z0-9_]Usage
# Match any digit
grep "[[:digit:]]" file.txt
# Match any whitespace
grep "[[:space:]]" file.txt
# Match uppercase letters
grep "[[:upper:]]" file.txtPractical Examples for Shell Scripts
Log File Analysis
# Find errors in last hour
grep "$(date -d '1 hour ago' '+%Y-%m-%d %H')" /var/log/app.log | grep -i error
# Count error types
grep -i "error" logfile.log | cut -d: -f2 | sort | uniq -c | sort -rn
# Extract IP addresses
grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' access.logConfiguration Validation
# Find uncommented lines in config
grep -v "^#" config.file | grep -v "^$"
# Check for specific settings
if grep -q "debug = true" config.ini; then
echo "Debug mode is enabled"
fiCode Search
# Find function definitions
grep -n "^function " script.sh
# Find TODO comments
grep -rn "TODO" --include="*.sh" .
# Find unquoted variables (simple check)
grep -n '\$[A-Za-z_][A-Za-z0-9_]*\s' script.shPerformance Tips
1. Use -F for Fixed Strings
# Faster when not using regex
grep -F "literal string" large_file.txt2. Use -q When Only Checking Existence
# Don't need output, just exit code
if grep -q "pattern" file.txt; then
echo "Found"
fi3. Limit Search Depth
# Don't recurse too deep
grep -r --max-depth=2 "pattern" /path4. Exclude Unnecessary Directories
grep -r --exclude-dir={.git,.svn,node_modules} "pattern" .Exit Codes
- 0: Match found
- 1: No match found
- 2: Error occurred
Common Pitfalls in Shell Scripts
1. Not Quoting Patterns with Spaces
# Wrong
grep $pattern file.txt
# Right
grep "$pattern" file.txt2. Using grep in Tests Without -q
# Inefficient
if [ "$(grep pattern file)" ]; then
# Better
if grep -q pattern file; then3. Useless Use of cat
# Wrong (UUOC)
cat file | grep pattern
# Right
grep pattern file
# or
< file grep pattern4. Not Handling No Match Case
# grep returns 1 if no match, can cause set -e to exit
grep "pattern" file || true
# Or check explicitly
if grep -q "pattern" file; then
echo "Found"
else
echo "Not found"
fi5. Forgetting to Escape Regex Metacharacters
# Wrong - . matches any character
grep "192.168.1.1" file
# Right - escape the dots
grep "192\.168\.1\.1" file
# Or use -F for literal match
grep -F "192.168.1.1" fileUseful Combinations
# Case-insensitive recursive search with line numbers
grep -rni "pattern" /path
# Count total matches across files
grep -r "pattern" . | wc -l
# Find and highlight matches
grep --color=always "pattern" file.txt | less -R
# Search compressed files
zgrep "pattern" file.gz
# Search with extended regex and only show matches
grep -Eo "pattern" file.txtResources
Regular Expressions Reference Guide
Overview
Regular expressions (regex) are patterns used to match character combinations in strings. POSIX defines two flavors: Basic (BRE) and Extended (ERE).
POSIX Specification: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html
BRE vs ERE
| Feature | BRE | ERE |
|---|---|---|
| One or more | \+ | + |
| Zero or one | \? | ? |
| Alternation | `\ | ` |
| Grouping | \(...\) | (...) |
| Quantifiers | \{m,n\} | {m,n} |
Tool Usage
# BRE (Basic)
grep 'pattern' file # BRE by default
sed 's/pattern/repl/' file # BRE by default
awk '/pattern/' file # ERE by default
# ERE (Extended)
grep -E 'pattern' file # ERE
egrep 'pattern' file # ERE (deprecated, use grep -E)
sed -E 's/pattern/repl/' file # EREBasic Metacharacters (Both BRE and ERE)
Single Character Matchers
. # Any single character except newline
[abc] # Any character in set (a, b, or c)
[^abc] # Any character NOT in set
[a-z] # Any character in range
[0-9] # Any digitAnchors
^ # Start of line
$ # End of line
\< # Start of word (GNU extension)
\> # End of word (GNU extension)
\b # Word boundary (some tools)
\B # Not a word boundary (some tools)Quantifiers (Zero or More)
* # Zero or more of previous (both BRE and ERE)Extended Metacharacters
ERE Quantifiers
+ # One or more (ERE: +) (BRE: \+)
? # Zero or one (ERE: ?) (BRE: \?)
{n} # Exactly n (ERE: {n}) (BRE: \{n\})
{n,} # n or more (ERE: {n,}) (BRE: \{n,\})
{n,m} # Between n and m (ERE: {n,m}) (BRE: \{n,m\})Grouping and Alternation
# ERE
(pattern) # Group
pattern1|pattern2 # Alternation (OR)
# BRE (requires backslashes)
\(pattern\) # Group
pattern1\|pattern2 # Alternation (OR)POSIX Character Classes
Must be used inside bracket expressions [[:class:]]:
[:alnum:] # Alphanumeric [A-Za-z0-9]
[:alpha:] # Alphabetic [A-Za-z]
[:digit:] # Digits [0-9]
[:lower:] # Lowercase [a-z]
[:upper:] # Uppercase [A-Z]
[:space:] # Whitespace [ \t\n\r\f\v]
[:blank:] # Space and tab [ \t]
[:punct:] # Punctuation
[:xdigit:] # Hexadecimal [0-9A-Fa-f]
[:word:] # Word characters [A-Za-z0-9_] (GNU extension)
[:graph:] # Visible characters (not space)
[:print:] # Printable characters (including space)
[:cntrl:] # Control charactersUsage
# Match any digit
grep '[[:digit:]]' file
# Match any whitespace
grep '[[:space:]]' file
# Match alphanumeric
grep '[[:alnum:]]' file
# Negation
grep '[^[:digit:]]' file # Not a digitCommon Patterns
Numbers
# BRE
[0-9] # Single digit
[0-9]\+ # One or more digits
[0-9]\{3\} # Exactly 3 digits
[0-9]\{3,5\} # 3 to 5 digits
# ERE
[0-9] # Single digit
[0-9]+ # One or more digits
[0-9]{3} # Exactly 3 digits
[0-9]{3,5} # 3 to 5 digitsIP Addresses
# Simple (BRE)
grep '[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}' file
# Simple (ERE)
grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' file
# More strict (ERE)
grep -E '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' fileEmail Addresses
# Simple (ERE)
grep -E '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' file
# BRE
grep '[a-zA-Z0-9._%+-]\+@[a-zA-Z0-9.-]\+\.[a-zA-Z]\{2,\}' fileURLs
# Simple HTTP/HTTPS (ERE)
grep -E 'https?://[a-zA-Z0-9./?=_-]+' file
# BRE
grep 'https\?://[a-zA-Z0-9./?=_-]\+' filePhone Numbers
# Format: 123-456-7890 (ERE)
grep -E '[0-9]{3}-[0-9]{3}-[0-9]{4}' file
# Format: (123) 456-7890 (ERE)
grep -E '\([0-9]{3}\) [0-9]{3}-[0-9]{4}' file
# BRE
grep '\([0-9]\{3\}\) [0-9]\{3\}-[0-9]\{4\}' fileDates
# YYYY-MM-DD (ERE)
grep -E '[0-9]{4}-[0-9]{2}-[0-9]{2}' file
# MM/DD/YYYY (ERE)
grep -E '[0-9]{2}/[0-9]{2}/[0-9]{4}' file
# BRE
grep '[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' fileShell Script Patterns
Variable Names
# Valid bash variable name (ERE)
grep -E '^[a-zA-Z_][a-zA-Z0-9_]*=' file
# Find variable usage
grep -E '\$[a-zA-Z_][a-zA-Z0-9_]*' file
grep -E '\$\{[a-zA-Z_][a-zA-Z0-9_]*\}' fileFunction Definitions
# POSIX function (ERE)
grep -E '^[a-zA-Z_][a-zA-Z0-9_]*\s*\(\)' file
# Bash function keyword (ERE)
grep -E '^function [a-zA-Z_][a-zA-Z0-9_]*' fileComments
# Shell comments
grep '^[[:space:]]*#' file
# Uncommented lines
grep -v '^[[:space:]]*#' fileEscaping Special Characters
Characters that need escaping (context-dependent):
. * [ ] ^ $ \ ( ) { } + ? |Literal Matching
# Match literal dot (BRE and ERE)
grep '\.' file
# Match literal asterisk
grep '\*' file
# Match literal dollar sign
grep '\$' file
# Match literal brackets
grep '\[' file
grep '\]' fileBackreferences
Capture and Reuse
# BRE - capture with \(...\), reference with \1, \2, etc.
sed 's/\([0-9]\+\)-\([0-9]\+\)/\2-\1/' file # Swap numbers
# ERE - capture with (...), reference with \1, \2, etc.
sed -E 's/([0-9]+)-([0-9]+)/\2-\1/' file
# Find duplicate words
grep -E '\b([a-z]+) \1\b' fileExamples
# Extract and rearrange (BRE)
sed 's/\([A-Z][a-z]*\), \([A-Z][a-z]*\)/\2 \1/' file
# ERE
sed -E 's/([A-Z][a-z]*), ([A-Z][a-z]*)/\2 \1/' file
# Find repeated lines
grep -E '^(.*)$\n\1$' fileGreedy vs Non-Greedy
POSIX regex is always greedy (matches longest possible string):
# Always greedy in POSIX
echo "foo bar baz" | grep -o 'f.*b' # Matches "foo bar b"
# For non-greedy, you need to be creative
echo "foo bar baz" | grep -o 'f[^b]*b' # Matches "foo b"Lookahead and Lookbehind
NOT supported in POSIX BRE/ERE. Only available in PCRE (Perl-Compatible Regular Expressions):
# PCRE only (not POSIX)
grep -P '(?<=foo)bar' file # Lookbehind
grep -P 'foo(?=bar)' file # LookaheadCommon Mistakes
1. Forgetting to Escape in BRE
# Wrong (BRE)
grep '(foo|bar)' file
# Right (BRE)
grep '\(foo\|bar\)' file
# Or use ERE
grep -E '(foo|bar)' file2. Using + in BRE Without Escape
# Wrong (BRE)
grep '[0-9]+' file
# Right (BRE)
grep '[0-9]\+' file
# Or use ERE
grep -E '[0-9]+' file3. Not Escaping Dots for Literal Match
# Wrong - matches any character
grep '192.168.1.1' file
# Right - matches literal dots
grep '192\.168\.1\.1' file4. Greedy Matching Issues
# Matches too much
echo '<tag>content</tag>' | sed 's/<.*>//' # Empty!
# Better
echo '<tag>content</tag>' | sed 's/<[^>]*>//'5. Character Class Mistakes
# Wrong - not a range
grep '[a-Z]' file # Undefined behavior
# Right
grep '[a-zA-Z]' file
# Or use POSIX class
grep '[[:alpha:]]' fileTesting Regex
Online Tools
- regex101.com (supports PCRE, not POSIX)
- regexr.com
- regexpal.com
Command Line Testing
# Test with echo
echo "test string" | grep 'pattern'
# Show matches only
echo "test string" | grep -o 'pattern'
# Test with multiple lines
printf 'line1\nline2\nline3\n' | grep 'pattern'
# Color highlighting
grep --color=always 'pattern' file | less -RQuick Reference Table
| Pattern | BRE | ERE | Matches |
|---|---|---|---|
| Literal | abc | abc | abc |
| Any char | . | . | any single character |
| Start | ^ | ^ | start of line |
| End | $ | $ | end of line |
| Zero or more | * | * | 0+ of previous |
| One or more | \+ | + | 1+ of previous |
| Zero or one | \? | ? | 0 or 1 of previous |
| Exactly n | \{n\} | {n} | exactly n |
| n or more | \{n,\} | {n,} | n or more |
| n to m | \{n,m\} | {n,m} | between n and m |
| Group | \(...\) | (...) | capture group |
| Alternation | `\ | ` | ` |
| Character class | [abc] | [abc] | a, b, or c |
| Negated class | [^abc] | [^abc] | not a, b, or c |
| Range | [a-z] | [a-z] | lowercase letters |
Resources
GNU sed Reference Guide
Overview
sed (Stream EDitor) is a powerful text processing tool that performs basic text transformations on an input stream (file or pipeline).
Official Manual: https://www.gnu.org/software/sed/manual/ Man Page: man sed
Basic Syntax
sed [OPTIONS] 'command' file
sed [OPTIONS] -e 'command1' -e 'command2' file
sed [OPTIONS] -f script.sed fileCommon Options
-n, --quiet, --silent # Suppress automatic output
-e SCRIPT # Add script to commands
-f FILE # Add contents of FILE as commands
-i[SUFFIX] # Edit files in-place
-r, -E # Use extended regular expressions
--debug # Annotate program executionBasic Commands
Substitution (s)
# Basic substitution
sed 's/old/new/' file # Replace first occurrence
sed 's/old/new/g' file # Replace all occurrences
sed 's/old/new/2' file # Replace second occurrence
sed 's/old/new/gi' file # Case-insensitive, all
# With different delimiter
sed 's|/old/path|/new/path|g' file
sed 's#old#new#g' fileDeletion (d)
# Delete lines
sed '5d' file # Delete line 5
sed '5,10d' file # Delete lines 5-10
sed '/pattern/d' file # Delete matching lines
sed '/^$/d' file # Delete empty lines
sed '/^#/d' file # Delete comment linesPrint (p)
# Print lines
sed -n '5p' file # Print only line 5
sed -n '5,10p' file # Print lines 5-10
sed -n '/pattern/p' file # Print matching linesAppend (a), Insert (i), Change (c)
# Append after line
sed '5a\New line' file
# Insert before line
sed '5i\New line' file
# Change line
sed '5c\Replacement line' file
# With pattern
sed '/pattern/a\New line after match' fileAddress Ranges
Line Numbers
sed '5s/old/new/' file # Line 5 only
sed '5,10s/old/new/' file # Lines 5-10
sed '5,$s/old/new/' file # Line 5 to end
sed '1,5d' file # Delete first 5 linesPatterns
sed '/start/,/end/d' file # Delete from start to end pattern
sed '/pattern/s/old/new/' file # Substitute in matching lines
sed '1,/pattern/d' file # Delete from line 1 to first matchSpecial Addresses
sed '$d' file # Delete last line
sed '1d' file # Delete first line
sed '$s/old/new/' file # Substitute in last lineAdvanced Substitution
Backreferences
# Capture and reuse
sed 's/\([0-9]\+\)/Number: \1/' file
# Multiple captures
sed 's/\([a-z]\+\) \([0-9]\+\)/\2 \1/' file
# With ERE (-E or -r)
sed -E 's/([0-9]+)/Number: \1/' file
sed -E 's/([a-z]+) ([0-9]+)/\2 \1/' fileFlags
s/old/new/ # Replace first
s/old/new/g # Replace all (global)
s/old/new/2 # Replace 2nd occurrence
s/old/new/i # Case-insensitive
s/old/new/I # Case-insensitive (same as i)
s/old/new/p # Print if substitution made
s/old/new/w file # Write if substitution madeSpecial Characters in Replacement
& # Matched string
\1, \2, etc # Backreferences
\L, \U # Convert to lower/upper (GNU sed)
\n # Newline (in replacement)
\\ # Literal backslashMultiple Commands
Multiple -e Options
sed -e 's/old/new/g' -e 's/foo/bar/g' fileSemicolon Separator
sed 's/old/new/g; s/foo/bar/g' fileMulti-line Script
sed '
s/old/new/g
s/foo/bar/g
/pattern/d
' fileIn-place Editing
# Edit file in-place
sed -i 's/old/new/g' file
# Create backup
sed -i.bak 's/old/new/g' file
# Multiple files
sed -i 's/old/new/g' *.txtPattern Matching
BRE (Basic Regular Expressions) - Default
sed 's/^/#/' file # Add # at beginning
sed 's/$/;/' file # Add ; at end
sed 's/[0-9]\+/X/g' file # Replace numbers (BRE)
sed 's/\<word\>/WORD/g' file # Word boundaries (BRE)ERE (Extended Regular Expressions)
sed -E 's/[0-9]+/X/g' file # Replace numbers (ERE)
sed -E 's/(foo|bar)/baz/g' file # Alternation
sed -E 's/\s+/ /g' file # Multiple spaces to onePractical Examples for Shell Scripts
Configuration File Editing
# Change a config value
sed -i 's/^Port .*/Port 2222/' /etc/ssh/sshd_config
# Uncomment a line
sed -i 's/^#\(.*option.*\)/\1/' config.file
# Comment out a line
sed -i 's/^\(.*dangerous.*\)/#\1/' config.file
# Add line after pattern
sed -i '/\[section\]/a new_setting = value' config.iniText Processing
# Remove trailing whitespace
sed 's/[[:space:]]*$//' file
# Remove leading whitespace
sed 's/^[[:space:]]*//' file
# Remove empty lines
sed '/^$/d' file
# Remove comments and empty lines
sed '/^#/d; /^$/d' file
# Double-space file
sed 'G' file
# Remove duplicate lines (consecutive)
sed '$!N; /^\(.*\)\n\1$/!P; D' filePath Manipulation
# Change paths
sed 's|/old/path|/new/path|g' file
# Extract filename from path
echo "/path/to/file.txt" | sed 's|.*/||'
# Extract directory from path
echo "/path/to/file.txt" | sed 's|/[^/]*$||'Log File Processing
# Extract IP addresses
sed -n 's/.*\([0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\).*/\1/p' log
# Filter by date
sed -n '/2025-01-01/,/2025-01-31/p' log
# Remove timestamp
sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\} [0-9]\{2\}:[0-9]\{2\}:[0-9]\{2\} //' logCode Refactoring
# Rename function
sed -i 's/\boldFunctionName\b/newFunctionName/g' *.sh
# Change variable
sed -i 's/\$old_var/\$new_var/g' script.sh
# Update shebang
sed -i '1s|^#!/bin/sh|#!/bin/bash|' *.shAdvanced Features
Hold Space
# Hold space commands
h # Copy pattern space to hold space
H # Append pattern space to hold space
g # Copy hold space to pattern space
G # Append hold space to pattern space
x # Exchange pattern and hold spacesHold Space Examples
# Reverse file
sed '1!G;h;$!d' file
# Print last line
sed -n '$p' file
sed -n 'h;$p' file
# Append next line to current
sed 'N;s/\n/ /' fileBranching
# Label and branch
:label # Define label
b label # Branch to label
t label # Branch if substitution succeeded
T label # Branch if substitution failedBranching Examples
# Remove C-style comments
sed -n '
/\/\*/ {
:loop
/\*\// {
s|/\*.*\*/||g
p
b
}
N
b loop
}
p
' fileCommon Patterns
Join Lines
# Join all lines
sed ':a;N;$!ba;s/\n/ /g' file
# Join lines ending with backslash
sed -e :a -e '/\\$/N; s/\\\n//; ta' fileNumber Lines
sed = file | sed 'N;s/\n/\t/'Reverse Lines
sed '1!G;h;$!d' filePrint Specific Lines
# Print line 5
sed -n '5p' file
# Print first 10 lines
sed -n '1,10p' file
# Print last 10 lines
sed -n -e :a -e '1,10!{P;N;D;};N;ba' fileCommon Pitfalls in Shell Scripts
1. Special Characters in Pattern
# Wrong - . matches any character
sed 's/192.168.1.1/new/' file
# Right - escape dots
sed 's/192\.168\.1\.1/new/' file
# Or use different delimiter
sed 's|192.168.1.1|new|' file2. Not Escaping Backreferences in BRE
# Wrong (BRE)
sed 's/([0-9]+)/\1/' file
# Right (BRE)
sed 's/\([0-9]\+\)/\1/' file
# Right (ERE)
sed -E 's/([0-9]+)/\1/' file3. In-place Editing Without Backup
# Dangerous
sed -i 's/old/new/' important_file
# Safer
sed -i.backup 's/old/new/' important_file4. Using sed for Line Counting
# Inefficient
sed -n '$=' file
# Better
wc -l < file5. Not Quoting Variables Properly
# Wrong
sed "s/$old/$new/g" file # Dangerous if vars contain /
# Better
old_escaped=$(printf '%s\n' "$old" | sed 's:[\\/&]:\\&:g')
new_escaped=$(printf '%s\n' "$new" | sed 's:[\\/&]:\\&:g')
sed "s/$old_escaped/$new_escaped/g" file
# Or use different delimiter
sed "s|$old|$new|g" filePerformance Tips
1. Use Appropriate Tools
# For simple replacements, consider using other tools
# sed is great for complex patterns, but for simple tasks:
# Instead of
sed 's/old/new/g' file
# Consider
tr 'old' 'new' < file # For single-character replacement2. Minimize Pattern Matching
# Less efficient
sed '/pattern/s/old/new/g' large_file
# More efficient if pattern is rare
grep 'pattern' large_file | sed 's/old/new/g'3. Combine Commands
# Less efficient
sed 's/old/new/g' file | sed 's/foo/bar/g'
# More efficient
sed 's/old/new/g; s/foo/bar/g' fileTesting sed Commands
# Test before in-place edit
sed 's/old/new/g' file | head
# Show only changes
sed -n 's/old/new/gp' file
# Count changes
sed -n 's/old/new/gp' file | wc -lResources
POSIX Shell (sh) Reference Guide
Overview
POSIX sh is the portable shell specification defined by POSIX standards. Scripts written for POSIX sh should work across different Unix-like systems (bash, dash, ksh, etc.).
Official Specification: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
Why POSIX Shell Matters
- Portability: Works across different Unix systems and shells
- Minimal dependencies: Available in minimal environments (containers, embedded systems)
- Faster: Shells like dash are faster than bash for simple scripts
- Compatibility: /bin/sh may not be bash (Ubuntu/Debian use dash)
Key Differences: sh vs bash
Features NOT Available in POSIX sh
1. Arrays
# Bash only - NOT POSIX
array=(one two three)
echo "${array[0]}"2. [[ ]] Test Construct
# Bash only - NOT POSIX
if [[ "$var" == "value" ]]; then
# POSIX sh - use [ ]
if [ "$var" = "value" ]; then3. == Operator
# Bash style - NOT POSIX
[ "$a" == "$b" ]
# POSIX sh - use single =
[ "$a" = "$b" ]4. Process Substitution
# Bash only - NOT POSIX
diff <(ls dir1) <(ls dir2)5. Brace Expansion
# Bash only - NOT POSIX
echo {1..10}6. function Keyword
# Bash style - NOT in original POSIX
function myfunc {
echo "hello"
}
# POSIX sh style
myfunc() {
echo "hello"
}7. local Keyword
# Common but not in POSIX standard
local var="value"
# POSIX alternative: use function scope carefully
# or use naming conventions
_func_var="value"8. source Command
# Bash style - NOT POSIX
source script.sh
# POSIX sh
. script.shPOSIX Shell Syntax
Variables
# Assignment
var="value"
readonly CONST="constant"
# Reading variables
echo "$var"
echo "${var}"
# Command substitution (POSIX)
result=$(command)
# Old-style command substitution (works but deprecated)
result=`command`
# Arithmetic (POSIX way)
result=$((5 + 3))Quoting
# Double quotes: Preserve literal value except $, `, and \
echo "Value: $var"
# Single quotes: Preserve everything literally
echo 'Value: $var'
# Always quote variables
cp "$file" "$destination"Control Structures
# If statement
if [ condition ]; then
# commands
elif [ condition ]; then
# commands
else
# commands
fi
# Case statement
case "$var" in
pattern1)
# commands
;;
pattern2|pattern3)
# commands
;;
*)
# default
;;
esac
# For loop
for item in list; do
echo "$item"
done
# While loop
while [ condition ]; do
# commands
done
# Until loop
until [ condition ]; do
# commands
doneTest Constructs
POSIX sh uses [ ] (also known as test command):
# String comparisons
[ "$a" = "$b" ] # Equal
[ "$a" != "$b" ] # Not equal
[ -z "$a" ] # String is empty
[ -n "$a" ] # String is not empty
# Numeric comparisons
[ "$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
# File tests
[ -e "$file" ] # File exists
[ -f "$file" ] # Regular file exists
[ -d "$file" ] # Directory exists
[ -r "$file" ] # File is readable
[ -w "$file" ] # File is writable
[ -x "$file" ] # File is executable
[ -s "$file" ] # File is not empty
# Logical operators
[ condition1 ] && [ condition2 ] # AND
[ condition1 ] || [ condition2 ] # OR
[ ! condition ] # NOT
[ condition1 -a condition2 ] # AND (inside test)
[ condition1 -o condition2 ] # OR (inside test)Functions
# POSIX function definition
function_name() {
# No 'local' in strict POSIX
# Use careful scoping or naming conventions
echo "$1" # First argument
return 0 # Exit status
}
# Call function
function_name arg1 arg2Input/Output Redirection
# Redirect stdout
command > file
# Redirect stderr
command 2> errors.txt
# Redirect both
command > output.txt 2>&1
# Append
command >> file
# Here document
cat <<EOF
multiple
lines
of text
EOF
# Read from stdin
while read -r line; do
echo "$line"
done < file.txtPOSIX Best Practices
1. Proper Shebang
#!/bin/sh
# Use /bin/sh for POSIX scripts, not /bin/bash2. Quote All Variables
# Good
cp "$source" "$destination"
# Bad
cp $source $destination3. Use = Not ==
# POSIX compliant
if [ "$var" = "value" ]; then
# NOT POSIX (bash-specific)
if [ "$var" == "value" ]; then4. Use $() for Command Substitution
# Preferred (POSIX)
result=$(command)
# Old style (works but less readable)
result=`command`5. Avoid Bashisms
Don't use:
- Arrays:
array=(one two) [[test construct- Process substitution:
<(command) - Brace expansion:
{1..10} functionkeywordsourcecommand (use.instead)==operator (use=)$RANDOMvariable
6. Check Command Existence
if command -v shellcheck >/dev/null 2>&1; then
echo "ShellCheck is installed"
fi7. Handle Errors
# Set errexit
set -e
# Or check manually
if ! command; then
echo "Command failed" >&2
exit 1
fi8. Use set -u for Undefined Variables
set -u
# Now accessing undefined variables causes errorCommon Portability Issues
1. echo Command
# Portable way to echo without newline
printf '%s' "text without newline"
# echo -n is not portable
echo -n "text" # Don't use in POSIX sh
# echo with backslashes
printf '%s\n' "text\twith\ttabs"
echo "text\twith\ttabs" # Behavior varies2. Array Alternatives
# Instead of arrays, use:
# 1. Positional parameters
set -- one two three
echo "$1" # one
# 2. Delimited strings
items="one:two:three"
IFS=:
for item in $items; do
echo "$item"
done3. String Manipulation
# POSIX parameter expansion
${var#pattern} # Remove shortest match from beginning
${var##pattern} # Remove longest match from beginning
${var%pattern} # Remove shortest match from end
${var%%pattern} # Remove longest match from end
# NOT POSIX (bash-specific)
${var/pattern/replacement}
${var,,} # lowercase
${var^^} # uppercase4. Arithmetic
# POSIX way
result=$((a + b))
# NOT POSIX (bash-specific)
((a++))
let "a = a + 1"5. read Command
# POSIX
while IFS= read -r line; do
echo "$line"
done < file
# Bash-specific flags to avoid:
read -p "prompt" # Not in POSIX
read -a array # Not in POSIX
read -t timeout # Not in POSIXPOSIX Parameter Expansion
${var} # Value of var
${var:-default} # Use default if var is unset or null
${var:=default} # Assign default if var is unset or null
${var:?error} # Error if var is unset or null
${var:+alternate} # Use alternate if var is set and not null
${#var} # Length of var
${var#pattern} # Remove shortest match from beginning
${var##pattern} # Remove longest match from beginning
${var%pattern} # Remove shortest match from end
${var%%pattern} # Remove longest match from endSpecial Variables (POSIX)
$0 # Script name
$1-$9 # Positional parameters
${10} # Parameters beyond 9 (braces required)
$# # Number of positional parameters
$* # All positional parameters (as single word)
$@ # All positional parameters (as separate words)
$$ # Process ID of shell
$! # PID of last background command
$? # Exit status of last commandTesting for POSIX Compliance
Use checkbashisms
# Install checkbashisms (Debian/Ubuntu)
apt-get install devscripts
# Check script
checkbashisms script.shUse ShellCheck with sh
# Validate as sh script
shellcheck -s sh script.shTest with Different Shells
# Test with dash (common /bin/sh)
dash script.sh
# Test with ash
ash script.sh
# Test with ksh
ksh script.shCommon POSIX Utilities
These utilities are standardized and safe to use in POSIX scripts:
cat,echo,printfgrep,sed,awkcut,sort,uniq,trhead,tail,wcfind,xargstest(same as[ ])cd,pwd,lscp,mv,rm,mkdirchmod,chownread,shift,set,export
Resources
ShellCheck Reference Guide
Overview
ShellCheck is a static analysis tool for shell scripts that provides warnings and suggestions for syntax and semantic issues to improve script quality and prevent errors.
Official Website: https://www.shellcheck.net/ GitHub: https://github.com/koalaman/shellcheck Wiki: https://github.com/koalaman/shellcheck/wiki
Installation
# macOS
brew install shellcheck
# Ubuntu/Debian
apt-get install shellcheck
# Fedora
dnf install shellcheck
# From source/binary
# See: https://github.com/koalaman/shellcheck#installingBasic Usage
# Check a script
shellcheck script.sh
# Specify shell dialect
shellcheck -s bash script.sh
shellcheck -s sh script.sh
shellcheck -s ksh script.sh
shellcheck -s zsh script.sh
# Different output formats
shellcheck -f gcc script.sh # GCC-style (for editors)
shellcheck -f checkstyle script.sh # Checkstyle XML
shellcheck -f json script.sh # JSON
shellcheck -f tty script.sh # TTY (default, with colors)
# Check multiple files
shellcheck *.sh
# Exclude specific warnings
shellcheck -e SC2086,SC2046 script.sh
# Set minimum severity
shellcheck -S error script.sh # Only errors
shellcheck -S warning script.sh # Warnings and aboveSeverity Levels
ShellCheck categorizes issues into four severity levels:
1. error - Critical issues that will cause failures 2. warning - Potential bugs or problematic patterns 3. info - Suggestions for improvement 4. style - Stylistic improvements
# Show only errors
shellcheck -S error script.sh
# Show errors and warnings
shellcheck -S warning script.sh
# Show everything (default)
shellcheck script.shCommon Error Codes
Critical Errors (SC2xxx series)
SC2086: Quote Variables to Prevent Word Splitting
# Problematic
cp $file $destination
# Fixed
cp "$file" "$destination"SC2046: Quote Command Substitutions
# Problematic
for file in $(ls *.txt); do
# Fixed
for file in *.txt; doSC2006: Use $() Instead of Backticks
# Problematic
result=`command`
# Fixed
result=$(command)SC2155: Declare and Assign Separately
# Problematic
local result=$(command) # Masks return value
# Fixed
local result
result=$(command)SC2164: Use || exit After cd
# Problematic
cd /some/directory
rm -rf *
# Fixed
cd /some/directory || exit
rm -rf *SC2181: Check Exit Code Directly
# Problematic
command
if [ $? -eq 0 ]; then
# Fixed
if command; thenSC2068: Quote Array Expansions
# Problematic
command $@
# Fixed
command "$@"SC2116: Useless echo with $()
# Problematic
var=$(echo $value)
# Fixed
var=$valueSC2162: read Without -r
# Problematic
while read line; do
# Fixed
while IFS= read -r line; doSC2005: Useless echo Piped to Command
# Problematic
echo "$var" | grep pattern
# Fixed
grep pattern <<< "$var"
# Or
printf '%s\n' "$var" | grep patternBashisms (SC3xxx series)
These warn about bash-specific features used in sh scripts:
SC3001: Using Bash [[ ]] in sh Script
# In #!/bin/sh script
if [[ condition ]]; then # Wrong
# Fixed
if [ condition ]; thenSC3037: Using Bash Arrays in sh Script
# In #!/bin/sh script
array=(one two) # Wrong
# No direct fix - arrays not in POSIX sh
# Use alternatives like positional parametersDisabling Checks
Disable Specific Line
# shellcheck disable=SC2086
variable=$unquotedDisable for Entire File
# At top of file
# shellcheck disable=SC2086,SC2046Disable Next Line
# shellcheck disable=SC2086
variable=$unquotedDisable for Block
# shellcheck disable=SC2086
{
var1=$unquoted1
var2=$unquoted2
}
# shellcheck enable=SC2086ShellCheck Directives
Shell Directive
# Specify shell dialect (overrides shebang)
# shellcheck shell=bash
# or
# shellcheck shell=shSource Directive
# Tell ShellCheck where to find sourced files
# shellcheck source=./lib/common.sh
. ./lib/common.shExternal Sources
# For dynamically sourced files
# shellcheck source=/dev/null
. "$config_file"Configuration File
Create .shellcheckrc in project root or ~/.shellcheckrc:
# Disable specific checks globally
disable=SC2086,SC2046,SC2068
# Enable optional checks
enable=all
enable=avoid-nullary-conditions
# Specify shell
shell=bashIntegration with CI/CD
GitHub Actions
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: warningGitLab CI
shellcheck:
script:
- shellcheck **/*.shPre-commit Hook
# .pre-commit-config.yaml
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.9.0.2
hooks:
- id: shellcheckCommon Patterns and Best Practices
1. Always Quote Variables
ShellCheck will flag unquoted variables in most contexts.
2. Use -r Flag with read
# Good
while IFS= read -r line; do
echo "$line"
done < file3. Check Command Existence
if command -v shellcheck >/dev/null 2>&1; then
echo "Found"
fi4. Use || exit After cd
cd /directory || exit 15. Use [[ ]] in Bash, [ ] in sh
ShellCheck knows your shell and will warn appropriately.
6. Proper Array Usage
# Good (bash)
args=("first arg" "second arg")
command "${args[@]}"7. Avoid Useless cat
# Instead of
cat file | grep pattern
# Use
grep pattern file
# or
< file grep patternAdvanced Features
Optional Checks
Some checks are not enabled by default:
# Enable all optional checks
# shellcheck enable=all
# Or specific ones
# shellcheck enable=avoid-nullary-conditions
# shellcheck enable=quote-safe-variables
# shellcheck enable=require-variable-bracesCustom Severity
# Change severity of specific check
# shellcheck severity=warning SC2086Exit Codes
- 0: No issues found
- 1: Some issues found
- 2: Syntax errors that prevent parsing
- 3: ShellCheck error (bad options, missing files)
- 4: ShellCheck not installed
Editor Integration
ShellCheck integrates with most editors:
- VS Code: ShellCheck extension
- Vim: via ALE, Syntastic, or vim-shellcheck
- Emacs: flycheck-shellcheck
- Sublime Text: SublimeLinter-shellcheck
- Atom: linter-shellcheck
Resources
- Main Website: https://www.shellcheck.net/
- Wiki with Error Codes: https://github.com/koalaman/shellcheck/wiki
- Try Online: https://www.shellcheck.net/
- GitHub Issues: https://github.com/koalaman/shellcheck/issues
Quick Reference Table
| Code | Issue | Fix |
|---|---|---|
| SC2086 | Unquoted variable | Add quotes: "$var" |
| SC2046 | Unquoted $() | Quote command substitution |
| SC2006 | Backticks | Use $() instead |
| SC2155 | Declare and assign together | Separate into two lines |
| SC2164 | cd without error check | Add ` |
| SC2181 | Checking $? | Check command directly |
| SC2068 | Unquoted $@ | Quote: "$@" |
| SC2162 | read without -r | Add -r flag |
| SC3001 | [[ in sh script | Use [ ] instead |
| SC3037 | Arrays in sh script | Use POSIX alternatives |
#!/usr/bin/env bash
#
# Example of a poorly-written bash script with common mistakes
#
# Missing: set -euo pipefail
LOG_FILE=/tmp/example.log
# Function defined after use (will fail)
main
log_info() {
# Bad: unquoted variable
echo [INFO] $*
}
# Bad: using backticks instead of $()
result=`date`
process_file() {
file=$1 # Not local
# Bad: not quoting variable
if [ ! -f $file ]; then
echo "File not found"
return 1
fi
# Bad: useless use of cat
cat $file | grep pattern
# Bad: eval with variable (security risk)
eval $user_command
}
main() {
# Bad: not checking if arguments provided
# Bad: unquoted $@
for file in $@; do
# Bad: not checking return value
cd /some/directory
rm -rf * # DANGEROUS!
process_file $file
done
# Bad: checking $? after multiple commands
if [ $? -eq 0 ]; then
echo "Success"
fi
}
# Bad: calling main without "$@"
main $*#!/bin/sh
#
# Example of a poorly-written shell script with bashisms and other mistakes
#
# Bad: using bash-specific [[ ]]
if [[ -f /etc/passwd ]]; then
echo "File exists"
fi
# Bad: using bash arrays in sh script
array=(one two three)
echo ${array[0]}
# Bad: using bash-specific function keyword
function process_data {
# Bad: using bash-specific 'local'
local data=$1
# Bad: unquoted variable
echo $data
}
# Bad: using 'source' instead of '.'
source /etc/profile
# Bad: using == instead of =
if [ "$var" == "value" ]; then
echo "match"
fi
# Bad: process substitution (bash-specific)
diff <(ls dir1) <(ls dir2)
# Bad: brace expansion (bash-specific)
echo {1..10}
# Bad: $RANDOM (bash-specific)
random_num=$RANDOM
# Bad: using [[ with regex (bash-specific)
if [[ "$string" =~ pattern ]]; then
echo "matches"
fi
# Bad: not quoting variables
file=/path/with spaces/file.txt
cat $file
# Bad: useless use of cat
cat file.txt | grep pattern
# Bad: using eval without sanitization
eval $user_input#!/usr/bin/env bash
#
# Example of a well-written bash script following best practices
#
set -euo pipefail
# Constants
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly LOG_FILE="/tmp/example.log"
# Functions
log_info() {
echo "[INFO] $*" | tee -a "$LOG_FILE"
}
log_error() {
echo "[ERROR] $*" >&2 | tee -a "$LOG_FILE"
}
cleanup() {
log_info "Cleaning up..."
rm -f "$temp_file"
}
trap cleanup EXIT
process_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
log_error "File not found: $file"
return 1
fi
log_info "Processing file: $file"
# Good: using modern command substitution
local line_count
line_count=$(wc -l < "$file")
log_info "File has $line_count lines"
return 0
}
main() {
log_info "Script started from $SCRIPT_DIR"
# Create temporary file
local temp_file
temp_file=$(mktemp)
# Good: proper argument handling
if [[ $# -eq 0 ]]; then
log_error "Usage: $0 <file1> [file2 ...]"
exit 1
fi
# Good: quoted "$@" preserves arguments
for file in "$@"; do
if ! process_file "$file"; then
log_error "Failed to process: $file"
exit 1
fi
done
log_info "Script completed successfully"
}
main "$@"#!/bin/sh
#
# Example of a well-written POSIX shell script
#
set -eu
# POSIX-compliant - no bashisms
readonly SCRIPT_NAME="${0##*/}"
readonly LOG_FILE="/tmp/example.log"
log_info() {
printf '[INFO] %s\n' "$*" | tee -a "$LOG_FILE"
}
log_error() {
printf '[ERROR] %s\n' "$*" >&2
}
cleanup() {
log_info "Cleaning up..."
rm -f "$temp_file"
}
trap cleanup EXIT INT TERM
process_file() {
file="$1"
# POSIX: using [ ] not [[ ]]
if [ ! -f "$file" ]; then
log_error "File not found: $file"
return 1
fi
log_info "Processing file: $file"
# POSIX: command substitution with $()
line_count=$(wc -l < "$file")
log_info "File has $line_count lines"
return 0
}
main() {
log_info "Script started"
# Create temporary file
temp_file=$(mktemp)
# Proper argument handling
if [ $# -eq 0 ]; then
log_error "Usage: $SCRIPT_NAME <file1> [file2 ...]"
exit 1
fi
# POSIX: iterate over positional parameters
for file in "$@"; do
if ! process_file "$file"; then
log_error "Failed to process: $file"
exit 1
fi
done
log_info "Script completed successfully"
}
main "$@"#!/usr/bin/env bash
#
# Deterministic CI runner for bash-script-validator.
# Uses system shellcheck only and fails fast when unavailable.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
TEST_SCRIPT="$SCRIPT_DIR/test_validate.sh"
if ! command -v shellcheck >/dev/null 2>&1; then
echo "[ERROR] System shellcheck is required for deterministic CI checks." >&2
echo " Install shellcheck and re-run scripts/run_ci_checks.sh." >&2
exit 1
fi
echo "[INFO] Running deterministic bash-script-validator regression suite..."
CI=1 \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_SHELLCHECK_MODE=system \
bash "$TEST_SCRIPT"
#!/usr/bin/env bash
#
# ShellCheck Wrapper with Temporary Virtual Environment
#
# This script creates a temporary Python virtual environment, installs shellcheck-py,
# runs shellcheck, and cleans up afterwards.
#
# Usage: ./shellcheck_wrapper.sh [shellcheck-options] <script-file>
#
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
VENV_DIR=""
CLEANUP_ON_EXIT=true
CACHE_VENV=false
VENV_CACHE_DIR="${HOME}/.cache/bash-script-validator/shellcheck-venv"
# Cleanup function
cleanup() {
if [[ "$CLEANUP_ON_EXIT" == "true" ]] && [[ -n "$VENV_DIR" ]] && [[ -d "$VENV_DIR" ]]; then
echo -e "${BLUE}[INFO]${NC} Cleaning up temporary virtual environment..." >&2
rm -rf "$VENV_DIR"
fi
}
trap cleanup EXIT INT TERM
# Check if Python 3 is available
check_python() {
if ! command -v python3 &>/dev/null; then
echo -e "${RED}[ERROR]${NC} python3 not found. Please install Python 3." >&2
exit 1
fi
}
# Check if shellcheck is already available system-wide
check_system_shellcheck() {
if command -v shellcheck &>/dev/null; then
# ShellCheck is available, use it directly
shellcheck "$@"
exit $?
fi
}
# Create and activate virtual environment
setup_venv() {
if [[ "$CACHE_VENV" == "true" ]] && [[ -d "$VENV_CACHE_DIR" ]]; then
echo -e "${GREEN}[INFO]${NC} Using cached virtual environment..." >&2
VENV_DIR="$VENV_CACHE_DIR"
CLEANUP_ON_EXIT=false
return 0
fi
echo -e "${BLUE}[INFO]${NC} Creating temporary virtual environment..." >&2
if [[ "$CACHE_VENV" == "true" ]]; then
mkdir -p "$(dirname "$VENV_CACHE_DIR")"
VENV_DIR="$VENV_CACHE_DIR"
CLEANUP_ON_EXIT=false
else
VENV_DIR=$(mktemp -d -t shellcheck-venv.XXXXXX)
fi
python3 -m venv "$VENV_DIR"
# Activate virtual environment
# shellcheck source=/dev/null
source "$VENV_DIR/bin/activate"
}
# Install shellcheck-py
install_shellcheck() {
local marker_file="$VENV_DIR/.shellcheck_installed"
if [[ -f "$marker_file" ]]; then
echo -e "${GREEN}[INFO]${NC} ShellCheck already installed in cached venv" >&2
return 0
fi
echo -e "${BLUE}[INFO]${NC} Installing shellcheck-py..." >&2
# Upgrade pip first (suppress output)
pip3 install --upgrade pip &>/dev/null
# Install shellcheck-py
if pip3 install shellcheck-py &>/dev/null; then
echo -e "${GREEN}[INFO]${NC} ShellCheck installed successfully" >&2
touch "$marker_file"
return 0
else
echo -e "${RED}[ERROR]${NC} Failed to install shellcheck-py" >&2
return 1
fi
}
# Run shellcheck
run_shellcheck() {
if [[ ! -f "$VENV_DIR/bin/shellcheck" ]]; then
echo -e "${RED}[ERROR]${NC} ShellCheck binary not found in virtual environment" >&2
exit 1
fi
# Run shellcheck with all provided arguments
"$VENV_DIR/bin/shellcheck" "$@"
}
# Main function
main() {
# If no arguments, show usage
if [[ $# -eq 0 ]]; then
echo "Usage: $0 [--cache] [--no-cache] [shellcheck-options] <script-file>"
echo ""
echo "Options:"
echo " --cache Cache the virtual environment for faster subsequent runs"
echo " --no-cache Don't use cached venv (default)"
echo " --clear-cache Clear the cached virtual environment"
echo ""
echo "Examples:"
echo " $0 script.sh"
echo " $0 --cache -s bash script.sh"
echo " $0 -f gcc script.sh"
exit 0
fi
# Parse wrapper-specific options
local args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--cache)
CACHE_VENV=true
shift
;;
--no-cache)
CACHE_VENV=false
shift
;;
--clear-cache)
if [[ -d "$VENV_CACHE_DIR" ]]; then
echo -e "${BLUE}[INFO]${NC} Clearing cached virtual environment..."
rm -rf "$VENV_CACHE_DIR"
echo -e "${GREEN}[INFO]${NC} Cache cleared"
else
echo -e "${YELLOW}[INFO]${NC} No cache to clear"
fi
exit 0
;;
*)
args+=("$1")
shift
;;
esac
done
# Check if system shellcheck is available
check_system_shellcheck "${args[@]}"
# System shellcheck not found, use venv approach
check_python
setup_venv
install_shellcheck
# Run shellcheck with remaining arguments
run_shellcheck "${args[@]}"
}
main "$@"#!/usr/bin/env bash
#
# Regression test suite for validate.sh
#
# Runs the validator against each example file and asserts the expected exit code.
# Exit 0 when all assertions pass; non-zero otherwise.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
VALIDATOR="$SCRIPT_DIR/validate.sh"
EXAMPLES_DIR="$SCRIPT_DIR/../examples"
TMP_DIR="$(mktemp -d)"
FAKE_SHELLCHECK_BIN="$TMP_DIR/fake-shellcheck-bin"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
# Counters
PASS=0
FAIL=0
# ─── helpers ────────────────────────────────────────────────────────────────
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
# Create a deterministic shellcheck stub so regression tests do not depend on host tooling.
mkdir -p "$FAKE_SHELLCHECK_BIN"
cat > "$FAKE_SHELLCHECK_BIN/shellcheck" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
mode="${FAKE_SHELLCHECK_MODE:-ok}"
target="${@: -1}"
case "$mode" in
ok)
exit 0
;;
warning)
echo "${target}:1:1: warning: fake shellcheck warning [SC9999]"
exit 1
;;
infra)
echo "fake shellcheck infrastructure failure" >&2
exit 3
;;
*)
echo "unknown FAKE_SHELLCHECK_MODE: $mode" >&2
exit 2
;;
esac
EOF
chmod +x "$FAKE_SHELLCHECK_BIN/shellcheck"
# Run the validator and return its exit code without aborting this script.
# Uses || to prevent set -e from treating a non-zero validator exit as fatal.
run_validator() {
local file="$1"
shift
local exit_code=0
if [[ $# -gt 0 ]]; then
env "$@" bash "$VALIDATOR" "$file" >/dev/null 2>&1 || exit_code=$?
else
bash "$VALIDATOR" "$file" >/dev/null 2>&1 || exit_code=$?
fi
echo "$exit_code"
}
validator_output() {
local file="$1"
shift
if [[ $# -gt 0 ]]; then
env "$@" bash "$VALIDATOR" "$file" 2>&1 || true
else
bash "$VALIDATOR" "$file" 2>&1 || true
fi
}
# Assert that the validator exits with a specific code for the given file.
assert_exit_code() {
local label="$1"
local file="$2"
local expected="$3"
shift 3
local actual
actual=$(run_validator "$file" "$@")
if [[ "$actual" -eq "$expected" ]]; then
pass "$label (exit $actual)"
else
fail "$label — expected exit $expected, got $actual"
# Re-run with output visible so the failure is diagnosable.
echo " --- validator output ---"
validator_output "$file" "$@" | sed 's/^/ /'
echo " --- end output ---"
fi
}
assert_exit_code_in() {
local label="$1"
local file="$2"
local expected_csv="$3"
shift 3
local actual
actual=$(run_validator "$file" "$@")
if [[ ",$expected_csv," == *",$actual,"* ]]; then
pass "$label (exit $actual)"
else
fail "$label — expected one of [$expected_csv], got $actual"
echo " --- validator output ---"
validator_output "$file" "$@" | sed 's/^/ /'
echo " --- end output ---"
fi
}
# Assert that a pattern IS found in the validator output for a given file.
assert_output_contains() {
local label="$1"
local file="$2"
local pattern="$3"
shift 3
local output
output=$(validator_output "$file" "$@")
if echo "$output" | grep -qE "$pattern"; then
pass "$label"
else
fail "$label — pattern not found: $pattern"
echo " --- validator output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
fi
}
# Assert that a pattern is NOT found in the validator output for a given file.
assert_output_not_contains() {
local label="$1"
local file="$2"
local pattern="$3"
shift 3
local output
output=$(validator_output "$file" "$@")
if echo "$output" | grep -qE "$pattern"; then
fail "$label — unexpected pattern found: $pattern"
echo " --- validator output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
else
pass "$label"
fi
}
assert_shellcheck_stage_verifiable() {
local label="$1"
local file="$2"
shift 2
local output
output=$(validator_output "$file" "$@")
if ! echo "$output" | grep -q '\[SHELLCHECK\]'; then
fail "$label — missing [SHELLCHECK] stage"
echo " --- validator output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
return
fi
if echo "$output" | grep -qE 'ShellCheck unavailable|ShellCheck not installed'; then
fail "$label — ShellCheck stage unavailable"
echo " --- validator output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
return
fi
if echo "$output" | grep -qE 'No ShellCheck issues found|SC[0-9]{4}|: (error|warning|note|style):'; then
pass "$label"
return
fi
fail "$label — ShellCheck produced no verifiable signal"
echo " --- validator output ---"
echo "$output" | sed 's/^/ /'
echo " --- end output ---"
}
# ─── test cases ─────────────────────────────────────────────────────────────
echo "Running bash-script-validator tests..."
echo ""
# --- good-bash.sh: well-written bash, must exit 0 ---
echo "[good-bash.sh]"
assert_exit_code \
"exits cleanly (code 0)" \
"$EXAMPLES_DIR/good-bash.sh" \
0
assert_output_not_contains \
"no false-positive errors" \
"$EXAMPLES_DIR/good-bash.sh" \
"✗"
assert_shellcheck_stage_verifiable \
"shellcheck stage is verifiable" \
"$EXAMPLES_DIR/good-bash.sh"
# --- good-shell.sh: well-written POSIX sh, must exit 0 ---
echo ""
echo "[good-shell.sh]"
assert_exit_code \
"exits cleanly (code 0)" \
"$EXAMPLES_DIR/good-shell.sh" \
0
assert_output_not_contains \
"no false-positive [[ ]] error from comment on line 31" \
"$EXAMPLES_DIR/good-shell.sh" \
"\[\["
assert_output_not_contains \
"no false-positive errors" \
"$EXAMPLES_DIR/good-shell.sh" \
"✗"
assert_shellcheck_stage_verifiable \
"shellcheck stage is verifiable" \
"$EXAMPLES_DIR/good-shell.sh"
# --- bad-bash.sh: intentionally bad bash, must be non-clean ---
echo ""
echo "[bad-bash.sh]"
assert_exit_code_in \
"exits non-clean (code 1 or 2)" \
"$EXAMPLES_DIR/bad-bash.sh" \
"1,2"
assert_output_contains \
"detects eval with variable" \
"$EXAMPLES_DIR/bad-bash.sh" \
"eval with variable"
assert_output_contains \
"detects useless cat" \
"$EXAMPLES_DIR/bad-bash.sh" \
"Useless use of cat"
# --- bad-shell.sh: intentionally bad POSIX sh, must exit 2 ---
echo ""
echo "[bad-shell.sh]"
assert_exit_code \
"exits with errors (code 2)" \
"$EXAMPLES_DIR/bad-shell.sh" \
2
assert_output_contains \
"detects [[ ]] in sh script (line 7, actual code)" \
"$EXAMPLES_DIR/bad-shell.sh" \
"Bash-specific \[\[ \]\]"
assert_output_contains \
"detects bash arrays in sh script" \
"$EXAMPLES_DIR/bad-shell.sh" \
"Bash-specific arrays"
assert_output_contains \
"detects function keyword in sh script" \
"$EXAMPLES_DIR/bad-shell.sh" \
"function.*keyword"
assert_output_contains \
"detects source command in sh script" \
"$EXAMPLES_DIR/bad-shell.sh" \
"source.*command"
assert_output_contains \
"detects eval with variable" \
"$EXAMPLES_DIR/bad-shell.sh" \
"eval with variable"
assert_output_contains \
"detects useless cat" \
"$EXAMPLES_DIR/bad-shell.sh" \
"Useless use of cat"
# Only real code lines flagged — not comment lines for [[ check
assert_output_not_contains \
"[[ check does not flag comment lines in bad-shell.sh" \
"$EXAMPLES_DIR/bad-shell.sh" \
"Line [0-9]*:# Bad: using bash-specific"
# --- p1 regression fixtures ---
INDENTED_SH="$TMP_DIR/indented-bashisms.sh"
cat > "$INDENTED_SH" <<'EOF'
#!/bin/sh
set -e
function helper {
echo "bad"
}
source /etc/profile
helper
EOF
COMMENT_ONLY_ERR_HANDLING="$TMP_DIR/comment-only-error-handling.sh"
cat > "$COMMENT_ONLY_ERR_HANDLING" <<'EOF'
#!/bin/sh
# set -e
# set -o errexit
# trap 'echo failed' ERR
echo "hello"
EOF
echo ""
echo "[p1 regressions]"
assert_output_contains \
"indented function in sh is detected" \
"$INDENTED_SH" \
"function.*keyword"
assert_output_contains \
"indented source in sh is detected" \
"$INDENTED_SH" \
"source.*command"
assert_output_contains \
"comment-only set -e does not suppress warning" \
"$COMMENT_ONLY_ERR_HANDLING" \
"Consider adding error handling"
# --- p2 deterministic shellcheck stage regressions ---
echo ""
echo "[p2 shellcheck-mode regressions]"
assert_exit_code \
"system mode accepts deterministic clean shellcheck run" \
"$EXAMPLES_DIR/good-bash.sh" \
0 \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_SHELLCHECK_MODE=system \
PATH="$FAKE_SHELLCHECK_BIN:/usr/bin:/bin"
assert_output_contains \
"system mode reports clean shellcheck output" \
"$EXAMPLES_DIR/good-bash.sh" \
"No ShellCheck issues found" \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_SHELLCHECK_MODE=system \
PATH="$FAKE_SHELLCHECK_BIN:/usr/bin:/bin"
assert_exit_code \
"system mode returns warning on shellcheck findings" \
"$EXAMPLES_DIR/good-bash.sh" \
1 \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_SHELLCHECK_MODE=system \
FAKE_SHELLCHECK_MODE=warning \
PATH="$FAKE_SHELLCHECK_BIN:/usr/bin:/bin"
assert_output_contains \
"system mode surfaces shellcheck issue codes" \
"$EXAMPLES_DIR/good-bash.sh" \
"SC9999" \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_SHELLCHECK_MODE=system \
FAKE_SHELLCHECK_MODE=warning \
PATH="$FAKE_SHELLCHECK_BIN:/usr/bin:/bin"
assert_exit_code \
"system mode fails on shellcheck infrastructure error" \
"$EXAMPLES_DIR/good-bash.sh" \
2 \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_SHELLCHECK_MODE=system \
FAKE_SHELLCHECK_MODE=infra \
PATH="$FAKE_SHELLCHECK_BIN:/usr/bin:/bin"
assert_output_contains \
"system mode reports shellcheck infrastructure error" \
"$EXAMPLES_DIR/good-bash.sh" \
"ShellCheck execution failed \(exit 3\)" \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_SHELLCHECK_MODE=system \
FAKE_SHELLCHECK_MODE=infra \
PATH="$FAKE_SHELLCHECK_BIN:/usr/bin:/bin"
assert_exit_code \
"invalid shellcheck mode exits with error" \
"$EXAMPLES_DIR/good-bash.sh" \
2 \
VALIDATOR_SHELLCHECK_MODE=invalid-mode
assert_output_contains \
"invalid shellcheck mode prints actionable error" \
"$EXAMPLES_DIR/good-bash.sh" \
"Invalid VALIDATOR_SHELLCHECK_MODE" \
VALIDATOR_SHELLCHECK_MODE=invalid-mode
# --- edge cases ---
echo ""
echo "[edge cases]"
# P0: strict mode must fail when ShellCheck is unavailable
assert_exit_code \
"strict mode fails when shellcheck is unavailable" \
"$EXAMPLES_DIR/good-bash.sh" \
2 \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_DISABLE_SHELLCHECK=1
assert_output_contains \
"strict mode reports shellcheck unavailable" \
"$EXAMPLES_DIR/good-bash.sh" \
"ShellCheck (unavailable|disabled by configuration)" \
VALIDATOR_REQUIRE_SHELLCHECK=1 \
VALIDATOR_DISABLE_SHELLCHECK=1
# Missing file → exit 1 from the validator's own error path
assert_exit_code \
"missing file exits non-zero" \
"/nonexistent/path/script.sh" \
1
# ─── summary ────────────────────────────────────────────────────────────────
echo ""
echo "Results: $PASS passed, $FAIL failed"
echo ""
if [[ $FAIL -gt 0 ]]; then
exit 1
fi
#!/usr/bin/env bash
#
# Bash/Shell Script Validator
# Validates bash and shell scripts for syntax errors, best practices, security issues, and optimizations
#
set -euo pipefail
# Colors for output
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Counters
ERROR_COUNT=0
WARNING_COUNT=0
INFO_COUNT=0
STYLE_COUNT=0
# Script path
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ShellCheck policy:
# - default strict in CI
# - override with VALIDATOR_REQUIRE_SHELLCHECK=0|1
REQUIRE_SHELLCHECK="${VALIDATOR_REQUIRE_SHELLCHECK:-}"
if [[ -z "$REQUIRE_SHELLCHECK" ]]; then
if [[ -n "${CI:-}" ]]; then
REQUIRE_SHELLCHECK=1
else
REQUIRE_SHELLCHECK=0
fi
fi
# ShellCheck provider selection:
# - auto (default): system shellcheck, then wrapper fallback
# - system: require system shellcheck in PATH
# - wrapper: require wrapper script
# - disabled: skip ShellCheck stage
SHELLCHECK_MODE="${VALIDATOR_SHELLCHECK_MODE:-auto}"
if [[ "${VALIDATOR_DISABLE_SHELLCHECK:-0}" == "1" ]]; then
SHELLCHECK_MODE="disabled"
fi
usage() {
cat <<EOF
Usage: $0 <script-file>
Validates bash and shell scripts for:
- Syntax errors
- ShellCheck warnings
- Security issues
- Performance optimizations
- Portability concerns
Options:
-h, --help Show this help message
Examples:
$0 myscript.sh
$0 /path/to/script.bash
EOF
exit 0
}
print_header() {
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}========================================${NC}"
}
print_section() {
echo ""
echo -e "${BLUE}[$1]${NC}"
}
print_error() {
echo -e "${RED}✗ $1${NC}"
((ERROR_COUNT++))
}
print_warning() {
echo -e "${YELLOW}⚠ $1${NC}"
((WARNING_COUNT++))
}
print_info() {
echo -e "${BLUE}ℹ $1${NC}"
((INFO_COUNT++))
}
print_success() {
echo -e "${GREEN}✓ $1${NC}"
}
# Detect shell type from shebang (returns shell:status format)
detect_shell() {
local file="$1"
local shebang
shebang=$(head -n 1 "$file")
# Check if shebang is present
if [[ ! "$shebang" =~ ^#! ]]; then
echo "bash:no-shebang"
return
fi
if [[ "$shebang" =~ ^#!.*bash ]]; then
echo "bash"
elif [[ "$shebang" =~ ^#!/bin/sh([[:space:]]|$) ]] || \
[[ "$shebang" =~ ^#!/usr/bin/sh([[:space:]]|$) ]] || \
[[ "$shebang" =~ /env[[:space:]]+sh([[:space:]]|$) ]]; then
echo "sh"
elif [[ "$shebang" =~ ^#!.*zsh ]]; then
echo "zsh"
elif [[ "$shebang" =~ ^#!.*ksh ]]; then
echo "ksh"
elif [[ "$shebang" =~ ^#!.*dash ]]; then
echo "dash"
else
# Unknown shebang
echo "bash:unknown-shebang:$shebang"
fi
}
# Run syntax validation
validate_syntax() {
local file="$1"
local shell_type="$2"
print_section "SYNTAX CHECK"
case "$shell_type" in
bash)
if bash -n "$file" 2>/dev/null; then
print_success "No syntax errors found (bash -n)"
return 0
else
local errors
errors=$(bash -n "$file" 2>&1)
print_error "Syntax errors found:"
echo "$errors" | sed 's/^/ /'
return 1
fi
;;
sh|dash)
if sh -n "$file" 2>/dev/null; then
print_success "No syntax errors found (sh -n)"
return 0
else
local errors
errors=$(sh -n "$file" 2>&1)
print_error "Syntax errors found:"
echo "$errors" | sed 's/^/ /'
return 1
fi
;;
*)
print_info "Syntax check skipped for shell type: $shell_type"
return 0
;;
esac
}
# Run shellcheck validation
run_shellcheck() {
local file="$1"
local shell_type="$2"
local shellcheck_mode="$SHELLCHECK_MODE"
print_section "SHELLCHECK"
local shell_name=""
case "$shell_type" in
bash) shell_name="bash" ;;
sh|dash) shell_name="sh" ;;
zsh) shell_name="zsh" ;;
ksh) shell_name="ksh" ;;
esac
# Determine which shellcheck to use
local -a shellcheck_cmd=()
local unavailable_reason="ShellCheck unavailable"
case "$shellcheck_mode" in
auto)
if command -v shellcheck &>/dev/null; then
shellcheck_cmd=("shellcheck")
elif [[ -x "$SCRIPT_DIR/shellcheck_wrapper.sh" ]]; then
shellcheck_cmd=("$SCRIPT_DIR/shellcheck_wrapper.sh" "--cache")
fi
;;
system)
if command -v shellcheck &>/dev/null; then
shellcheck_cmd=("shellcheck")
else
unavailable_reason="System shellcheck required (VALIDATOR_SHELLCHECK_MODE=system) but not found in PATH"
fi
;;
wrapper)
if [[ -x "$SCRIPT_DIR/shellcheck_wrapper.sh" ]]; then
shellcheck_cmd=("$SCRIPT_DIR/shellcheck_wrapper.sh" "--cache")
else
unavailable_reason="Shellcheck wrapper required (VALIDATOR_SHELLCHECK_MODE=wrapper) but scripts/shellcheck_wrapper.sh is missing or not executable"
fi
;;
disabled)
unavailable_reason="ShellCheck disabled by configuration"
;;
*)
print_error "Invalid VALIDATOR_SHELLCHECK_MODE='$shellcheck_mode'. Use one of: auto, system, wrapper, disabled"
return 2
;;
esac
if [[ ${#shellcheck_cmd[@]} -eq 0 ]]; then
if [[ "$REQUIRE_SHELLCHECK" == "1" ]]; then
print_error "$unavailable_reason; static analysis coverage is required"
echo " Install options:"
echo " 1. System-wide: brew install shellcheck (macOS)"
echo " apt-get install shellcheck (Debian/Ubuntu)"
echo " dnf install shellcheck (Fedora)"
echo " 2. Python venv: pip3 install shellcheck-py"
echo " 3. Wrapper auto-installs when python3 is available"
return 2
fi
print_warning "$unavailable_reason. Static analysis coverage is partial."
echo " Install options:"
echo " 1. System-wide: brew install shellcheck (macOS)"
echo " apt-get install shellcheck (Debian/Ubuntu)"
echo " dnf install shellcheck (Fedora)"
echo " 2. Python venv: pip3 install shellcheck-py"
echo " 3. Wrapper auto-installs when python3 is available"
return 0
fi
local -a shellcheck_args=()
if [[ -n "$shell_name" ]]; then
shellcheck_args+=("-s" "$shell_name")
fi
local output
local shellcheck_exit=0
if output=$("${shellcheck_cmd[@]}" "${shellcheck_args[@]}" -f gcc "$file" 2>&1); then
print_success "No ShellCheck issues found"
return 0
else
shellcheck_exit=$?
fi
if [[ "$shellcheck_exit" -eq 1 ]]; then
local error_lines warning_lines info_lines style_lines total_lines
error_lines=$(echo "$output" | grep -c ": error:" || true)
warning_lines=$(echo "$output" | grep -c ": warning:" || true)
info_lines=$(echo "$output" | grep -c ": note:" || true)
style_lines=$(echo "$output" | grep -c ": style:" || true)
total_lines=$((error_lines + warning_lines + info_lines + style_lines))
ERROR_COUNT=$((ERROR_COUNT + error_lines))
WARNING_COUNT=$((WARNING_COUNT + warning_lines))
INFO_COUNT=$((INFO_COUNT + info_lines))
STYLE_COUNT=$((STYLE_COUNT + style_lines))
# Keep validator non-green even if output format is unexpected.
if [[ "$total_lines" -eq 0 ]]; then
print_warning "ShellCheck reported issues, but no severity markers were parsed"
fi
echo "$output"
echo ""
print_info "See https://www.shellcheck.net/wiki/ for detailed explanations"
return 1
fi
print_error "ShellCheck execution failed (exit $shellcheck_exit)"
if [[ -n "$output" ]]; then
echo "$output"
fi
return 2
}
# Grep for a pattern in a file, excluding comment-only lines.
# A comment-only line is one whose first non-whitespace character is '#'.
# Returns 0 with output when non-comment matches exist; 1 with no output otherwise.
# Usage: grep_code [-E] 'pattern' file
grep_code() {
local ext_flag=""
if [[ "${1:-}" == "-E" ]]; then
ext_flag="-E"
shift
fi
local output
output=$(grep -n ${ext_flag:+$ext_flag} "$1" "$2" 2>/dev/null | awk '
{
colon = index($0, ":")
content = substr($0, colon + 1)
if (content !~ /^[[:space:]]*#/) print $0
}')
if [[ -n "$output" ]]; then
echo "$output"
return 0
else
return 1
fi
}
# Run custom security and optimization checks
run_custom_checks() {
local file="$1"
local shell_type="$2"
print_section "CUSTOM CHECKS"
local found_issues=0
# Security: Check for eval with variables
if grep_code -E 'eval.*\$' "$file" >/dev/null 2>&1; then
print_warning "Potential command injection: eval with variable found"
grep_code -E 'eval.*\$' "$file" | sed 's/^/ Line /'
found_issues=1
fi
# Security: Check for unsafe use of rm -rf
if grep_code -E '(rm -(rf|fr).*\$|rm -(rf|fr) /)' "$file" >/dev/null 2>&1; then
print_warning "Dangerous rm -rf usage detected"
grep_code -E '(rm -(rf|fr).*\$|rm -(rf|fr) /)' "$file" | sed 's/^/ Line /'
found_issues=1
fi
# Performance: Useless use of cat (UUOC)
# Match: cat <filename> | grep/awk/sed
# Use [^|]+ to match one or more non-pipe characters (the filename)
if grep_code -E 'cat[[:space:]]+[^|]+[[:space:]]*\|[[:space:]]*(grep|awk|sed)' "$file" >/dev/null 2>&1; then
print_info "Useless use of cat detected. Consider using redirection instead:"
grep_code -E 'cat[[:space:]]+[^|]+[[:space:]]*\|[[:space:]]*(grep|awk|sed)' "$file" | sed 's/^/ Line /'
found_issues=1
fi
# Portability: Bash-specific features in sh scripts
if [[ "$shell_type" == "sh" ]]; then
# Check for [[ ]] (bash-specific)
if grep_code "\[\[" "$file" >/dev/null 2>&1; then
print_error "Bash-specific [[ ]] found in sh script. Use [ ] instead"
grep_code "\[\[" "$file" | sed 's/^/ Line /'
found_issues=1
fi
# Check for arrays (bash-specific)
if grep_code -E '(declare -a|array=\()' "$file" >/dev/null 2>&1; then
print_error "Bash-specific arrays found in sh script"
grep_code -E '(declare -a|array=\()' "$file" | sed 's/^/ Line /'
found_issues=1
fi
# Check for function keyword (bash-specific)
if grep_code -E '^[[:space:]]*function[[:space:]]' "$file" >/dev/null 2>&1; then
print_warning "Bash-specific 'function' keyword in sh script"
grep_code -E '^[[:space:]]*function[[:space:]]' "$file" | sed 's/^/ Line /'
found_issues=1
fi
# Check for source command (bash-specific, use . instead)
if grep_code -E '^[[:space:]]*source[[:space:]]' "$file" >/dev/null 2>&1; then
print_warning "Bash-specific 'source' command in sh script. Use '.' instead"
grep_code -E '^[[:space:]]*source[[:space:]]' "$file" | sed 's/^/ Line /'
found_issues=1
fi
fi
# Check for missing error handling
local has_error_handling=0
if grep_code -E '^[[:space:]]*set[[:space:]]+-[[:alpha:]]*e[[:alpha:]]*([[:space:]]|$)' "$file" >/dev/null 2>&1; then
has_error_handling=1
fi
if grep_code -E '^[[:space:]]*set[[:space:]]+-o[[:space:]]+errexit([[:space:]]|$)' "$file" >/dev/null 2>&1; then
has_error_handling=1
fi
if grep_code -E '^[[:space:]]*trap[[:space:]].*ERR([[:space:]]|$)' "$file" >/dev/null 2>&1; then
has_error_handling=1
fi
if [[ "$has_error_handling" -eq 0 ]]; then
print_info "Consider adding error handling (set -e/-o errexit or trap ERR)"
found_issues=1
fi
# Check for missing quotes around variables in dangerous contexts
if grep_code -E '\$[A-Za-z_][A-Za-z0-9_]*[[:space:]]*>' "$file" >/dev/null 2>&1; then
print_warning "Unquoted variables in redirection context"
grep_code -E '\$[A-Za-z_][A-Za-z0-9_]*[[:space:]]*>' "$file" | sed 's/^/ Line /'
found_issues=1
fi
if [[ $found_issues -eq 0 ]]; then
print_success "No custom issues found"
fi
return 0
}
# Print summary
print_summary() {
local file="$1"
local shell_type="$2"
echo ""
print_header "VALIDATION SUMMARY"
echo "File: $file"
echo "Detected Shell: $shell_type"
echo ""
if [[ $ERROR_COUNT -eq 0 && $WARNING_COUNT -eq 0 ]]; then
print_success "All checks passed! ✓"
else
echo -e "${RED}Errors:${NC} $ERROR_COUNT"
echo -e "${YELLOW}Warnings:${NC} $WARNING_COUNT"
echo -e "${BLUE}Info:${NC} $INFO_COUNT"
echo -e "Style: $STYLE_COUNT"
fi
echo ""
}
# Main validation function
validate_script() {
local file="$1"
# Check if file exists
if [[ ! -f "$file" ]]; then
echo "Error: File '$file' not found"
exit 1
fi
# Check if file is readable
if [[ ! -r "$file" ]]; then
echo "Error: File '$file' is not readable"
exit 1
fi
# Check if file is a text file (not binary)
# Using file command to detect binary files, or grep -I as fallback
if command -v file &>/dev/null; then
local file_type
file_type=$(file -b --mime-encoding "$file")
if [[ "$file_type" == "binary" ]]; then
echo "Error: File '$file' appears to be a binary file, not a text script"
exit 1
fi
elif ! grep -qI . "$file" 2>/dev/null; then
# Fallback: grep -I skips binary files, if it fails the file is likely binary
echo "Error: File '$file' appears to be a binary file, not a text script"
exit 1
fi
local shell_type_raw shell_type shell_status
shell_type_raw=$(detect_shell "$file")
# Parse shell type and status
shell_type="${shell_type_raw%%:*}"
shell_status="${shell_type_raw#*:}"
print_header "BASH/SHELL SCRIPT VALIDATOR"
echo "File: $file"
echo "Detected Shell: $shell_type"
# Print warnings for special cases
if [[ "$shell_status" == "no-shebang" ]]; then
print_warning "No shebang found. Defaulting to bash validation."
elif [[ "$shell_status" =~ ^unknown-shebang ]]; then
local unknown_shebang="${shell_status#unknown-shebang:}"
print_warning "Unknown shebang: $unknown_shebang. Defaulting to bash validation."
fi
echo ""
# Run validations
validate_syntax "$file" "$shell_type" || true
run_shellcheck "$file" "$shell_type" || true
run_custom_checks "$file" "$shell_type" || true
# Print summary
print_summary "$file" "$shell_type"
# Exit with appropriate code
if [[ $ERROR_COUNT -gt 0 ]]; then
exit 2
elif [[ $WARNING_COUNT -gt 0 ]]; then
exit 1
else
exit 0
fi
}
# Parse arguments
if [[ $# -eq 0 ]]; then
usage
fi
case "${1:-}" in
-h|--help)
usage
;;
*)
validate_script "$1"
;;
esac
Related skills
How it compares
Use bash-script-validator for quick bash review gates rather than full infrastructure-as-code or container security audit skills.
FAQ
What does bash-script-validator check?
bash-script-validator reviews bash scripts for syntax errors, common pitfalls like unquoted variables, and unsafe patterns in deploy hooks or CI steps, producing a validation report before merge or deployment.
When should I run bash-script-validator?
Run bash-script-validator during ship-phase review when bash deploy scripts, cron jobs, or CI shell steps change, so syntax and safety issues surface in the pull request instead of production.