bash-lint

Installation
SKILL.md

Bash Linting

Shellcheck and shfmt integration for bash script quality assurance.

Shellcheck

Installation

# Debian/Ubuntu
apt install shellcheck

# macOS
brew install shellcheck

# From source
cabal update && cabal install ShellCheck

Basic Usage

# Check single file
shellcheck script.sh

# Check multiple files
shellcheck *.sh

# With specific shell dialect
shellcheck --shell=bash script.sh
shellcheck --shell=sh script.sh

# Exclude specific rules
shellcheck --exclude=SC2086 script.sh
shellcheck --exclude=SC2086,SC2046 script.sh

# Output formats
shellcheck --format=gcc script.sh    # GCC-style
shellcheck --format=json script.sh   # JSON for tooling
shellcheck --format=diff script.sh   # Unified diff

Common Shellcheck Codes

Code Issue Fix
SC2086 Double quote to prevent globbing/splitting "$var"
SC2046 Quote command substitution "$(cmd)"
SC2006 Use $() instead of backticks $(cmd)
SC2034 Variable appears unused Remove or export
SC2155 Declare and assign separately Split local var; var=$(...)
SC2164 Use cd ... || exit Handle cd failure
SC2181 Check exit status directly if cmd; then
SC2129 Consider grouping writes Use { } > file
SC1090 Can't follow sourced file Use # shellcheck source=path
SC2154 Variable referenced but not assigned Initialize or declare

Shellcheck Directives

# Disable for next line
# shellcheck disable=SC2086
echo $unquoted_var

# Disable for entire file (at top)
# shellcheck disable=SC2086,SC2046

# Specify source file for sourcing
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"

# Specify shell dialect
# shellcheck shell=bash

# Disable for block (not supported - use per-line)

Inline Directive Patterns

# Disable specific warning with explanation
# shellcheck disable=SC2034 # Variable used by sourcing script
readonly CONFIG_VERSION="1.0"

# Disable multiple codes
# shellcheck disable=SC2086,SC2046
result=$(echo $var)

# Source directive for dynamic paths
# shellcheck source=/dev/null
source "${DYNAMIC_PATH}/config.sh"

shfmt

Installation

# macOS
brew install shfmt

# Go install
go install mvdan.cc/sh/v3/cmd/shfmt@latest

# Snap
snap install shfmt

# Binary download
# From https://github.com/mvdan/sh/releases

Basic Usage

# Format and print to stdout
shfmt script.sh

# Format in place
shfmt -w script.sh

# Check formatting (exit 1 if unformatted)
shfmt -d script.sh

# Recursive directory
shfmt -w .
shfmt -w scripts/

Formatting Options

# Indentation
shfmt -i 2 script.sh  # 2-space indent
shfmt -i 4 script.sh  # 4-space indent
shfmt -i 0 script.sh  # tabs (default)

# Binary operators at start of line
shfmt -bn script.sh

# Switch cases indented
shfmt -ci script.sh

# Redirect operators followed by space
shfmt -sr script.sh

# Keep column alignment paddings
shfmt -kp script.sh

# Function opening brace on separate line
shfmt -fn script.sh

# Combined
shfmt -i 4 -ci -bn script.sh

Configuration (.editorconfig)

# .editorconfig
[*.sh]
indent_style = space
indent_size = 4
shell_variant = bash
binary_next_line = true
switch_case_indent = true
space_redirects = true

Example Transformations

Before shfmt:

if [ -f "$file" ];then
echo "exists"
fi

for i in 1 2 3;do
    process $i
done

After shfmt -i 4 -ci:

if [ -f "$file" ]; then
    echo "exists"
fi

for i in 1 2 3; do
    process $i
done

Pre-commit Integration

.pre-commit-config.yaml

repos:
  - repo: https://github.com/koalaman/shellcheck-precommit
    rev: v0.9.0
    hooks:
      - id: shellcheck
        args: ["--severity=warning"]

  - repo: https://github.com/scop/pre-commit-shfmt
    rev: v3.7.0-1
    hooks:
      - id: shfmt
        args: ["-i", "4", "-ci", "-w"]

  # Alternative: local hooks
  - repo: local
    hooks:
      - id: shellcheck
        name: shellcheck
        entry: shellcheck
        language: system
        types: [shell]
        args: ["--severity=warning", "-x"]

      - id: shfmt
        name: shfmt
        entry: shfmt
        language: system
        types: [shell]
        args: ["-i", "4", "-ci", "-w"]

Running Pre-commit

# Install hooks
pre-commit install

# Run on all files
pre-commit run --all-files

# Run specific hook
pre-commit run shellcheck --all-files
pre-commit run shfmt --all-files

# Run on specific files
pre-commit run --files script.sh

Integration with CI/CD

GitHub Actions

name: Shell Lint

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run ShellCheck
        uses: ludeeus/action-shellcheck@master
        with:
          severity: warning

      - name: Check formatting with shfmt
        uses: mvdan/github-action-shfmt@master
        with:
          flags: -d -i 4 -ci

GitLab CI

shellcheck:
  image: koalaman/shellcheck-alpine:stable
  script:
    - find . -name "*.sh" -exec shellcheck {} +

shfmt:
  image: mvdan/shfmt:latest
  script:
    - shfmt -d -i 4 -ci .

Fixing Common Issues

SC2086: Quote to prevent splitting

# Bad
echo $var

# Good
echo "$var"
printf '%s\n' "$var"

SC2155: Declare and assign separately

# Bad - masks exit status
local var=$(some_command)

# Good
local var
var=$(some_command)

SC2164: Use cd || exit

# Bad
cd "$dir"
rm -rf *

# Good
cd "$dir" || exit 1
rm -rf *

# Or with subshell
(cd "$dir" && rm -rf *)

SC2181: Check exit directly

# Bad
command
if [ $? -eq 0 ]; then

# Good
if command; then

SC1090/SC1091: Source issues

# Add directive for dynamic source
# shellcheck source=/dev/null
source "$DYNAMIC_PATH/lib.sh"

# Or specify actual path
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"

Editor Integration

VS Code

Install "ShellCheck" extension by Timon Wong.

// settings.json
{
    "shellcheck.enable": true,
    "shellcheck.run": "onSave",
    "shellcheck.executablePath": "shellcheck",
    "editor.formatOnSave": true,
    "[shellscript]": {
        "editor.defaultFormatter": "foxundermoon.shell-format"
    }
}

Vim/Neovim

" With ALE
let g:ale_linters = {'sh': ['shellcheck']}
let g:ale_fixers = {'sh': ['shfmt']}
let g:ale_sh_shfmt_options = '-i 4 -ci'

" With coc.nvim
" Install coc-sh extension

Best Practices

  1. Run shellcheck early - integrate into editor and CI
  2. Fix issues, don't suppress - only disable with good reason
  3. Document suppressions - explain why rule is disabled
  4. Use severity levels - --severity=warning for CI
  5. Consistent formatting - use shfmt in pre-commit
  6. Version lock tools - pin versions in CI/pre-commit
Related skills

More from jamie-bitflight/claude_skills

Installs
5
GitHub Stars
40
First Seen
Mar 29, 2026