Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
06bf2fd
Add comprehensive ZSH support to ShellCheck
agoodkind Dec 30, 2025
c0a23c8
Add comprehensive ZSH test suite and fix pattern match failures
agoodkind Dec 30, 2025
705b041
Add comprehensive README for ZSH test suite
agoodkind Dec 30, 2025
8328975
Add comprehensive implementation summary
agoodkind Dec 30, 2025
142224c
Add ZSH-specific ShellCheck error codes SC2400-SC2406
agoodkind Dec 30, 2025
44109a9
Add comprehensive ZSH-specific error codes SC2407-SC2422
agoodkind Dec 30, 2025
b9d16c0
Replace /bin/zsh with /usr/bin/env zsh and add shell=zsh directive tests
agoodkind Dec 30, 2025
cf46430
Update Dependabot schedule
agoodkind Jun 17, 2026
67908ed
Add comprehensive ZSH support to ShellCheck
agoodkind Dec 30, 2025
d03c48c
Add comprehensive ZSH test suite and fix pattern match failures
agoodkind Dec 30, 2025
24614a8
Add comprehensive README for ZSH test suite
agoodkind Dec 30, 2025
339ba20
Add comprehensive implementation summary
agoodkind Dec 30, 2025
f93c9a4
Add ZSH-specific ShellCheck error codes SC2400-SC2406
agoodkind Dec 30, 2025
92ebd2c
Add comprehensive ZSH-specific error codes SC2407-SC2422
agoodkind Dec 30, 2025
41f310e
Replace /bin/zsh with /usr/bin/env zsh and add shell=zsh directive tests
agoodkind Dec 30, 2025
c71ecde
Fix zsh regression QuickCheck props after rebase
agoodkind Jul 26, 2026
921f0f6
Add zsh golden test harness and wire it into CI
agoodkind Jul 26, 2026
cfdb4b7
Parse zsh glob qualifiers on arbitrary patterns (*.txt(.))
agoodkind Jul 26, 2026
de99813
Update glob qualifiers golden: fixture now parses clean in zsh mode
agoodkind Jul 26, 2026
84aad88
Parse zsh anonymous functions and always blocks
agoodkind Jul 26, 2026
2cde894
Track zsh setopt state and fix zsh-specific semantics
agoodkind Jul 26, 2026
3aa4a17
Audit SC2400-SC2422: drop duplicates and fix false claims
agoodkind Jul 26, 2026
a1158a5
Parse zsh bare subscripts, =(...) and foreach loops
agoodkind Jul 26, 2026
0cfdb81
Add zsh Test/*.ztst corpus harness and CI parse baseline
agoodkind Jul 26, 2026
d4f220b
Document zsh dialect support in user-facing docs
agoodkind Jul 26, 2026
a7496f5
Fix -s wrapping in the man page and drop the working checklist
agoodkind Jul 26, 2026
d524ed5
Scope zsh internal and array variables to the zsh dialect
agoodkind Jul 26, 2026
de98c9e
Record the zsh corpus remote and full revision instead of a local path
agoodkind Jul 26, 2026
163e0c0
Parse zsh numeric for-loop variable names (for 1 in ...).
agoodkind Jul 26, 2026
494c2af
Parse math function calls in zsh arithmetic expressions.
agoodkind Jul 26, 2026
17920a2
Parse zsh extended-glob patterns inside [[ ]] tests.
agoodkind Jul 26, 2026
c7110ac
Update corpus parse baseline to 151/2914 failures (94.82%).
agoodkind Jul 26, 2026
12ba76e
Parse zsh nofork ${|...} and ${{var} ...} command expansions.
agoodkind Jul 26, 2026
c16246c
Parse zsh A01grammar short forms and null bang.
agoodkind Jul 26, 2026
fb8081d
Parse zsh bare cond globs, colon param flags, newline-then.
agoodkind Jul 26, 2026
29889c4
Add safe upstream sync workflow for fork master
agoodkind Jul 26, 2026
6b9b27d
Add safe upstream sync workflow for fork master
agoodkind Jul 26, 2026
8867654
Parse zsh funcdef forms, repeat, nofork arrays, [ ] tests.
agoodkind Jul 26, 2026
1fc8248
Merge upstream koalaman/shellcheck master
agoodkind Jul 26, 2026
f196348
Reach 100% effective zsh corpus parse coverage.
agoodkind Jul 26, 2026
2ecdf7a
Merge origin/master into feature/zsh-rebased.
agoodkind Jul 26, 2026
3f9c033
Merge zsh dialect support (100% effective corpus parse)
agoodkind Jul 26, 2026
2e64633
Merge remote-tracking branch 'origin/sync/upstream-master'
agoodkind Jul 27, 2026
a2eb0bd
Add E03posix_013.zsh to corpus skip list and baseline
agoodkind Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
version: 2

updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
interval: "cron"
cronjob: 0 6 * * *
timezone: America/Los_Angeles
37 changes: 37 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,43 @@ jobs:
cd source
cabal test ${{ matrix.cabal_flags }}

zsh_golden:
name: Zsh golden fixtures
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup Haskell
uses: haskell-actions/setup@v2
with:
ghc-version: 'latest'

# The golden harness runs the real binary, so it is not covered by the
# sdist-based run_tests job (test/zsh/ is not shipped in the tarball).
- name: Build shellcheck
run: cabal build --allow-newer exe:shellcheck

- name: Run zsh golden fixtures
run: ./test/zsh/run-golden.sh --no-build

# zsh's own test suite is the widest sample of real zsh syntax available,
# so a change in how much of it parses should be deliberate.
# Pinned because the chunk numbering, and therefore the baseline, moves
# whenever zsh edits its tests. Bump this and the baseline together.
- name: Check out zsh
uses: actions/checkout@v6
with:
repository: zsh-users/zsh
ref: c0fe1189905e6bd6ef227068478638cfb52b1255
path: zsh-source

- name: Extract the zsh corpus
run: ./test/zsh/extract-ztst.sh "$GITHUB_WORKSPACE/zsh-source"

- name: Report corpus parse coverage
run: ./test/zsh/corpus-report.sh

build_source:
name: Build
needs: package_source
Expand Down
112 changes: 112 additions & 0 deletions .github/workflows/sync-upstream.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Sync fork default branch (master) from koalaman/shellcheck upstream.
#
# Operator notes:
# - Merge this workflow to master on the fork (agoodkind/shellcheck) to enable it.
# - Runs on workflow_dispatch and weekly (Monday 06:00 UTC).
# - Updates ONLY the fork default branch (master). Feature branches are never touched.
# - Strategy: try fast-forward merge first; if that fails, try a regular merge.
# On merge conflicts the job aborts without pushing. Force-push is never used.
# - GITHUB_TOKEN (contents: write) is sufficient to push back to the same fork repo.
# - Failed runs mean master was left unchanged; resolve conflicts locally if needed.

name: Sync upstream

on:
workflow_dispatch:
schedule:
- cron: '0 6 * * 1'

permissions:
contents: write

jobs:
sync:
name: Sync master from upstream
runs-on: ubuntu-latest
steps:
- name: Checkout fork master
uses: actions/checkout@v6
with:
ref: master
fetch-depth: 0

- name: Fetch upstream master
run: |
git remote add upstream https://github.com/koalaman/shellcheck.git
git fetch upstream master

- name: Merge upstream into master
id: merge
run: |
set -euo pipefail

before_head="$(git rev-parse HEAD)"
before_upstream="$(git rev-parse upstream/master)"

if git merge-base --is-ancestor HEAD upstream/master; then
echo "Fork master is behind or equal to upstream; attempting fast-forward."
if git merge --ff-only upstream/master; then
echo "result=fast-forward" >> "$GITHUB_OUTPUT"
echo "pushed=true" >> "$GITHUB_OUTPUT"
echo "before_head=${before_head}" >> "$GITHUB_OUTPUT"
echo "after_head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "upstream_head=${before_upstream}" >> "$GITHUB_OUTPUT"
exit 0
fi
fi

echo "Fast-forward not possible; attempting regular merge."
if git merge upstream/master --no-edit; then
echo "result=merge" >> "$GITHUB_OUTPUT"
echo "pushed=true" >> "$GITHUB_OUTPUT"
echo "before_head=${before_head}" >> "$GITHUB_OUTPUT"
echo "after_head=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
echo "upstream_head=${before_upstream}" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Merge failed; aborting without push."
git merge --abort || true
echo "result=conflict" >> "$GITHUB_OUTPUT"
echo "pushed=false" >> "$GITHUB_OUTPUT"
echo "before_head=${before_head}" >> "$GITHUB_OUTPUT"
echo "upstream_head=${before_upstream}" >> "$GITHUB_OUTPUT"
exit 1

- name: Push updated master
if: steps.merge.outputs.pushed == 'true'
run: git push origin master

- name: Job summary
if: always()
run: |
{
echo "## Upstream sync"
echo ""
echo "| Field | Value |"
echo "| --- | --- |"
echo "| Fork branch | \`master\` |"
echo "| Upstream | \`koalaman/shellcheck\` @ \`master\` |"
echo "| Upstream HEAD | \`${{ steps.merge.outputs.upstream_head || 'n/a' }}\` |"
echo "| Fork HEAD before | \`${{ steps.merge.outputs.before_head || 'n/a' }}\` |"
echo "| Fork HEAD after | \`${{ steps.merge.outputs.after_head || 'unchanged' }}\` |"
echo "| Result | \`${{ steps.merge.outputs.result || 'fetch/checkout failed' }}\` |"
echo ""
case "${{ steps.merge.outputs.result }}" in
fast-forward)
echo "Master was fast-forwarded to upstream and pushed."
;;
merge)
echo "Master was merged with upstream (merge commit) and pushed."
;;
conflict)
echo "Merge conflict. Master was **not** pushed and remains unchanged on the fork."
echo "Resolve locally on \`master\`, then push normally (no force-push)."
;;
*)
echo "Sync did not complete. Check the job log."
;;
esac
echo ""
echo "Feature branches (for example \`feature/zsh-rebased\`) are never modified by this workflow."
} >> "$GITHUB_STEP_SUMMARY"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ stack.yaml.lock

### misc ###
/shellcheck.1

# Extracted from a zsh checkout by test/zsh/extract-ztst.sh, not vendored.
/test/zsh/corpus/
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Git
### Added
- Zsh dialect support (`-s zsh`, `#!/usr/bin/env zsh`, `# shellcheck shell=zsh`, `.zsh` extension).
- Zsh-specific checks SC2400-SC2408 and SC2412-SC2420 for portability and dialect semantics. SC2409, SC2410, SC2411, SC2421 and SC2422 were dropped during the audit because they duplicated existing SC3xxx codes or fired on constructs the parser never produces.
- Golden fixture harness under `test/zsh/` and CI job `zsh_golden`.
- Optional zsh test corpus extractor (`test/zsh/extract-ztst.sh`) with parse baseline (`test/zsh/corpus-report.sh`).

### Changed

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# ShellCheck - A shell script static analysis tool

ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell scripts:
ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh/zsh shell scripts:

![Screenshot of a terminal showing problematic shell script lines highlighted](doc/terminal.png)

Expand Down
141 changes: 141 additions & 0 deletions ZSH_IMPLEMENTATION_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# ZSH Support Implementation Summary

## Overview

Comprehensive ZSH support has been added to ShellCheck, including bug fixes for variable tracking, AST/parser expansions, and a complete test suite.

## Changes Made

### 1. Core Bug Fixes

- **AnalyzerLib.hs**: Fixed T_ForShort variable tracking in `assignFirst`, `getModifiedVariables`, and `willSplit`
- **ASTLib.hs**: Added T_ForShort recognition to `isLoop` function
- **Analytics.hs**: Integrated T_ForShort into 5 analysis functions for proper loop handling

### 2. AST & Parser Enhancements

- **AST.hs**: Expanded ZshParamFlag (11→21 variants) and GlobQual (11→25 variants)
- **Parser.hs**: Enhanced `readZshParamFlags` and `readZshGlobQualifier` with full ZSH syntax coverage

### 3. Pattern Match Fixes

- **AnalyzerLib.hs**: Added Zsh cases to `hasLastpipe`, `hasInheritErrexit`, `hasPipefail`
- **Analytics.hs**: Added Zsh case to `checkFunctionDeclarations`

### 4. Test Suite (test/zsh/)

Created 18 comprehensive test files covering:

#### General Shell Issues Detected in ZSH Context (31 instances)

- **SC2086**: Unquoted variable expansions (5 files)
- **SC2034**: Unused variables (2 files)
- **SC2154**: Undefined variable references (3 files)
- **SC2128**: Array expansion without index (2 files)
- **SC2094**: Reading/writing same file (1 file)
- **SC2069**: Incorrect redirect order (1 file)
- **SC2261**: Competing redirects (1 file)
- **SC2100**: Incorrect arithmetic syntax (1 file)
- **SC2331**: Deprecated -a operator (1 file)
- **SC2166**: Deprecated -o operator (1 file)
- **SC2045**: Iterating over ls output (1 file)
- **SC2162**: read without -r flag (1 file)

#### ZSH-Specific Checks (New SC Codes)

- **SC2400**: ZSH parameter flags used in non-ZSH script
- **SC2401**: ZSH glob qualifiers used in non-ZSH script
- **SC2402**: ZSH anonymous functions used in non-ZSH script
- **SC2403**: ZSH short for loop syntax used in non-ZSH script
- **SC2404**: Using 0-based array indexing in ZSH (should be 1-based)
- **SC2405**: Using bash-style =~ regex operator in ZSH (works differently)
- **SC2406**: Using extended glob without setopt extended_glob (informational)

#### ZSH Features Validated (No False Positives)

- ✓ Short for loops with variable tracking
- ✓ Parameter expansion flags (U, L, q, s, etc.)
- ✓ Glob qualifiers (partially - parser needs work)
- ✓ Anonymous functions (partially - parser needs work)

## Test Results

```
Total test files: 18
Issues correctly detected: 31
False positives: 0 (for supported features)
Pattern match crashes: Fixed
```

## Known Limitations

1. **Anonymous Functions**: Parser currently has difficulty with ZSH anonymous functions with arguments
2. **Glob Qualifiers**: Traditional for loops with glob qualifiers in list position need parser improvements
3. **Some ZSH Features**: Extended globs and other advanced ZSH syntax may need additional parser work

## Files Modified

### Source Files (9)

- src/ShellCheck/AST.hs
- src/ShellCheck/ASTLib.hs
- src/ShellCheck/Analytics.hs
- src/ShellCheck/AnalyzerLib.hs
- src/ShellCheck/CFG.hs
- src/ShellCheck/Checker.hs
- src/ShellCheck/Data.hs
- src/ShellCheck/Interface.hs
- src/ShellCheck/Parser.hs

### Test Files (19)

- test/zsh/README.md (documentation)
- test/zsh/test_*.zsh (18 test scripts)

## Examples

### Before Fix

```zsh
# T_ForShort not tracked
for i (1 2 3) { echo $i }
echo $i # SC2154: i is referenced but not assigned ❌
```

### After Fix

```zsh
# T_ForShort properly tracked
for i (1 2 3) { echo $i }
echo $i # No warning ✓
```

### Parameter Flags Working

```zsh
text="hello"
echo "${(U)text}" # No SC2154 for 'text' ✓
echo "${(U)undefined}" # SC2154 for 'undefined' ✓
```

## Verification

All changes tested and validated:

- Unit tests: 16 new ZSH-specific property tests pass
- Integration tests: 18 test scripts with expected warnings
- Real-world validation: Complex ZSH scripts analyze correctly
- No regressions: Existing tests still pass

## Repository

Fork: <https://github.com/agoodkind/shellcheck>
Branch: master
Commits: 3 (initial implementation, test suite, documentation)

## Next Steps

1. Submit PR to koalaman/shellcheck
2. Consider improving anonymous function parser
3. Enhance glob qualifier parsing in traditional for loops
4. Add more ZSH-specific checks as needed
9 changes: 5 additions & 4 deletions shellcheck.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ shellcheck - Shell script analysis tool

# DESCRIPTION

ShellCheck is a static analysis and linting tool for sh/bash scripts. It's
ShellCheck is a static analysis and linting tool for sh/bash/zsh scripts. It's
mainly focused on handling typical beginner and intermediate level syntax
errors and pitfalls where the shell just gives a cryptic error message or
strange behavior, but it also reports on a few more advanced issues where
Expand Down Expand Up @@ -98,10 +98,11 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
**-s**\ *shell*,\ **--shell=***shell*

: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
and *busybox*.
*zsh*, and *busybox*.
The default is to deduce the shell from the file's `shell` directive,
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
POSIX `sh` (not the system's), and will warn of portability issues.
shebang, or `.bash/.bats/.dash/.ksh/.zsh` extension, in that order. *sh*
refers to POSIX `sh` (not the system's), and will warn of portability
issues.

**-S**\ *SEVERITY*,\ **--severity=***severity*

Expand Down
2 changes: 1 addition & 1 deletion shellcheck.hs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ options = [
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh, busybox)",
"Specify dialect (sh, bash, dash, ksh, zsh, busybox)",
Option "S" ["severity"]
(ReqArg (Flag "severity") "SEVERITY")
"Minimum severity of errors to consider (error, warning, info, style)",
Expand Down
Loading
Loading