curl | sh installers with portable shell functions
You publish binaries on GitHub Releases and want your users to run one line:
curl -sSfL https://raw.githubusercontent.com/OWNER/REPO/master/install.sh | sh -s -- -b /usr/local/binshlib generates that install.sh. It is the replacement for the archived
godownloader
The generated script detects the platform, resolves latest or a pinned tag,
downloads the right asset, verifies its SHA-256 against its checksums file,
unpacks it, and installs the binary — on everything in the
test matrix.
An install script is two files concatenated:
config.sh yours, ~12 lines
dist/install-base.sh ours: the shlib functions + the installer flow
The config describes the release naming. This is
install/examples/gosec.sh:
OWNER=securego
REPO=gosec
BINARY=gosec
FORMAT=tar.gz
BINDIR=${BINDIR:-./bin}
# declare the build matrix
PLATFORMS="darwin/amd64 darwin/arm64
linux/amd64 linux/arm64 linux/ppc64le linux/s390x
windows/amd64 windows/arm64"
archive_name() { echo "${BINARY}_${VERSION}_${OS}_${ARCH}"; }
checksum_name() { echo "${BINARY}_${VERSION}_checksums.txt"; }To make the install.sh, do this in your release process. cat
the config with the latest install-base.sh:
curl -sSfL -o /tmp/base.sh \
https://raw.githubusercontent.com/client9/shlib/master/dist/install-base.sh
cat config.sh /tmp/base.sh > install.shCommit install.sh:
sh install.sh # latest release, into ./bin
sh install.sh -b /usr/local/bin
sh install.sh -b ./bin v2.22.0 # a specific tagEverything is an ordinary shell function, which can be over-ridden.
adjust_format() { case ${OS} in windows) FORMAT=zip ;; esac; }
adjust_os() { case ${OS} in darwin) OS=macOS ;; esac; }
adjust_arch() { case ${ARCH} in amd64) ARCH=x86_64 ;; esac; }
binary_path() { echo "${NAME}/$1"; } # binary nested inside the archive
unpack() { :; } # the asset IS the binary, no archiveSome sample curl|sh installers for live projects:
| Repo | Example | Variations |
|---|---|---|
| securego/gosec | gosec.sh |
the simple case — no hooks at all |
| ory/hydra | hydra.sh |
renamed OS and arch, .zip on windows |
| go-task/task | task.sh |
no version in the archive name |
| golangci/golangci-lint | golangci-lint.sh |
27 platforms, binary nested in a versioned directory |
| gohugoio/hugo | hugo.sh |
BSDs, Solaris, illumos, and build variants |
| koalaman/shellcheck | shellcheck.sh |
not a Go project — raw x86_64/aarch64 names |
| hadolint/hadolint | hadolint.sh |
no archive at all — the asset is the binary |
See docs/INSTALLERS.md for details.
The installer is assembled from a library of standalone functions, and you can
use them directly — for a build script, a bootstrap, a CI step. Each lives in
its own <name>.sh and is meant to be concatenated into your script, not
sourced as a dependency: a curl | sh installer cannot have dependencies, so
the code has to travel with it.
uname_os uname_arch http_download hash_sha256_verify untar
install_exe mktmpdir github_release log_* — 33 in total, indexed in
docs/API.md.
Pre-built bundles are committed here and attached to every release:
| file | what it is |
|---|---|
dist/install-base.sh |
shlib + installer logic; prepend your config |
dist/shlib.sh |
just the functions |
curl -sSfL -o vendor/shlib.sh \
https://raw.githubusercontent.com/client9/shlib/master/dist/shlib.shFetch a bundle at build time rather than pasting a copy that goes stale — docs/EMBEDDING.md explains why that matters more than it sounds like it should.
Or build a custom subset — cat is the whole build step. List the files in
dependency order; most functions report errors through log_err / log_crit,
so include echoerr.sh and log.sh whenever you include something that can
fail:
cat \
license.sh \
is_command.sh \
echoerr.sh \
log.sh \
uname_os.sh \
uname_arch.sh \
untar.sh \
mktmpdir.sh \
http_download.sh \
hash_sha256.sh \
license_end.sh > vendor/shlib.shEvery push runs the whole suite against each shell below.
| platform | shells tested |
|---|---|
| Linux (glibc) | dash bash ksh93 mksh yash posh busybox ash |
| Alpine (musl) | busybox ash |
| macOS | sh bash 3.2 ksh zsh dash |
| FreeBSD 14, 15 | sh dash bash ksh93 mksh yash zsh |
| OpenBSD 7.9, 7.8 | sh (OpenBSD ksh) |
| NetBSD 11.0, 10.1 | sh ksh |
| DragonFly 6.4 | sh |
| Solaris 11.4, OmniOS | sh |
| Windows | git bash msys2 |
python:3-slim |
sh — no downloader at all; python3 only |
node:22-slim |
sh — no downloader at all; node only |
Several legs also assert something the suite alone cannot:
- Solaris and OmniOS, that
SunOSresolves tosolarisandillumosrespectively -- both systems report that ancient name, and telling them apart has been the most bug-prone mapping in this library. - FreeBSD, OpenBSD, NetBSD and DragonFly, that curl and wget really are
absent. They are the only legs that exercise
http_download'sfetch(1)andftp(1)branches, and a dependency quietly installing curl would stop that without anything going red. - OmniOS builds a real installer from
install/examples/hugo.sh, runs it, and executes the downloaded binary. python:3-slimandnode:22-slimassert that curl, wget, fetch and ftp are all absent, then run the whole suite and a real install throughhttp_download_python/http_download_node. Container images routinely ship a language runtime and no downloader at all.
The library recognises a wider set than it can practically test: 17 operating systems and 16 architectures. See Platforms for the generated lists.
make test # everything under /bin/sh
make test TEST_SHELL=dash # ... under one specific shell
make test-all # ... under every shell installed locally
make lint # shellcheck (sh, bash, dash, ksh) + shfmt| document | what it covers |
|---|---|
| docs/INSTALLERS.md | building a curl | sh installer |
| docs/API.md | generated index of all 33 functions |
| docs/EMBEDDING.md | vendoring shlib without going stale |
| docs/PORTABILITY.md | which shells break which idiom, which systems ship pre-POSIX tools |
| docs/RELEASING.md | cutting a release |
| CONTRIBUTING.md | changing shlib itself |
| CHANGELOG.md | what changed |
I've sadly written a lot of shell scripts. Mostly for installers on completely alien environments.
Really shell code should only be used for boot-strapping to something sane. Until then you might need some truly portable functions. I hope you never need to use them, but if you do they are public domain. Do whatever you'd like with them.
However acknowledgement (and pull requests) are appreciated. You can optionally include license.sh so the next person knows where to find them.