279 lines
5.3 KiB
Bash
Executable File
279 lines
5.3 KiB
Bash
Executable File
#!/bin/sh
|
|
set -f
|
|
|
|
## ephemeral limit, keep in sync with verify_depth()
|
|
max_depth=99
|
|
default_depth=20
|
|
|
|
me="${0##*/}"
|
|
usage() {
|
|
cat >&2 <<-EOF
|
|
# usage: ${me} [options] <directory> [<directory> ..]
|
|
# options:
|
|
# -d <N>, --depth <N>, --depth=<N>
|
|
# limit search depth to <N> (default: ${default_depth}, max: ${max_depth})
|
|
# -z, --zero
|
|
# separate entries with NUL instead of LF
|
|
# -s, --split-path
|
|
# separate directory and file name parts with "|" instead of "/"
|
|
EOF
|
|
exit "${1:-0}"
|
|
}
|
|
[ $# != 0 ] || usage
|
|
|
|
msg() { echo ${1:+"# ${me}: $*"} >&2 ; }
|
|
msgf() {
|
|
_____fmt="$1" ; shift
|
|
env printf "# ${me}: ${_____fmt}\\n" ${@+"$@"} >&2
|
|
unset _____fmt
|
|
}
|
|
|
|
verify_depth() {
|
|
case "$1" in
|
|
[1-9] | [1-9][0-9] ) return 0 ;;
|
|
esac
|
|
msgf 'error: wrong depth specifier: %q' "$1"
|
|
usage 1
|
|
}
|
|
|
|
o_depth=
|
|
f_zero_eol=
|
|
f_split_path=
|
|
|
|
## process options
|
|
want_value=
|
|
n_opt=0
|
|
for i ; do
|
|
if [ -n "${want_value}" ] ; then
|
|
case "${want_value}" in
|
|
depth )
|
|
o_depth="$i"
|
|
verify_depth "${o_depth}"
|
|
;;
|
|
esac
|
|
|
|
want_value=
|
|
n_opt=$((n_opt+1))
|
|
continue
|
|
fi
|
|
|
|
case "$i" in
|
|
-d | --depth | --depth=* )
|
|
if [ -n "${o_depth}" ] ; then
|
|
msg 'error: "depth" option already set'
|
|
usage 1
|
|
fi
|
|
case "$i" in
|
|
*=* )
|
|
o_depth="${i#*=}"
|
|
verify_depth "${o_depth}"
|
|
;;
|
|
* ) want_value=depth ;;
|
|
esac
|
|
;;
|
|
-z | --zero )
|
|
if [ -n "${f_zero_eol}" ] ; then
|
|
msg 'error: "zero" flag already set'
|
|
usage 1
|
|
fi
|
|
f_zero_eol=1
|
|
;;
|
|
-s | --split-path )
|
|
if [ -n "${f_split_path}" ] ; then
|
|
msg 'error: "split-path" flag already set'
|
|
usage 1
|
|
fi
|
|
f_split_path=1
|
|
;;
|
|
-* )
|
|
msgf 'unknown option: %q' "$i"
|
|
usage 1
|
|
;;
|
|
* ) break ;;
|
|
esac
|
|
|
|
n_opt=$((n_opt+1))
|
|
done
|
|
|
|
[ ${n_opt} = 0 ] || shift ${n_opt}
|
|
|
|
[ $# != 0 ] || usage 1
|
|
|
|
: "${o_depth:=${default_depth}}"
|
|
|
|
path_sep='/'
|
|
[ -z "${f_split_path}" ] || path_sep='|'
|
|
|
|
entry_sep='\n'
|
|
[ -z "${f_zero_eol}" ] || entry_sep='\0'
|
|
|
|
entry_fmt="%s${path_sep}%s${entry_sep}"
|
|
|
|
## work directory
|
|
w=$(mktemp -d) ; : "${w:?}"
|
|
|
|
_cleanup() {
|
|
cd /
|
|
rm -rf -- "$w"
|
|
}
|
|
|
|
adjust_with_skiplist() {
|
|
[ -s "$1" ] || return 0
|
|
[ -s "$2" ] || return 0
|
|
|
|
__skiptype="$3"
|
|
while read -r __skipname ; do
|
|
[ -n "${__skipname}" ] || continue
|
|
|
|
mawk \
|
|
-v "n=${__skipname}" \
|
|
-v "t=${__skiptype}" \
|
|
'
|
|
BEGIN {
|
|
RS = ORS = "\0";
|
|
FS = OFS = "|";
|
|
}
|
|
{
|
|
if ($3 != n) { print; next; }
|
|
if (t == "") { next; }
|
|
if ($1 ~ t) { next; }
|
|
print;
|
|
}
|
|
' < "$2" > "$2.t"
|
|
mv -f "$2.t" "$2"
|
|
done < "$1"
|
|
unset __skipname __skiptype
|
|
}
|
|
|
|
: > "$w/all"
|
|
|
|
## process arguments
|
|
for i ; do
|
|
## arguments with '|' are skipped - try naming paths simplier
|
|
case "$i" in
|
|
*\|* )
|
|
msgf 'argument with "|" is IGNORED: %q' "$i"
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
## non-existent directories are silently skipped
|
|
[ -d "$i" ] || continue
|
|
|
|
## generate directory listing
|
|
## paths with '\n' and '|' are silently skipped - try naming paths simplier
|
|
find "$i/" -follow -mindepth 1 -maxdepth 1 ! -name '*|*' -printf '%y|%h|%P\0' \
|
|
| sed -zE '/\n/d' > "$w/current"
|
|
|
|
## filter out uncommon entry types
|
|
## allowed types:
|
|
## d - directory
|
|
## f - file
|
|
## less common types (but also allowed):
|
|
## b - block device
|
|
## c - character device
|
|
## p - pipe
|
|
## s - socket
|
|
sed -i -zE '/^[^bcdfps]/d' "$w/current"
|
|
|
|
## empty directory - continue with next argument
|
|
[ -s "$w/current" ] || continue
|
|
|
|
## generate skiplist
|
|
grep -zE '\.-$' < "$w/current" \
|
|
| cut -z -d '|' -f 3 \
|
|
| sed -zE 's/\.-$//' \
|
|
| tr '\0' '\n' > "$w/skip"
|
|
## adjust current list: remove entries from skiplist
|
|
sed -i -zE '/\.-$/d' "$w/current"
|
|
|
|
## adjust accumulated list: remove "skip" entries
|
|
adjust_with_skiplist "$w/skip" "$w/all"
|
|
rm -f "$w/skip"
|
|
|
|
## adjust accumulated list: override all entries with "non-dir" entries from current list
|
|
## NB: entry type from current list overrides entry from previously accumulated list
|
|
sed -zEn '/^[^d]/p' < "$w/current" \
|
|
| cut -z -d '|' -f 3 \
|
|
| tr '\0' '\n' > "$w/nondir"
|
|
adjust_with_skiplist "$w/nondir" "$w/all"
|
|
rm -f "$w/nondir"
|
|
|
|
## adjust accumulated list: override "non-dir" entries with "dir" entries from current list
|
|
## NB: entry type from current list overrides entry from previously accumulated list
|
|
sed -zEn '/^d/p' < "$w/current" \
|
|
| cut -z -d '|' -f 3 \
|
|
| tr '\0' '\n' > "$w/dir"
|
|
adjust_with_skiplist "$w/dir" "$w/all" '[^d]'
|
|
rm -f "$w/dir"
|
|
|
|
## merge lists
|
|
sort -zV -t '|' -k 3 < "$w/current" >> "$w/all"
|
|
rm -f "$w/current"
|
|
done
|
|
|
|
# nothing to do?
|
|
if ! [ -s "$w/all" ] ; then
|
|
_cleanup
|
|
exit 0
|
|
fi
|
|
|
|
cut -z -d '|' -f '3' < "$w/all" \
|
|
| sort -zuV \
|
|
| tr '\0' '\n' > "$w/names"
|
|
|
|
: > "$w/dirnames"
|
|
|
|
sub_depth=$(( o_depth - 1 ))
|
|
|
|
while read -r name ; do
|
|
mawk \
|
|
-v "n=${name}" \
|
|
'
|
|
BEGIN {
|
|
RS = "\0"; ORS = "\n";
|
|
FS = OFS = "|";
|
|
}
|
|
|
|
$3 == n { print; }
|
|
' < "$w/all" > "$w/list"
|
|
|
|
while IFS='|' read -r dtype dir _name ; do
|
|
[ -n "${dtype}" ] || continue
|
|
|
|
# ${_name} is unused
|
|
case "${dtype}" in
|
|
d )
|
|
[ "${sub_depth}" != 0 ] || continue
|
|
|
|
if grep -Fxq -e "${name}" "$w/dirnames" ; then
|
|
continue
|
|
fi
|
|
|
|
printf '%s\n' "${name}" >> "$w/dirnames"
|
|
|
|
mawk \
|
|
-v "n=${name}" \
|
|
'
|
|
BEGIN {
|
|
ORS = "\0";
|
|
FS = "|"; OFS = "/";
|
|
}
|
|
|
|
$3 == n { print $2,$3; }
|
|
' < "$w/list" > "$w/dir.args"
|
|
|
|
xargs -0 -a "$w/dir.args" "$0" ${f_zero_eol:+-z} ${f_split_path:+-s} -d "${sub_depth}"
|
|
rm -f "$w/dir.args"
|
|
;;
|
|
* )
|
|
printf "${entry_fmt}" "${dir}" "${name}"
|
|
;;
|
|
esac
|
|
done < "$w/list"
|
|
|
|
done < "$w/names"
|
|
|
|
_cleanup
|
|
exit 0
|