Lint and test shell scripts using ShellCheck and BATS...
Comprehensive shell script linting and testing using ShellCheck and BATS with 2025 best practices.
Copy this workflow checklist and track your progress:
Shell Script Quality Workflow:
- [ ] Step 1: Lint with ShellCheck
- [ ] Step 2: Fix reported issues
- [ ] Step 3: Write BATS tests
- [ ] Step 4: Verify tests pass
- [ ] Step 5: Integrate into CI/CD
# Lint single file
shellcheck script.sh
# Lint all scripts
find scripts -name "*.sh" -exec shellcheck {} +
# Use config file if present
shellcheck -x script.sh
Common fixes: See SHELLCHECK.md for fix patterns
Apply fixes for common warnings:
"$var" not $varif ! commandFor detailed fixes: See SHELLCHECK.md
#!/usr/bin/env bats
setup() {
source "$BATS_TEST_DIRNAME/../scripts/example.sh"
}
@test "function succeeds with valid input" {
run example_function "test"
[ "$status" -eq 0 ]
[ -n "$output" ]
}
@test "function fails with invalid input" {
run example_function ""
[ "$status" -ne 0 ]
[[ "$output" =~ "ERROR" ]]
}
Test patterns: See BATS.md for comprehensive testing guide
# Run all tests
bats tests/
# Run with verbose output
bats -t tests/
# Run specific file
bats tests/example.bats
If tests fail: Review error output, fix issues, re-run validation
GitHub Actions: See CI-CD.md for complete workflows
Quick integration:
- name: ShellCheck
uses: ludeeus/action-shellcheck@master
- name: Run BATS
run: |
sudo apt-get install -y bats
bats tests/
Use this template for new scripts:
#!/bin/bash
set -euo pipefail
# Script directory (portable)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Error handler
error_exit() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
# Main function
main() {
[[ $# -lt 1 ]] && {
echo "Usage: $0 <argument>" >&2
exit 1
}
# Your logic here
}
# Run if executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
ShellCheck:
brew install shellcheck # macOS
sudo apt-get install shellcheck # Linux
BATS:
brew install bats-core # macOS
sudo apt-get install bats # Linux
.shellcheckrc in project root:
shell=bash
disable=SC1090
enable=all
source-path=SCRIPTDIR
For configuration details: See CONFIG.md
Test scripts using CLAUDE_PLUGIN_ROOT:
@test "plugin script works" {
export CLAUDE_PLUGIN_ROOT="$BATS_TEST_DIRNAME/.."
run bash "$CLAUDE_PLUGIN_ROOT/scripts/search.sh" "query"
[ "$status" -eq 0 ]
}
Test hooks with JSON:
@test "hook provides suggestions" {
local input='{"tool":"Edit","params":{"file_path":"test.txt"}}'
run bash "$HOOK_DIR/pre-edit.sh" <<< "$input"
[ "$status" -eq 0 ]
echo "$output" | jq empty
}
More plugin patterns: See PATTERNS.md
ShellCheck:
# shellcheck source=path/to/file.sh# shellcheck disable=SCxxxxBATS:
teardown() cleanup$BATS_TEST_DIRNAME for relative pathsDetailed troubleshooting: See TROUBLESHOOTING.md
For quality-critical operations:
shellcheck script.shbats tests/script.batsRun this command for complete validation:
# Check everything
find scripts -name "*.sh" -exec shellcheck {} + && bats tests/
# Or use quality check script
bash scripts/check-quality.sh
