Tested script

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

Run it

./clean-old-logs.sh -d 30 /var/log/myapp
./clean-old-logs.sh -d 30 -p '*.log.gz' --delete /var/log/myapp

How it works

Arguments

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.