]> git.ipfire.org Git - people/ms/suricata.git/blob - scripts/clang-format.sh
tools: bash from env
[people/ms/suricata.git] / scripts / clang-format.sh
1 #!/usr/bin/env bash
2 #
3 # Script to clang-format suricata C code changes
4 #
5 # Rewriting branch parts of it is inspired by
6 # https://www.thetopsites.net/article/53885283.shtml
7
8 #set -x
9
10 # We verify the minimal clang-format version for better error messaging as older clang-format
11 # will barf on unknown settings with a generic error.
12 CLANG_FORMAT_REQUIRED_VERSION=9
13
14 EXIT_CODE_ERROR=2
15 EXIT_CODE_FORMATTING_REQUIRED=1
16 EXIT_CODE_OK=0
17
18 PRINT_DEBUG=0
19 # Debug output if PRINT_DEBUG is 1
20 function Debug {
21 if [ $PRINT_DEBUG -ne 0 ]; then
22 echo "DEBUG: $@"
23 fi
24 }
25
26 # ignore text formatting by default
27 bold=
28 normal=
29 italic=
30 # $TERM is set to dumb when calling scripts in github actions.
31 if [ -n "$TERM" -a "$TERM" != "dumb" ]; then
32 Debug "TERM: '$TERM'"
33 # tput, albeit unlikely, might not be installed
34 command -v tput >/dev/null 2>&1 # built-in which
35 if [ $? -eq 0 ]; then
36 Debug "Setting text formatting"
37 bold=$(tput bold)
38 normal=$(tput sgr0)
39 italic=$(echo -e '\E[3m')
40 fi
41 else
42 Debug "No text formatting"
43 fi
44
45 EXEC=$(basename $0)
46 pushd . >/dev/null # we might change dir - save so that we can revert
47
48 USAGE=$(cat << EOM
49 usage: $EXEC --help
50 $EXEC help <command>
51 $EXEC <command> [<args>]
52
53 Format selected changes using clang-format.
54
55 Note: This does ONLY format the changed code, not the whole file! It
56 uses ${italic}git-clang-format${normal} for the actual formatting. If you want to format
57 whole files, use ${italic}clang-format -i <file>${normal}.
58
59 It auto-detects the correct clang-format version and compared to ${italic}git-clang-format${normal}
60 proper it provides additional functionality such as reformatting of all commits on a branch.
61
62 Commands used in various situations:
63
64 Formatting branch changes (compared to master):
65 branch Format all changes in branch as additional commit
66 rewrite-branch Format every commit in branch and rewrite history
67
68 Formatting single changes:
69 cached Format changes in git staging
70 commit Format changes in most recent commit
71
72 Checking if formatting is correct:
73 check-branch Checks if formatting for branch changes is correct
74
75 More info an a command:
76 help Display more info for a particular <command>
77 EOM
78 )
79
80 HELP_BRANCH=$(cat << EOM
81 ${bold}NAME${normal}
82 $EXEC branch - Format all changes in branch as additional commit
83
84 ${bold}SYNOPSIS${normal}
85 $EXEC branch [--force]
86
87 ${bold}DESCRIPTION${normal}
88 Format all changes in your branch enabling you to add it as an additional
89 formatting commit. It automatically detects all commits on your branch.
90
91 Requires that all changes are committed unless --force is provided.
92
93 You will need to commit the reformatted code.
94
95 This is equivalent to calling:
96 $ git clang-format --extensions c,h [--force] first_commit_on_current_branch^
97
98 ${bold}OPTIONS${normal}
99 -f, --force
100 Allow changes to unstaged files.
101
102 ${bold}EXAMPLES${normal}
103 On your branch whose changes you want to reformat:
104
105 $ $EXEC branch
106
107 ${bold}EXIT STATUS${normal}
108 $EXEC exits with a status of zero if the changes were successfully
109 formatted, or if no formatting change was required. A status of two will
110 be returned if any errors were encountered.
111 EOM
112 )
113
114 HELP_CACHED=$(cat << EOM
115 ${bold}NAME${normal}
116 $EXEC cached - Format changes in git staging
117
118 ${bold}SYNOPSIS${normal}
119 $EXEC cached [--force]
120
121 ${bold}DESCRIPTION${normal}
122 Format staged changes using clang-format.
123
124 You will need to commit the reformatted code.
125
126 This is equivalent to calling:
127 $ git clang-format --extensions c,h [--force]
128
129 ${bold}OPTIONS${normal}
130 -f, --force
131 Allow changes to unstaged files.
132
133 ${bold}EXAMPLES${normal}
134 Format all changes in staging, i.e. in files added with ${italic}git add <file>${normal}.
135
136 $ $EXEC cached
137
138 ${bold}EXIT STATUS${normal}
139 $EXEC exits with a status of zero if the changes were successfully
140 formatted, or if no formatting change was required. A status of two will
141 be returned if any errors were encountered.
142 EOM
143 )
144
145 HELP_CHECK_BRANCH=$(cat << EOM
146 ${bold}NAME${normal}
147 $EXEC check-branch - Checks if formatting for branch changes is correct
148
149 ${bold}SYNOPSIS${normal}
150 $EXEC check-branch [--show-commits] [--quiet]
151 $EXEC check-branch --diff [--show-commits] [--quiet]
152 $EXEC check-branch --diffstat [--show-commits] [--quiet]
153
154 ${bold}DESCRIPTION${normal}
155 Check if all branch changes are correctly formatted.
156
157 Note, it does not check every commit's formatting, but rather the
158 overall diff between HEAD and master.
159
160 Returns 1 if formatting is off, 0 if it is correct.
161
162 ${bold}OPTIONS${normal}
163 -d, --diff
164 Print formatting diff, i.e. diff of each file with correct formatting.
165 -s, --diffstat
166 Print formatting diffstat output, i.e. files with wrong formatting.
167 -c, --show-commits
168 Print branch commits.
169 -q, --quiet
170 Do not print any error if formatting is off, only set exit code.
171
172 ${bold}EXIT STATUS${normal}
173 $EXEC exits with a status of zero if the formatting is correct. A
174 status of one will be returned if the formatting is not correct. A status
175 of two will be returned if any errors were encountered.
176 EOM
177 )
178
179 HELP_COMMIT=$(cat << EOM
180 ${bold}NAME${normal}
181 $EXEC commit - Format changes in most recent commit
182
183 ${bold}SYNOPSIS${normal}
184 $EXEC commit
185
186 ${bold}DESCRIPTION${normal}
187 Format changes in most recent commit using clang-format.
188
189 You will need to commit the reformatted code.
190
191 This is equivalent to calling:
192 $ git clang-format --extensions c,h HEAD^
193
194 ${bold}EXAMPLES${normal}
195 Format all changes in most recent commit:
196
197 $ $EXEC commit
198
199 Note that this modifies the files, but doesn’t commit them – you’ll likely want to run
200
201 $ git commit --amend -a
202
203 ${bold}EXIT STATUS${normal}
204 $EXEC exits with a status of zero if the changes were successfully
205 formatted, or if no formatting change was required. A status of two will
206 be returned if any errors were encountered.
207 EOM
208 )
209
210 HELP_REWRITE_BRANCH=$(cat << EOM
211 ${bold}NAME${normal}
212 $EXEC rewrite-branch - Format every commit in branch and rewrite history
213
214 ${bold}SYNOPSIS${normal}
215 $EXEC rewrite-branch
216
217 ${bold}DESCRIPTION${normal}
218 Reformat all commits in branch off master one-by-one. This will ${bold}rewrite
219 the branch history${normal} using the existing commit metadata!
220 It automatically detects all commits on your branch.
221
222 This is handy in case you want to format all of your branch commits
223 while keeping the commits.
224
225 This can also be helpful if you have multiple commits in your branch and
226 the changed files have been reformatted, i.e. where a git rebase would
227 fail in many ways over-and-over again.
228
229 You can achieve the same manually on a separate branch by:
230 ${italic}git checkout -n <original_commit>${normal},
231 ${italic}git clang-format${normal} and ${italic}git commit${normal} for each original commit in your branch.
232
233 ${bold}OPTIONS${normal}
234 None
235
236 ${bold}EXAMPLES${normal}
237 In your branch that you want to reformat. Commit all your changes prior
238 to calling:
239
240 $ $EXEC rewrite-branch
241
242 ${bold}EXIT STATUS${normal}
243 $EXEC exits with a status of zero if the changes were successfully
244 formatted, or if no formatting change was required. A status of two will
245 be returned if any errors were encountered.
246 EOM
247 )
248
249 # Error message on stderr
250 function Error {
251 echo "${bold}ERROR${normal}: $@" 1>&2
252 }
253
254 # Exit program (and reset path)
255 function ExitWith {
256 popd >/dev/null # we might have changed dir
257
258 if [ $# -ne 1 ]; then
259 # Huh? No exit value provided?
260 Error "Internal: ExitWith requires parameter"
261 exit $EXIT_CODE_ERROR
262 else
263 exit $1
264 fi
265 }
266
267 # Failure exit with error message
268 function Die {
269 Error $@
270 ExitWith $EXIT_CODE_ERROR
271 }
272
273 # Ensure required program exists. Exits with failure if not found.
274 # Call with
275 # RequireProgram ENVVAR_TO_SET program ...
276 # One can provide multiple alternative programs. Returns first program found in
277 # provided list.
278 function RequireProgram {
279 if [ $# -lt 2 ]; then
280 Die "Internal - RequireProgram: Need env and program parameters"
281 fi
282
283 # eat variable to set
284 local envvar=$1
285 shift
286
287 for program in $@; do
288 command -v $program >/dev/null 2>&1 # built-in which
289 if [ $? -eq 0 ]; then
290 eval "$envvar=$(command -v $program)"
291 return
292 fi
293 done
294
295 if [ $# -eq 1 ]; then
296 Die "$1 not found"
297 else
298 Die "None of $@ found"
299 fi
300 }
301
302 # Make sure we are running from the top-level git directory.
303 # Same approach as for setup-decoder.sh. Good enough.
304 # We could probably use git rev-parse --show-toplevel to do so, as long as we
305 # handle the libhtp subfolder correctly.
306 function SetTopLevelDir {
307 if [ -e ./src/suricata.c ]; then
308 # Do nothing.
309 true
310 elif [ -e ./suricata.c -o -e ../src/suricata.c ]; then
311 cd ..
312 else
313 Die "This does not appear to be a suricata source directory."
314 fi
315 }
316
317 # print help for given command
318 function HelpCommand {
319 local help_command=$1
320 local HELP_COMMAND=$(echo "HELP_$help_command" | sed "s/-/_/g" | tr [:lower:] [:upper:])
321 case $help_command in
322 branch|cached|check-branch|commit|rewrite-branch)
323 echo "${!HELP_COMMAND}";
324 ;;
325
326 "")
327 echo "$USAGE";
328 ;;
329
330 *)
331 echo "$USAGE";
332 echo "";
333 Die "No manual entry for $help_command"
334 ;;
335 esac
336 }
337
338 # Return first commit of branch (off master).
339 #
340 # Use $first_commit^ if you need the commit on master we branched off.
341 # Do not compare with master directly as it will diff with the latest commit
342 # on master. If our branch has not been rebased on the latest master, this
343 # would result in including all new commits on master!
344 function FirstCommitOfBranch {
345 local first_commit=$(git rev-list origin/master..HEAD | tail -n 1)
346 echo $first_commit
347 }
348
349 # Check if branch formatting is correct.
350 # Compares with master branch as baseline which means it's limited to branches
351 # other than master.
352 # Exits with 1 if not, 0 if ok.
353 function CheckBranch {
354 # check parameters
355 local quiet=0
356 local show_diff=0
357 local show_diffstat=0
358 local show_commits=0
359 local git_clang_format="$GIT_CLANG_FORMAT --diff"
360 while [[ $# -gt 0 ]]
361 do
362 case "$1" in
363 -q|--quiet)
364 quiet=1
365 shift
366 ;;
367
368 -d|--diff)
369 show_diff=1
370 shift
371 ;;
372
373 -s|--diffstat)
374 show_diffstat=1
375 git_clang_format="$GIT_CLANG_FORMAT_DIFFSTAT --diffstat"
376 shift
377 ;;
378
379 -c|--show-commits)
380 show_commits=1
381 shift
382 ;;
383
384 *) # unknown option
385 echo "$HELP_CHECK_BRANCH";
386 echo "";
387 Die "Unknown $command option: $1"
388 ;;
389 esac
390 done
391
392 if [ $show_diffstat -eq 1 -a $show_diff -eq 1 ]; then
393 echo "$HELP_CHECK_BRANCH";
394 echo "";
395 Die "Cannot combine $command options --diffstat with --diff"
396 fi
397
398 # Find first commit on branch. Use $first_commit^ if you need the
399 # commit on master we branched off.
400 local first_commit=$(FirstCommitOfBranch)
401
402 # git-clang-format is a python script that does not like SIGPIPE shut down
403 # by "| head" prematurely. Use work-around with writing to tmpfile first.
404 local format_changes="$git_clang_format --extensions c,h $first_commit^"
405 local tmpfile=$(mktemp /tmp/clang-format.check.XXXXXX)
406 $format_changes > $tmpfile
407 local changes=$(cat $tmpfile | head -1)
408 if [ $show_diff -eq 1 -o $show_diffstat -eq 1 ]; then
409 cat $tmpfile
410 echo ""
411 fi
412 rm $tmpfile
413
414 # Branch commits can help with trouble shooting. Print after diff/diffstat
415 # as output might be tail'd
416 if [ $show_commits -eq 1 ]; then
417 echo "Commits on branch (new -> old):"
418 git log --oneline $first_commit^..HEAD
419 echo ""
420 else
421 if [ $quiet -ne 1 ]; then
422 echo "First commit on branch: $first_commit"
423 fi
424 fi
425
426 # Exit code of git-clang-format is useless as it's 0 no matter if files
427 # changed or not. Check actual output. Not ideal, but works.
428 if [ "${changes}" != "no modified files to format" -a \
429 "${changes}" != "clang-format did not modify any files" ]; then
430 if [ $quiet -ne 1 ]; then
431 Error "Branch requires formatting"
432 Debug "View required changes with clang-format: ${italic}$format_changes${normal}"
433 Error "View required changes with: ${italic}$EXEC $command --diff${normal}"
434 Error "Use ${italic}$EXEC rewrite-branch${normal} or ${italic}$EXEC branch${normal} to fix formatting"
435 ExitWith $EXIT_CODE_FORMATTING_REQUIRED
436 else
437 return $EXIT_CODE_FORMATTING_REQUIRED
438 fi
439 else
440 if [ $quiet -ne 1 ]; then
441 echo "no modified files to format"
442 fi
443 return $EXIT_CODE_OK
444 fi
445 }
446
447 # Reformat all changes in branch as a separate commit.
448 function ReformatBranch {
449 # check parameters
450 local with_unstaged=
451 if [ $# -gt 1 ]; then
452 echo "$HELP_BRANCH";
453 echo "";
454 Die "Too many $command options: $1"
455 elif [ $# -eq 1 ]; then
456 if [ "$1" == "--force" -o "$1" == "-f" ]; then
457 with_unstaged='--force'
458 else
459 echo "$HELP_BRANCH";
460 echo "";
461 Die "Unknown $command option: $1"
462 fi
463 fi
464
465 # Find first commit on branch. Use $first_commit^ if you need the
466 # commit on master we branched off.
467 local first_commit=$(FirstCommitOfBranch)
468 echo "First commit on branch: $first_commit"
469
470 $GIT_CLANG_FORMAT --style file --extensions c,h $with_unstaged $first_commit^
471 if [ $? -ne 0 ]; then
472 Die "Cannot reformat branch. git clang-format failed"
473 fi
474 }
475
476 # Reformat changes in commit
477 function ReformatCommit {
478 # check parameters
479 local commit=HEAD^ # only most recent for now
480 if [ $# -gt 0 ]; then
481 echo "$HELP_MOST_RECENT";
482 echo "";
483 Die "Too many $command options: $1"
484 fi
485
486 $GIT_CLANG_FORMAT --style file --extensions c,h $commit
487 if [ $? -ne 0 ]; then
488 Die "Cannot reformat most recent commit. git clang-format failed"
489 fi
490 }
491
492 # Reformat currently staged changes
493 function ReformatCached {
494 # check parameters
495 local with_unstaged=
496 if [ $# -gt 1 ]; then
497 echo "$HELP_CACHED";
498 echo "";
499 Die "Too many $command options: $1"
500 elif [ $# -eq 1 ]; then
501 if [ "$1" == "--force" -o "$1" == "-f" ]; then
502 with_unstaged='--force'
503 else
504 echo "$HELP_CACHED";
505 echo "";
506 Die "Unknown $command option: $1"
507 fi
508 fi
509
510 $GIT_CLANG_FORMAT --style file --extensions c,h $with_unstaged
511 if [ $? -ne 0 ]; then
512 Die "Cannot reformat staging. git clang-format failed"
513 fi
514 }
515
516 # Reformat all commits of a branch (compared with master) and rewrites
517 # the history with the formatted commits one-by-one.
518 # This is helpful for quickly reformatting branches with multiple commits,
519 # or where the master version of a file has been reformatted.
520 #
521 # You can achieve the same manually by git checkout -n <commit>, git clang-format
522 # for each commit in your branch.
523 function ReformatCommitsOnBranch {
524 # Do not allow rewriting of master.
525 # CheckBranch below will also tell us there are no changes compared with
526 # master, but let's make this foolproof and explicit here.
527 local current_branch=$(git rev-parse --abbrev-ref HEAD)
528 if [ "$current_branch" == "master" ]; then
529 Die "Must not rewrite master branch history."
530 fi
531
532 CheckBranch "--quiet"
533 if [ $? -eq 0 ]; then
534 echo "no modified files to format"
535 else
536 # Only rewrite if there are changes
537 # Squelch warning. Our usage of git filter-branch is limited and should be ok.
538 # Should investigate using git-filter-repo in the future instead.
539 export FILTER_BRANCH_SQUELCH_WARNING=1
540
541 # Find first commit on branch. Use $first_commit^ if you need the
542 # commit on master we branched off.
543 local first_commit=$(FirstCommitOfBranch)
544 echo "First commit on branch: $first_commit"
545 # Use --force in case it's run a second time on the same branch
546 git filter-branch --force --tree-filter "$GIT_CLANG_FORMAT $first_commit^" -- $first_commit..HEAD
547 if [ $? -ne 0 ]; then
548 Die "Cannot rewrite branch. git filter-branch failed"
549 fi
550 fi
551 }
552
553 if [ $# -eq 0 ]; then
554 echo "$USAGE";
555 Die "Missing arguments. Call with one argument"
556 fi
557
558 SetTopLevelDir
559
560 RequireProgram GIT git
561 # ubuntu uses clang-format-{version} name for newer versions. fedora not.
562 RequireProgram GIT_CLANG_FORMAT git-clang-format-11 git-clang-format-10 git-clang-format-9 git-clang-format
563 GIT_CLANG_FORMAT_BINARY=clang-format
564 if [[ $GIT_CLANG_FORMAT =~ .*git-clang-format-11$ ]]; then
565 # default binary is clang-format, specify the correct version.
566 # Alternative: git config clangformat.binary "clang-format-11"
567 GIT_CLANG_FORMAT_BINARY="clang-format-11"
568 elif [[ $GIT_CLANG_FORMAT =~ .*git-clang-format-10$ ]]; then
569 # default binary is clang-format, specify the correct version.
570 # Alternative: git config clangformat.binary "clang-format-10"
571 GIT_CLANG_FORMAT_BINARY="clang-format-10"
572 elif [[ $GIT_CLANG_FORMAT =~ .*git-clang-format-9$ ]]; then
573 # default binary is clang-format, specify the correct version.
574 # Alternative: git config clangformat.binary "clang-format-9"
575 GIT_CLANG_FORMAT_BINARY="clang-format-9"
576 elif [[ $GIT_CLANG_FORMAT =~ .*git-clang-format$ ]]; then
577 Debug "Using regular clang-format"
578 else
579 Debug "Internal: unhandled clang-format version"
580 fi
581
582 # enforce minimal clang-format version as required by .clang-format
583 clang_format_version=$($GIT_CLANG_FORMAT_BINARY --version | sed 's/.*clang-format version \([0-9]*\.[0-9]*\.[0-9]*\).*/\1/')
584 Debug "Found clang-format version: $clang_format_version"
585 clang_format_version_major=$(echo $clang_format_version | sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1/')
586 Debug "clang-format version major: $clang_format_version_major"
587 if [ $((clang_format_version_major + 0)) -lt $((CLANG_FORMAT_REQUIRED_VERSION + 0)) ]; then
588 Die "Require clang version $CLANG_FORMAT_REQUIRED_VERSION, found $clang_format_version_major ($clang_format_version)."
589 fi
590
591 # overwite git-clang-version for --diffstat as upstream does not have that yet
592 RequireProgram GIT_CLANG_FORMAT_DIFFSTAT scripts/git-clang-format-custom
593 if [ "$GIT_CLANG_FORMAT_BINARY" != "clang-format" ]; then
594 GIT_CLANG_FORMAT="$GIT_CLANG_FORMAT --binary $GIT_CLANG_FORMAT_BINARY"
595 GIT_CLANG_FORMAT_DIFFSTAT="$GIT_CLANG_FORMAT_DIFFSTAT --binary $GIT_CLANG_FORMAT_BINARY"
596 fi
597 Debug "Using $GIT_CLANG_FORMAT"
598 Debug "Using $GIT_CLANG_FORMAT_DIFFSTAT"
599
600 command_rc=0
601 command=$1
602 case $command in
603 branch)
604 shift;
605 ReformatBranch "$@";
606 ;;
607
608 check-branch)
609 shift;
610 CheckBranch "$@";
611 command_rc=$?;
612 ;;
613
614 cached)
615 shift;
616 ReformatCached "$@";
617 ;;
618
619 commit)
620 shift;
621 ReformatCommit "$@";
622 ;;
623
624 rewrite-branch)
625 ReformatCommitsOnBranch
626 ;;
627
628 help)
629 shift;
630 HelpCommand $1;
631 ;;
632
633 -h|--help)
634 echo "$USAGE";
635 ;;
636
637 *)
638 Die "$EXEC: '$command' is not a command. See '$EXEC --help'"
639 ;;
640 esac
641
642 ExitWith $command_rc