Sample output
sending incremental file list
*deleting assets/hero-old.png
*deleting posts/draft.html
>f.s....... assets/app.css
>f+++++++++ assets/theme.js
>f..t...... posts/022.html
>f+++++++++ posts/023.html
sent 248 bytes received 69 bytes 634.00 bytes/sec
total size is 182 speedup is 0.57 (DRY RUN)
Read the codes column by column. The first character is the update type (> means data is being sent, * means the rest of the line is a message), the second is the file type (f for file, d for directory), and the nine after that are attributes: c checksum, s size, t modification time, p permissions, o owner, g group, then access time, ACLs, and extended attributes. A dot means unchanged and a row of + means the file does not exist on the destination yet. So assets/app.css is a genuine content edit, posts/022.html has identical bytes and a newer timestamp from the rebuild, and two files are about to be deleted. Everything not listed, including index.html, matches already and will not be touched.
When you would reach for it
You are about to mirror a build directory onto a server, a backup disk, or a staging copy, and the command includes --delete. The destination holds something you cannot recreate by running the build again. Any sync where the destination has its own history deserves this step: rsync will happily mirror a source that is emptier than you think, and the dry run is the only place that mistake is reversible. It is also the fastest way to answer “did that deploy actually change anything,” because a clean sync prints nothing at all.
Gotchas
- The trailing slash on the source is the whole ballgame.
rsync -ain --delete site/ backup/mirrors the contents ofsiteintobackup. Remove one character andrsync -ain --delete site backup/copies the directory itself, producingbackup/site/index.html, then deletes everything already inbackupbecause none of it matches. The dry run catches this instantly: if the output lines start withsite/, the slash is missing. --deletemeasures against whatever the source actually contains right now. Point it at a path that is empty, mistyped into existence, or a mount that failed to come up, and the mirror is faithfully applied: every file on the destination is queued for removal. Run it against an empty directory in dry-run mode once and watch rsync list your entire destination under*deleting. That output is the argument for-n.- Comparison defaults to size and modification time, not content. A file edited in place to the same length with its timestamp restored looks untouched and gets skipped. Add
-cto compare checksums instead, and the itemize code changes from>f.s.......to>fcs........ It reads every byte on both sides, so reserve it for the case where you actively distrust the timestamps. - Excluded files are protected on the destination, not deleted from it.
--exclude 'assets/app.css'means rsync stops considering that path entirely, so the stale copy inbackup/survives the--delete. If you want exclusions to be removed from the destination too, that is--delete-excluded, and it is worth previewing separately because the two flags read as though they should mean the same thing. - macOS 15 replaced the bundled rsync with openrsync, and macOS 14 and earlier shipped rsync 2.6.9 from 2006. Neither is the 3.x you are probably testing against, and flag coverage differs, so check
rsync --versionandrsync --help | grep itemizebefore trusting a script across machines.brew install rsyncputs a current build in your path.
Variants
$ rsync -avni --delete site/ deploy@web01:/var/www/site/
The same preview against a remote host. rsync tunnels over SSH by default, so a working ssh deploy@web01 is the only prerequisite
$ rsync -ain --delete site/ backup/ | grep -c '^\*deleting'
Count what the mirror would remove before you commit to it. A number larger than you expected means stop and read the full list
$ rsync -ai --delete --exclude '.git' site/ backup/
The real run, once the preview matched your intent. Dropping the -n is the only change, and this one deletes on the destination for real