Skip to content

Repository files navigation

🌲 spruce

Spruce up your files. Scan, sort, dedupe, and bulk-rename — from the command line.

  ███████╗██████╗ ██████╗ ██╗   ██╗ ██████╗███████╗
  ██╔════╝██╔══██╗██╔══██╗██║   ██║██╔════╝██╔════╝
  ███████╗██████╔╝██████╔╝██║   ██║██║     █████╗
  ╚════██║██╔═══╝ ██╔══██╗██║   ██║██║     ██╔══╝
  ███████║██║     ██║  ██║╚██████╔╝╚██████╗███████╗
  ╚══════╝╚═╝     ╚═╝  ╚═╝ ╚═════╝  ╚═════╝╚══════╝

spruce is a single, zero-dependency command-line toolkit that brings order to messy directories. It scans, sorts, dedupes, and renames files — safely.

spruce

license python
deps CI
tests coverage

github.com/sunji-droid/Spruce


What it does

Command What it does
spruce scan [PATH…] Analyze directories: totals, sizes by category, largest files, duplicate hints. --json for machines.
spruce tidy [PATH…] Sort files into folders — by category, extension, or date (2024/03/15).
spruce dedupe [PATH…] Find byte-identical files (SHA-256) and trash/delete all but one copy.
spruce rename [PATH…] Bulk-rename through a composable transform pipeline.
spruce config Inspect or initialize the JSON config file.
spruce completions Generate bash/zsh/fish tab-completion scripts.
spruce logo Print the logo. Obviously.

Why you'll like it

  • Zero dependencies — pure Python standard library. Works on Python 3.9+, Linux, macOS, and Windows. pip install . and go.
  • Safe by default — every mutating command supports --dry-run, asks before touching anything, never overwrites a file (conflicts become name_1.ext), and sends duplicates to a local .spruce-trash/ instead of straight to the void.
  • Never guesses — non-interactive without --yes? It refuses rather than silently destroying anything.
  • Junk-aware.git, node_modules, caches, hidden files and other noise are skipped automatically; --all overrides, --exclude extends.
  • Composablespruce scan --json | jq '.categories' works out of the box.
  • Deterministic — same input, same output, always.

Install

git clone https://github.com/sunji-droid/Spruce.git
cd Spruce
pip install .
spruce --version

Or run straight from the checkout without installing:

PYTHONPATH=src python -m spruce --help

A real session

Run bash examples/demo.sh to watch this yourself (on a throwaway temp folder — safe anywhere). Here's the actual output, with paths trimmed:

$ spruce scan ~/demo

Scan ~/demo
12 files · 136.8 KB

By category
Category   Files     Size
---------  -----  -------
code           2     20 B
data           1      7 B
documents      5     70 B
images         2  58.6 KB
other          1      5 B
videos         1  78.1 KB

Largest files (top 10)
Path                                        Size
---------------------------------------  -------
~/demo/downloads/clip.mp4               78.1 KB
~/demo/photo_1.jpg                      29.3 KB
~/demo/photo_1_copy.jpg                 29.3 KB
~/demo/documents/report.txt                16 B
~/demo/backup/scan_001.pdf                 14 B
~/demo/downloads/scan_001.pdf              14 B
~/demo/notes.txt                           14 B
~/demo/hello world.txt                     12 B
~/demo/script copy.py                      10 B
~/demo/script.py                           10 B

Duplicate hints (same size)
3 groups · 7 files · ~29.3 KB reclaimable — run `spruce dedupe` to confirm by content
$ spruce dedupe ~/demo --dry-run

Dedupe

[1] scan_001.pdf — 14 B each
  keep   ~/demo/backup/scan_001.pdf (newest)
  remove ~/demo/downloads/scan_001.pdf -> ~/demo/.spruce-trash/2026-08-06_193912/scan_001.pdf

[2] photo_1.jpg — 29.3 KB each
  keep   ~/demo/photo_1_copy.jpg (newest)
  remove ~/demo/photo_1.jpg -> ~/demo/.spruce-trash/2026-08-06_193912/photo_1.jpg

[3] script copy.py — 10 B each
  keep   ~/demo/script copy.py (newest)
  remove ~/demo/script.py -> ~/demo/.spruce-trash/2026-08-06_193912/script.py

3 duplicates would be removed · 29.3 KB freed (dry run — nothing changed)
$ spruce tidy ~/demo --strategy category --yes

Tidy ~/demo — category layout
code/  script copy.py
data/  data.json
documents/  scan_001.pdf, hello world.txt, notes.txt
images/  photo_1_copy.jpg
other/  junk.tmp
videos/  clip.mp4
8 files moved
$ spruce rename ~/demo --recursive --lower --spaces-to-underscores --yes

Rename
  script copy.py → script_copy.py
  hello world.txt → hello_world.txt
2 files renamed
$ spruce scan ~/demo --json | jq '.total_files, .categories | keys'
9
["code", "data", "documents", "images", "other", "videos"]

(Output trimmed to fit; bash examples/demo.sh shows every section, including a JSON dump and both dry-run + real passes.)

Command reference

spruce scan [PATH…]

Analyze without changing anything.

spruce scan ~/Downloads
spruce scan . --json | jq '.largest[0]'
spruce scan ~/Pictures ~/Documents --top 5
  • --json — emit pure JSON on stdout (human output suppressed), so it pipes cleanly into jq and friends.
  • --top N — how many largest files to list (default 10).
  • --all — include hidden files. --exclude GLOB — skip extra globs.

spruce tidy [PATH…]

Sort files into bucket folders. Subdirectories are flattened into the buckets; files already in place are left alone.

