Skip to content

Instantly share code, notes, and snippets.

@janosgyerik
Last active May 17, 2026 08:53
Show Gist options
  • Select an option

  • Save janosgyerik/f77b0dd2e43704772171e629751d988b to your computer and use it in GitHub Desktop.

Select an option

Save janosgyerik/f77b0dd2e43704772171e629751d988b to your computer and use it in GitHub Desktop.
flatten leaf dirs
#!/usr/bin/env bash
#
# SCRIPT: flatten.sh
# AUTHOR: janos <janos@MacBook-Pro.fritz.box>
# DATE: 2026-05-16
#
# PLATFORM: Not platform dependent
#
# PURPOSE: Flatten deeply nested leaf folders into numeric folders
# named NNN at the base level.
#
set -euo pipefail
usage() {
local exitcode=0
if [ $# != 0 ]; then
echo "$*" >&2
exitcode=1
fi
cat << EOF
Usage: $0 [OPTION]... [ARG]...
Flatten deeply nested leaf folders into numeric folders named NNN at the base level.
Options:
-t, --tabular default = $tabular
-d, --dry-run default = $dry_run
--no-dry-run default = ! $dry_run
-y, --yes default = ! $dry_run
-h, --help Print this help
EOF
exit "$exitcode"
}
args=()
dry_run=on
tabular=off
no_ds=off
nonleafs=off
while test $# != 0; do
case $1 in
-h|--help) usage ;;
-d|--dry-run) dry_run=on ;;
--no-dry-run|-y|--yes) dry_run=off ;;
-t|--tabular) tabular=on ;;
--no-ds) no_ds=on ;;
--nonleafs) nonleafs=on ;;
--) shift; while test $# != 0; do args+=("$1"); shift; done; break ;;
-|-?*) usage "Unknown option: $1" ;;
*) args+=("$1") ;; # script that takes multiple arguments
esac
shift
done
set -- "${args[@]}" # save arguments in $@
fatal() {
echo >&2 "Fatal: $*"
exit 1
}
cmd() {
echo "+ $*"
test "$dry_run" = off || return 0
"$@"
}
countdir() {
local count=$1
test "$count" -le 999 || fatal "Too many directories: $count"
printf "%03d" "$count"
}
find_count() {
local basedir=$1
local i
for ((i = 1; i < 1000; ++i)); do
if ! test -d "$basedir/$(countdir "$i")"; then
echo "$i"
return
fi
done
fatal "Too many directories: $i"
}
find_leaf_dirs() {
local basedir=$1
find "$basedir" \
-depth \
-type d \
! -empty \
| awk '!p || p !~ "^" $0 "/"; { p=$0 }' \
| sort
}
flatten() {
local basedir=$1
local count leafdir
count=$(find_count "$basedir")
find_leaf_dirs "$basedir" | while IFS= read leafdir; do
if [[ $leafdir == $basedir/[0-9][0-9][0-9] ]]; then
continue
fi
cmd mv "$leafdir" "$basedir/$(countdir "$count")"
((++count))
done
}
flatten_all() {
for basedir; do
flatten "$basedir"
done
}
tabular() {
sed -e 's?^\+ mv work/??' -e 's? work/?\t?'
}
no_ds() {
cmd find "$@" -name .DS_Store -delete
}
find_nonleaf_dirs_with_files() {
find "$@" -depth -type d ! -empty | while IFS= read path; do
d=0
f=0
for p in "$path"/*; do
if test -f "$p"; then
((++f))
elif test -d "$p"; then
((++d))
else
fatal "unknown type: $p"
fi
done
if ((d > 0 && f > 0)); then
echo "$path"
fi
done
}
main() {
if test "$tabular" = on; then
tabular
elif test "$no_ds" = on; then
no_ds "$@"
elif test "$nonleafs" = on; then
find_nonleaf_dirs_with_files "$@"
else
flatten_all "$@"
fi
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment