Delete log files older than N days, dry run first (Bash)
Finds log files older than a number of days in a directory and lists them. Only when you add –delete does it remove them. Use it to tidy an application log folder that has no rotation.
Tested
The script
#!/usr/bin/env bash
# clean-old-logs.sh: find (and optionally delete) old log files in a directory.
#
# Usage: clean-old-logs.sh [-d DAYS] [-p PATTERN] [--delete] DIRECTORY
# -d DAYS files last modified more than DAYS days ago (default 30)
# -p PATTERN file name pattern, quoted (default '*.log')
# --delete actually delete. Without it, this is a dry run that only lists.
#
# Searches subdirectories too. Never follows symlinks.
set -euo pipefail
usage() { sed -n '4,7p' "$0" | sed 's/^# \{0,1\}//'; }
days=30
pattern='*.log'
delete=false
dir=''
need_value() { [[ $# -ge 2 && -n "$2" ]] || { echo "Error: $1 needs a value" >&2; exit 2; }; }
while [[ $# -gt 0 ]]; do
case "$1" in
-d) need_value "$@"; days="$2"; shift 2 ;;
-p) need_value "$@"; pattern="$2"; shift 2 ;;
--delete) delete=true; shift ;;
-h|--help) usage; exit 0 ;;
-*) echo "Error: unknown option $1" >&2; usage >&2; exit 2 ;;
*) [[ -z "$dir" ]] || { echo "Error: give only one directory" >&2; exit 2; }
dir="$1"; shift ;;
esac
done
[[ "$days" =~ ^[0-9]+$ ]] || { echo "Error: -d must be a whole number" >&2; exit 2; }
[[ -n "$dir" ]] || { usage >&2; exit 2; }
[[ -d "$dir" ]] || { echo "Error: not a directory: $dir" >&2; exit 1; }
mapfile -d '' files < <(find "$dir" -type f -name "$pattern" -mtime +"$days" -print0)
if [[ ${#files[@]} -eq 0 ]]; then
echo "No files matching '$pattern' older than $days days in $dir."
exit 0
fi
# Hand the list over NUL-separated so odd file names and long lists are safe
total=$(printf '%s\0' "${files[@]}" | du -ch --files0-from=- | tail -n 1 | cut -f1)
if [[ "$delete" == true ]]; then
printf '%s\0' "${files[@]}" | xargs -0 rm -f --
echo "Deleted ${#files[@]} file(s), $total freed."
else
printf '%s\n' "${files[@]}"
echo "Dry run: ${#files[@]} file(s), $total. Add --delete to remove them."
fi
A while loop with case reads the options, because getopts cannot handle a long option like --delete. -d sets the age in days, -p the file name pattern and the one plain argument is the directory. A small need_value function catches -d or -p given without a value. Quote the pattern, for example '*.log', or the shell expands it before the script sees it.
Checks
The age must be a whole number, exactly one directory must be given and it must exist. Mistakes exit with code 2 (bad usage) or 1 (missing directory).
Find the files
find -type f -name -mtime +N finds regular files matching the pattern. -type f means symlinks are skipped, so a link cannot trick it into deleting something elsewhere. find counts whole days and rounds down, so -d 30 matches files at least 31 days old. -print0 and mapfile -d '' store the names in an array safely, even with spaces in them.
Dry run or delete
The total size comes from du -ch, with tail and cut picking out the total. Without --delete the script lists the files and the total and stops. With --delete it passes the list to rm through xargs -0, which copes with long lists and odd file names.
Tip: always run it once without --delete and read the list.
Cookies
No adverts, and nothing that follows you to another site. What we set is what signing in
and the forum need. We count page views with Umami, which sets no cookie and identifies nobody. The one exception is Google reCAPTCHA, which checks our forms are being used by
a person, and that one is yours to decline — nothing is loaded from Google until you
say so.
No adverts, and nothing that follows you to another site. We count page views without a cookie and identify nobody. Google reCAPTCHA checks our forms, and that one is yours to decline.
Cookie Policy ·
Privacy Policy