Skip to main content

PyPI version Supported Python versions Downloads License pre-commit.ci status

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.2.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.2.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, -ln and friends disable every EditorConfig formatting option, not just the one you overrode. -w, -d and -l are generic flags and leave it alone, so the default args: [-w] keeps .editorconfig in charge. Configure your style in one place, not both.
  • ignore = true is skipped for explicitly named files, and pre-commit always names files explicitly. Use args: [-w, --apply-ignore] to honor it, or pre-commit's own exclude:.

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:

  1. 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.
  2. From source, platform in the download table. The other platforms this package pins a download for — 32-bit Windows, Linux armv7 (hosts whose uname -m reports armv7l), 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.
  3. 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 shfmt it finds on your PATH (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 --version is worth a look. With no shfmt on PATH, 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-py itself (e.g. dropping a Python version, renaming the pre-commit hook id).
  • Minor — new upstream shfmt release 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.

Metadata

Release files for shfmt-py 4.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for shfmt-py 4.2.0
File Size Uploaded
shfmt_py-4.2.0.tar.gz 30.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for shfmt-py 4.2.0
File
shfmt_py-4.2.0-py2.py3-none-win_amd64.whl Python 3, Python 2 none Windows x86-64 Details
shfmt_py-4.2.0-py2.py3-none-manylinux2014_x86_64.whl Python 3, Python 2 none Linux glibc 2.17+ x86-64 Details
shfmt_py-4.2.0-py2.py3-none-manylinux2014_aarch64.whl Python 3, Python 2 none Linux glibc 2.17+ ARM64 Details
shfmt_py-4.2.0-py2.py3-none-macosx_11_0_arm64.whl Python 2, Python 3 none macOS 11.0+ ARM64 Details
shfmt_py-4.2.0-py2.py3-none-macosx_10_9_x86_64.whl Python 2, Python 3 none macOS 10.9+ x86-64 Details

Total release size: 7.6 MB

Release files / shfmt_py-4.2.0.tar.gz

Download URL shfmt_py-4.2.0.tar.gz
Size 30.0 kB
Tags Source
SHA-256 checksum
How to use checksums
cf7842d69e9f787ce97503c1280e65417f2c08014d594018dda245fcba96ff51
BLAKE2b-256 checksum
How to use checksums
ee910ae8bbc703ac6779427fcdb40da7784b844278032f7896dbe54cb7e11e0e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / shfmt_py-4.2.0-py2.py3-none-win_amd64.whl

Download URL shfmt_py-4.2.0-py2.py3-none-win_amd64.whl
Size 1.7 MB
Tags Python 2 Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
566f46c036cb475ac84ac952d83122b67c9557418c24b04628c5782f9f57eccf
BLAKE2b-256 checksum
How to use checksums
bd4a755b7c7cde56dd7306cea2a50e75a8a6a9281df52e124a5bb0b03474c6ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / shfmt_py-4.2.0-py2.py3-none-manylinux2014_x86_64.whl

Download URL shfmt_py-4.2.0-py2.py3-none-manylinux2014_x86_64.whl
Size 1.6 MB
Tags Linux glibc 2.17+ x86-64 Python 2 Python 3
SHA-256 checksum
How to use checksums
867b55792952d4e4aa27a1b2d578e63b745a77e11b7742d2f6258ef123a9c8f2
BLAKE2b-256 checksum
How to use checksums
018a03246179f8bafea1c4f0b664ef5fcad1814f2cf55af6acb2f614463eca20
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / shfmt_py-4.2.0-py2.py3-none-manylinux2014_aarch64.whl

Download URL shfmt_py-4.2.0-py2.py3-none-manylinux2014_aarch64.whl
Size 1.4 MB
Tags Linux glibc 2.17+ ARM64 Python 2 Python 3
SHA-256 checksum
How to use checksums
e615cfcfe3e184f59e3ac2574895542976995a46f7506b327c8af71ae02de84c
BLAKE2b-256 checksum
How to use checksums
53329448afcca6739c64810143d4dacae6a817308aab71d083d39e91b13b31ee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / shfmt_py-4.2.0-py2.py3-none-macosx_11_0_arm64.whl

Download URL shfmt_py-4.2.0-py2.py3-none-macosx_11_0_arm64.whl
Size 1.4 MB
Tags Python 2 Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
7b7fa81b120fdccebbe60b9c3411ce1b549fa2828f2809e8fa5e231a9bac65dd
BLAKE2b-256 checksum
How to use checksums
e3393b5c4061a4fa7dcfab88ca4bd17566132a82225aad78d6d5b42cab4beee7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log

Release files / shfmt_py-4.2.0-py2.py3-none-macosx_10_9_x86_64.whl

Download URL shfmt_py-4.2.0-py2.py3-none-macosx_10_9_x86_64.whl
Size 1.6 MB
Tags Python 2 Python 3 macOS 10.9+ x86-64
SHA-256 checksum
How to use checksums
aaf9e195c2e115309c32ef0d8d05b42afc4a23468b61230a09a8a4bdac95d027
BLAKE2b-256 checksum
How to use checksums
8a87b07a16ffebc4f04ad7b7f1a6928edbe0c1d024be089e8e37bcf9bb963091
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page