Archive Old Logs
Problem statement
Pack every log file older than 3 days into one dated .tar.gz, check the archive, and delete only the files that are really inside it. This is what log rotation does behind the scenes, and doing it by hand teaches the safe order: archive, verify, then delete.
The script creates these logs and sets their ages:
logs/
logs/app-0926.log 5 days oldlogs/app-0927.log 4 days oldlogs/app-0930.log 1 day oldlogs/web access.log 6 days old (the name has a space)logs/current.log today- List the logs older than 3 days.
- Pack exactly those into
archive/logs-2026-10-01.tar.gz, safely, even with the space in a name. - Check the archive and list what is inside.
- Delete only the files listed in the archive, then list what is left.
Expected output:
== logs older than 3 days ==logs/app-0926.loglogs/app-0927.loglogs/web access.log== inside the archive (this also checks it is readable) ==logs/app-0926.loglogs/app-0927.loglogs/web access.log== logs left ==logs/app-0930.loglogs/current.logHints
find logs -name '*.log' -mtime +3 -print0 | tar --null -czf archive.tar.gz -T - tells tar to read the null-separated file list from the pipe.Approach
Optimal: find into tar, verify, then delete
Covers: find -mtime -print0, tar --null -T - (GNU), tar -tzf to verify, &&, while IFS= read -r, rm --, dated archive names.
The safe order is archive, verify, delete. If you delete first, or delete even when the archive failed, you lose logs. Each step only runs when the one before it worked:
Handing find's list to tar. tar can read the list of files to pack from a file with -T, and -T - means "read the list from the pipe". --null says the names are separated by null characters, which matches find -print0, so web access.log arrives as one name. These two flags are GNU tar, the tar on almost every Linux server.
Verifying the archive. tar -tzf archive.tar.gz reads the whole archive and lists it. If the file is cut short or not valid gzip, it fails with an error. Joining with && means the delete step only runs if that check succeeds.
Delete from the archive's list. Running a second find ... -delete sounds simpler, but between the two runs a file can turn 3 days old, and it would be deleted without being archived. Reading the names back from the archive guarantees you only delete what is safely packed:
| Part | Does |
|---|---|
tar -tzf "$archive" |
prints each path in the archive, one per line |
while IFS= read -r f |
reads each line whole, spaces and backslashes kept |
rm -- "$f" |
deletes it; -- protects names starting with - |
Dated names. The archive name holds the date, so each run makes a new file instead of overwriting yesterday's. In a real script you would write logs-$(date +%F).tar.gz; here the date is fixed so the output is the same every time.
Walking through the code. The # Setup: lines only create the logs and fake their ages with touch -d, so skip past them.
- The
findwith-mtime +3lists three files, including the one with a space. - The
find | tarpipeline creates the archive. tar -tzf ... | sortchecks it and shows the contents.- The
while readloop deletes those files, andfindshows the two recent logs left.
Edge cases. If nothing is old enough, tar creates an empty archive; add a check like [ -n "$(find logs -name '*.log' -mtime +3 -print -quit)" ] to skip the run. A log a program still writes to should not be deleted under it; real rotation renames it and tells the program to reopen its log, which is what logrotate does.
# Setup: create logs and set their ages with touch -d, in a fresh temporary folder
cd "$(mktemp -d)"
mkdir logs archive
touch -d '5 days ago' logs/app-0926.log
touch -d '4 days ago' logs/app-0927.log
touch -d '1 day ago' logs/app-0930.log
touch -d '6 days ago' "logs/web access.log"
touch logs/current.log
archive=archive/logs-2026-10-01.tar.gz # a real script would use $(date +%F)
echo "== logs older than 3 days =="
find logs -name '*.log' -mtime +3 | sort
find logs -name '*.log' -mtime +3 -print0 | tar --null -czf "$archive" -T -
echo "== inside the archive (this also checks it is readable) =="
tar -tzf "$archive" | sort
# Delete only what is really in the archive, and only if the archive reads cleanly
tar -tzf "$archive" > /dev/null &&
tar -tzf "$archive" | while IFS= read -r f; do rm -- "$f"; done
echo "== logs left =="
find logs -name '*.log' | sortInterview follow-ups
Send the archive to another server before deleting anything.
Add one more step between verify and delete: copy the archive, then check the copy. For example
rsync -a "$archive" backup:/srv/logs/ && ssh backup "tar -tzf /srv/logs/$(basename "$archive") > /dev/null". Only when both succeed, run the delete loop. With&&between every step, any failure stops the chain and the local logs stay where they are.
Frequently asked questions
On a real server, you should: logrotate is installed almost everywhere and runs daily from cron or a systemd timer. A short config file tells it which logs to rotate, how often, how many old copies to keep, and to compress them. It also handles the hard part, telling the app to reopen its log file, with postrotate or copytruncate. Writing it by hand, as here, is how you understand what logrotate does and how to debug it.
GNU tar can delete files right after adding them with --remove-files, which looks perfect. The risk is that it removes each file as it goes, so if tar fails part way, for example because the disk fills, some files are gone and the archive is broken. The archive, verify, delete order on this page never leaves you with neither. If you do use --remove-files, write the archive to a different disk from the logs.
Dated names sort in time order, so list them oldest first and drop the newest 7: ls -1 archive/logs-*.tar.gz | head -n -7 (GNU head). Check that list, then delete it with | xargs -r rm --. The names here are made by your own script and have no spaces, so plain ls and xargs are safe. For names you do not control, use find with -print0 instead.