shlib

August 31, 2026 · View on GitHub

curl | sh installers with portable shell functions

lint linux macos freebsd openbsd netbsd dragonflybsd sunos alpine windows runtimes

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/bin

shlib 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.

Build an installer

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.sh

Commit 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 tag

If your filenames are unusual

Everything 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 archive

Real Examples

Some sample curl|sh installers for live projects:

RepoExampleVariations
securego/gosecgosec.shthe simple case — no hooks at all
ory/hydrahydra.shrenamed OS and arch, .zip on windows
go-task/tasktask.shno version in the archive name
golangci/golangci-lintgolangci-lint.sh27 platforms, binary nested in a versioned directory
gohugoio/hugohugo.shBSDs, Solaris, illumos, and build variants
koalaman/shellcheckshellcheck.shnot a Go project — raw x86_64/aarch64 names
hadolint/hadolinthadolint.shno archive at all — the asset is the binary

See docs/INSTALLERS.md for details.

The building blocks

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:

filewhat it is
dist/install-base.shshlib + installer logic; prepend your config
dist/shlib.shjust the functions
curl -sSfL -o vendor/shlib.sh \
  https://raw.githubusercontent.com/client9/shlib/master/dist/shlib.sh

Fetch a bundle at build time rather than pasting a copy that goes staledocs/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.sh

Tested where it actually has to run

Every push runs the whole suite against each shell below.

platformshells tested
Linux (glibc)dash bash ksh93 mksh yash posh busybox ash
Alpine (musl)busybox ash
macOSsh bash 3.2 ksh zsh dash
FreeBSD 14, 15sh dash bash ksh93 mksh yash zsh
OpenBSD 7.9, 7.8sh (OpenBSD ksh)
NetBSD 11.0, 10.1sh ksh
DragonFly 6.4sh
Solaris 11.4, OmniOSsh
Windowsgit bash msys2
python:3-slimsh — no downloader at all; python3 only
node:22-slimsh — no downloader at all; node only

Several legs also assert something the suite alone cannot:

  • Solaris and OmniOS, that SunOS resolves to solaris and illumos respectively -- 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's fetch(1) and ftp(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-slim and node:22-slim assert that curl, wget, fetch and ftp are all absent, then run the whole suite and a real install through http_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

Documentation

documentwhat it covers
docs/INSTALLERS.mdbuilding a curl | sh installer
docs/API.mdgenerated index of all 33 functions
docs/EMBEDDING.mdvendoring shlib without going stale
docs/PORTABILITY.mdwhich shells break which idiom, which systems ship pre-POSIX tools
docs/RELEASING.mdcutting a release
CONTRIBUTING.mdchanging shlib itself
CHANGELOG.mdwhat changed

Why this exists

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.