Your Bash Toolset is Software Now

bash <(curl -sL https://github.com/tothimre/packagesh/releases/latest/download/packagesh_loader.run)
source ~/.bashrc

That's the install. No sudo. No package manager. No runtime. After that, packagesh is a function in your shell — ready to bundle and publish your own Bash tools the same way.


The Problem it Solves

Bash functions loaded into the global namespace are the fastest CLI you can have. No PATH lookup. No subprocess spawn. No runtime to boot. You call my_tool and it runs — because it is your shell.

source ~/.bashrc
my_tool --do-the-thing   # instant. it's a function.

But sharing those functions has always been awkward. Copy-paste into ~/.bashrc and it rots. git clone a dotfiles repo and you're managing a repo per machine. None of these fit the shape of "I have 30 bash functions I want everywhere, always current, zero ceremony."

packagesh fits that shape.


What it Does

Point it at a directory, tell it which functions are public:

packagesh --project=/path/to/my-tool \
    --helpable-functions="my_tool" \
    --public-functions="my_tool,my_tool_helper"

It runs the full pipeline:

1. Bundle       — collects your .sh files into one loader
2. Inject help  — bakes -h documentation into the bundle
3. Snapshot     — saves a .readable pre-mangle copy for debugging
4. Mangle       — renames private functions with random suffixes
5. Wrap         — generates a self-extracting .run installer
6. Smoke test   — runs your main command to verify the bundle works
7. Publish      — uploads to GitHub Releases (skips if unchanged or --no-push)

Output lands in dist/:

dist/
├── my_tool_loader          # mangled production bundle
├── my_tool_loader.readable # pre-mangle, for debugging
└── my_tool_loader.run      # self-extracting installer

Your users install it the same way you installed packagesh:

bash <(curl -sL https://github.com/you/my-tool/releases/latest/download/my_tool_loader.run)
source ~/.bashrc
my_tool   # it's there

Namespace Safety

Sourcing multiple Bash scripts into the same shell session causes collisions. A _helper in one tool stomps the _helper in another.

packagesh mangles every private function with a random per-bundle suffix at build time:

# source
_private_helper() { ... }

# after mangling
_private_helper_a3f9c2e1b8d47f05() { ... }

Public functions stay clean. Everything internal is isolated. Source ten bundles into the same session, nothing collides. The .readable snapshot keeps it debuggable when you need to look inside.


Idempotent Publish

The publish step normalizes the bundle before hashing — stripping the random suffixes mangling introduces — then compares against the last published hash.

Run it a hundred times. It only cuts a release when something actually changed. Safe to wire into CI on every merge:

# in your github actions workflow
- run: publish_my_tool

No noise. No 400 identical releases.


For the Monorepo

packagesh works equally well in a large org with a monorepo full of Bash utilities. Build multiple projects in one shot:

packagesh --project=/tools/linter;/tools/formatter;/tools/deployer \
    --helpable-functions="main_cmd" \
    --public-functions="main_cmd"

All bundles land in dist/ inside the first path. The platform team publishes a blessed toolset. Engineers run the .run installer once, source their bashrc, and the org's standard CLI is just there — on laptops, in CI containers, in onboarding scripts.

No wiki page saying "copy this script to /usr/local/bin." No Ansible playbook to maintain. Just a .run file and a bashrc hook.


The Meta Part

packagesh is distributed using packagesh. The installer you ran at the top of this article was built and published by the tool itself.


gst

What's Next

Part 2 covers the development loop — the transparency map that lets you iterate on live functions without rebuilding the bundle, and packagesh_shell for interactive testing inside a loaded session.

Part 3 covers vendoring: how shared utilities get bundled into your tool without becoming a dependency you manage.

View on GitHub: 311ecode/packagesh