blob: b1792a619175c2a3b08680fa5f93c24d6fbb0ad7 [file] [log] [blame]
#!/usr/bin/env bash
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
changed_files()
{
userFiles=""
while IFS= read -r line; do
userFiles+=("$line")
done < <(git diff --cached --name-only --diff-filter=ACMR)
}
##==============================================================================
##
## Echo if verbose flag (ignores quiet flag)
##
##==============================================================================
log_verbose()
{
if [[ ${verbose} -eq 1 ]]; then
echo "$1"
fi
}
##==============================================================================
##
## Echo if whatif flag is specified but not quiet flag
##
##==============================================================================
log_whatif()
{
if [[ ${whatif} -eq 1 && ${quiet} -ne 1 ]]; then
echo "$1"
fi
}
##==============================================================================
##
## Process command-line options:
##
##==============================================================================
for opt in "$@"
do
case $opt in
-h | --help)
usage=1
;;
-v | --verbose)
verbose=1
;;
-q | --quiet)
quiet=1
;;
-w | --whatif)
whatif=1
;;
-s | --staged)
# Split into array
changed_files
;;
--exclude-dirs=*)
userExcludeDirs="${opt#*=}"
;;
--include-exts=*)
userIncludeExts="${opt#*=}"
;;
--files=*)
read -ra userFiles <<< "${opt#*=}"
;;
*)
echo "$0: unknown option: $opt"
exit 1
;;
esac
done
##==============================================================================
##
## Display help
##
##==============================================================================
if [[ ${usage} -eq 1 ]]; then
cat<<EOF
OVERVIEW:
Formats all C/C++ source files based on the .clang-format rules
$ format-code [-h] [-q] [-s] [-v] [-w] [--exclude-dirs="..."] [--include-exts="..."] [--files="..."]
OPTIONS:
-h, --help Print this help message.
-q, --quiet Display only clang-format output and errors.
-s, --staged Only format files which are staged to be committed.
-v, --verbose Display verbose output.
-w, --whatif Run the script without actually modifying the files
and display the diff of expected changes, if any.
--exclude-dirs Subdirectories to exclude. If unspecified, then
./external, ./packages and ./x64 are excluded.
All subdirectories are relative to the current path.
--include-exts File extensions to include for formatting. If
unspecified, then *.h, *.hpp, *.c, *.cpp, *idl, and
*.acf are included.
--files Only run the script against the specified files from
the current directory.
EXAMPLES:
To determine what lines of each file in the default configuration would be
modified by format-code, you can run from the root folder:
$ ./scripts/format-code -w
To update only all .c and .cpp files in src/ except for src/tools/netsh, you
can run from the src folder:
src$ ../scripts/format-code --exclude-dirs="tools/netsh" \
--include-exts="c cpp"
To run only against a specified set of comma separated files in the current directory:
$ ./scripts/format-code -w --files="file1 file2"
EOF
exit 0
fi
##==============================================================================
##
## Check for acceptable version of clang-format
##
##==============================================================================
check_version()
{
# shellcheck disable=SC2206
v1=(${1//./ })
# shellcheck disable=SC2206
v2=(${2//./ })
if [[ ${#v1[@]} -ne ${#v2[@]} ]]; then
echo "Cannot parse clang-format version number: $2"
exit 1
fi
for ((i=0; i<${#v1[@]}; i++));
do
# Because clang-format is not invariant across versions, this
# is an explicit equivalence check.
if [[ ${v1[$i]} -ne ${v2[$i]} ]]; then
echo "Warning: format-code prefers clang-format version $1, installed version is $2"
fi
done
}
##==============================================================================
##
## Check for installed clang-format tool
##
##==============================================================================
check_clang-format()
{
# First check explicitly for the clang-format-7 executable.
cf=$(command -v clang-format-7 2> /dev/null)
if [[ -x ${cf} ]]; then
cf="clang-format-7"
return
else
cf=$(command -v clang-format 2> /dev/null)
if [[ ! -x ${cf} ]]; then
echo "clang-format is not installed"
exit 1
fi
cf="clang-format"
fi
local required_cfver='11.0.1'
# shellcheck disable=SC2155
local cfver=$(${cf} --version | grep -o -E '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
check_version "${required_cfver}" "${cfver}"
}
check_clang-format
##==============================================================================
##
## Determine parameters for finding files to format
##
##==============================================================================
findargs='find .'
get_find_args()
{
local defaultExcludeDirs='./.git ./external ./packages ./x64'
local defaultIncludeExts='h hpp c cpp idl acf'
if [[ -z ${userExcludeDirs} ]]; then
read -r -a excludeDirs <<< "${defaultExcludeDirs}"
else
log_verbose "Using user directory exclusions: ${userExcludeDirs}"
read -r -a excludeDirs <<< "${userExcludeDirs}"
fi
for exc in "${excludeDirs[@]}"
do
findargs="${findargs} ! \( -path '${exc}' -prune \)"
done
if [[ -z ${userIncludeExts} ]]; then
# not local as this is used in get_file_list() too
read -r -a includeExts <<< "${defaultIncludeExts}"
else
log_verbose "Using user extension inclusions: ${userIncludeExts}"
read -r -a includeExts <<< "${userIncludeExts}"
fi
findargs="${findargs} \("
for ((i=0; i<${#includeExts[@]}; i++));
do
findargs="${findargs} -iname '*.${includeExts[$i]}'"
if [[ $((i + 1)) -lt ${#includeExts[@]} ]]; then
findargs="${findargs} -o"
fi
done
findargs="${findargs} \)"
}
get_find_args
log_verbose "Query for files for format:"
log_verbose "${findargs}"
log_verbose ""
##==============================================================================
##
## Call clang-format for each file to be formatted
##
##==============================================================================
filecount=0
changecount=0
cfargs="${cf} -style=file"
if [[ ${whatif} -ne 1 ]]; then
cfargs="${cfargs} -i"
fi
get_file_list()
{
if [[ -z "${userFiles[*]}" ]]; then
mapfile -t file_list < <(eval "$findargs")
if [[ ${#file_list[@]} -eq 0 ]]; then
echo "No files were found to format!"
exit 1
fi
else
log_verbose "Using user files: ${userFiles[*]}"
file_list=()
for file in "${userFiles[@]}"; do
for ext in "${includeExts[@]}"; do
if [[ ${file##*\.} == "$ext" ]]; then
file_list+=( "$file" )
log_verbose "Checking user file: ${file}"
break
fi
done
done
fi
}
get_file_list
for file in "${file_list[@]}"
do
((filecount+=1))
cf="${cfargs} $file"
log_whatif "Formatting $file ..."
if [[ ${whatif} -eq 1 ]]; then
${cf} | diff -u "$file" -
else
if [[ ${verbose} -eq 1 ]]; then
${cf}
else
${cf} > /dev/null
fi
fi
#shellcheck disable=SC2181
if [[ $? -ne 0 ]]; then
if [[ ${whatif} -eq 1 ]]; then
((changecount+=1))
else
echo "clang-format failed on file: $file."
fi
fi
done
log_whatif "${filecount} files processed, ${changecount} changed."
# If files are being edited, this count is zero so we exit with success.
exit "$changecount"