Production-honed command-line utilities from 15 years of pipeline work. One pip install for the one-off scripts everyone keeps rewriting: file sequences, batch operations with undo, folder watching, duplicate detection, and log tailing.
Every destructive command previews by default and requires --apply to touch
disk. Applied operations write a JSON undo manifest next to the result, so
tb bulk undo can reverse them. Two runtime dependencies: Typer and Rich.
pip install git+https://github.com/dilenshah23/toolbelt.git
Or from a clone:
git clone https://github.com/dilenshah23/toolbelt.git
cd toolbelt
pip install .
This installs two identical entry points: toolbelt and the short alias tb.
# find broken render sequences (missing frames) in a directory
tb seq check ./renders
# preview a regex rename, then actually do it
tb bulk rename ./photos --pattern "^IMG_" --replace "vacation_"
tb bulk rename ./photos --pattern "^IMG_" --replace "vacation_" --apply
# changed your mind? every applied operation leaves an undo manifest
tb bulk undo ./photos/toolbelt-undo-20260716-114821-460491.json --apply
# re-run tests whenever a source file changes
tb watch ./src --glob "*.py" --run "pytest -q"
# reclaim disk space
tb dedup ~/Downloads --delete
# follow a log, errors in red, only lines mentioning the request id
tb logs tail app.log --grep "req-42" --highlight "timeout"
Detects name.####.ext style sequences (padded or not) in a directory.
tb seq check ./renders # table: frame count, range, missing frames
tb seq check ./renders --json # machine-readable output
tb seq check ./renders --strict # exit 1 if any gaps/duplicates/mixed padding
seq check also flags mixed padding (shot.001.exr next to shot.1.exr)
and duplicate frame numbers caused by it.
tb seq rename ./renders --start 1001 # preview renumber
tb seq rename ./renders --start 1001 --apply # do it
tb seq rename ./renders --start 1 --padding 6 # change padding too
tb seq rename ./renders --start 1 --name "shotA." # pick one of several
Renumbering preserves gaps (it shifts, it does not compact) and uses a two-phase rename, so shifting a range onto currently-occupied frame numbers is safe. Applying writes an undo manifest into the directory.
tb bulk rename ./files --pattern "(\d{4})-(\d{2})-(\d{2})_report" --replace "report_\1\2\3"
tb bulk move ./incoming --dest ./archive --glob "*.log"
tb bulk undo ./archive/toolbelt-undo-<stamp>.json
All three preview by default; add --apply to execute. Renames and moves
abort before touching anything if two files would map to the same name or a
target already exists. undo refuses to run if the recorded state no longer
matches the disk (a file was deleted or its original name is occupied).
The undo manifest is plain JSON living next to the operation, so you can inspect it, keep it as an audit trail, or delete it when you no longer care.
tb watch ./src --glob "*.py" --run "pytest -q" --debounce 1.0
Poll-based (stdlib only), so it behaves the same on Windows, Linux, macOS,
and network shares where inotify-style APIs silently fail. Changes are
batched: the command fires once after the directory has been quiet for the
debounce period, not once per file. The command receives the changed paths
in the TOOLBELT_CHANGES environment variable (newline-separated) and the
count in TOOLBELT_CHANGE_COUNT.
tb dedup ./assets # table of duplicate groups
tb dedup ./assets --json # for scripting
tb dedup ./assets --delete # interactive confirm, keeps first of each group
tb dedup ./assets --delete --yes # non-interactive
Files are grouped by size first; only same-size candidates get a full
SHA-256 pass, so large trees scan quickly. Zero-byte files are ignored
unless you pass --include-empty.
tb logs tail app.log # last 10 lines, then follow
tb logs tail app.log -n 50 --no-follow # just the last 50 lines
tb logs tail app.log --grep "ERROR|WARN" # filter with a regex
tb logs tail app.log --highlight "user=\w+" # emphasize matches
Lines are colored by level (ERROR/CRITICAL red, WARNING yellow, INFO cyan, DEBUG dim). Truncation and copy-truncate rotation are handled: if the file shrinks, tailing restarts from the top of the new content.
0— success; nothing wrong found.1— problems found or the operation aborted safely:--strictfindings, name collisions, undo conflicts, declined confirmation.2— invalid invocation (bad flag, invalid regex, missing path).
# nightly render QC: fail the farm job if any sequence has holes
tb seq check /jobs/shot042/renders --strict || exit 1
# conform delivery: renumber plates to start at 1001 with 4-digit padding
tb seq rename ./plates --start 1001 --padding 4 --apply
# strip camera prefixes from a photo dump
tb bulk rename ./import --pattern "^(DSC|IMG|GOPR)_?" --replace "" --apply
# date-stamp scans into an archive, reversibly
tb bulk move ./scans --dest ./archive/2026-07 --glob "*.pdf" --apply
# undo the last operation in a directory (PowerShell)
tb bulk undo (ls ./archive/2026-07/toolbelt-undo-*.json | sort | select -Last 1) --apply
# auto-rebuild docs while editing
tb watch ./docs --glob "*.md" --run "mkdocs build"
# lint only the files that just changed (bash)
tb watch ./src --glob "*.py" --run 'echo "$TOOLBELT_CHANGES" | xargs ruff check'
# how much space are duplicate assets wasting? (JSON + jq)
tb dedup ./assets --json | jq "[.[].wasted_bytes] | add"
# watch a render log for the frames that actually failed
tb logs tail render.log --grep "frame \d+" --highlight "FAILED"
# find sequences with gaps across many shot folders (bash)
for d in /jobs/*/renders; do tb seq check "$d" --strict >/dev/null || echo "gaps in $d"; done
git clone https://github.com/dilenshah23/toolbelt.git
cd toolbelt
python -m venv .venv
.venv/Scripts/activate # Windows; use .venv/bin/activate elsewhere
pip install -e ".[dev]"
pytest
ruff check .
Pre-commit hooks (ruff, ruff-format, isort) are configured in
.pre-commit-config.yaml; enable them with pre-commit install.
MIT — see LICENSE.