shfmt-py
pip install shfmt-py puts shfmt, the shell script formatter, on the PATH of your Python
environment and gives you a ready-made pre-commit hook.
This is packaging only — no patches, no Python API, nothing to import, no python -m shfmt. On
common platforms the wheel bundles upstream's binary; elsewhere the build downloads and
checksum-verifies it, or — where no build is pinned for your platform — copies an existing shfmt
from your PATH unverified ("How the binary gets installed" below has the details). For what
shfmt does and which flags it takes, run shfmt --help or read the upstream docs.
Modeled after shellcheck-py, adapted for shfmt.
Install
pip install shfmt-py
Or as a standalone tool, isolated from any project environment:
uv tool install shfmt-py
# or
pipx install shfmt-py
Requires Python 3.9 or newer; the binary itself has no Python runtime dependency. CI covers CPython 3.9, 3.13 and 3.14 on Linux, macOS (arm64 and x86_64) and Windows.
The distribution installs exactly one executable — shfmt, or shfmt.exe on Windows — into the
environment's scripts directory, so it is on PATH whenever that environment is active.
pip uninstall shfmt-py removes it again.
Pre-commit hook
Add to .pre-commit-config.yaml:
- repo: https://github.com/MaxWinterstein/shfmt-py
rev: v4.1.0
hooks:
- id: shfmt
rev is the shfmt-py release tag, not the shfmt version. pre-commit autoupdate moves it to
the newest tag; pre-commit run --all-files formats the whole repository.
The hook runs on every file identify tags as shell, excluding csh and tcsh. It defaults to
args: [-w], which rewrites files in place.
Overriding args
pre-commit replaces the default args; it does not extend them. Drop -w by accident and
shfmt prints the formatted script to stdout, changes nothing and exits 0 — the hook goes green
while your files stay unformatted. So re-add it:
- repo: https://github.com/MaxWinterstein/shfmt-py
rev: v4.1.0
hooks:
- id: shfmt
args: [-w, -i, "2", -ci, -bn] # -w must be re-added
Those flags — two-space indent, indented case arms, binary operators allowed to start a line —
are the set the upstream manual describes as closely resembling Google's shell style. Quote the 2:
pre-commit expects every argument to be a string, and YAML would otherwise make it an integer.
For a check-only run that reports instead of rewriting, swap the args above for:
- id: shfmt
args: [-d] # print a diff and fail; no -w on purpose
EditorConfig
shfmt reads formatting options from .editorconfig. Two of its behaviors matter for hook users:
- Any parser or printer flag turns EditorConfig off entirely —
-i,-ci,-s,-lnand friends disable every EditorConfig formatting option, not just the one you overrode.-w,-dand-lare generic flags and leave it alone, so the defaultargs: [-w]keeps.editorconfigin charge. Configure your style in one place, not both. ignore = trueis skipped for explicitly named files, and pre-commit always names files explicitly. Useargs: [-w, --apply-ignore]to honor it, or pre-commit's ownexclude:.
Hook installs need network access
pre-commit installs language: python hooks by running pip install . inside its own clone, so it
never uses the published wheels. Installing the hook therefore downloads the binary from the
mvdan/sh GitHub release the first time a given rev is used, and caches the result afterwards.
Restricted runners need github.com and its release-asset host (*.githubusercontent.com)
reachable — a PyPI mirror is not enough.
Command line
shfmt --version # the upstream version this release bundles
shfmt -w script.sh # format in place
shfmt -d . # print a diff, exit 1 if anything differs
shfmt -l . # list files that differ, exit 1 if any do
shfmt --help prints the full flag list. Dialects, EditorConfig keys and the default style are
upstream's documentation, not this project's: see mvdan/sh and its
man page source (or run man shfmt).
How the binary gets installed
Three paths, in this order:
- A matching wheel. Published for Linux x86_64 and aarch64 (manylinux2014), macOS arm64 and x86_64, and Windows amd64. The binary is already inside the wheel, so nothing is fetched at install time and a PyPI mirror is enough.
- From source, platform in the download table. The other platforms this package pins a
download for — 32-bit Windows, Linux armv7 (hosts whose
uname -mreportsarmv7l), Cygwin, and musl-based distros such as Alpine, which use the ordinary static Linux binaries — plus any install that bypasses wheels (pip install --no-binary :all: shfmt-py, or pre-commit). The build downloads the official release asset and verifies it against a sha256 pinned in setup.py. A mismatch aborts the install, and if GitHub is unreachable the install fails rather than silently using something else. - From source, platform not in the download table. FreeBSD, illumos, 32-bit x86 Linux, older
32-bit ARM and other architectures with no pinned download here: the build copies whatever
shfmtit finds on yourPATH(on Windows it must be a real.exe). That copy is neither checksummed nor version-checked, so it may differ from the version this release pins —shfmt --versionis worth a look. With noshfmtonPATH, the install fails with an error telling you to install one, instead of installing something broken.
For air-gapped environments, build one wheel per target platform on a machine that can reach GitHub
(python -m build --wheel; on Linux set _PYTHON_HOST_PLATFORM=manylinux2014_x86_64, as the
release workflow does, or the wheel comes out tagged linux_x86_64) and serve them from your
internal index. That covers pip / uv / pipx installs only: pre-commit builds the hook from
source and still reaches for GitHub, so an air-gapped runner additionally needs a mirror of the
mvdan/sh release asset or a pre-populated ~/.cache/pre-commit.
Versioning
Which shfmt do you have? Ask the binary — shfmt --version is always right. Before installing,
read SHFMT_VERSION in setup.py at that release's tag —
https://github.com/MaxWinterstein/shfmt-py/blob/vX.Y.Z/setup.py.
shfmt-py is independently versioned; the PyPI version does not mirror the bundled shfmt
version.
- Major — breaking change to
shfmt-pyitself (e.g. dropping a Python version, renaming the pre-commit hook id). - Minor — new upstream
shfmtrelease bundled. - Patch — wrapper-only fix (hash regeneration, CI changes affecting users, etc.).
Releases 3.x.y.z and earlier used a 4-segment scheme aligned with upstream shfmt — 3.13.0.3
bundled shfmt 3.13.0. From v4.0.0 onwards shfmt-py follows standard semver.
FAQ
The hook passes but nothing gets formatted.
You set args: without -w. pre-commit replaces the default args: [-w] instead of extending it,
so re-add -w to your list.
shfmt: command not found after pip install.
The executable lands in the target environment's scripts directory. Activate that virtualenv, or
use uv tool install shfmt-py / pipx install shfmt-py to get it on your user PATH. If your OS
package manager also ships shfmt, PATH order decides which one runs — shfmt --version tells
you which one you got.
It won't get updated via e.g. Renovate Bot.
Releases v4.0.0 and onwards use standard semver — no special Renovate config needed. For older
3.x.y.z releases you'll need "versioning": "pep440" (or see
shfmt-py/update-via-renovate). For the pre-commit hook, pre-commit autoupdate
works either way.
I get something like SSL: CERTIFICATE_VERIFY_FAILED on macOS.
This only happens on the from-source paths that download from GitHub at build time — which include
every pre-commit hook install — never when a wheel is used. Install certificates with e.g.
"/Applications/Python 3.x/Install Certificates.command" for the Python you are installing with.
See this MerossIot comment or this Stack Overflow answer for a solution.
Issues
Formatting behavior, flags and feature requests belong upstream, at mvdan/sh issues. Packaging, wheels, platform coverage and the hook definition belong in this project's issue tracker.
License
shfmt-py is MIT licensed; see LICENSE. The shfmt binary it ships or downloads is the work of
the mvdan/sh project and is redistributed unmodified under its own
BSD-3-Clause license.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file shfmt_py-4.1.0.tar.gz.
File metadata
- Download URL: shfmt_py-4.1.0.tar.gz
- Upload date:
- Size: 29.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e863c07885e2e976b1441e38f660b3262a87a52bf84124415b866e48eed7c00b
|
|
| MD5 |
c441e7ed60ffc19665d3ef028af01fc0
|
|
| BLAKE2b-256 |
9e2fc9616d3716a8e75a4c36427bede49d61b68c2d1595a76b3de2347e040fa7
|
Provenance
The following attestation bundles were made for shfmt_py-4.1.0.tar.gz:
Publisher:
python-publish.yml on MaxWinterstein/shfmt-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shfmt_py-4.1.0.tar.gz -
Subject digest:
e863c07885e2e976b1441e38f660b3262a87a52bf84124415b866e48eed7c00b - Sigstore transparency entry: 2636750141
- Sigstore integration time:
-
Permalink:
MaxWinterstein/shfmt-py@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/MaxWinterstein
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Trigger Event:
release
-
Statement type:
File details
Details for the file shfmt_py-4.1.0-py2.py3-none-win_amd64.whl.
File metadata
- Download URL: shfmt_py-4.1.0-py2.py3-none-win_amd64.whl
- Upload date:
- Size: 1.7 MB
- Tags: Python 2, Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e5c55ac4b619fc4d215259459ac80e8513735245ec621ca290574fa7ab2811d
|
|
| MD5 |
7af1f7826ae39a32bfb795711056be5f
|
|
| BLAKE2b-256 |
e0e8be46d2141ec18d30c713258c97c4a8d85f5e39cedf8c9b2ba642c28a2199
|
Provenance
The following attestation bundles were made for shfmt_py-4.1.0-py2.py3-none-win_amd64.whl:
Publisher:
python-publish.yml on MaxWinterstein/shfmt-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shfmt_py-4.1.0-py2.py3-none-win_amd64.whl -
Subject digest:
7e5c55ac4b619fc4d215259459ac80e8513735245ec621ca290574fa7ab2811d - Sigstore transparency entry: 2636750440
- Sigstore integration time:
-
Permalink:
MaxWinterstein/shfmt-py@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/MaxWinterstein
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Trigger Event:
release
-
Statement type:
File details
Details for the file shfmt_py-4.1.0-py2.py3-none-manylinux2014_x86_64.whl.
File metadata
- Download URL: shfmt_py-4.1.0-py2.py3-none-manylinux2014_x86_64.whl
- Upload date:
- Size: 1.6 MB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f1573856d84725f148175ef56532365dc4a7e5332e27c31b6c82be544ad2bda
|
|
| MD5 |
cd52f8c54f4d43a433c6366acd47351c
|
|
| BLAKE2b-256 |
0fb2101c166f6f1895a7e4b2062d350bf156097087ffc95f772877e51c04358d
|
Provenance
The following attestation bundles were made for shfmt_py-4.1.0-py2.py3-none-manylinux2014_x86_64.whl:
Publisher:
python-publish.yml on MaxWinterstein/shfmt-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shfmt_py-4.1.0-py2.py3-none-manylinux2014_x86_64.whl -
Subject digest:
4f1573856d84725f148175ef56532365dc4a7e5332e27c31b6c82be544ad2bda - Sigstore transparency entry: 2636750294
- Sigstore integration time:
-
Permalink:
MaxWinterstein/shfmt-py@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/MaxWinterstein
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Trigger Event:
release
-
Statement type:
File details
Details for the file shfmt_py-4.1.0-py2.py3-none-manylinux2014_aarch64.whl.
File metadata
- Download URL: shfmt_py-4.1.0-py2.py3-none-manylinux2014_aarch64.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
887a7a04a297302f3472edaeabb5f33f146d9a681234743acc5932a2a39bdb22
|
|
| MD5 |
e197538064d60111253f293e9ecc5a69
|
|
| BLAKE2b-256 |
ff910bc95e1f3ba2b591986701d00c8ee80eef228fb08c338a873d0df3444092
|
Provenance
The following attestation bundles were made for shfmt_py-4.1.0-py2.py3-none-manylinux2014_aarch64.whl:
Publisher:
python-publish.yml on MaxWinterstein/shfmt-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shfmt_py-4.1.0-py2.py3-none-manylinux2014_aarch64.whl -
Subject digest:
887a7a04a297302f3472edaeabb5f33f146d9a681234743acc5932a2a39bdb22 - Sigstore transparency entry: 2636750230
- Sigstore integration time:
-
Permalink:
MaxWinterstein/shfmt-py@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/MaxWinterstein
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Trigger Event:
release
-
Statement type:
File details
Details for the file shfmt_py-4.1.0-py2.py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: shfmt_py-4.1.0-py2.py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.4 MB
- Tags: Python 2, Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e0201449fc35f3cf16abed3189f1de0c5c30d5c4c989b8d367fd5cd1d35e779
|
|
| MD5 |
20d4751696b9b05af3f56b5beeefbe4f
|
|
| BLAKE2b-256 |
3603e8f10813c498247f6ddff63c82e4bb351fda188ec73e3821433f913cb651
|
Provenance
The following attestation bundles were made for shfmt_py-4.1.0-py2.py3-none-macosx_11_0_arm64.whl:
Publisher:
python-publish.yml on MaxWinterstein/shfmt-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shfmt_py-4.1.0-py2.py3-none-macosx_11_0_arm64.whl -
Subject digest:
8e0201449fc35f3cf16abed3189f1de0c5c30d5c4c989b8d367fd5cd1d35e779 - Sigstore transparency entry: 2636750345
- Sigstore integration time:
-
Permalink:
MaxWinterstein/shfmt-py@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/MaxWinterstein
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Trigger Event:
release
-
Statement type:
File details
Details for the file shfmt_py-4.1.0-py2.py3-none-macosx_10_9_x86_64.whl.
File metadata
- Download URL: shfmt_py-4.1.0-py2.py3-none-macosx_10_9_x86_64.whl
- Upload date:
- Size: 1.6 MB
- Tags: Python 2, Python 3, macOS 10.9+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7186d0cdb21d8a5e6cb647c1b0ae7ee264751be3cac755277d060df90b1cfa8f
|
|
| MD5 |
7e1736a62b7fbcb79b9071e6bb79d320
|
|
| BLAKE2b-256 |
88e11d0129c8baaf943921a8b1402fc9bb2a1fc9753a35ddc068a639828e91d8
|
Provenance
The following attestation bundles were made for shfmt_py-4.1.0-py2.py3-none-macosx_10_9_x86_64.whl:
Publisher:
python-publish.yml on MaxWinterstein/shfmt-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shfmt_py-4.1.0-py2.py3-none-macosx_10_9_x86_64.whl -
Subject digest:
7186d0cdb21d8a5e6cb647c1b0ae7ee264751be3cac755277d060df90b1cfa8f - Sigstore transparency entry: 2636750413
- Sigstore integration time:
-
Permalink:
MaxWinterstein/shfmt-py@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Branch / Tag:
refs/tags/v4.1.0 - Owner: https://github.com/MaxWinterstein
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@dc4b7ed99c83f03cf02b5ccfa9b61aa9fc931b96 -
Trigger Event:
release
-
Statement type: