# Bash Linting

> Shellcheck and shfmt integration for bash script quality assurance.

- Skill: `tools-only/bash-linting` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/bash-linting`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/bash-linting/raw
- Safety review: pending (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/bash-linting

---


# Bash Linting

Shellcheck and shfmt integration for bash script quality assurance.

## Shellcheck

### Installation

```bash
# Debian/Ubuntu
apt install shellcheck

# macOS
brew install shellcheck

# From source
cabal update && cabal install ShellCheck
```

### Basic Usage

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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)

```ini
# .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:**

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

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

**After shfmt -i 4 -ci:**

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

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

## Pre-commit Integration

### .pre-commit-config.yaml

```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

```bash
# 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

```yaml
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

```yaml
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

```bash
# Bad
echo $var

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

### SC2155: Declare and assign separately

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

# Good
local var
var=$(some_command)
```

### SC2164: Use cd || exit

```bash
# Bad
cd "$dir"
rm -rf *

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

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

### SC2181: Check exit directly

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

# Good
if command; then
```

### SC1090/SC1091: Source issues

```bash
# 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.

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

### Vim/Neovim

```vim
" 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

