Metadata-Version: 2.4
Name: pureshellcheck
Version: 0.1.0
Summary: A pure Python reimplementation of ShellCheck's most common checks
Author: adam2go
License: MIT
Project-URL: Homepage, https://github.com/adam2go/pureshellcheck
Keywords: shellcheck,shell,bash,lint,static-analysis,pure-python
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# pureshellcheck

[![CI](https://github.com/adam2go/pureshellcheck/actions/workflows/ci.yml/badge.svg)](https://github.com/adam2go/pureshellcheck/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/pureshellcheck)](https://pypi.org/project/pureshellcheck/)
[![Conformance](https://img.shields.io/badge/ShellCheck%20test%20suite-619%2F620%20(99.8%25)-brightgreen)](tests/data/expected_failures.txt)

A pure Python reimplementation of [ShellCheck](https://github.com/koalaman/shellcheck)'s
most common checks. No binaries, no Haskell runtime, no compilation —
`pip install pureshellcheck` and it works anywhere Python runs, including
AWS Lambda, Pyodide/WASM, and locked-down CI sandboxes where you can't
install the real ShellCheck binary.

```console
$ pip install pureshellcheck
$ pureshellcheck deploy.sh

In deploy.sh line 8:
rm -rf $BUILD_DIR/*
       ^--------^ SC2086 (info): Double quote to prevent globbing and word splitting.
```

## Why

- **Agent & tooling friendly.** LLM-generated shell scripts fail in
  exactly the ways ShellCheck catches (unquoted expansions, word
  splitting, `cd` without `|| exit`). Existing Python packages such as
  `shellcheck-py` just download the 30 MB Haskell binary — useless in
  WASM, Lambda layers, or hermetic build sandboxes. pureshellcheck is
  ~7000 lines of stdlib-only Python.
- **In-process speed.** Calling `pureshellcheck.check()` takes ~2 ms for a
  typical script vs ~40 ms to spawn the shellcheck binary — and it's
  8–13× faster than the binary even on 1200-line scripts (see
  [Benchmarks](#benchmarks)).
- **Verified against the real thing.** Test cases are extracted from
  ShellCheck's own test suite and the output is differentially tested
  against the shellcheck binary on real-world scripts.

## What it checks

71 SC codes are implemented, chosen by real-world frequency — the quoting
and word-splitting family (SC2086, SC2046, SC2068, SC2206/2207...),
variable lifecycle (SC2034 unused, SC2154 unassigned, SC2155),
command pitfalls (SC2164 unchecked `cd`, SC2162 `read` without `-r`,
useless `cat`/`echo`, `ls | grep`, `find | xargs`, printf argument
counting, catastrophic `rm -rf`), structural mistakes
(`A && B || C`, constant test expressions, `$?` anti-patterns), and more.

<details>
<summary>All implemented codes</summary>

SC2002 SC2003 SC2004 SC2005 SC2006 SC2007 SC2009 SC2010 SC2011 SC2012
SC2015 SC2016 SC2026 SC2027 SC2028 SC2034 SC2035 SC2038 SC2041 SC2042
SC2043 SC2046 SC2048 SC2050 SC2059 SC2064 SC2065 SC2066 SC2068 SC2086
SC2089 SC2090 SC2093 SC2094 SC2103 SC2114 SC2115 SC2116 SC2126 SC2128
SC2140 SC2145 SC2148 SC2153 SC2154 SC2155 SC2162 SC2164 SC2174 SC2178
SC2179 SC2181 SC2182 SC2183 SC2187 SC2188 SC2189 SC2206 SC2207 SC2223
SC2239 SC2246 SC2248 SC2250 SC2258 SC2304 SC2305 SC2306 SC2307 SC2308

</details>

## Conformance scoreboard

The repository vendors **1508 test cases extracted from ShellCheck's own
test suite** (`tests/data/corpus.json`), run by pytest on every commit:

| metric | result |
|---|---|
| official test cases for the implemented checks | **619/620 (99.8%)** |
| whole official corpus (incl. unimplemented checks) | 1025/1508 (68.0%) |
| real-world differential test vs `shellcheck` 0.11.0 | **113/113 findings agree, 0 missed, 0 false positives** (48 scripts from Homebrew/npm) |

The single implemented-check failure is documented in
`tests/data/expected_failures.txt` (a test of ShellCheck's non-default
`check-unassigned-uppercase` mode); every other non-passing corpus case is
listed there with a reason. Reproduce with:

```console
$ python tools/conformance.py                  # scoreboard
$ python tools/diff_shellcheck.py *.sh         # vs the real binary
```

## Usage

### CLI

```console
$ pureshellcheck [-s bash] [-f tty|gcc|json|json1] [-e SC2086] \
                 [-S error|warning|info|style] script.sh [more.sh ...]
```

Exit status is 0 for a clean script, 1 if there are findings, 2 on file
errors — same convention as shellcheck. `# shellcheck disable=SC2086`
and `# shellcheck shell=dash` directives are honored.

### Library

```python
import pureshellcheck

for f in pureshellcheck.check('echo $foo', shell='bash'):
    print(f.line, f.column, f.code, f.severity, f.message)
# 1 6 2086 info Double quote to prevent globbing and word splitting.
```

`check()` returns a list of findings with `code`, `severity`
(`error|warning|info|style`), `message`, and 1-based
`line`/`column`/`end_line`/`end_column`. `pureshellcheck.parse()` exposes
the bash AST if you want to build your own analyses.

## Benchmarks

Measured with `python tools/bench.py` (median of 7 runs, after verifying
both tools report identical findings on the workload; CPython 3.12,
shellcheck 0.11.0, Apple Silicon):

| workload | shellcheck | pureshellcheck | speedup |
|---|---|---|---|
| CLI, brew.sh (1216 lines) | 604 ms | 68 ms | **8.9×** |
| embedded `check()`, brew.sh | 604 ms | 45 ms | **13.3×** |
| CLI, 75-line script | 42 ms | 24 ms | 1.8× |
| embedded `check()`, 75-line script | 42 ms | 2.4 ms | **17×** |

The embedded rows are what an agent or editor integration pays per call:
no process spawn, no binary.

## Compatibility notes

- Targets bash (default), sh/dash/ash and ksh dialects are accepted via
  shebang, directive, or `-s`; sh-specific portability checks (the
  SC2039/SC3xxx family) are not implemented yet.
- The parser is deliberately lenient: it keeps checking past constructs
  the real shellcheck refuses to parse (e.g. `[ $tar --version ]`).
- Optional checks (`SC2002`, `SC2248`, `SC2250`) are off by default,
  matching shellcheck 0.11; enable with `-o` / `include_optional=True`.
- Wiki links work the same: see <https://www.shellcheck.net/wiki/SC2086>
  for any reported code.

## Development

```console
$ pip install -e . pytest
$ pytest                                # corpus + unit tests, < 1 s
$ python tools/extract_corpus.py /path/to/shellcheck   # refresh corpus
$ python tools/update_expected_failures.py             # refresh scoreboard
$ python tools/bench.py                                # benchmarks
```

The package itself is MIT licensed and has zero runtime dependencies
(CPython 3.9–3.14 and PyPy). The vendored test corpus in `tests/data/` is
extracted from the GPLv3-licensed ShellCheck project and is used only for
development-time testing; it is not part of the distributed wheel.

## See also

- [purejq](https://github.com/adam2go/purejq) — pure Python jq, same
  philosophy: vendored official test suite, differential testing, no
  binaries.
