11#! /usr/bin/env bash
2+ set -u
23
34usage () {
45 cat << 'EOF '
@@ -23,23 +24,27 @@ Rules enforced:
2324EOF
2425}
2526
27+ # If sourced, "exit" would kill the caller shell. Use safe_exit.
28+ safe_exit () {
29+ local code=" ${1:- 0} "
30+ if [[ " ${BASH_SOURCE[0]} " != " $0 " ]]; then
31+ return " $code "
32+ fi
33+ exit " $code "
34+ }
35+
2636process_file () {
2737 local file=" $1 "
28- local tmp
38+ local tmp dir
2939
30- # Only operate on regular files
3140 [[ -f " $file " ]] || return 0
3241
33- tmp=" $( mktemp -- " ${file} .fix_indent.XXXXXX" ) " || {
42+ dir=" $( dirname -- " $file " ) "
43+ tmp=" $( mktemp --tmpdir=" $dir " " .fix_indent.XXXXXX" ) " || {
3444 echo " fix_indent: mktemp failed for: $file " >&2
3545 return 1
3646 }
3747
38- # AWK filter:
39- # - Convert tabs to 4 spaces
40- # - Collapse blank runs to a single indented blank line (indent from next non-blank line)
41- # - Drop trailing blanks at EOF
42- # - Preserve CRLF if present
4348 awk '
4449 BEGIN { pending_blank = 0; use_crlf = 0 }
4550
@@ -52,15 +57,16 @@ process_file() {
5257 s = $0
5358 if (sub(/\r$/, "", s)) use_crlf = 1
5459
60+ # Tabs are forbidden: replace with 4 spaces (everywhere)
5561 gsub(/\t/, " ", s)
5662
57- # whitespace-only line => treat as blank
63+ # whitespace-only line => treat as blank (after tab expansion this is spaces-only)
5864 if (s ~ /^[ ]*$/) {
5965 pending_blank = 1
6066 next
6167 }
6268
63- # if we have pending blanks, emit exactly one blank indented to this line
69+ # If we have pending blanks, emit exactly one blank indented to this line
6470 if (pending_blank) {
6571 match(s, /^[ ]*/)
6672 emit(substr(s, RSTART, RLENGTH))
@@ -71,7 +77,7 @@ process_file() {
7177 }
7278
7379 END {
74- # If file ends with blanks, emit nothing (no blank lines at/beyond EOF )
80+ # Trailing blanks at EOF are dropped (no output here )
7581 }
7682 ' " $file " > " $tmp "
7783
@@ -81,20 +87,22 @@ process_file() {
8187 return 1
8288 fi
8389
84- # Avoid touching files that would be unchanged
8590 if cmp -s -- " $file " " $tmp " ; then
8691 rm -f -- " $tmp "
8792 return 0
8893 fi
8994
90- # Overwrite in place (preserves mode/owner; updates mtime)
91- if ! cat -- " $tmp " > " $file " ; then
92- echo " fix_indent: write failed for: $file " >&2
95+ # Preserve permissions where possible
96+ chmod --reference=" $file " " $tmp " 2> /dev/null || true
97+ chown --reference=" $file " " $tmp " 2> /dev/null || true
98+
99+ # Atomic replace
100+ if ! mv -f -- " $tmp " " $file " ; then
101+ echo " fix_indent: replace failed for: $file " >&2
93102 rm -f -- " $tmp "
94103 return 1
95104 fi
96105
97- rm -f -- " $tmp "
98106 return 0
99107}
100108
@@ -104,13 +112,13 @@ main() {
104112
105113 if [[ $# -eq 0 ]]; then
106114 usage >&2
107- exit 2
115+ safe_exit 2
108116 fi
109117
110118 case " $1 " in
111119 -h)
112120 usage
113- exit 0
121+ safe_exit 0
114122 ;;
115123 -R)
116124 recursive=1
@@ -123,40 +131,44 @@ main() {
123131 fi
124132 if [[ $# -ne 0 ]]; then
125133 usage >&2
126- exit 2
134+ safe_exit 2
127135 fi
128136 ;;
129137 -* )
130138 usage >&2
131- exit 2
139+ safe_exit 2
132140 ;;
133141 * )
134142 if [[ $# -ne 1 ]]; then
135143 usage >&2
136- exit 2
144+ safe_exit 2
137145 fi
138146 target=" $1 "
139147 ;;
140148 esac
141149
142150 if [[ $recursive -eq 0 ]]; then
143- # Single file mode
144- process_file " $target " || exit $?
145- exit 0
151+ process_file " $target " || safe_exit $?
152+ safe_exit 0
146153 fi
147154
148- # Recursive mode
149155 if [[ ! -e " $target " ]]; then
150156 echo " fix_indent: path not found: $target " >&2
151- exit 1
157+ safe_exit 1
152158 fi
153159
154160 local rc=0
155161 while IFS= read -r -d ' ' f; do
156162 process_file " $f " || rc=1
157163 done < <( find " $target " -type f -name ' *.py' -print0)
158164
159- exit $rc
165+ safe_exit " $rc "
160166}
161167
168+ # If someone sources it, don't run it implicitly; tell them what to do.
169+ if [[ " ${BASH_SOURCE[0]} " != " $0 " ]]; then
170+ echo " fix_indent: don’t source this script. Run it: ./fix_indent [args]" >&2
171+ return 2
172+ fi
173+
162174main " $@ "
0 commit comments