#!/usr/bin/env bash # ------------------------------------------------------------------------------ # Copyright (c) 2013-2026 Geoffray Levasseur # Protected by the BSD3 license. Please read bellow for details. # # * Redistribution and use in source and binary forms, # * with or without modification, are permitted provided # * that the following conditions are met: # * # * Redistributions of source code must retain the above # * copyright notice, this list of conditions and the # * following disclaimer. # * # * Redistributions in binary form must reproduce the above # * copyright notice, this list of conditions and the following # * disclaimer in the documentation and/or other materials # * provided with the distribution. # * # * Neither the name of the copyright holder nor the names # * of any other contributors may be used to endorse or # * promote products derived from this software without # * specific prior written permission. # * # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND # * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, # * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR # * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # * OF SUCH DAMAGE. # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Expand wildcards in a file/directory list and quote the results # Usage: expandlist [options] expandlist() { local separator=" " local PARSED PARSED=$(getopt -o hs:n --long help,separator:,newline -n 'expandlist' -- "$@") if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"expandlist --help\" to display usage." return 1 fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "expandlist: expand globs and wrap matched items in double quotes.\n\n" printf "Usage: expandlist [options] \n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" printf "\t-s, --separator SEP\tSet output separator (default: space)\n" printf "\t-n, --newline\t\tUse a newline as separator\n" return 0 ;; -s|--separator) separator="$2" shift 2 ;; -n|--newline) separator=$'\n' shift ;; --) shift break ;; *) disp E "Invalid options, use \"expandlist --help\" to display usage." return 1 ;; esac done local item="" result="" matched=0 shopt -s nullglob for item in "$@"; do local expanded=() # True glob expansion when wildcards are present. if [[ "$item" == *'*'* || "$item" == *'?'* || "$item" == *'['* ]]; then expanded=( $item ) else expanded=( "$item" ) fi if [[ ${#expanded[@]} -eq 0 ]]; then continue fi for content in "${expanded[@]}"; do if (( matched )); then result+="$separator" fi result+="\"$content\"" matched=1 done done shopt -u nullglob printf '%s\n' "$result" } export -f expandlist # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Clean a directory tree from temporary or backup files # Usage: clean [options] [directory1] [...[directoryX]] # Options: # -h, --help: display help screen # -r, --recurs: do a recursive cleaning # -f, --force: do not ask for confirmation (use with care) # -s, --shell: do nothing and display what will be executed clean() { local recursive=0 force=0 outshell=0 # Define short and long options local PARSED PARSED=$(getopt -o hrsf --long help,recurs,shell,force -n 'clean' -- "$@") if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"clean --help\" to display usage." return 1 fi eval set -- "$PARSED" while true; do case "$1" in -r|--recurs) recursive=1 shift ;; -h|--help) printf "clean: erase backup files in the given directories.\n\n" printf "Usage: clean [option] [directory1] [...[directoryX]]\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay that help screen\n" printf "\t-r, --recurs\t\tDo a recursive cleaning\n" printf "\t-f, --force\t\tDo not ask for confirmation (use with care)\n" printf "\t-s, --shell\t\tDo nothing and display what will be executed\n" printf "\n" return 0 ;; -s|--shell) outshell=1 shift ;; -f|--force) force=1 shift ;; --) shift break ;; *) disp E "Invalid parameter, use \"clean --help\" to display options list" return 1 ;; esac done # Handle remaining arguments as directories local dirlist=("$@") [[ ${#dirlist[@]} -eq 0 ]] && dirlist=(".") local findopt=() rmopt=() (( ! recursive )) && findopt=(-maxdepth 1) (( ! force )) && rmopt=(-i) for dir in "${dirlist[@]}"; do find "$dir" "${findopt[@]}" -type f \( -name "*~" -o -name "#*#" -o -name "*.bak" -o -name ".~*#" \) -print0 | while IFS= read -r -d '' f; do if (( outshell )); then if (( ${#rmopt[@]} )); then printf 'rm %s -- "%s"\n' "${rmopt[*]}" "$f" else printf 'rm -- "%s"\n' "$f" fi else rm "${rmopt[@]}" -- "$f" fi done done } export -f clean # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Create a directory then goes inside # Usage: mcd mcd() { if [[ "$1" == "-h" || "$1" == "--help" ]]; then printf "mcd: Create a directory and enter it.\n\n" printf "Usage: mcd \n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" return 0 fi if [[ ! $# -eq 1 ]]; then disp E "Missing parameter. Use \"mcd --help\" to display usage." return 1 fi mkdir -pv "$1" && cd "$1" || { printf "Failed create and/or change directory.\n" return 1 } } export -f mcd # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Rename files and directories to replace spaces with another character # Usage: rmspc [options] # Options: # -h, --help: display help screen # -r, --recursive: treat subdirectories of the given directory # -c, --subst-char: change the replacement character (default is underscore) # -v, --verbose: display more details (recursive mode only) # -s, --shell: do nothing and display commands that would be executed rmspc() { local recurs=0 verb=0 shell=0 local substchar="_" substchar_set=0 local mvopt=() local PARSED PARSED=$(getopt -o hr:c::vs --long help,recursive,subst-char::,verbose,shell -n 'rmspc' -- "$@") if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"rmspc --help\" to display usage." return 1 fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "rmspc: remove spaces from all filenames in current directories\n\n" printf "Usage: rmspc [option]\n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay that help screen\n" printf "\t-r, --recursive\t\tTreat subdirectories of the given directory\n" printf "\t-c, --subst-char\tChange the replacement character (default is underscore)\n" printf "\t-v, --verbose\t\tDisplay more details (recursive mode only)\n" printf "\t-s, --shell\t\tDo nothing and display commands that would be executed\n\n" printf "Note: if the --subst-char option is given without parameters, spaces will be\n" printf " replaced with nothing (concatenation).\n" return 0 ;; -r|--recursive) recurs=1 shift ;; -c|--subst-char) substchar_set=1 substchar="$2" shift 2 ;; -v|--verbose) verb=1 shift ;; -s|--shell) shell=1 shift ;; --) shift break ;; *) disp E "Invalid parameter, use \"rmspc --help\" to display options list" return 1 ;; esac done [[ "$substchar" == "none" ]] && substchar="" (( verb )) && mvopt=(-v) shopt -s nullglob for f in *; do if (( recurs )) && [[ -d "$f" ]]; then ( local lastdir=$f (( verb )) && disp I "Entering directory $(pwd)/$f ..." pushd "$f" >/dev/null || return 1 if (( substchar_set )); then rmspc ${recurs:+-r} -c "$substchar" ${verb:+-v} ${shell:+-s} else rmspc ${recurs:+-r} ${verb:+-v} ${shell:+-s} fi popd >/dev/null || return 1 (( verb )) && disp I "Leaving directory $(pwd)/$lastdir" ) fi if [[ "$f" == *" "* ]]; then local newf="${f// /${substchar}}" [[ "$f" == "$newf" ]] && continue if (( shell )); then if (( ${#mvopt[@]} )); then printf 'mv %s -- "%s" "%s"\n' "${mvopt[*]}" "$f" "$newf" else printf 'mv -- "%s" "%s"\n' "$f" "$newf" fi else mv "${mvopt[@]}" -- "$f" "$newf" || { disp E "Failed renaming \"$f\" to \"$newf\"." continue } fi fi done shopt -u nullglob } export -f rmspc # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Display statistics about a file tree # Usage: file_stats [options] [path] # Options: # -H, --human Human readable sizes\n" # -d, --details Display details (min/max/average/median) # -m, --average Display only average size # -M, --median Display only median size # -c, --count Display only count of files # -t, --total Display only total size # -a, --all Display all stats in human readable format (shortcut for -H -d) # -x, --ext [ext] Filter by extension (e.g. -x log for .log files) # -X, --ext-list [list] Filter by multiple extensions (e.g. -X log,txt) # --min [size] Minimum size (e.g., 10M) # --max [size] Maximum size (e.g., 100M) file_stats() { local human=0 details=0 only_avg=0 only_med=0 only_count=0 only_total=0 local path="." show_all=1 ext_filter="" ext_list="" min_size="" max_size="" local PARSED # Short: H, d, m, M, c, t, a, x:, X: # Long: human, details, average, median, count, total, all, ext:, ext-list:, min:, max:, help PARSED=$(getopt -o HdmMctax:X:h --long human,details,average,median,count,total,all,ext:,ext-list:,min:,max:,help -n 'file_stats' -- "$@") if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"file_stats --help\" to display usage." return 1 fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "Usage: file_stats [options] [path]\n\n" printf "Options:\n" printf "\t-H, --human\t\tHuman readable sizes\n" printf "\t-d, --details\t\tShow detailed histogram\n" printf "\t-m, --average\t\tShow only average size\n" printf "\t-M, --median\t\tShow only median size\n" printf "\t-c, --count\t\tShow only file count\n" printf "\t-t, --total\t\tShow only total size\n" printf "\t-a, --all\t\tShow all (human + details)\n" printf "\t-x, --ext [ext]\t\tFilter by extension\n" printf "\t-X, --ext-list [list]\tFilter by comma-separated list\n" printf "\t--min [size]\t\tMinimum size (e.g., 10M)\n" printf "\t--max [size]\t\tMaximum size (e.g., 100M)\n" return 0 ;; -H|--human) human=1 shift ;; -d|--details) details=1 shift ;; -m|--average) only_avg=1 show_all=0 shift ;; -M|--median) only_med=1 show_all=0 shift ;; -c|--count) only_count=1 show_all=0 shift ;; -t|--total) only_total=1 show_all=0 shift ;; -a|--all) human=1 details=1 shift ;; -x|--ext) ext_filter="${2#.}" shift 2 ;; -X|--ext-list) ext_list="$2" shift 2 ;; --min) min_size="$2" shift 2 ;; --max) max_size="$2" shift 2 ;; --) shift break ;; *) disp E "Invalid option: $1" return 1 ;; esac shift done [[ -n "$1" ]] && path="$1" # Prepare find filters local find_cmd=(find "$path" -type f) # Single extension filter if [[ -n "$ext_filter" ]]; then find_cmd+=(-iname "*.$ext_filter") fi # Extension list filter if [[ -n "$ext_list" ]]; then IFS=',' read -ra exts <<< "$ext_list" find_cmd+=('(') for i in "${!exts[@]}"; do [[ $i -ne 0 ]] && find_cmd+=(-o) find_cmd+=(-iname "*.${exts[$i]}") done find_cmd+=(')') fi # Minimum/maximum size filters (evaluated in bytes) if [[ -n "$min_size" ]]; then find_cmd+=(-size +"$(numfmt --from=iec "$min_size")"c) fi if [[ -n "$max_size" ]]; then find_cmd+=(-size -"$(( $(numfmt --from=iec "$max_size") + 1 ))"c) fi # Execution "${find_cmd[@]}" -printf "%s\n" 2>/dev/null | sort -n | \ awk -v human="$human" -v details="$details" -v only_avg="$only_avg" \ -v only_med="$only_med" -v only_count="$only_count" \ -v only_total="$only_total" -v show_all="$show_all" -v path="$path" ' # Convert function function human_readable(x) { split("B KiB MiB GiB TiB", units) i = 1 while (x >= 1024 && i < 5) { x /= 1024 i++ } return sprintf("%.2f %s", x, units[i]) } # Display function function out(label, val, is_size) { if (human == 1 && is_size == 1) val = human_readable(val) printf "%-20s : %s\n", label, val } { sizes[NR] = $1 total += $1 if (min == "" || $1 < min) min = $1 if (max == "" || $1 > max) max = $1 if ($1 == 0) bucket[0]++ else { b = int(log($1)/log(1024)) bucket[b]++ } } END { count = NR if (count == 0) { print "No files found." exit } average = total / count # Median calculation: exact using sorted array values if (count % 2 == 1) { median = sizes[(count + 1) / 2] } else { idx = count / 2 median = (sizes[idx] + sizes[idx + 1]) / 2 } if (only_avg) out("Average size", average, 1) else if (only_med) out("Median size", median, 1) else if (only_count) out("Number of files", count, 0) else if (only_total) out("Total size", total, 1) else { if (show_all || human || details) { printf "Statistics for \"%s\"\n", path printf "-------------------------\n" } out("Number of files", count, 0) out("Total size", total, 1) out("Average size", average, 1) out("Median size", median, 1) out("Minimum size", min, 1) out("Maximum size", max, 1) } if (details) { print "\nSize histogram:" # Use a separate array for the loop to avoid collision for (b in bucket) { # Pre-calculate label parts # 1024^0 = 1 (B), 1024^1 = 1K, etc. low = (b == 0) ? 0 : (1024^b) high = 1024^(b+1) label = sprintf("%-9s – %-9s", (b == 0) ? "0" : human_readable(low), human_readable(high)) # We store buckets in an array, access them by index b printf "%-25s : %6d fichiers\n", label, bucket[b] } } }' } export -f file_stats # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Find the biggest files in a directory tree # Usage: findbig [options] [directory] # Options: # -h : display help screen # -d : display details (ls -l) for each file # -x : do not cross filesystem boundaries # -l : limit : number of files to return (default is 10) findbig() { local details=0 limit=10 one_fs=0 local PARSED PARSED=$(getopt -o hdl:x --long help,details,limit:,one-fs -n 'findbig' -- "$@") if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"findbig --help\" to display usage." return 1 fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "findbig: Find the N biggest files in a directory tree.\n\n" printf "Usage: findbig [options] [directory]\n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" printf "\t-d, --details\t\tShow detailed file info (ls -ld)\n" printf "\t-l, --limit N\t\tNumber of files to return (default: 10)\n" printf "\t-x, --one-fs\t\tDo not cross filesystem boundaries\n" return 0 ;; -d|--details) details=1 shift ;; -l|--limit) limit="$2" [[ "$limit" =~ ^[0-9]+$ ]] || { disp E "Invalid limit: must be a positive integer." return 1 } shift 2 ;; -x|--one-fs) one_fs=1 shift ;; --) shift break ;; *) disp E "Invalid option: $1" return 1 ;; esac done local dir="${1:-.}" # Prepare find arguments in an array for cleaner handling local find_args=(-L "$dir") (( one_fs )) && find_args+=(-xdev) find_args+=(-type f) # Logic: find files, print size and path, sort numeric reverse, take N if (( details )); then find "${find_args[@]}" -printf "%s %p\n" 2>/dev/null | sort -rn | head -n "$limit" | while IFS= read -r line; do local path="${line#* }" ls -ld -- "$path" done else find "${find_args[@]}" -printf "%s %p\n" 2>/dev/null | sort -rn | head -n "$limit" fi } export -f findbig # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Find empty files in a directory tree # Usage: findzero [options] [directory] # Options: # -h : display help screen # -d : display details (ls -l) for each file # -x : do not cross filesystem boundaries # --delete : delete empty files and display their paths findzero() { local delete=0 details=0 one_fs=0 local PARSED # o: options, long: long equivalents PARSED=$(getopt -o hdx --long help,details,one-fs,delete -n 'findzero' -- "$@") if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"findzero --help\" to display usage." return 1 fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "findzero: Find or delete empty files in a directory tree.\n\n" printf "Usage: findzero [options] [directory]\n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" printf "\t-d, --details\t\tShow detailed file info (ls -ls)\n" printf "\t-x, --one-fs\t\tDo not cross filesystem boundaries\n" printf "\t--delete\t\tActually remove the empty files\n" return 0 ;; -d|--details) details=1 shift ;; -x|--one-fs) one_fs=1 shift ;; --delete) delete=1 shift ;; --) shift break ;; *) disp E "Invalid option: $1" return 1 ;; esac done local dir="${1:-.}" local find_args=("-L" "$dir" "-type" "f" "-empty") (( one_fs )) && find_args+=("-xdev") # Execution logic if (( delete )); then disp W "Deleting empty files in $dir..." find "${find_args[@]}" -delete -print elif (( details )); then find "${find_args[@]}" -ls else find "${find_args[@]}" fi } export -f findzero # ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------ # Find dead symbolic links in a directory tree # Usage: finddead [options] [directory] # Options: # -h : display help screen # -d : display details (ls -l) for each link # -x : do not cross filesystem boundaries # --delete : delete dead links and display their paths finddead() { local delete=0 details=0 one_fs=0 local PARSED PARSED=$(getopt -o hdx --long help,details,one-fs,delete -n 'finddead' -- "$@") if [[ $? -ne 0 ]]; then disp E "Invalid options, use \"finddead --help\" to display usage." return 1 fi eval set -- "$PARSED" while true; do case "$1" in -h|--help) printf "finddead: Find or delete dead/broken symbolic links.\n\n" printf "Usage: finddead [options] [directory]\n\n" printf "Options:\n" printf "\t-h, --help\t\tDisplay this help screen\n" printf "\t-d, --details\t\tShow detailed symlink info (ls -ls)\n" printf "\t-x, --one-fs\t\tDo not cross filesystem boundaries\n" printf "\t--delete\t\tActually remove the dead links\n" return 0 ;; -d|--details) details=1 shift ;; -x|--one-fs) one_fs=1 shift ;; --delete) delete=1 shift ;; --) shift break ;; *) disp E "Invalid option: $1" return 1 ;; esac done local dir="${1:-.}" # -xtype l searches for links that do not point to an existing file local find_args=("$dir" "-xtype" "l") (( one_fs )) && find_args+=("-xdev") # Execution logic if (( delete )); then disp W "Deleting dead symlinks in $dir..." find "${find_args[@]}" -delete -print elif (( details )); then find "${find_args[@]}" -ls else find "${find_args[@]}" fi } export -f finddead # ------------------------------------------------------------------------------ # EOF