Bash: Find the largest files/directories (top N) safely
bashThis script safely searches your filesystem to identify the largest files and directories consuming disk space. It provides human-readable output showing the top N results, with built-in safety features like system path exclusions, depth limits, and low-priority execution to prevent system impact during scans.
bashdisk-managementstoragesystem-administrationhomelabfilesystemcleanup-prep
Script Code
Bashbash
#!/usr/bin/env bash
# =============================================================================
# Script Name: find-largest-files.sh
# Description: Find the largest files and directories in a filesystem safely with size limits and throttling.
# Author: ScriptedOps (scriptedops.com)
# Version: 1.0
# Requires: bash 4.0+, du, find, sort
# Tested on: Ubuntu 22.04, Debian 12
#
# Usage:
# ./find-largest-files.sh [--dry-run] [--log-file /path/to/log] [--top-n 10] [--search-path /path] [--type files|dirs|both]
#
# Notes:
# - Uses safe traversal with depth limits and excludes system paths by default
# - Handles permission errors gracefully without stopping execution
# - Provides human-readable output with size formatting
# =============================================================================
set -euo pipefail
# =============================================================================
# CONFIGURATION BLOCK
# =============================================================================
# Default configuration - modify these as needed
DEFAULT_TOP_N=10
DEFAULT_SEARCH_PATH="/"
DEFAULT_TYPE="both" # files, dirs, both
DEFAULT_LOG_FILE="/tmp/find-largest-files.log"
DEFAULT_MAX_DEPTH=20 # Prevent infinite loops in complex mount structures
DEFAULT_EXCLUDE_PATHS="/proc /sys /dev /run /tmp /var/lib/docker /snap" # System paths to skip
# Performance and safety limits
MAX_RESULTS_INTERNAL=1000 # Internal limit to prevent memory issues
NICE_LEVEL=19 # Run with lowest priority to avoid system impact
# =============================================================================
# FUNCTIONS
# =============================================================================
# Logging function
log_message() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE"
}
# Display usage information
show_usage() {
cat << 'EOF'
Usage: find-largest-files.sh [OPTIONS]
OPTIONS:
--dry-run Show what would be done without executing
--top-n N Number of results to return (default: 10, max: 100)
--search-path PATH Directory to search (default: /)
--type TYPE What to find: files, dirs, both (default: both)
--log-file FILE Log file location (default: /tmp/find-largest-files.log)
--max-depth N Maximum directory depth (default: 20)
--help Show this help message
EXAMPLES:
# Find top 10 largest files and directories from root (dry-run)
./find-largest-files.sh --dry-run
# Find top 20 largest files in /home
./find-largest-files.sh --top-n 20 --search-path /home --type files
# Find largest directories in current directory
./find-largest-files.sh --search-path . --type dirs --top-n 5
EOF
}
# Check if required commands exist
check_dependencies() {
local missing_deps=()
for cmd in du find sort awk; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing_deps+=("$cmd")
fi
done
if [[ ${#missing_deps[@]} -gt 0 ]]; then
log_message "ERROR" "Missing required dependencies: ${missing_deps[*]}"
exit 2
fi
log_message "INFO" "Dependency check passed"
}
# Format bytes to human readable format
format_size() {
local bytes=$1
local units=("B" "K" "M" "G" "T" "P")
local unit_index=0
local size=$bytes
while (( size >= 1024 && unit_index < 5 )); do
size=$((size / 1024))
((unit_index++))
done
printf "%6d%s" "$size" "${units[$unit_index]}"
}
# Build exclude arguments for find command
build_exclude_args() {
local exclude_args=""
local IFS=' '
for path in $DEFAULT_EXCLUDE_PATHS; do
if [[ -d "$path" ]]; then
exclude_args="$exclude_args -path $path -prune -o"
fi
done
echo "$exclude_args"
}
# Find largest files
find_largest_files() {
local search_path="$1"
local max_depth="$2"
local exclude_args
exclude_args=$(build_exclude_args)
log_message "INFO" "Searching for largest files in: $search_path"
# Use find with du to get file sizes, exclude system paths
eval "nice -n $NICE_LEVEL find \"$search_path\" $exclude_args -maxdepth $max_depth -type f -exec du -b {} + 2>/dev/null" | \
sort -rn | \
head -n "$MAX_RESULTS_INTERNAL"
}
# Find largest directories
find_largest_dirs() {
local search_path="$1"
local max_depth="$2"
local exclude_args
exclude_args=$(build_exclude_args)
log_message "INFO" "Searching for largest directories in: $search_path"
# Use du to calculate directory sizes, exclude system paths
{
for exclude_path in $DEFAULT_EXCLUDE_PATHS; do
if [[ -d "$exclude_path" && "$search_path" == "/" ]]; then
exclude_args="$exclude_args --exclude=$exclude_path"
fi
done
nice -n $NICE_LEVEL du -b --max-depth="$max_depth" $exclude_args "$search_path" 2>/dev/null
} | \
sort -rn | \
head -n "$MAX_RESULTS_INTERNAL"
}
# Format and display results
format_results() {
local type="$1"
local top_n="$2"
log_message "INFO" "Formatting top $top_n $type results"
echo
echo "Top $top_n Largest $type:"
echo "========================================"
printf "%-10s %-60s\n" "Size" "Path"
echo "----------------------------------------"
local count=0
while IFS=$'\t' read -r size path && [[ $count -lt $top_n ]]; do
if [[ -n "$size" && -n "$path" ]]; then
formatted_size=$(format_size "$size")
printf "%-10s %-60s\n" "$formatted_size" "$path"
((count++))
fi
done
}
# Main execution function
main() {
local dry_run=false
local top_n="$DEFAULT_TOP_N"
local search_path="$DEFAULT_SEARCH_PATH"
local type="$DEFAULT_TYPE"
local log_file="$DEFAULT_LOG_FILE"
local max_depth="$DEFAULT_MAX_DEPTH"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
dry_run=true
shift
;;
--top-n)
if [[ -n "${2:-}" ]] && [[ "$2" =~ ^[0-9]+$ ]] && [[ "$2" -le 100 ]]; then
top_n="$2"
shift 2
else
log_message "ERROR" "Invalid --top-n value. Must be a number between 1 and 100."
exit 1
fi
;;
--search-path)
if [[ -n "${2:-}" ]] && [[ -d "$2" ]]; then
search_path="$2"
shift 2
else
log_message "ERROR" "Invalid --search-path. Directory does not exist: ${2:-}"
exit 1
fi
;;
--type)
if [[ -n "${2:-}" ]] && [[ "$2" =~ ^(files|dirs|both)$ ]]; then
type="$2"
shift 2
else
log_message "ERROR" "Invalid --type value. Must be: files, dirs, or both."
exit 1
fi
;;
--log-file)
if [[ -n "${2:-}" ]]; then
log_file="$2"
shift 2
else
log_message "ERROR" "Invalid --log-file value."
exit 1
fi
;;
--max-depth)
if [[ -n "${2:-}" ]] && [[ "$2" =~ ^[0-9]+$ ]] && [[ "$2" -le 50 ]]; then
max_depth="$2"
shift 2
else
log_message "ERROR" "Invalid --max-depth value. Must be a number between 1 and 50."
exit 1
fi
;;
--help)
show_usage
exit 0
;;
*)
log_message "ERROR" "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Set global LOG_FILE variable
LOG_FILE="$log_file"
# Create log file directory if it doesn't exist
local log_dir
log_dir=$(dirname "$LOG_FILE")
if [[ ! -d "$log_dir" ]]; then
mkdir -p "$log_dir"
fi
log_message "INFO" "Starting find-largest-files.sh v1.0"
log_message "INFO" "Configuration: top_n=$top_n, search_path=$search_path, type=$type, max_depth=$max_depth"
if [[ "$dry_run" == true ]]; then
log_message "INFO" "DRY RUN MODE - No actual searching will be performed"
echo "DRY RUN: Would search for top $top_n largest $type in: $search_path"
echo "DRY RUN: Max depth: $max_depth"
echo "DRY RUN: Excluded paths: $DEFAULT_EXCLUDE_PATHS"
echo "DRY RUN: Log file: $LOG_FILE"
exit 0
fi
# Check dependencies
check_dependencies
# Verify search path exists and is readable
if [[ ! -r "$search_path" ]]; then
log_message "ERROR" "Cannot read search path: $search_path"
exit 1
fi
# Execute the search based on type
case "$type" in
"files")
find_largest_files "$search_path" "$max_depth" | format_results "Files" "$top_n"
;;
"dirs")
find_largest_dirs "$search_path" "$max_depth" | format_results "Directories" "$top_n"
;;
"both")
echo "=== FILES ==="
find_largest_files "$search_path" "$max_depth" | format_results "Files" "$top_n"
echo
echo "=== DIRECTORIES ==="
find_largest_dirs "$search_path" "$max_depth" | format_results "Directories" "$top_n"
;;
esac
log_message "INFO" "Search completed successfully"
}
# =============================================================================
# SCRIPT ENTRY POINT
# =============================================================================
# Ensure script is not run as root unless necessary
if [[ $EUID -eq 0 ]] && [[ "${ALLOW_ROOT:-false}" != "true" ]]; then
echo "WARNING: Running as root. Set ALLOW_ROOT=true if this is intentional."
echo "This script can run safely as a regular user for most searches."
read -p "Continue as root? (y/N): " -r
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# Run main function with all arguments
main "$@"Get new scripts weekly.
Subscribe to receive fresh PowerShell scripts, automation tips, and IT workflow improvements.
Subscribe to Newsletter