Skip to content

Latest commit

Β 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ—„οΈ shelf

Put a folder on a shelf instead of deleting it.

shelf compresses a directory in place, verifies the archive contains every file, then moves the original to the Trash. One command puts it back.

shelf listing six shelved repositories, followed by its help output


The commands this replaces

You already do this. Everyone does:

cp config.yml{,.bak}      # keep a copy "just in case"
mv old-project archived/  # get it out of the way
mv plugin.so{,.disabled}  # turn it off without losing it

Each of them rots in its own way.

.bak files carry no date and no provenance β€” six months on you cannot tell which is current, whether the original still exists, or if it was ever safe to delete. They also stay uncompressed, so a "backup" of a big tree costs you its full size forever.

archived/ becomes a junk drawer. It grows without bound, nothing in it is ever looked at again, and it silently doubles the size of every backup you take.

.disabled prefixes clutter the listing they were supposed to clean up, and they only work for things whose name nobody depends on.

All three share the same real problem: nothing verified that the copy was good, so nothing is safe to remove. So you never remove any of it.

shelf closes that loop:

shelf old-project      # compress β†’ verify β†’ original to Trash
shelf                  # see what's shelved here
shelf old-project      # put it back, exactly as it was
shelf trash old-project  # once you're sure

The archive lands beside where the folder lived, named .shelved-2026-08-14-old-project.aar β€” dot-prefixed, so ls shows only live work while shelf shows what you put away.


Install

brew install moonexpr/tap/shelf

Or from source β€” requires macOS and the Command Line Tools (xcode-select --install, no full Xcode needed):

git clone https://github.com/moonexpr/shelf.git
cd shelf
make install          # swift build -c release, then into ~/.local/bin

No third-party dependencies. One binary, ~8 ms startup. Verify an install with shelf --selftest.


Usage

shelf                    list what's shelved here, then show this help
shelf <dir|file>         shelve it: compress, verify, trash the original
shelf <archive>          restore it from the shelf
shelf list [-r]          list shelved items (-r walks subdirectories)
shelf add <target> ...   shelve explicitly
shelf restore <name> ... restore explicitly
shelf trash <name> ...   send the archive itself to the Trash

  --format <name>        archive format for this run
  --force                shelve even if verification reports differences
  --selftest             run the built-in checks
  -h, --help             this help

Bare shelf <thing> reads what the argument is: an archive restores, anything else gets shelved. There is deliberately no remove β€” the ambiguity between "take it off the shelf" and "throw it away" is exactly the confusion that loses data, so the verbs are restore and trash.

It also restores tarballs you made yourself β€” .tar, .tar.gz, .tar.xz, .tgz, .tar.bz2:

shelf old-backup.tar.gz

Those are extracted but left in place. shelf didn't create them, so disposing of them isn't its call.


What makes it safe to actually delete things

It verifies before it trashes. After compressing, every path in the source tree is checked against the finished archive's own listing. If anything is missing, the original is left untouched and the archive is kept for inspection. The one unrecoverable outcome β€” a truncated archive whose source got trashed anyway β€” is the thing this tool is built to prevent.

Nothing is ever rm'd. Originals and archives both go to the macOS Trash, with Finder put-back intact. The only files removed outright are half-written archives shelf itself just created.

Ctrl-C is safe. Interrupt a shelve and the partial archive is removed; the original is untouched. Interrupt a restore and the partial extraction is removed; the archive is untouched. A failed extraction cleans up after itself too β€” a damaged tarball won't leave half a tree next to it.

Your archives don't depend on this tool. /usr/bin/aa ships with macOS and reads them directly:

aa list -i .shelved-2026-08-14-old-project.aar
aa extract -i .shelved-2026-08-14-old-project.aar -d .

Formats

Six, all reading and writing symlinks, permissions, ownership and timestamps:

name notes
aar (default) Apple Archive + LZMA. Fastest. macOS only
tar.xz best ratio of the portable formats
tar.gz most universal
tar.bz2
tar.zst fast, needs a recent tar to read elsewhere
tar no compression

Pick one for a single run, for your shell, or once and for all:

shelf add old-project --format tar.xz
SHELF_FORMAT=tar.xz shelf old-project
defaults write com.moonexpr.shelf format tar.xz

In that order of precedence. Restoring ignores the setting entirely β€” every archive is read by whatever engine its suffix names β€” so changing the format never strands what you have already shelved, and a shelf can hold a mix.

Anything unrecognised is an error listing the valid names, rather than a silent fall back to a default you didn't ask for.

Measured against tar -cJf on this machine:

corpus .aar .tar.xz ratio time
179 MB repo (git objects, binaries) 180.3 MB 178.5 MB 1.01Γ— 4.9 s vs 67 s
2.1 MB of pure source text 575 kB 557 kB 1.03Γ— comparable

Essentially identical size, and roughly 14Γ— faster on large trees because Apple Archive threads and xz does not.

The trade-off: .aar is macOS-only. A .tar.xz opens anywhere for the next thirty years; an Apple Archive needs a Mac. If cross-platform archives matter more to you than speed, this is the wrong tool β€” but note that reading them never requires shelf itself, only macOS.


How it's built

Two abstractions, on two axes β€” and the file layout is those two axes:

Sources/ShelfKit/
  ArchiveEngine.swift  AppleArchiveEngine.swift  TarEngine.swift   ← axis 1: format
  EntryVisitor.swift   FileTree.swift                              ← axis 2: traversal
  CmdLine.swift  FileSystem.swift  Archive.swift  SelfTest.swift
  • ArchiveEngine β€” a facade over the archive format. Application logic never names a format or a suffix; adding one means writing one engine and one registry row.
  • EntryVisitor β€” a visitor over entries. Two different things are traversable, an archive and a directory on disk, and both accept the same visitors. Verification is therefore literally one PathCollector run over each and the sets subtracted, rather than bespoke string comparison.

ShelfKit exposes exactly one public symbol, ShelfCLI.run(); a three-line executable calls it.

Tests live in the binary (shelf --selftest) rather than in a test target, because XCTest and swift-testing both need frameworks that ship with Xcode β€” swift test cannot run on a Command Line Tools install at all. Building the checks in keeps them runnable everywhere and makes them a genuine post-install verification.


License

MIT β€” see LICENSE.

About

πŸ—„οΈ Put a folder on a shelf instead of deleting it β€” compress in place, verify every file, then Trash the original. One command puts it back.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages