shellpack
Pack a shell script and the fragments it sources into one standalone file, or a script and the sibling scripts it runs into one archive, so it runs on a host that does not have the checkout.
You write scripts the way a repository of them wants to be written: shared
functions in fragments, sourced where they are needed. shellpack inlines every
sourced fragment in place, in the order the shell would have read them, and
writes a single script that behaves the same for deployment.
Install
# Install with uv ...
uv tool install shellpack
# ... or pipx ...
pipx install shellpack
# ... or run it without installing
uvx shellpack --help
shellpack needs shfmt 3.7 or newer on the
PATH. It reads the syntax tree shfmt --to-json prints, which is what tells a
source statement from the same words inside a heredoc or a string.
Use
The arguments read as cp's do: sources, then a destination.
# Write to the given file
shellpack install/setup.sh /tmp/setup.sh
# Write under its own name into the given folder
shellpack install/setup.sh /tmp/
# Write several into the given folder
shellpack a.sh b.sh c.sh /tmp/packed/
# Write to stdout
shellpack install/setup.sh -
A source or . line is resolved, in order of preference, by:
- a
# shellcheck source=<path>directive on the line before it, the path relative to the project root (the git toplevel, or--root); - the
"$(dirname -- "${BASH_SOURCE[0]}")/<rel>"idiom, relative to the sourcing file; - a plain relative path with no shell expansion, relative to the sourcing file.
A source that resolves to nothing, because the path is built from a variable
or the file does not exist yet, is left as it is. An optional include of a local
override file thus keeps working. Each fragment is inlined once. File-level
# shellcheck disable= directives are hoisted to the top of the packed script,
which stays shellcheck-clean.
Directives
A script says what shellpack may do with it in its leading comment block, before the first command.
#!/usr/bin/env bash
# shellpack: requires ./configure.sh
# shellpack: requires-dynamic ./steps/10-prepare.sh
# shellpack: non-packable - <reason>marks a script that cannot work outside a checkout, because it resolves a repository path at run time or runs a program in another language. shellpack refuses to pack it, and refuses any archive whose closure reaches it.# shellpack: requires <path>declares a sibling shell script the script runs, written relative to itself exactly as the call is written in the code. Such a script cannot travel as one file; it must be packed with--archive.# shellpack: requires-dynamic <path>declares a sibling the script runs without naming it. For instance an installer that finds its steps withfind ./steps. It is packed just like arequirestarget, but the lint does not expect to see an explicit call to it.
Archives
shellpack --archive install/setup.sh /tmp/setup.tar.gz
shellpack --combine install/a.sh install/b.sh /tmp/installers.tar.gz
--archive packs the script together with the transitive closure of its
requires directives. Every script is packed on its own and stored at its path
below the closure's common directory, the smallest tree in which the relative
calls resolve as they do in the checkout. The archive is run by the file at its
root named after the entry: the entry itself when it already sits there,
otherwise a forwarder of that name, so the archive runs directly from wherever
it was unpacked.
--combine packs several entries into one archive sharing one tree. A script
required by two entries is stored once, and each entry is run at the path it has
in the checkout. There is no forwarder.
Archived scripts are linted against that same tree.
--wrapper
Projects that run siblings through a wrapper function of their own can give it
with --wrapper:
shellpack --archive --wrapper run_command install/setup.sh /tmp/
--keep
A packed file inlines everything by default. Project that wnat to keep certain
fragments in the archive can exclude them with --keep:
shellpack --archive --keep _env.sh install/setup.sh /tmp/
The # shellcheck source= directive on a kept line is rewritten to the
archive-relative path, so shellcheck -x from the archive root follows it.
As a library
import shellpack
text, warnings, kept = shellpack.pack(entry, root)
members, entry_points, warnings = shellpack.archive_members(
[entry], root, keep=["_env.sh"], wrappers=["run_command"]
)
data, names, warnings = shellpack.build_archive([entry], root, keep=["_env.sh"])
Everything shellpack refuses raises shellpack.ShellpackError with a message
that names the script and the change at the source that fixes it.
License
MIT.
Metadata
Release files for shellpack 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| shellpack-0.1.0.tar.gz | 16.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| shellpack-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 35.8 kB
Release files / shellpack-0.1.0.tar.gz
| Download URL | shellpack-0.1.0.tar.gz |
|---|---|
| Size | 16.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ee9265ed58936e718c7b8910c10e30fd6dae87fc9845ea849393d1d7077aa46c
|
|
BLAKE2b-256 checksum How to use checksums |
8e963ddee7c5ebc46624357e8bbc1c41700864ac0fbeca7ca24ff3e20027eb36
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 2, 2026.
Transparency logRelease files / shellpack-0.1.0-py3-none-any.whl
| Download URL | shellpack-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dc2908ef51f71661844a72835f22e2205280e7a63cbbf78b78149eaada53496c
|
|
BLAKE2b-256 checksum How to use checksums |
9aa9ab325a235be3259c9506d204bd5a1a18f0ce1f676be9fd8bcc1211ac5c8e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 2, 2026.
Transparency log