1+ #! /bin/bash
2+
3+ # Script to compare actual file stats with override stats from xattrs
4+ #
5+ # Usage: ./compare_stat_overrides.sh [-L level] <directory>
6+ #
7+ # Description:
8+ # This script displays the actual file stat (uid:gid:mode) alongside the
9+ # user.containers.override_stat xattr value if present. This allows easy
10+ # comparison between the real file permissions and the virtualized ones.
11+ #
12+ # Options:
13+ # -L level Descend only level directories deep (like tree -L)
14+ # -a Show all files including hidden files
15+ # -h, --help Show help message
16+ #
17+ # Examples:
18+ # ./compare_stat_overrides.sh . # Compare stats in current directory (all depths)
19+ # ./compare_stat_overrides.sh -L 2 /home/user # Compare stats up to 2 levels deep
20+ # ./compare_stat_overrides.sh -L 1 -a ~/ # Compare stats 1 level deep, including hidden files
21+ #
22+ # Notes:
23+ # - Requires 'xattr' command to be available on the system
24+ # - On macOS, xattr is built-in
25+ # - On Linux, you may need to install 'attr' package
26+
27+ # Default values
28+ MAX_DEPTH=" "
29+ SHOW_HIDDEN=false
30+ DIR=" "
31+ TOTAL_COUNT=0
32+
33+ # Parse command line arguments
34+ while [[ $# -gt 0 ]]; do
35+ case $1 in
36+ -L)
37+ if [[ -z " $2 " ]] || ! [[ " $2 " =~ ^[0-9]+$ ]]; then
38+ echo " Error: -L requires a numeric level argument"
39+ exit 1
40+ fi
41+ MAX_DEPTH=" $2 "
42+ shift 2
43+ ;;
44+ -a)
45+ SHOW_HIDDEN=true
46+ shift
47+ ;;
48+ -h|--help)
49+ echo " compare_stat_overrides.sh - Compare actual file stats with override stats from xattrs"
50+ echo " "
51+ echo " Usage: $0 [-L level] [-a] <directory>"
52+ echo " "
53+ echo " Options:"
54+ echo " -L level Descend only level directories deep"
55+ echo " -a Show all files including hidden files"
56+ echo " -h, --help Show this help message"
57+ echo " "
58+ echo " Examples:"
59+ echo " $0 . # Current directory, all depths"
60+ echo " $0 -L 2 /home/user # Up to 2 levels deep"
61+ echo " $0 -L 1 -a ~/ # 1 level deep, include hidden files"
62+ exit 0
63+ ;;
64+ * )
65+ if [[ -z " $DIR " ]]; then
66+ DIR=" $1 "
67+ else
68+ echo " Error: Too many arguments"
69+ echo " Usage: $0 [-L level] [-a] <directory>"
70+ exit 1
71+ fi
72+ shift
73+ ;;
74+ esac
75+ done
76+
77+ # Check if directory argument is provided
78+ if [[ -z " $DIR " ]]; then
79+ echo " Error: No directory specified"
80+ echo " Usage: $0 [-L level] [-a] <directory>"
81+ echo " Try '$0 --help' for more information."
82+ exit 1
83+ fi
84+
85+ # Check if directory exists
86+ if [ ! -d " $DIR " ]; then
87+ echo " Error: Directory '$DIR ' does not exist"
88+ exit 1
89+ fi
90+
91+ # Function to print tree-like indentation
92+ print_indent () {
93+ local level=$1
94+ local is_last=$2
95+ local prefix=" "
96+
97+ for (( i= 0 ; i< level; i++ )) ; do
98+ if [ $i -eq $(( level- 1 )) ]; then
99+ if [ " $is_last " = " true" ]; then
100+ prefix=" ${prefix} └── "
101+ else
102+ prefix=" ${prefix} ├── "
103+ fi
104+ else
105+ prefix=" ${prefix} │ "
106+ fi
107+ done
108+
109+ echo -n " $prefix "
110+ }
111+
112+ # Function to display stat comparison for a file with tree formatting
113+ show_stat_comparison () {
114+ local file=" $1 "
115+ local level=" $2 "
116+ local is_last=" $3 "
117+ local basename=$( basename " $file " )
118+
119+ # Skip hidden files if -a flag not set
120+ if [ " $SHOW_HIDDEN " = false ] && [[ " $basename " == .* ]] && [ " $level " -gt 0 ]; then
121+ return
122+ fi
123+
124+ # Get actual file stat in uid:gid:mode format
125+ local actual_stat=$( stat -c " %u:%g:%04a" " $file " 2> /dev/null)
126+ if [ -d " $file " ] && [ -n " $actual_stat " ]; then
127+ # Prepend file type bits to mode for directories
128+ local mode=$( stat -c " %04a" " $file " )
129+ local uid=$( stat -c " %u" " $file " )
130+ local gid=$( stat -c " %g" " $file " )
131+ actual_stat=" ${uid} :${gid} :04${mode} "
132+ fi
133+
134+ # Get override stat from xattr
135+ local override_stat=$( xattr -p user.containers.override_stat " $file " 2> /dev/null | tr -d ' \n' )
136+
137+ # Only show files that have either stat info or override stat
138+ if [ -z " $actual_stat " ] && [ -z " $override_stat " ]; then
139+ return
140+ fi
141+
142+ # Print the file/directory name with tree formatting
143+ if [ $level -gt 0 ]; then
144+ print_indent $level " $is_last "
145+ fi
146+
147+ # Format the output with actual vs override stat
148+ local name_part=" "
149+ if [ -d " $file " ]; then
150+ name_part=" \033[1;34m${basename} /\033[0m"
151+ else
152+ name_part=" $basename "
153+ fi
154+
155+ # Build stat comparison string
156+ local stat_part=" "
157+ if [ -n " $actual_stat " ] && [ -n " $override_stat " ]; then
158+ if [ " $actual_stat " = " $override_stat " ]; then
159+ stat_part=" [${actual_stat} ]"
160+ else
161+ stat_part=" [\033[33m${actual_stat} \033[0m → \033[32m${override_stat} \033[0m]"
162+ fi
163+ (( TOTAL_COUNT++ ))
164+ elif [ -n " $actual_stat " ]; then
165+ stat_part=" [${actual_stat} ]"
166+ elif [ -n " $override_stat " ]; then
167+ stat_part=" [→ \033[32m${override_stat} \033[0m]"
168+ (( TOTAL_COUNT++ ))
169+ fi
170+
171+ echo -e " ${name_part}${stat_part} "
172+ }
173+
174+ # Function to recursively process directory
175+ process_directory () {
176+ local dir=" $1 "
177+ local current_level=" $2 "
178+
179+ # Check if we've reached max depth
180+ if [ -n " $MAX_DEPTH " ] && [ " $current_level " -ge " $MAX_DEPTH " ]; then
181+ return
182+ fi
183+
184+ # Get list of items in directory
185+ local items=()
186+ if [ " $SHOW_HIDDEN " = true ]; then
187+ while IFS= read -r -d ' ' item; do
188+ items+=(" $item " )
189+ done < <( find " $dir " -maxdepth 1 -mindepth 1 -print0 | sort -z)
190+ else
191+ while IFS= read -r -d ' ' item; do
192+ items+=(" $item " )
193+ done < <( find " $dir " -maxdepth 1 -mindepth 1 ! -name ' .*' -print0 | sort -z)
194+ fi
195+
196+ local total=${# items[@]}
197+ local count=0
198+
199+ for item in " ${items[@]} " ; do
200+ (( count++ ))
201+ local is_last=" false"
202+ if [ $count -eq $total ]; then
203+ is_last=" true"
204+ fi
205+
206+ show_stat_comparison " $item " " $current_level " " $is_last "
207+
208+ # Recurse into directories
209+ if [ -d " $item " ]; then
210+ process_directory " $item " $(( current_level + 1 ))
211+ fi
212+ done
213+ }
214+
215+ # Main execution
216+ echo -e " \033[1mComparing file stats with override stats in: $DIR \033[0m"
217+ if [ -n " $MAX_DEPTH " ]; then
218+ echo " Max depth: $MAX_DEPTH "
219+ fi
220+ echo " ----------------------------------------"
221+
222+ # Show root directory
223+ show_stat_comparison " $DIR " 0 " false"
224+
225+ # Process subdirectories
226+ process_directory " $DIR " 1
227+
228+ # Summary
229+ echo " ----------------------------------------"
230+ echo " Total files with stat overrides: $TOTAL_COUNT "
0 commit comments