Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

haul

I'm too lazy with managing self-downloading and self-installing tools. Maybe you're too! So I asked AI to make me one tool to manage them. I ended up with this. I'm too lazy to give it a name so I let AI suggest one, then I thought it's good enough.

A declarative, self-contained GitHub release binary installer and manager written in pure Bash.

Define the apps you want once in apps.conf. The script handles fetching, downloading, extracting, installing, updating, and removing them — with zero package manager dependency.

~/.config/haul/
├── apps.conf           declarative app registry
├── vars.conf           optional; user-defined @PLACEHOLDER@ values
└── state/              auto-created; one .state file per managed app
    ├── croc.state
    └── spf.state

Features

  • Config Generator — Use haul-add to automatically generate apps.conf entries for any GitHub repo
  • Inline editorhaul-add lets you edit the generated entry in $EDITOR before saving
  • XDG config — config, state, and vars live in ~/.config/haul/ by default
  • Migration prompt — if the XDG config dir is absent but legacy files exist in the current directory, both scripts offer to copy them over automatically
  • Declarative config — one INI section per app, no scripting required
  • Auto update — compares installed vs. latest GitHub release; skips if already current
  • Smart extraction — handles .tar.gz, .tar.xz, .tar.bz2, .zip, and raw binaries
  • Subdirectory-aware — locates the binary even when it's nested inside a subdirectory in the archive
  • Regex asset matching — uses full POSIX ERE (via jq/Oniguruma) to select the right release asset
  • Built-in placeholders@PLATFORM@ and @ARCH@ expand to the current OS and CPU arch
  • Custom placeholders — define your own @KEY@ tokens in vars.conf; no scripting required
  • Case-insensitive matching — opt-in via (?i) inline flag in any pattern
  • State tracking — records install path, version, repo, and timestamp in state/
  • List command — tabular view of all managed apps, their versions, and install paths
  • Uninstall command — removes the binary and cleans up state
  • No external state — no package database, no system-wide config, no root required to read state

Requirements

Tool Purpose
bash 4.2+ Associative arrays, declare -g
curl GitHub API requests
jq JSON parsing and regex asset selection
wget Asset download
tar / unzip Archive extraction
sudo Optional — only needed when installing to system paths like /usr/local/bin

Quick Start

# Clone or put haul somewhere on your PATH
git clone https://github.com/you/haul
cd haul

# Add your first app (generates the entry, opens editor if you choose)
haul-add schollz/croc

# Install everything in the config
haul install

# Install a specific app
haul install croc

# List what's managed
haul list

# Remove an app
haul uninstall croc

Config Location

haul reads its configuration from:

Priority Path
1 --config FILE (explicit override)
2 $XDG_CONFIG_HOME/haul/apps.conf
3 ~/.config/haul/apps.conf (default)

State and vars files live in the same directory as apps.conf (either the XDG default or the custom --config directory).

Migration prompt

If ~/.config/haul/ does not exist but the current working directory contains apps.conf, state/, or vars.conf (a legacy workspace layout), both haul and haul-add will detect this and ask:

haul: config directory not found: /home/you/.config/haul
      But the current directory looks like a haul workspace:
        apps.conf  found in /home/you/haul
        state/     found in /home/you/haul
        vars.conf  found in /home/you/haul

  Migrate these files to /home/you/.config/haul? [Y/n]

Answering Y (or Enter) copies all found files/directories to the XDG location. Answering n skips migration and continues.


Usage Reference

haul

haul [--config FILE] <command> [options] [section...]

Run haul or haul help for the full built-in man-page.

Command Description
haul install Install or update all apps in the config
haul install <section> [...] Install/update only the named section(s)
haul update Update all currently installed apps (skips uninstalled ones)
haul update <section> [...] Update specific app(s), only if already installed
haul list Print a table of all managed apps (version, path, repo)
haul uninstall <section> [...] Remove the binary and state for each named app
haul remove <section> [...] Alias for uninstall
haul help / haul --help Show the full help page
haul version / haul --version / haul -v Print the version and exit
haul --config FILE install Use a custom config file

