Shell Scripting Packaging Distribution Intermediate May 2026

Shell Scripting Script Packaging & Distribution

Build professional CLI tools with argument parsing, help text, and version flags. Package scripts as self-contained installers, create Debian and RPM packages, and distribute via Git with proper versioning.

A script buried in someone's home directory and a properly packaged CLI tool are the same code with very different levels of trust, discoverability, and maintainability. This page covers the patterns that turn a working script into something you can confidently distribute to your team or install across a fleet.

BASH
#!/usr/bin/env bash
# backup.sh — Production backup tool with proper CLI

readonly VERSION="1.2.0"
readonly SCRIPT_NAME=$(basename "${0}")

usage() {
  cat << EOF
Usage: ${SCRIPT_NAME} [OPTIONS] <database>

Backup a MySQL database to a compressed archive.

Options:
  -h HOST       Database host (default: localhost)
  -p PORT       Database port (default: 3306)
  -u USER       Database user (default: root)
  -o DIR        Output directory (default: /backups)
  -c            Compress output with gzip
  -v            Verbose output
  -V            Print version and exit
  --help        Show this help

Examples:
  ${SCRIPT_NAME} myapp
  ${SCRIPT_NAME} -h prod-db-01 -u app_user -c myapp
  ${SCRIPT_NAME} -o /tmp/backups -v myapp
EOF
}

# Defaults
DB_HOST="localhost"
DB_PORT="3306"
DB_USER="root"
OUTPUT_DIR="/backups"
COMPRESS=false
VERBOSE=false

# Parse options
while getopts ":h:p:u:o:cvV" opt; do
  case "${opt}" in
    h) DB_HOST="${OPTARG}"  ;;
    p) DB_PORT="${OPTARG}"  ;;
    u) DB_USER="${OPTARG}"  ;;
    o) OUTPUT_DIR="${OPTARG}" ;;
    c) COMPRESS=true   ;;
    v) VERBOSE=true    ;;
    V) echo "${SCRIPT_NAME} v${VERSION}"; exit 0 ;;
    :) echo "Error: -${OPTARG} requires an argument" >&2; usage; exit 1 ;;
    ?) echo "Error: unknown option -${OPTARG}" >&2; usage; exit 1 ;;
  esac
done

shift $(( OPTIND - 1 ))   # remove parsed options, leave positional args

# Validate positional argument
[[ $# -eq 0 ]] && { echo "Error: database name required" >&2; usage; exit 1; }
DATABASE="${1}"

VERBOSE && echo "Backing up ${DATABASE} from ${DB_HOST}:${DB_PORT}"
BASH
# getopts only handles single-char options
# For --long-options, parse manually

ENV="production"
DRY_RUN=false
FORCE=false

while [[ $# -gt 0 ]]; do
  case "${1}" in
    --env=*)
      ENV="${1#--env=}"; shift ;;
    --env)
      ENV="${2:?--env requires a value}"; shift 2 ;;
    --dry-run)
      DRY_RUN=true; shift ;;
    --force)
      FORCE=true; shift ;;
    --help|-h)
      usage; exit 0 ;;
    --version|-V)
      echo "v${VERSION}"; exit 0 ;;
    --)
      shift; break ;;          # end of options
    -*)
      echo "Unknown option: ${1}" >&2; exit 1 ;;
    *)
      break ;;                  # first non-option arg
  esac
done

echo "ENV=${ENV} DRY_RUN=${DRY_RUN} FORCE=${FORCE}"
echo "Remaining args: $*"
BASH
#!/usr/bin/env bash
# install.sh — Self-contained installer for myapp-scripts

set -euo pipefail

readonly VERSION="2.1.0"
readonly INSTALL_DIR="/usr/local/lib/myapp"
readonly BIN_DIR="/usr/local/bin"

echo "Installing myapp-scripts v${VERSION}"

# Check root
[[ $(id -u) -eq 0 ]] || { echo "Run as root" >&2; exit 1; }

# Create directories
mkdir -p "${INSTALL_DIR}/lib" "${INSTALL_DIR}/config"

# Install library files
for lib in lib/logging.sh lib/db.sh lib/network.sh; do
  install -m 644 "${lib}" "${INSTALL_DIR}/lib/"
  echo "  ✔ ${lib}"
done

# Install executables
for bin in bin/deploy bin/backup bin/health-check; do
  install -m 755 "${bin}" "${BIN_DIR}/"
  echo "  ✔ ${bin} → ${BIN_DIR}/"
done

# Install default config (don't overwrite existing)
[[ -f /etc/myapp/config.env ]] || {
  install -Dm 640 config/defaults.env /etc/myapp/config.env
  echo "  ✔ Default config installed"
}

echo "Installation complete — run 'deploy --help' to get started"
BASH
#!/usr/bin/env bash
# release.sh — Tag and package a new release

set -euo pipefail

VERSION="${1:?Usage: release.sh VERSION}"
PKG_NAME="myapp-scripts-${VERSION}"

# Validate semver format
[[ "${VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \
  || { echo "ERROR: Version must be X.Y.Z" >&2; exit 1; }

# Update version in scripts
sed -i "s/^readonly VERSION=.*/readonly VERSION=\"${VERSION}\"/" bin/*

# Commit version bump
git add bin/ lib/
git commit -m "Release v${VERSION}"
git tag -a "v${VERSION}" -m "Release v${VERSION}"

# Create release tarball
git archive --prefix="${PKG_NAME}/" \
             -o "/tmp/${PKG_NAME}.tar.gz" \
             "v${VERSION}"

echo "  ✔ Created /tmp/${PKG_NAME}.tar.gz"
echo "  Size: $(du -sh /tmp/${PKG_NAME}.tar.gz | cut -f1)"

# Push tag
git push origin main "v${VERSION}"
echo "  ✔ Pushed v${VERSION} to origin"
bash — CLI tool in action
vriddh@prod-01:~$backup --help
Usage: backup [OPTIONS] <database>
Backup a MySQL database to a compressed archive.
Options:
-h HOST Database host (default: localhost)
-c Compress output with gzip
vriddh@prod-01:~$backup -h prod-db-01 -u app -c myapp
Backing up myapp from prod-db-01:3306
✔ Saved: /backups/myapp_20260501_021402.sql.gz (87MB)
vriddh@prod-01:~$./release.sh 2.2.0
✔ Created /tmp/myapp-scripts-2.2.0.tar.gz
Size: 42K
✔ Pushed v2.2.0 to origin
✔ Packaging rules — Always implement --help and --version. Use getopts for single-char options; manual parsing for --long-options. Always validate positional arguments. Use install -m MODE instead of cp + chmod for proper permissions. Version every release with semver tags and git archive for clean tarballs without development files.