compass

orient-files: rename -x to --list-unclassified, expand its scope

commit fd4110d019b18e60ff3c9a0d0538b98a8d19a0ff · parent 93b6dc49ff · browse this tree
Ximianth, 2026-06-24 16:53:05 UTC

The flag's old name was tied to the original implementation that only
ever lumped extensionless files into (other).  Now (other) routinely
contains files with rare extensions too, and the dotfile-shaped stuff
isn't the only interesting content in there.  The flag now itemizes
every file that landed in (other), grouped by type with per-type
counts and basenames as nested bullets.

-x is preserved as a short alias.

The 'extensionless' type bucket is renamed from internal '(none)' to
'extensionless' so the listing reads naturally.  No behavior change
when the flag is off.
diff --git a/orient-files b/orient-files
index eae58dc..3321eba 100755
--- a/orient-files
+++ b/orient-files
@@ -1,7 +1,7 @@
 #!/bin/bash
 # Classifies files by type and counts them across a project area.
 #
-# Usage: orient-files [-x|--list-extensionless] [PATH]
+# Usage: orient-files [-x|--list-unclassified] [PATH]
 #   PATH defaults to the git toplevel of cwd, else cwd itself.
 #
 # Output: a bullet list, one row per type, sorted by count descending.
@@ -36,9 +36,12 @@
 #
 # Extensionless files (e.g. `LICENSE`, `Makefile`) and dotfiles whose
 # name carries no further dot (`.gitignore`, `.editorconfig`) are folded
-# into `(other)` by default.
-# With `-x`/`--list-extensionless`, those filenames are itemized as
-# nested bullets so it's clear what they are.
+# into `(other)` along with files of rare extensions.
+# With `-x`/`--list-unclassified`, every file that landed in `(other)`
+# is itemized as nested bullets, grouped by type, so it's clear what
+# the lump actually contains.  In practice this is a useful peek: the
+# `(other)` bucket often hides dotfile-shaped config that's central to
+# a project's identity.
 #
 # Silent (no output, exit 0) when no countable files are found.
 #
@@ -51,10 +54,10 @@
 
 set -euo pipefail
 
-list_extensionless=0
+list_unclassified=0
 while [ $# -gt 0 ]; do
 	case "$1" in
-		-x|--list-extensionless) list_extensionless=1; shift ;;
+		-x|--list-unclassified) list_unclassified=1; shift ;;
 		-h|--help)
 			sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
 			exit 0 ;;
@@ -185,7 +188,7 @@ mapfile -t classified < <(
 			exec_flag = $1; name = $2; path = $3
 			ext = classify_ext(name)
 			# Output: <ext>\t<basename>\t<exec_flag>\t<path>
-			# The basename matters only for the extensionless listing under -x.
+			# The basename is needed for the --list-unclassified listing.
 			# The path is only used for the shebang sniff, and only when
 			# exec_flag == "x"; otherwise it is empty.
 			print ext "\t" name "\t" exec_flag "\t" path
@@ -253,11 +256,15 @@ sniff_shebang() {
 # `type_shebang[type]`     -- count of files whose shebang chose this type.
 # `ext_counts[ext]`        -- per-extension count, used for sorting the hint
 #                             list within a type (dominant variant first).
+# `type_files[type]`       -- newline-separated filenames, populated only
+#                             when --list-unclassified is set (the listing
+#                             needs basenames per type; we skip the bookkeeping
+#                             otherwise).
 declare -A type_counts=()
 declare -A type_exts=()
 declare -A type_shebang=()
 declare -A ext_counts=()
-extensionless_files=()
+declare -A type_files=()
 for entry in "${classified[@]}"; do
 	IFS=$'\t' read -r ext name exec_flag path <<< "$entry"
 
@@ -278,8 +285,11 @@ for entry in "${classified[@]}"; do
 		# That happens naturally below when non-shebang files of that
 		# extension are processed.
 	elif [ "$ext" = "(none)" ]; then
-		extensionless_files+=("$name")
-		continue
+		# Extensionless files with no shebang verdict get their own type
+		# bucket, so the downstream lumping and listing flow treats them
+		# uniformly with other rare types.  Almost always small, almost
+		# always interesting (dotfiles, LICENSE, Makefile, ...).
+		type="extensionless"
 	else
 		type=${ext_to_type[$ext]:-$ext}
 		ext_counts[$ext]=$((${ext_counts[$ext]:-0} + 1))
@@ -294,6 +304,13 @@ for entry in "${classified[@]}"; do
 		fi
 	fi
 	type_counts[$type]=$((${type_counts[$type]:-0} + 1))
+	if [ "$list_unclassified" -eq 1 ]; then
+		if [ -z "${type_files[$type]:-}" ]; then
+			type_files[$type]=$name
+		else
+			type_files[$type]+=$'\n'$name
+		fi
+	fi
 done
 
 # Sort types by count descending, ties broken alphabetically.
@@ -308,6 +325,7 @@ mapfile -t sorted_types < <(
 # signal about a project's character; 3 starts to suggest a pattern.)
 shown_types=()
 shown_counts=()
+other_types=()
 other_files=0
 other_kinds=0
 for line in "${sorted_types[@]}"; do
@@ -317,6 +335,7 @@ for line in "${sorted_types[@]}"; do
 		shown_types+=("$type")
 		shown_counts+=("$count")
 	else
+		other_types+=("$type")
 		other_files=$((other_files + count))
 		other_kinds=$((other_kinds + 1))
 	fi
@@ -364,29 +383,27 @@ for i in "${!shown_types[@]}"; do
 	printf -- "- %-16s %5d  (%s)\n" "${type}:" "$count" "$hint"
 done
 
-# Handle extensionless files.
-# Default: fold into the `(other)` count.
-# With -x: emit them as their own bullet with nested filename list.
-n_extensionless=${#extensionless_files[@]}
-if [ "$list_extensionless" -eq 1 ] && [ "$n_extensionless" -gt 0 ]; then
-	echo "- extensionless files (${n_extensionless}):"
-	# Sort for deterministic output.
-	mapfile -t sorted_files < <(printf '%s\n' "${extensionless_files[@]}" | sort)
-	for f in "${sorted_files[@]}"; do
-		echo "  - \`${f}\`"
-	done
-else
-	other_files=$((other_files + n_extensionless))
-	# Count extensionless as a single "kind" if there were any, so the
-	# `(other)` line acknowledges them.  Approximate but honest.
-	if [ "$n_extensionless" -gt 0 ]; then
-		other_kinds=$((other_kinds + 1))
-	fi
-fi
-
+# Render the (other) summary line, if anything ended up there.
 # Pluralize cleanly.
 if [ "$other_kinds" -gt 0 ]; then
 	file_word="files"; [ "$other_files" -eq 1 ] && file_word="file"
 	kind_word="kinds"; [ "$other_kinds" -eq 1 ] && kind_word="kind"
 	echo "- (other): ${other_files} ${file_word} across ${other_kinds} ${kind_word}"
 fi
+
+# With --list-unclassified, expand (other) into a nested listing,
+# grouped by type so it's easy to see what each rare type is.
+# Types are listed in the same order as the lumping loop saw them
+# (count descending, then alphabetical), and filenames within each
+# type are sorted alphabetically for deterministic output.
+if [ "$list_unclassified" -eq 1 ] && [ "${#other_types[@]}" -gt 0 ]; then
+	for type in "${other_types[@]}"; do
+		count=${type_counts[$type]}
+		echo "  - ${type} (${count}):"
+		# `type_files` entries are newline-separated; sort them.
+		mapfile -t sorted_files < <(printf '%s\n' "${type_files[$type]}" | sort)
+		for f in "${sorted_files[@]}"; do
+			echo "    - \`${f}\`"
+		done
+	done
+fi