spruce tidy ~/Downloads --strategy category          # images/, documents/, …
spruce tidy ~/Desktop  --strategy date --dry-run     # 2024/03/…, preview first
spruce tidy ~/Downloads ~/Desktop --strategy ext --target ~/Sorted
  • -s, --strategy {category,ext,date} — bucket layout (default: category, overridable in config). Date buckets are YYYY, YYYY/MM, or YYYY/MM/DD via --date-level {day,month,year}.
  • --target DIR — consolidate everything under DIR instead of each input dir. --copy — copy instead of move.
  • --dry-run, -y/--yes, --all, --exclude.

spruce dedupe [PATH…]

Find byte-identical files by content hash (only same-sized files are hashed, so it's fast on unique folders) and remove all but one copy.

spruce dedupe ~/Pictures --dry-run
spruce dedupe ~/Pictures --keep largest --yes       # keep the biggest copy
spruce dedupe ~/Downloads --delete --yes            # skip the trash
  • --keep {newest,oldest,first,largest,smallest} — which copy survives (default: newest; ties break by path, deterministically).
  • Duplicates move to <root>/.spruce-trash/<timestamp>/ by default. --delete removes them permanently instead; --trash-dir DIR trashes them into a custom location.

spruce rename [PATH…]

Bulk-rename via a fixed, documented transform pipeline: strip → case → separator → prefix → numbering → date prefix → suffix.

spruce rename *.jpg --lower --spaces-to-underscores
spruce rename ~/Downloads --recursive --strip 'IMG_\d{8}_'
spruce rename ~/Photos --date-prefix --numbered --dry-run
spruce rename ~/Desktop --prefix 'backup-' --suffix '.old'
  • --lower | --upper | --title — case transforms on the stem.
  • --spaces-to-underscores | --spaces-to-dashes — separator cleanup.
  • --strip REGEX — remove regex matches.
  • --prefix TEXT, --suffix TEXT — wrap the name.
  • --date-prefix — prefix with YYYY-MM-DD (from file mtime).
  • --numbered [START] — prefix with 001_, 002_, …; --pad N for width.
  • -r, --recursive — include files in subdirectories.

spruce config

spruce config path        # ~/.config/spruce/config.json
spruce config show        # effective config (defaults + user file)
spruce config init        # write a config file with defaults

Config is JSON, deep-merged over built-ins. You can override the category mapping, default strategies, keep rules, and exclusion globs:

{
  "tidy": { "strategy": "date", "date_level": "month" },
  "dedupe": { "keep": "largest" },
  "rename": { "pad": 4 },
  "exclude": { "dirs": ["secrets"], "files": ["*.tmp"] },
  "categories": { "my-special-type": ["zzz"] }
}

Set $SPRUCE_CONFIG or pass --config PATH to point at a custom file.

.spruceignore — gitignore-style rules

Drop a .spruceignore file in any directory to tell spruce what to skip (works with scan, tidy, dedupe, and rename; disable with --no-ignore):

# junk
*.log
build/
cache/
!important.log

Rules apply to that directory and everything below it: basename patterns match at any depth, dir/ matches directories only, /anchored and a/b are relative to the ignore file, ! re-includes (last match wins), and ** spans any depth. Nested .spruceignore files stack on top of their parents.

Shell completions

spruce completions bash   # → source this / put in /etc/bash_completion.d/
spruce completions zsh    # → ~/.zshrc or a compdef-enabled location
spruce completions fish   # → ~/.config/fish/completions/spruce.fish

Scripts are generated by introspecting the argument parser, so they always match the installed CLI. (On macOS zsh, run compinit once for compdef.)

Design notes

  • Safety is structural. Mutating commands print a preview, then require a confirmation prompt — or --yes if non-interactive. --dry-run always available. Files are never overwritten; name collisions get _1, _2
  • Efficiency matters. Dedupe only hashes files that already share a size. Walking prunes excluded and hidden directories before descending.
  • Honest output. Scan's duplicate hints are labeled as size-only hints; only dedupe confirms by content. Exit codes: 0 success (incl. dry runs), 1 spruce error, 2 usage error.
  • Colors follow the terminal (disabled when piped or when NO_COLOR is set); --no-color forces it off.

Project layout

spruce/
├── .github/workflows/ci.yml   # lint + test matrix (3.9–3.13, 3 OSes)
├── examples/demo.sh           # self-demo: messy folder → spotless
├── src/spruce/
│   ├── cli.py                 # argument parsing, dispatch, presentation
│   ├── tidy.py                # sort-into-buckets engine
│   ├── dedupe.py              # content-hash duplicate removal
│   ├── rename.py              # transform pipeline
│   ├── scanner.py             # file walking + hashing
│   ├── categories.py          # extension → category mapping
│   ├── config.py              # defaults + JSON deep-merge
│   ├── paths.py               # junk-aware walking + conflict claims
│   └── ui.py                  # colors, tables, human sizes
└── tests/                     # 76 tests, 94% coverage

Development

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"

make test      # pytest with coverage
make lint      # ruff check
make format    # ruff format
make demo      # watch it work

See CONTRIBUTING.md for how to add commands and keep the bar high.

Companion web app

Want to preview renames in your browser before running them? Try Spruce Lab — a zero-dependency, offline web tool that mirrors the exact same transform pipeline and collision rules, with live before/after diffs. Live at sunji-droid.github.io/spruce-lab.

Author

Kabo Merapelo Onamile — built with ☕ and an unreasonable love of tidy folders. You can reach them at mkonamile@gmail.com.

License

MIT

About

Zero-dependency CLI to scan, sort, dedupe, and bulk-rename your files. .spruceignore, shell completions, and more.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages