-
Notifications
You must be signed in to change notification settings - Fork 55
/
apply-format
executable file
·342 lines (293 loc) · 10.2 KB
/
apply-format
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
#! /bin/bash
#
# Copyright 2018 Undo Ltd.
#
# https://github.com/barisione/clang-format-hooks
# Force variable declaration before access.
set -u
# Make any failure in piped commands be reflected in the exit code.
set -o pipefail
readonly bash_source="${BASH_SOURCE[0]:-$0}"
##################
# Misc functions #
##################
function error_exit() {
for str in "$@"; do
echo -n "$str" >&2
done
echo >&2
exit 1
}
########################
# Command line parsing #
########################
function show_help() {
if [ -t 1 ] && hash tput 2> /dev/null; then
local -r b=$(tput bold)
local -r i=$(tput sitm)
local -r n=$(tput sgr0)
else
local -r b=
local -r i=
local -r n=
fi
cat << EOF
${b}SYNOPSIS${n}
To reformat git diffs:
${i}$bash_source [OPTIONS] [FILES-OR-GIT-DIFF-OPTIONS]${n}
To reformat whole files, including unchanged parts:
${i}$bash_source [-f | --whole-file] FILES${n}
${b}DESCRIPTION${n}
Reformat C or C++ code to match a specified formatting style.
This command can either work on diffs, to reformat only changed parts of
the code, or on whole files (if -f or --whole-file is used).
${b}FILES-OR-GIT-DIFF-OPTIONS${n}
List of files to consider when applying clang-format to a diff. This is
passed to "git diff" as is, so it can also include extra git options or
revisions.
For example, to apply clang-format on the changes made in the last few
revisions you could use:
${i}\$ $bash_source HEAD~3${n}
${b}FILES${n}
List of files to completely reformat.
${b}-f, --whole-file${n}
Reformat the specified files completely (including parts you didn't
change).
The fix is printed on stdout by default. Use -i if you want to modify
the files on disk.
${b}--staged, --cached${n}
Reformat only code which is staged for commit.
The fix is printed on stdout by default. Use -i if you want to modify
the files on disk.
${b}-i${n}
Reformat the code and apply the changes to the files on disk (instead
of just printing the fix on stdout).
${b}--apply-to-staged${n}
This is like specifying both --staged and -i, but the formatting
changes are also staged for commit (so you can just use "git commit"
to commit what you planned to, but formatted correctly).
${b}--style STYLE${n}
The style to use for reformatting code.
If no style is specified, then it's assumed there's a .clang-format
file in the current directory or one of its parents.
${b}--help, -h, -?${n}
Show this help.
EOF
}
# getopts doesn't support long options.
# getopt mangles stuff.
# So we parse manually...
declare positionals=()
declare has_positionals=false
declare whole_file=false
declare apply_to_staged=false
declare staged=false
declare in_place=false
declare style=file
declare ignored=()
while [ $# -gt 0 ]; do
declare arg="$1"
shift # Past option.
case "$arg" in
-h | -\? | --help )
show_help
exit 0
;;
-f | --whole-file )
whole_file=true
;;
--apply-to-staged )
apply_to_staged=true
;;
--cached | --staged )
staged=true
;;
-i )
in_place=true
;;
--style=* )
style="${arg//--style=/}"
;;
--style )
[ $# -gt 0 ] || \
error_exit "No argument for --style option."
style="$1"
shift
;;
--internal-opt-ignore-regex=* )
ignored+=("${arg//--internal-opt-ignore-regex=/}")
;;
--internal-opt-ignore-regex )
ignored+=("${arg//--internal-opt-ignore-regex=/}")
[ $# -gt 0 ] || \
error_exit "No argument for --internal-opt-ignore-regex option."
ignored+=("$1")
shift
;;
-- )
# Stop processing further arguments.
if [ $# -gt 0 ]; then
positionals+=("$@")
has_positionals=true
fi
break
;;
-* )
error_exit "Unknown argument: $arg"
;;
*)
positionals+=("$arg")
;;
esac
done
# Restore positional arguments, access them from "$@".
if [ ${#positionals[@]} -gt 0 ]; then
set -- "${positionals[@]}"
has_positionals=true
fi
[ -n "$style" ] || \
error_exit "If you use --style you need to specify a valid style."
#######################################
# Detection of clang-format & friends #
#######################################
# clang-format.
declare format="${CLANG_FORMAT:-}"
if [ -z "$format" ]; then
format=$(type -p clang-format)
fi
if [ -z "$format" ]; then
error_exit \
$'You need to install clang-format.\n' \
$'\n' \
$'On Ubuntu/Debian this is available in the clang-format package or, in\n' \
$'older distro versions, clang-format-VERSION.\n' \
$'On Fedora it\'s available in the clang package.\n' \
$'You can also specify your own path for clang-format by setting the\n' \
$'$CLANG_FORMAT environment variable.'
fi
# clang-format-diff.
if [ "$whole_file" = false ]; then
invalid="/dev/null/invalid/path"
if [ "${OSTYPE:-}" = "linux-gnu" ]; then
readonly sort_version=-V
else
# On macOS, sort doesn't have -V.
readonly sort_version=-n
fi
declare paths_to_try=()
# .deb packages directly from upstream.
# We try these first as they are probably newer than the system ones.
while read -r f; do
paths_to_try+=("$f")
done < <(compgen -G "/usr/share/clang/clang-format-*/clang-format-diff.py" | sort "$sort_version" -r)
# LLVM official releases (just untarred in /usr/local).
while read -r f; do
paths_to_try+=("$f")
done < <(compgen -G "/usr/local/clang+llvm*/share/clang/clang-format-diff.py" | sort "$sort_version" -r)
# Maybe it's in the $PATH already? This is true for Ubuntu and Debian.
paths_to_try+=( \
"$(type -p clang-format-diff 2> /dev/null || echo "$invalid")" \
"$(type -p clang-format-diff.py 2> /dev/null || echo "$invalid")" \
)
# Fedora.
paths_to_try+=( \
/usr/share/clang/clang-format-diff.py \
)
# Gentoo.
while read -r f; do
paths_to_try+=("$f")
done < <(compgen -G "/usr/lib/llvm/*/share/clang/clang-format-diff.py" | sort -n -r)
# Homebrew.
while read -r f; do
paths_to_try+=("$f")
done < <(compgen -G "/usr/local/Cellar/clang-format/*/share/clang/clang-format-diff.py" | sort -n -r)
declare format_diff=
# Did the user specify a path?
if [ -n "${CLANG_FORMAT_DIFF:-}" ]; then
format_diff="$CLANG_FORMAT_DIFF"
else
for path in "${paths_to_try[@]}"; do
if [ -e "$path" ]; then
# Found!
format_diff="$path"
if [ ! -x "$format_diff" ]; then
format_diff="python $format_diff"
fi
break
fi
done
fi
if [ -z "$format_diff" ]; then
error_exit \
$'Cannot find clang-format-diff which should be shipped as part of the same\n' \
$'package where clang-format is.\n' \
$'\n' \
$'Please find out where clang-format-diff is in your distro and report an issue\n' \
$'at https://github.com/barisione/clang-format-hooks/issues with details about\n' \
$'your operating system and setup.\n' \
$'\n' \
$'You can also specify your own path for clang-format-diff by setting the\n' \
$'$CLANG_FORMAT_DIFF environment variable, for instance:\n' \
$'\n' \
$' CLANG_FORMAT_DIFF="python /.../clang-format-diff.py" \\\n' \
$' ' "$bash_source"
fi
readonly format_diff
fi
############################
# Actually run the command #
############################
if [ "$whole_file" = true ]; then
[ "$has_positionals" = true ] || \
error_exit "No files to reformat specified."
[ "$staged" = false ] || \
error_exit "--staged/--cached only make sense when applying to a diff."
read -r -a format_args <<< "$format"
format_args+=("-style=file")
[ "$in_place" = true ] && format_args+=("-i")
"${format_args[@]}" "$@"
else # Diff-only.
if [ "$apply_to_staged" = true ]; then
[ "$staged" = false ] || \
error_exit "You don't need --staged/--cached with --apply-to-staged."
[ "$in_place" = false ] || \
error_exit "You don't need -i with --apply-to-staged."
staged=true
readonly patch_dest=$(mktemp)
trap '{ rm -f "$patch_dest"; }' EXIT
else
readonly patch_dest=/dev/stdout
fi
declare git_args=(git diff -U0 --no-color)
[ "$staged" = true ] && git_args+=("--staged")
# $format_diff may contain a command ("python") and the script to excute, so we
# need to split it.
read -r -a format_diff_args <<< "$format_diff"
[ "$in_place" = true ] && format_diff_args+=("-i")
# Build the regex for paths to consider or ignore.
# We use negative lookahead assertions which preceed the list of allowed patterns
# (that is, the extensions we want).
exclusions_regex=
if [ "${#ignored[@]}" -gt 0 ]; then
for pattern in "${ignored[@]}"; do
exclusions_regex="$exclusions_regex(?!$pattern)"
done
fi
"${git_args[@]}" "$@" \
| "${format_diff_args[@]}" \
-p1 \
-style="$style" \
-iregex="$exclusions_regex"'.*\.(c|cpp|cxx|cc|h|hpp|m|mm|js|java)' \
> "$patch_dest" \
|| exit 1
if [ "$apply_to_staged" = true ]; then
if [ ! -s "$patch_dest" ]; then
echo "No formatting changes to apply."
exit 0
fi
patch -p0 < "$patch_dest" || \
error_exit "Cannot apply fix to local files."
git apply -p0 --cached < "$patch_dest" || \
error_exit "Cannot apply fix to git staged changes."
fi
fi