list and uninstall work even if apps.conf has been deleted — they only read state/.

haul-add

haul-add automates the creation of apps.conf sections. It fetches the latest release from GitHub, detects your current OS/Architecture/libc, and generates a robust regex pattern using @PLATFORM@ and @ARCH@ placeholders.

haul-add [--help] [-o|--out FILE] [-n|--name NAME] <owner/repo>
Option Description
-o, --out FILE Write entry to FILE instead of ~/.config/haul/apps.conf. Creates the file and its parent dirs if they don't exist.
-n, --name NAME Use NAME as the section name and binary_name instead of deriving from the repo name.
--help, -h Show the built-in help page.
--version, -v Print the version and exit.

Examples:

# Add croc using auto-detection
haul-add schollz/croc

# Specify a custom binary name
haul-add -n spf yorukot/superfile

# Write to a project-local config
haul-add -o ./apps.conf schollz/croc

Interactive append flow

After the config block is generated and displayed, haul-add asks:

What would you like to do?
  [a] Append as-is to ~/.config/haul/apps.conf
  [e] Edit the block first, then append
  [n] Abort
Choice [a/e/n]:

The response is case-insensitive (E, edit, EDIT all trigger the editor).

  • a / Enter / anything else — appends the block as-is.
  • e — opens the block in $EDITOR (falls back to vi, then nano). After you save and exit, the edited block is printed and you get a final [Y/n] confirmation before anything is written.
  • n — aborts with no changes.

apps.conf Reference

The config is an INI-style file. Each [section] declares one application.

Keys

Key Required Description
repo GitHub repository in owner/name format
binary_name The binary filename to install and look up in PATH
asset_pattern_<arch> ✅* Regex to select the release asset for a given CPU architecture
asset_pattern ✅* Generic fallback pattern if no arch-specific key matches
version_flag CLI flag that prints the installed version (e.g. --version, -v)

* At least one asset_pattern key is required (either arch-specific or generic).

Supported architecture keys

Key suffix uname -m value Typical CPU
_x86_64 x86_64 AMD/Intel 64-bit
_aarch64 aarch64 ARM 64-bit (Apple Silicon, Raspberry Pi 4+)
_armv7l armv7l ARM 32-bit hard-float
_armv5tel armv5tel ARM 32-bit soft-float
_riscv64 riscv64 RISC-V 64-bit

arm64 (macOS naming) is automatically normalized to aarch64.

Asset pattern syntax

Patterns are POSIX ERE regexes matched against each release asset's filename. The underlying engine is Oniguruma (via jq), which is a superset of POSIX ERE.

Plain substrings

A literal string is a valid regex. No change required for simple cases:

asset_pattern_x86_64=linux-64bit

Anchors to prevent false matches

Use ^ to anchor to the start of the filename when a project ships multiple assets that share a common prefix (e.g. myapp and myapp-extra):

# matches tuios_0.7.0_Linux_x86_64.tar.gz
# does NOT match tuios-web_0.7.0_Linux_x86_64.tar.gz
asset_pattern_x86_64=^tuios_.*Linux_x86_64

Template placeholders

Placeholders are expanded by bash before the pattern is passed to jq. They use @KEY@ delimiters, which have no meaning in ERE and cannot interfere with regex syntax.

Built-in (always available):

Placeholder Expands to Example value
@PLATFORM@ uname -s output Linux, Darwin, FreeBSD
@ARCH@ uname -m output x86_64, aarch64
# Works on Linux (→ Linux_amd64) and macOS (→ Darwin_amd64) without config changes
asset_pattern_x86_64=(?i)@PLATFORM@_amd64

User-defined (via vars.conf):

See Custom Placeholders (vars.conf) below.

Case-insensitive matching

Prefix the pattern with the Oniguruma inline flag (?i) to match regardless of case:

asset_pattern_x86_64=(?i)linux-64bit   # matches Linux-64bit, linux-64bit, LINUX-64BIT

Flags can be combined: (?is) enables case-insensitive + single-line mode.

Alternation

# Accept either naming convention from the same release
asset_pattern_x86_64=(?i)(x86_64|amd64)-linux

Custom Placeholders (vars.conf)

Create a vars.conf file in ~/.config/haul/ to define your own @KEY@ placeholders. They expand in asset_pattern values in apps.conf, alongside the built-in ones.

vars.conf format (KEY=value, comments with #):

# vars.conf
CHANNEL=stable
VARIANT=musl
MY_ORG=mycompany

Use them in apps.conf:

[mytool]
repo=mycompany/mytool
binary_name=mytool
asset_pattern_x86_64=(?i)@MY_ORG@-@CHANNEL@-@PLATFORM@-x86_64-@VARIANT@
version_flag=--version

Key naming rules:

Rule Detail
Must start with [A-Z] Lowercase keys are rejected with a warning
Allowed characters A-Z, 0-9, _
Reserved names PLATFORM and ARCH cannot be used (built-in)

The file is optional — if vars.conf is absent, the script is silent and built-in placeholders still work normally.


Example apps.conf

[croc]
repo=schollz/croc
binary_name=croc
asset_pattern_x86_64=(?i)linux-64bit
asset_pattern_aarch64=(?i)linux-arm64
asset_pattern_armv7l=(?i)linux-arm
asset_pattern_armv5tel=(?i)linux-armv5
asset_pattern_riscv64=(?i)linux-riscv64
version_flag=--version

[superfile]
repo=yorukot/superfile
binary_name=spf
# Assets: spf_<version>_<linux|darwin>_<amd64|arm64>.tar.gz
asset_pattern_x86_64=(?i)@PLATFORM@_amd64
asset_pattern_aarch64=(?i)@PLATFORM@_arm64
version_flag=-v

Architecture & Workflow

Repository layout

~/.config/haul/
├── apps.conf               declarative app registry (INI format)
├── vars.conf               optional; user-defined @PLACEHOLDER@ values
└── state/                  created on first install
    └── <section>.state     one file per managed app (key=value)

Script internals

haul is structured in three layers:

┌──────────────────────────────────────────────────────┐
│  1. Bootstrap (top-level, runs at start)             │  resolve config path, detect
│                                                      │  SYS_OS / SYS_ARCH, early flags
├──────────────────────────────────────────────────────┤
│  2. Function library (definitions only)              │  cmd_help · offer_migration
│                                                      │  parse_conf · load_vars
│                                                      │  record_install · read_state
│                                                      │  cmd_list · cmd_uninstall
│                                                      │  install_app
├──────────────────────────────────────────────────────┤
│  3. Main (subcommand dispatch + loop)                │  routes to help / version /
│                                                      │  list / uninstall / install
└──────────────────────────────────────────────────────┘

Bootstrap

  • Resolves DEFAULT_CONFIG_DIR ($XDG_CONFIG_HOME/haul or ~/.config/haul)
  • Parses --config <path> before any subcommand detection; updates STATE_DIR and VARS_FILE relative to that path
  • Dispatches help/--help/no-args → cmd_help; version/--version/-v → version string
  • Detects SYS_OS (uname -s) and SYS_ARCH (uname -m) once, globally

parse_conf

Reads the INI config into the global associative array CONF keyed as "<section>.<key>" (e.g. CONF["croc.repo"]). Also builds SECTIONS — an ordered list of section names that drives the default install loop.

State management

Three functions form the state layer:

Function Role
record_install Atomically writes state/<section>.state via mktemp + mv
read_state Parses a state file into STATE_* global vars using declare -g
cmd_list Iterates state/*.state and prints a formatted table
cmd_uninstall Reads state, removes the binary (sudo if needed), deletes state file

State file format (state/croc.state):

binary_name=croc
install_path=/usr/local/bin/croc
version=10.0.3
repo=schollz/croc
installed_at=2026-06-30T07:35:00Z

install_app — the six-step pipeline

For each application the script runs:

Step 1 — Check existing install
         command -v <binary>  →  resolve current install path
         <binary> <version_flag>  →  parse first semver from output

Step 2 — Fetch latest GitHub release
         curl → GitHub API /repos/<owner>/<name>/releases/latest
         jq extracts tag_name

Step 3 — Compare versions
         Strip leading 'v' from tag; skip if current == latest

Step 4 — Download & extract
         jq selects asset URL:
           · asset_pattern regex (test()) matches filename
           · sidecar filter rejects .sha256 / .sig / .asc etc.
         wget downloads; dispatch on file extension:
           .tar.gz / .tgz  →  tar -xzf
           .tar.xz         →  tar -xJf
           .tar.bz2        →  tar -xjf
           .zip            →  unzip
           (anything else) →  raw binary

Step 5 — Install binary
         If already installed:  cp in-place (sudo if path not writable)
         Fresh install, priority order:
           1. /usr/local/bin      (writable?)
           2. /usr/local/bin      (sudo)
           3. ~/.local/bin        (fallback, with PATH warning)

Step 6 — Record state
         record_install writes state/<section>.state atomically

Subcommand dispatch

The main block handles early-exit commands before requiring apps.conf:

haul help / --help / (no args)  →  cmd_help   (no config needed)
haul version / --version / -v   →  print version (no config needed)
haul list                        →  cmd_list   (reads state/, no config needed)
haul uninstall / remove ...      →  cmd_uninstall (reads state/, no config needed)
haul install [sections...]       →  validate config, run install loop
haul update  [sections...]       →  install loop, skip uninstalled apps

Asset selection — jq filter detail

.assets[]
| select(.name | test($pat))                                          # user pattern
| select(.name | test("\\.(sha256|sha512|md5|sig|asc|pem|sbom)$") | not)  # strip sidecars
| .browser_download_url
  • $pat is passed as a --arg (data, not code) — no jq injection possible
  • Checksum and signature files are excluded automatically even if they match the pattern

Security notes

Concern Mitigation
Repo value injection Validated against ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ before use in URL
jq code injection via pattern Pattern is --arg (data), never interpolated into jq code
State file injection read_state uses an explicit key allow-list via case — only known keys are set
Temp file race mktemp + atomic mv for state writes
Arbitrary archive paths find with -name <binary_name> limits extraction search to the expected binary

Troubleshooting

No asset matching pattern found

The script lists all available asset names when a pattern fails to match:

Error: No asset matching pattern 'Linux_amd64' found for 'spf'.
Available assets:
  spf_0.2.0_linux_amd64.tar.gz
  spf_0.2.0_linux_arm64.tar.gz
  ...

Adjust your pattern to match the actual filename. Use (?i) if casing is inconsistent.

Could not parse installed version

The version_flag output doesn't contain a MAJOR.MINOR.PATCH semver. The script will still proceed with a fresh download on every run. Check the flag with:

<binary> <version_flag> 2>&1

Binary not in PATH after install

The script fell back to ~/.local/bin. Add it to your shell:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Pattern matching a sidecar file

If your pattern is too broad it might match a .sha256 or .sig file. The sidecar filter handles this automatically — those extensions are always excluded regardless of whether the base name matches.

No editor found when using haul-add -e

Set the EDITOR environment variable to your preferred editor:

export EDITOR=nvim   # or vim, nano, emacs, etc.

Config file not found

If ~/.config/haul/apps.conf does not exist and there are no legacy files in the current directory, create it manually:

mkdir -p ~/.config/haul
touch ~/.config/haul/apps.conf
haul-add schollz/croc    # add your first app

License

MIT

About

For lazy people!

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages