Why a Bash Installer Becomes a Liability - 4/9
Building a careful user-level installer for a Python CLI and discovering which packaging responsibilities it still cannot solve well.
The CLI now behaves like a tool inside its project environment. Sharing it exposes a different problem: another user needs the source file, the right Python interpreter, Requests, and a reliable command named tihttp.
A Bash installer looks like a direct solution. It can automate the manual steps and teach us what installation actually involves. It also reveals how quickly an application-specific installer starts taking responsibility for environment management, filesystem layout, upgrades, and platform compatibility.
Series
- From a Script to a Command-Line Tool
- When
sys.argvStops Scaling - Designing a CLI with
argparse - Why a Bash Installer Becomes a Liability
- Packaging a Python CLI with Console Scripts
- Testing a Python CLI and Automating CI
- Documentation as Part of the Product
- Building a Reliable PyPI Release Pipeline
- Publishing a Pure-Python CLI with Conda
What the installer must provide
For this experiment, assume the user has downloaded a directory containing tihttp.py, requirements.txt, and install.sh. The installer should:
- Install into user-owned directories without requiring root access.
- Create an isolated virtual environment.
- Install the declared dependencies.
- Expose a stable
tihttpcommand. - Fail instead of continuing after a partial installation.
The dependency file is deliberately small:
requests>=2.21
Here is a compact Unix-oriented installer:
#!/usr/bin/env bash
set -euo pipefail
readonly SOURCE_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
readonly APP_HOME="$DATA_HOME/tinyhttpie"
readonly VENV_HOME="$APP_HOME/.venv"
readonly BIN_HOME="$HOME/.local/bin"
readonly LAUNCHER="$BIN_HOME/tihttp"
install -d "$APP_HOME" "$BIN_HOME"
install -m 644 "$SOURCE_DIR/tihttp.py" "$APP_HOME/tihttp.py"
python3 -m venv "$VENV_HOME"
"$VENV_HOME/bin/python" -m pip install \
--requirement "$SOURCE_DIR/requirements.txt"
printf '#!/usr/bin/env sh\nexec "%s" "%s" "$@"\n' \
"$VENV_HOME/bin/python" \
"$APP_HOME/tihttp.py" \
> "$LAUNCHER"
chmod 755 "$LAUNCHER"
printf 'Installed tihttp at %s\n' "$LAUNCHER"
printf 'Ensure %s is on your PATH.\n' "$BIN_HOME"
This version avoids modifying .bashrc or .zshrc. Shell configuration belongs to the user, and editing it automatically introduces duplicate entries and shell-specific behavior. The script also quotes paths, uses an isolated environment, and stops on common command failures.
set -euo pipefail is useful defensive configuration, not a substitute for design. It cannot roll back a half-finished installation, validate every external command, or make a Unix filesystem layout work on Windows.
The launcher is an integration boundary
The file in ~/.local/bin is a small wrapper. It forwards every argument to the Python script while selecting the interpreter whose environment contains Requests:
#!/usr/bin/env sh
exec "/home/user/.local/share/tinyhttpie/.venv/bin/python" \
"/home/user/.local/share/tinyhttpie/tihttp.py" "$@"
That solves invocation on one machine, but the absolute paths reveal the remaining coupling. Moving the environment breaks the launcher. Updating the tool means defining replacement behavior. Uninstalling it means remembering every file the script created.
We have started building a package manager
Even this improved installer does not provide:
- standardized project metadata and version constraints;
- wheel installation or dependency resolution as a reusable protocol;
- conflict handling with an existing command of the same name;
- portable entry-point generation;
- reliable upgrade and uninstall operations;
- a common artifact that other tools can inspect.
Those are not tiny missing features. They are the responsibilities of packaging tools. The Bash experiment is valuable because it makes the abstraction boundary visible: installation is broader than copying a script and adding an alias.
In part 5, we will describe the project with standard Python metadata and let the packaging ecosystem generate the command.