bash Provider
The most flexible provider — let it run any shell script you want. When something doesn’t fit any
other provider (installing Homebrew itself, running a third-party installer script, writing an odd
environment file), bash is the escape hatch.
Flexibility comes with one responsibility: bash can’t figure out on its own whether “this thing
has already been done.” So you have to write a check: command. With a good check, apply stays
idempotent no matter how many times you run it.
Platforms: macOS, Linux
Usage
# Run the step associated with a URN
hams bash run "urn:hams:bash:install-homebrew"
# See what's managed
hams bash listThere’s no install or remove here — every entry is a declarative “this thing needs to be done.”
Hamsfile example
# macOS/bash.hams.yaml
schema_version: 1
provider: bash
groups:
- tag: bootstrap
items:
- urn: "urn:hams:bash:install-homebrew"
step: Install Homebrew
run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
check: command -v brew
- urn: "urn:hams:bash:install-pnpm"
step: Install pnpm
run: curl -fsSL https://get.pnpm.io/install.sh | sh -
check: command -v pnpm
- tag: config
items:
- urn: "urn:hams:bash:git-rerere"
step: Enable git rerere globally
run: git config --global rerere.autoUpdate true
check: git config --global --get rerere.autoUpdateFields:
run— the script. One line, or a YAML multi-line string (run: |)check— the idempotency check. Exit 0 means “skip.” Non-zero means “run it again”
Writing a check that won’t let you down
check is the soul of this provider. Get it right, and apply is a joy. Get it wrong, and you
either reinstall things every time or miss installs that never happened.
A few habits worth picking up:
- For command existence:
command -v foo. Notwhich foo—whichbehaves inconsistently across systems - For file existence:
test -f /path/to/file - For config values:
git config --global --get some.key | grep -q "expected" - For environment variables:
test -n "$FOO" - Don’t let check have side effects. It runs a lot. Imagine it running 100 times — still fine?
Anti-patterns:
# ❌ Always returns 0 — apply thinks it's done and never runs it
check: "true"
# ❌ Always returns non-zero — apply reruns it forever
check: "false"
# ❌ Depends on cwd, but hams's cwd might not be where you think
check: test -f ./some-fileThe execution environment
When hams runs run:
- shell —
/bin/bashby default. Change this globally inhams.config.yamlto/bin/shor/bin/zsh - cwd —
$HOME. Need a different directory?cdinside the script - env — inherits hams’s own environment. For secrets, use the
env_from_keychainfield - stdin — closed. If your script needs input, use another provider or switch to a non-interactive mode (
-yand friends)
When bash isn’t the right tool
bash is powerful, but it lacks the structured metadata the specialised providers bring. Default to something more specific when you can:
- Installing a Homebrew formula? Use the Homebrew provider, don’t write
brew installin a bash step - Setting a git config key? Use
hams git config— it reads better than rawgit config --global - Cloning a repo? That’s
hams git clone
Save bash for the things that truly have no better home: installing Homebrew itself, running a third-party installer, triggering one-off system initialisation.
There’s no “uninstall” for a bash resource — deleting a bash entry from the Hamsfile doesn’t reverse the script. If you need to undo something, do it manually, or add a separate bash entry that performs the inverse operation.