715 lines
23 KiB
Bash
715 lines
23 KiB
Bash
#!/usr/bin/env bash
|
||
# ------------------------------------------------------------------------------
|
||
# Copyright (c) 2013-2022 Geoffray Levasseur <fatalerrors@geoffray-levasseur.org>
|
||
# 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.
|
||
# ------------------------------------------------------------------------------
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# expandlist : treat wildcards in a file/directory list
|
||
# Usage: expandlist <item1 [item2 ... itemN]>
|
||
expandlist()
|
||
{
|
||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||
printf "expandlist: Wraps a list of items in double quotes.\n\n"
|
||
printf "Usage: expandlist <item1 [item2 ... itemN]>\n\n"
|
||
printf "Options:\n"
|
||
printf "\t-h, --help\t\tDisplay this help screen\n"
|
||
return 0
|
||
fi
|
||
|
||
local result=""
|
||
for item in "$1"; do
|
||
for content in "$item"; do
|
||
result+="\"$content\" "
|
||
done
|
||
done
|
||
echo $result
|
||
}
|
||
export -f expandlist
|
||
# ------------------------------------------------------------------------------
|
||
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Clean a directory or a 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)
|
||
local 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)
|
||
local outshell=1
|
||
shift
|
||
;;
|
||
-f|--force)
|
||
local 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=(".")
|
||
|
||
[[ ! $recursive ]] && local findopt="-maxdepth 1"
|
||
[[ ! $force ]] && local rmopt="-i"
|
||
unset recursive force
|
||
|
||
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
|
||
rm $rmopt $f
|
||
else
|
||
echo "rm $rmopt $f"
|
||
fi
|
||
done
|
||
done
|
||
unset outshell dirlist dellist findopt rmopt
|
||
}
|
||
export -f clean
|
||
# ------------------------------------------------------------------------------
|
||
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# Create a directory then goes inside
|
||
# Usage: mcd <directory>
|
||
mcd()
|
||
{
|
||
if [[ "$1" == "-h" || "$1" == "--help" ]]; then
|
||
printf "mcd: Create a directory and enter it.\n\n"
|
||
printf "Usage: mcd <directory>\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 all files in current directory to replace spaces with _
|
||
# 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 lst=""
|
||
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)
|
||
local recurs=1
|
||
shift
|
||
;;
|
||
-c|--subst-char)
|
||
# Handle optional argument for short/long options
|
||
case "$2" in
|
||
"")
|
||
substchar=""
|
||
;;
|
||
*)
|
||
substchar="$2"
|
||
;;
|
||
esac
|
||
shift 2
|
||
;;
|
||
-v|--verbose)
|
||
local verb=1
|
||
shift
|
||
;;
|
||
-s|--shell)
|
||
local shell=1
|
||
shift
|
||
;;
|
||
--)
|
||
shift
|
||
break
|
||
;;
|
||
*)
|
||
disp E "Invalid parameter, use \"rmspc --help\" to display options list"
|
||
echo
|
||
return 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
[[ ! $substchar ]] && substchar="_"
|
||
[[ $substchar == "none" ]] && local substchar=""
|
||
[[ $verb ]] && local mvopt="-v"
|
||
|
||
for f in *; do
|
||
[[ $recurs ]] && [[ -d "$f" ]] && (
|
||
[[ $verb ]] && disp I "Entering directory $(pwd)/$f ..."
|
||
local lastdir=$f
|
||
pushd "$f" >/dev/null
|
||
rmspc $@
|
||
popd >/dev/null
|
||
[[ $verb ]] && disp I "Leaving directory $(pwd)/$lastdir"
|
||
unset lastdir
|
||
)
|
||
|
||
if [[ $(echo $f | grep " ") ]]; then
|
||
local newf="${f// /${substchar}}"
|
||
local command="mv $mvopt \"$f\" \"$newf\""
|
||
if [[ $shell ]]; then
|
||
echo $command
|
||
else
|
||
$command
|
||
fi
|
||
fi
|
||
done
|
||
unset lst substchar verb shell newf command mvopt
|
||
}
|
||
export -f rmspc
|
||
# ------------------------------------------------------------------------------
|
||
|
||
|
||
# ------------------------------------------------------------------------------
|
||
# display stats about a file structure
|
||
# 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)
|
||
|
||
# Extension simple
|
||
if [[ -n "$ext_filter" ]]; then
|
||
find_cmd+=(-iname "*.$ext_filter")
|
||
fi
|
||
|
||
# Extension liste
|
||
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
|
||
|
||
# Taille min/max (à évaluer en octets)
|
||
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
|
||
|
||
# Exécution
|
||
"${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
|
||
# Simple sort for median (awk is not very good at this, we use existing logic)
|
||
if (count % 2 == 1) median = sizes[(count + 1) / 2]
|
||
else median = (sizes[count / 2] + sizes[count / 2 + 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 no_change=0 one_fs=0
|
||
|
||
local PARSED
|
||
PARSED=$(getopt -o hd:l:x --long help,details,one-fs,limit: -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
|
||
;;
|
||
-n|--no-change)
|
||
no_change=1
|
||
shift
|
||
;;
|
||
-l|--limit)
|
||
limit="$2"
|
||
shift 2
|
||
;;
|
||
--)
|
||
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" "-type" "f")
|
||
(( one_fs )) && find_args+=("-xdev")
|
||
|
||
# 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 read -r size path; do
|
||
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 no_change=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 no_change=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
|