#!/bin/bash
# file - file and directory support library
set -e
lib_load 'core/qsort'
######
# Library Settings
core_file_config_init() {
# $file_find_opts[] - Passed to find(1) when called by file_find.
lib_setting_arrays file_find_opts
# $file_find_sort - If set to `true`, output from ``file_find()``
# will be sorted.
lib_setting_vars file_find_sort
file_find_sort=false
# $file_hashbin - Name of default hashing program
lib_setting_vars file_hashbin
file_hashbin=sha1sum
# $file_hash_texts[] - Hashing algorithm output (see ``file_hash()``).
# $file_hash_name[] - Hashed file names (see ``file_hash()``).
# $file_hash_sizes[] - Hashed file sizes (see ``file_hash()``).
# $file_hash_modes[] - Hashed file modes (see ``file_hash()``).
lib_setting_arrays \
file_hash_texts file_hash_names \
file_hash_sizes file_hash_modes
}
######
# File I/O
# file_read() - Reads file ($2) into an array variable ($1)
file_read() {
local var=$1
local name=$2
readarray -t $var <"$name"
}
# file_read_func() - Reads file ($1) and calls a command ($@) with
# lines as its arguments.
#
# $1 - File name
# $@ - Command to receive the file contents as arguments
file_read_func() {
local name=$1
shift
local -a lines=()
file_read lines "$name"
"$@" "${lines[@]}"
}
# file_capture_func() - Capture the output of a command ($@) to a
# temporary file, storing the name of the file in a named variable ($1).
#
# $1 - Name of variable to receive the name of the temporary file.
# $2 - Command to execute
# $@ - Command arguments (optional)
file_capture_func() {
min_args 2 "$@"
local -n name=$1
shift
name=$(cmd_tempfile)
[ "$name" ] || error "unable to create temporary file"
"$@" >"$name"
}
# file_capture() - Captures `stdin` to a temporary file, storing
# the name of the file in a named variable ($1).
# $1 - Name of variable to receive the name of the temporary file.
file_capture() {
has_args 1 "$@"
file_capture_func "$1" cat
}
######
# File Variable I/O
# file_var_func() - Captures the output of a command ($@) and
# assigns its output to the given variable ($1).
# $1 - Name of variable to receive output from the command.
# $2 - Command to execute
# $@ - Command arguments (optional)
file_var_func() {
min_args 2 "$@"
local -n var=$1
shift
local -a lines
file_array_func lines "$"@
var=${lines[*]}
}
# file_array_func() - Captures the output of a command ($@) and
# assigns its output to the given array variable ($1).
# $1 - Name of array variable to receive lines from the command.
# $2 - Command to execute
# $@ - Command arguments (optional)
file_array_func() {
min_args 2 "$@"
local var=$1
shift
local str
file_capture_func str "$@"
file_read $var "$str"
}
######
# Directory and Path Functions
# run_mkdir() - Wraps ``mkdir -p``. This function should be used instead
# of the bare command in most cases.
run_mkdir() {
local opts
! $debug || opts=-v
run mkdir -p $opts "$@" >&2
}
# run_pushd() - Wraps ``pushd`` to be silent or report a useful error
run_pushd() { run pushd "$1" >/dev/null || error "$1: pushd failed"; }
# run_popd() - Wraps ``popd`` to be silent
run_popd() { run popd >/dev/null; }
# run_in_dir() - Runs the command ($@) after changing a new directory ($1).
# $1 - New directory
# $@ - Command to run
run_in_dir() {
min_args 2 "$@"
local dir=$1
shift
run_pushd "$dir"
run "$@"
run_popd
}
# file_readdir() - Prints list of files in the given directory ($1).
# $1 - Directory to search
# $@ - Optional arguments
file_readdir() {
local dir=$1
shift
run find "$dir" -mindepth 1 -maxdepth 1 "$@" -exec basename '{}' ';'
}
short_path() {
local name=$1
local dir dname prefix
dir=$(dirname "$name")
dir=$(realpath "$dir")
if [ "$dir" != '.' ]; then
dir=$(basename "$dir")
prefix=".../"
fi
name=$(basename "$name")
echo "$prefix$dir/$name"
}
# file_mkdir() - Creates directory portion of file name ($1).
file_mkdir() {
has_args 1 "$@"
local f=$1
local dir
dir=$(dirname "$f")
[ -d "$dir" ] || run_mkdir "$dir"
}
######
# Finding Files
# file_find - Fills an array with names of things found by ``find(1)``.
# The ``$file_find_opts[]`` variable controls the behavior of this function,
# allowing additional arguments to be passed to ``find``.
# $1 - Name of array variable
# $@ - Path names to search
file_find() {
min_args 2 "$@"
local var=$1
shift
local fftmp
fftmp=$(cmd_tempfile)
find "$@" "${file_find_opts[@]}" -print >"$fftmp"
local -a __files
readarray -t $var <"$fftmp"
! $file_find_sort || qsort_list $var
}
######
# File Checking
# file_size() - Prints the size of the given file(s)
# $@ - File names
file_size() { stat -c'%s' "$@"; }
# file_test() - Performs a existential test ($1) on the given files ($@)
# using the ``test`` shell command.
file_test() {
min_args 2 "$@"
local tst=$1
shift
local i
for i in "$@"; do
test $tst "$i" || error "$i: not found"
done
}
# file_is_terminal() - Returns success if the named fd ($1) is a terminal.
file_is_terminal() {
has_args 1 "$@"
local fd=$1
case "$1" in
(stdin) fd=0 ;;
(stdout) fd=1 ;;
(stderr) fd=2 ;;
esac
[ -t $fd ]
}
######
# File Hashing
# file_hash_raw() - Runs the ``$file_hashbin`` program on the given files ($@).
file_hash_raw() { min_args 1 "$@"; run $file_hashbin "$@"; }
# file_hash_list() - Runs ``file_hash_raw()`` on the given files ($@)
# and reads the results in the named array variable ($1).
file_hash_list() {
local var=$1
shift
local tmp
tmp=$(cmd_tempfile)
file_hash_raw "$@" >"$tmp"
readarray -t $var <"$tmp"
}
# file_hash() - Loads the hash information for the given files ($@) into
# the ``file_hash_*`` arrays
file_hash() {
min_args 1 "$@"
local -a lines
file_hash_list lines "$@"
for_each _file_hash_split "${lines[@]}"
}
# _file_hash_split() - Splits apart the hash results ($1) for a single file
# and appends the various pieces to the respective ``$file_hash_*[]`` arrays.
_file_hash_split() {
local line=$1
local text=${line%% *}
local name=${line#* }
local mode=${name:0:1}
# remove mode from name
local len=$((${#name} - 1))
name=${name:1:$len}
local size
size=$(file_size "$name")
list_append file_hash_names "$name"
list_append file_hash_modes "$mode"
list_append file_hash_texts "$text"
list_append file_hash_sizes "$size"
}
# file_hash_print() - Prints the stored hash information.
# This function exists primarily for verification purposes.
file_hash_print() {
local count=$((${#file_hash_names[@]} - 1))
for_each _file_hash_print_index $(seq 0 $count)
}
# _file_hash_print_index() - Prints stored hash information for a single file.
# $1 - Index into ``$file_hash_*[]`` arrays
_file_hash_print_index() {
local i=$1
local text name size mode
text=${file_hash_texts[$i]}
name=${file_hash_names[$i]}
size=${file_hash_sizes[$i]}
mode=${file_hash_modes[$i]}
echo "$text $mode$name ($size)"
}
######
# Duplicate Detection
# file_duplicate_scan() - Scans the provided paths ($@) for duplicate files.
file_duplicate_scan() {
min_args 1 "$@"
info "duplicate scan started..."
local -a fd_files
local file_find_opts=( -type f )
file_find fd_files "$@"
info "hashing ${#fd_files[@]} files..."
file_hash "${fd_files[@]}"
! $pretend || error "file duplicate detection"
local -A fd_hashmap
local -A fd_dups
for_each _file_duplicate_scan_index $(seq 0 $((${#fd_files[@]} - 1)))
info "printing list of duplicates..."
local -a dups=( "${fd_dups[@]}" )
list_unique dups
qsort_list dups
for_each _file_duplicate_scan_check "${dups[@]}"
info "duplicate scan... done."
}
# _file_duplicate_scan_index() - Checks an individual file for duplicity.
_file_duplicate_scan_index() {
local i=$1
local text name
text=${file_hash_texts[$i]}
name=${file_hash_names[$i]}
if [ "${fd_hashmap[$text]}" ]; then
fd_dups[$name]=$text
else
fd_hashmap[$text]=$name
fi
}
# _file_duplicate_scan_check() - Prints the full list of duplicate files.
# The first of a group of duplicates files is prefixed with ``-``; the
# remaining duplicates in the group are prefixed with ``+``.
_file_duplicate_scan_check() {
local dup_text=$1
local dup_name=${fd_hashmap[$dup_text]}
echo "-$dup_name"
for i in $(seq 0 $((${#fd_files[@]} - 1))); do
local text=${file_hash_texts[$i]}
[[ "$text" == "$dup_text" ]] || continue
local name=${file_hash_names[$i]}
[[ "$name" != "$dup_name" ]] || continue
echo "+$name"
done
}
# file_duplicate_scan_list() - Calls ``file_duplicate_scan "$@"`` and
# stores the results in an array variable ($1).
file_duplicate_scan_list() {
local var=$1
shift
local tmp
tmp=$(cmd_tempfile)
file_duplicate_scan "$@" >"$tmp"
readarray -t $var <"$tmp"
}
# for_each_file_duplicate() - Iterates through a list of duplicates, as
# produced by ``file_duplicate_scan_list()``. For each duplicate, a
# given function ($1) will be called with two arguments: the original and
# duplicate file names.
for_each_file_duplicate() {
local func=$1
shift
local item src dst
for item in "$@"; do
case "$item" in
(-*) src=${item:1} ;;
(+*) dst=${item:1}; $func "$src" "$dst" ;;
(*) error "$item: unknown duplicate item" ;;
esac
done
}
######
# Duplicate Elimination
# file_duplicate_symlink() - Finds all duplicates and eliminates them
# by replacing them with symbolic links.
file_duplicate_symlink() {
local file_duplicate_link_type=soft
file_duplicate_link "$@"
}
# file_duplicate_link() - Finds all duplicates and eliminates them
# by replacing them with links.
file_duplicate_link() {
local confirm_func=$1
shift
local -a dups
file_duplicate_scan_list dups "$@"
local file_duplicate_link_type=${file_duplicate_link_type:-hard}
case "$file_duplicate_link_type" in
hard) ;;
soft) file_duplicate_link_opt="-s" ;;
*) error "$file_duplicate_link_type: unknown link type" ;;
esac
$confirm_func "${dups[@]}" || error "$__FUNCNAME__: aborted"
for_each_file_duplicate _file_duplicate_link_item "${dups[@]}"
}
_file_duplicate_link_item() {
local src=$1
local dst=$2
info "linking: '$src' -> '$dst'"
run rm "$dst"
run ln ${file_duplicate_link_opt} "$src" "$dst"
}
# file_duplicate_delete() - Finds duplicates and deletes them.
file_duplicate_delete() {
local confirm_func=$1
shift
local -a dups
file_duplicate_scan_list dups "$@"
$confirm_func "${dups[@]}" || error "$__FUNCNAME__: aborted"
for_each_file_duplicate _file_duplicate_delete_item "${dups[@]}"
}
_file_duplicate_delete_item() {
local dst=$2
run rm "$dst"
}
Generated on Fri Jul 28 14:34:54 PDT 2017 by mcsh d14 v0.23.0.