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|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
|