Last active
May 17, 2026 08:53
-
-
Save janosgyerik/f77b0dd2e43704772171e629751d988b to your computer and use it in GitHub Desktop.
flatten leaf dirs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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