glance.bash
· 6.3 KiB · Bash
Orginalformat
# glance - Provides a quick and intelligent overview of a directory's contents.
#
# Authors: anduin
#
# Features:
# - Auto-detects Git repositories to intelligently list files.
# - Dynamically calculates and adjusts the `tree` view depth to prevent clutter.
# - File content display strictly adheres to the calculated tree depth.
# - Limits the maximum lines printed per file to avoid long scrolls.
# - Supports powerful filtering by file glob patterns.
#
# Usage:
# glance [DIRECTORY] [OPTIONS...]
#
# Options:
# --max-files-preview <count> : Max items (files+dirs) in tree view. (Default: 100)
# --max-file-lines <count> : Max lines to print per file. (Default: 150)
# --filter <patterns> : Comma-separated glob patterns to filter files (e.g., '*.md,*.cs')
#
glance() {
local target_dir="."
local max_items_preview=100
local max_file_lines=150
local filter_patterns=""
local positional_args=()
# --- Internal helper function to process and print a single file ---
_glance_process_file() {
local file="$1"
if [ ! -f "$file" ]; then return; fi
# Check if it's a text file
if file -b --mime-type "$file" | grep -q '^text/'; then
local total_lines
total_lines=$(wc -l < "$file") # More efficient line count
local extension="${file##*.}"; local lang=""
case "$extension" in
js) lang="javascript" ;; py) lang="python" ;; rb) lang="ruby" ;;
sh) lang="bash" ;; css) lang="css" ;; html) lang="html" ;;
json) lang="json" ;; md) lang="markdown" ;; yml|yaml) lang="yaml" ;;
java) lang="java" ;; c) lang="c" ;; cpp) lang="cpp" ;; go) lang="go" ;;
*) lang="" ;;
esac
echo
echo "Content of file \"$file\":"
echo "\`\`\`$lang"
if [ "$total_lines" -gt "$max_file_lines" ]; then
head -n "$max_file_lines" "$file"
echo
echo "... (Only shown top $max_file_lines lines of $total_lines) ..."
else
cat "$file"
fi
echo
echo "\`\`\`"
echo "----------------------------------------------------------"
else
echo
echo "--> Skipped binary file: \"$file\""
echo "----------------------------------------------------------"
fi
}
# --- Step 1: Parse arguments ---
while [[ $# -gt 0 ]]; do
case "$1" in
--max-files-preview)
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then max_items_preview="$2"; shift 2;
else echo "Error: '--max-files-preview' requires a positive integer argument." >&2; return 1; fi ;;
--max-file-lines)
if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then max_file_lines="$2"; shift 2;
else echo "Error: '--max-file-lines' requires a positive integer argument." >&2; return 1; fi ;;
--filter)
if [[ -n "$2" && "$2" != -* ]]; then filter_patterns="$2"; shift 2;
else echo "Error: '--filter' requires a pattern string argument." >&2; return 1; fi ;;
-*) echo "Error: Unknown option '$1'" >&2; return 1 ;;
*) positional_args+=("$1"); shift ;;
esac
done
[[ ${#positional_args[@]} -gt 0 ]] && target_dir="${positional_args[0]}"
# --- Step 2: Check dependencies and directory validity ---
if ! command -v tree &>/dev/null; then echo "Error: 'tree' command not found. Please install it first." >&2; return 1; fi
if [ ! -d "$target_dir" ]; then echo "Error: Directory '$target_dir' does not exist." >&2; return 1; fi
echo "=========================================================="
echo "🔎 Overview for directory: $(realpath "$target_dir")"
echo "=========================================================="
echo
# --- Step 3: Dynamically calculate the optimal tree depth (based on the full, unfiltered directory) ---
echo "🌳 Directory Structure:"
local best_depth=1; local max_depth_to_check=6; local prev_item_count=-1
for depth in $(seq 1 $max_depth_to_check); do
local current_item_count; current_item_count=$(tree -L "$depth" -a --noreport "$target_dir" | wc -l)
if [ "$depth" -gt 1 ] && [ "$current_item_count" -gt "$max_items_preview" ]; then
best_depth=$((depth - 1)); break;
fi
# If item count stops increasing, we've reached the maximum effective depth
if [ "$depth" -gt 1 ] && [ "$current_item_count" -eq "$prev_item_count" ]; then
best_depth=$depth; break;
fi
best_depth=$depth; prev_item_count=$current_item_count
done
# --- Step 3.5: Apply filter and print the tree ---
local tree_args=(-L "$best_depth")
if [ -n "$filter_patterns" ]; then
local tree_pattern; tree_pattern=$(echo "$filter_patterns" | tr ',' '|')
tree_args+=(-P "$tree_pattern")
echo "(Filter active: ${filter_patterns})"
fi
echo "(Dynamic depth set to L${best_depth} to fit item limit: ${max_items_preview})"
tree "${tree_args[@]}" "$target_dir"
echo
# --- Step 4: Iterate and print file contents (applying filters and depth limits) ---
local header_suffix=""
if [ -n "$filter_patterns" ]; then header_suffix=" (filter: ${filter_patterns})"; fi
echo "📄 File Content Details (within depth L${best_depth}, max ${max_file_lines} lines per file${header_suffix}):"
(
cd "$target_dir" || exit 1
echo "----------------------------------------------------------"
local filter_array=()
[ -n "$filter_patterns" ] && IFS=',' read -ra filter_array <<< "$filter_patterns"
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
# Git mode
local git_files
if [ ${#filter_array[@]} -gt 0 ]; then
git_files=$(git ls-files -- "${filter_array[@]}")
else
git_files=$(git ls-files)
fi
# Use awk to filter paths by slash count to simulate depth limit
echo "$git_files" | awk -F'/' -v depth="$best_depth" 'NF <= depth' | while read -r file; do
_glance_process_file "$file"
done
else
# Normal mode
local find_args=(-maxdepth "$best_depth" -type f)
if [ ${#filter_array[@]} -gt 0 ]; then
find_args+=("(")
for i in "${!filter_array[@]}"; do
[ "$i" -ne 0 ] && find_args+=("-o")
find_args+=(-name "${filter_array[$i]}")
done
find_args+=(")")
fi
# Use find's -maxdepth parameter and name patterns
find . "${find_args[@]}" | while read -r file; do
# Remove leading './' from file path for cleaner output
_glance_process_file "${file#./}"
done
fi
)
}
1 | # glance - Provides a quick and intelligent overview of a directory's contents. |
2 | # |
3 | # Authors: anduin |
4 | # |
5 | # Features: |
6 | # - Auto-detects Git repositories to intelligently list files. |
7 | # - Dynamically calculates and adjusts the `tree` view depth to prevent clutter. |
8 | # - File content display strictly adheres to the calculated tree depth. |
9 | # - Limits the maximum lines printed per file to avoid long scrolls. |
10 | # - Supports powerful filtering by file glob patterns. |
11 | # |
12 | # Usage: |
13 | # glance [DIRECTORY] [OPTIONS...] |
14 | # |
15 | # Options: |
16 | # --max-files-preview <count> : Max items (files+dirs) in tree view. (Default: 100) |
17 | # --max-file-lines <count> : Max lines to print per file. (Default: 150) |
18 | # --filter <patterns> : Comma-separated glob patterns to filter files (e.g., '*.md,*.cs') |
19 | # |
20 | glance() { |
21 | local target_dir="." |
22 | local max_items_preview=100 |
23 | local max_file_lines=150 |
24 | local filter_patterns="" |
25 | local positional_args=() |
26 | |
27 | # --- Internal helper function to process and print a single file --- |
28 | _glance_process_file() { |
29 | local file="$1" |
30 | if [ ! -f "$file" ]; then return; fi |
31 | |
32 | # Check if it's a text file |
33 | if file -b --mime-type "$file" | grep -q '^text/'; then |
34 | local total_lines |
35 | total_lines=$(wc -l < "$file") # More efficient line count |
36 | |
37 | local extension="${file##*.}"; local lang="" |
38 | case "$extension" in |
39 | js) lang="javascript" ;; py) lang="python" ;; rb) lang="ruby" ;; |
40 | sh) lang="bash" ;; css) lang="css" ;; html) lang="html" ;; |
41 | json) lang="json" ;; md) lang="markdown" ;; yml|yaml) lang="yaml" ;; |
42 | java) lang="java" ;; c) lang="c" ;; cpp) lang="cpp" ;; go) lang="go" ;; |
43 | *) lang="" ;; |
44 | esac |
45 | |
46 | echo |
47 | echo "Content of file \"$file\":" |
48 | echo "\`\`\`$lang" |
49 | |
50 | if [ "$total_lines" -gt "$max_file_lines" ]; then |
51 | head -n "$max_file_lines" "$file" |
52 | echo |
53 | echo "... (Only shown top $max_file_lines lines of $total_lines) ..." |
54 | else |
55 | cat "$file" |
56 | fi |
57 | |
58 | echo |
59 | echo "\`\`\`" |
60 | echo "----------------------------------------------------------" |
61 | else |
62 | echo |
63 | echo "--> Skipped binary file: \"$file\"" |
64 | echo "----------------------------------------------------------" |
65 | fi |
66 | } |
67 | |
68 | # --- Step 1: Parse arguments --- |
69 | while [[ $# -gt 0 ]]; do |
70 | case "$1" in |
71 | --max-files-preview) |
72 | if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then max_items_preview="$2"; shift 2; |
73 | else echo "Error: '--max-files-preview' requires a positive integer argument." >&2; return 1; fi ;; |
74 | --max-file-lines) |
75 | if [[ -n "$2" && "$2" =~ ^[0-9]+$ ]]; then max_file_lines="$2"; shift 2; |
76 | else echo "Error: '--max-file-lines' requires a positive integer argument." >&2; return 1; fi ;; |
77 | --filter) |
78 | if [[ -n "$2" && "$2" != -* ]]; then filter_patterns="$2"; shift 2; |
79 | else echo "Error: '--filter' requires a pattern string argument." >&2; return 1; fi ;; |
80 | -*) echo "Error: Unknown option '$1'" >&2; return 1 ;; |
81 | *) positional_args+=("$1"); shift ;; |
82 | esac |
83 | done |
84 | [[ ${#positional_args[@]} -gt 0 ]] && target_dir="${positional_args[0]}" |
85 | |
86 | # --- Step 2: Check dependencies and directory validity --- |
87 | if ! command -v tree &>/dev/null; then echo "Error: 'tree' command not found. Please install it first." >&2; return 1; fi |
88 | if [ ! -d "$target_dir" ]; then echo "Error: Directory '$target_dir' does not exist." >&2; return 1; fi |
89 | |
90 | echo "==========================================================" |
91 | echo "🔎 Overview for directory: $(realpath "$target_dir")" |
92 | echo "==========================================================" |
93 | echo |
94 | |
95 | # --- Step 3: Dynamically calculate the optimal tree depth (based on the full, unfiltered directory) --- |
96 | echo "🌳 Directory Structure:" |
97 | local best_depth=1; local max_depth_to_check=6; local prev_item_count=-1 |
98 | for depth in $(seq 1 $max_depth_to_check); do |
99 | local current_item_count; current_item_count=$(tree -L "$depth" -a --noreport "$target_dir" | wc -l) |
100 | if [ "$depth" -gt 1 ] && [ "$current_item_count" -gt "$max_items_preview" ]; then |
101 | best_depth=$((depth - 1)); break; |
102 | fi |
103 | # If item count stops increasing, we've reached the maximum effective depth |
104 | if [ "$depth" -gt 1 ] && [ "$current_item_count" -eq "$prev_item_count" ]; then |
105 | best_depth=$depth; break; |
106 | fi |
107 | best_depth=$depth; prev_item_count=$current_item_count |
108 | done |
109 | |
110 | # --- Step 3.5: Apply filter and print the tree --- |
111 | local tree_args=(-L "$best_depth") |
112 | if [ -n "$filter_patterns" ]; then |
113 | local tree_pattern; tree_pattern=$(echo "$filter_patterns" | tr ',' '|') |
114 | tree_args+=(-P "$tree_pattern") |
115 | echo "(Filter active: ${filter_patterns})" |
116 | fi |
117 | echo "(Dynamic depth set to L${best_depth} to fit item limit: ${max_items_preview})" |
118 | tree "${tree_args[@]}" "$target_dir" |
119 | echo |
120 | |
121 | # --- Step 4: Iterate and print file contents (applying filters and depth limits) --- |
122 | local header_suffix="" |
123 | if [ -n "$filter_patterns" ]; then header_suffix=" (filter: ${filter_patterns})"; fi |
124 | echo "📄 File Content Details (within depth L${best_depth}, max ${max_file_lines} lines per file${header_suffix}):" |
125 | ( |
126 | cd "$target_dir" || exit 1 |
127 | echo "----------------------------------------------------------" |
128 | |
129 | local filter_array=() |
130 | [ -n "$filter_patterns" ] && IFS=',' read -ra filter_array <<< "$filter_patterns" |
131 | |
132 | if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
133 | # Git mode |
134 | local git_files |
135 | if [ ${#filter_array[@]} -gt 0 ]; then |
136 | git_files=$(git ls-files -- "${filter_array[@]}") |
137 | else |
138 | git_files=$(git ls-files) |
139 | fi |
140 | # Use awk to filter paths by slash count to simulate depth limit |
141 | echo "$git_files" | awk -F'/' -v depth="$best_depth" 'NF <= depth' | while read -r file; do |
142 | _glance_process_file "$file" |
143 | done |
144 | else |
145 | # Normal mode |
146 | local find_args=(-maxdepth "$best_depth" -type f) |
147 | if [ ${#filter_array[@]} -gt 0 ]; then |
148 | find_args+=("(") |
149 | for i in "${!filter_array[@]}"; do |
150 | [ "$i" -ne 0 ] && find_args+=("-o") |
151 | find_args+=(-name "${filter_array[$i]}") |
152 | done |
153 | find_args+=(")") |
154 | fi |
155 | # Use find's -maxdepth parameter and name patterns |
156 | find . "${find_args[@]}" | while read -r file; do |
157 | # Remove leading './' from file path for cleaner output |
158 | _glance_process_file "${file#./}" |
159 | done |
160 | fi |
161 | ) |
162 | } |