#!/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] [ ..] # options: # -d , --depth , --depth= # limit search depth to (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