#!/bin/bash # This script will run clang-format on all modified, non-3rd-party C++/Header files. # Optionally runs cmake-format on all modified cmake files. # Uses clang-format cmake-format git diff find # Never, ever, should this receive a path with a newline in it. Don't bother proofing it for that. set -o pipefail # go to the project root directory, this file should be located in the project root directory olddir="$PWD" cd "${BASH_SOURCE%/*}/" || exit 2 # could not find path, this could happen with special links etc. # defaults include=("common" \ "cockatrice/src" \ "dbconverter/src" \ "oracle/src" \ "servatrice/src" \ "tests") exclude=("servatrice/src/smtp" \ "common/sfmt" \ "common/lib" \ "oracle/src/zip" \ "oracle/src/lzma" \ "oracle/src/qt-json") exts=("cpp" "h" "proto") cf_cmd="clang-format" branch="origin/master" cmakefile="CMakeLists.txt" cmakedir="cmake/.*\\.cmake" cmakeinclude=("cmake/gtest-CMakeLists.txt.in") color="--" # parse options while [[ $* ]]; do case "$1" in '-b'|'--branch') branch=$2 set_branch=1 shift 2 ;; '--cmake') do_cmake=1 shift ;; '-c'|'--color-diff') color="--color=always" mode="diff" shift ;; '-d'|'--diff') mode="diff" shift ;; '-h'|'--help') cat <s are given, all source files in those directories of the project root path are formatted. To only format changed files in these directories use the --branch option in combination. has to be a path relative to the project root path or a full path inside $PWD. USAGE: $0 [option] [--branch ] [ ...] DEFAULTS: Current includes are: ${include[@]/%/ } Default excludes are: ${exclude[@]/%/ } OPTIONS: -b, --branch Compare to this git branch and format only files that differ. If unspecified it defaults to origin/master. To not compare to a branch this has to be explicitly set to "". When not comparing to a branch, git will not be used at all and every source file in the entire project will be parsed. --cmake Use cmake-format to format cmake files as well. -c, --color-diff Display a colored diff. Implies --diff. Only available on systems which support 'diff --color'. -d, --diff Display a diff. Implies --test. -h, --help Display this message and exit. -n, --names Display a list of filenames that require formatting. Implies --test. -t, --test Do not edit files in place. Set exit code to 1 if changes are required. --cf-version Print the version of clang-format being used before continuing. EXIT CODES: 0 on a successful format or if no files require formatting. 1 if a file requires formatting. 2 if given incorrect arguments. 3 if clang-format could not be found. EXAMPLES: $0 --branch $USER/patch-2 ${include[0]} Formats all changed files compared to the git branch "$USER/patch-2" in the directory ${include[0]}. $0 --test . || echo "code requires formatting" Tests if the source files in the current directory are correctly formatted and prints an error message if formatting is required. $0 --cmake --branch "" "" Unconditionally format all cmake files and no source files. EOM exit 0 ;; '-n'|'--names') mode="name" shift ;; '-t'|'--test') mode="code" shift ;; '--cf-version') print_version=1 shift ;; '--') dashdash=1 shift ;; *) if [[ ! $dashdash && $1 =~ ^-- ]]; then echo "error in parsing arguments of $0: $1 is an unrecognized option" >&2 exit 2 # input error fi if [[ ! $1 ]] || next_dir=$(cd "$olddir" && cd -- "$1" && pwd); then if ! [[ $set_include ]]; then include=() # remove default includes set_include=1 fi if [[ $1 ]]; then if [[ $next_dir != $PWD/* ]]; then echo "error in parsing arguments of $0: $next_dir is not in $PWD" >&2 exit 2 # input error fi include+=("$next_dir") fi else echo "error in parsing arguments of $0: $1 is not a directory" >&2 exit 2 # input error fi if ! [[ $set_branch ]]; then unset branch # unset branch if not set explicitly fi shift ;; esac done # check availability of clang-format if ! hash $cf_cmd 2>/dev/null; then echo "could not find $cf_cmd" >&2 # find any clang-format-x.x in /usr/bin cf_cmd=$(find /usr/bin -regex '.*/clang-format-[0-9]+\.[0-9]+' -print -quit) if [[ $cf_cmd ]]; then echo "found $cf_cmd instead" >&2 else exit 3 # special exit code for missing dependency fi fi # check availability of cmake-format if [[ $do_cmake ]] && ! hash cmake-format 2>/dev/null; then echo "could not find cmake-format" >&2 exit 3 fi if [[ $branch ]]; then # get all dirty files through git if ! base=$(git merge-base "$branch" HEAD); then echo "could not find git merge base" >&2 exit 2 # input error fi mapfile -t basenames < <(git diff --diff-filter=d --name-only "$base") names=() for ex in "${exts[@]}"; do for path in "${include[@]}"; do for name in "${basenames[@]}"; do rx="^$path/.*\\.$ex$" if [[ $name =~ $rx ]]; then names+=("$name") fi done done done if [[ $do_cmake ]]; then cmake_names=() for name in "${basenames[@]}"; do dirrx="^$cmakedir$" filerx="(^|/)$cmakefile$" if [[ $name =~ $dirrx || $name =~ $filerx ]]; then cmake_names+=("$name") fi for include in "${cmakeinclude[@]}"; do if [[ $name == "$include" ]]; then cmake_names+=("$name") fi done done fi else exts_o=() for ext in "${exts[@]}"; do exts_o+=(-o -name "*\\.$ext") done unset "exts_o[0]" # remove first -o mapfile -t names < <(find "${include[@]}" -type f "${exts_o[@]}") if [[ $do_cmake ]]; then mapfile -t cmake_names < <(find . -maxdepth 2 -type f -name "$cmakefile" -o -path "./${cmakedir/.}") cmake_names+=("${cmakeinclude[@]}") fi fi # filter excludes for path in "${exclude[@]}"; do for i in "${!names[@]}"; do rx="^$path/" if [[ ${names[$i]} =~ $rx ]]; then unset "names[$i]" fi done done # optionally print version if [[ $print_version ]]; then $cf_cmd -version [[ $do_cmake ]] && echo "cmake-format $(cmake-format --version)" echo "----------" fi if [[ ! ${cmake_names[*]} ]]; then unset do_cmake fi if [[ ! ( ${names[*]} || $do_cmake ) ]]; then exit 0 # nothing to format means format is successful! fi # format case $mode in diff) declare -i code=0 for name in "${names[@]}"; do if ! $cf_cmd "$name" | diff "$name" - -p "$color"; then code=1 fi done for name in "${cmake_names[@]}"; do if ! cmake-format "$name" | diff "$name" - -p "$color"; then code=1 fi done exit $code ;; name) declare -i code=0 for name in "${names[@]}"; do if ! $cf_cmd "$name" | diff "$name" - -q >/dev/null; then echo "$name" code=1 fi done for name in "${cmake_names[@]}"; do if ! cmake-format "$name" --check; then echo "$name" code=1 fi done exit $code ;; code) for name in "${names[@]}"; do $cf_cmd "$name" | diff "$name" - -q >/dev/null || exit 1 done for name in "${cmake_names[@]}"; do cmake-format "$name" --check || exit 1 done ;; *) if [[ "${names[*]}" ]]; then $cf_cmd -i "${names[@]}" fi if [[ $do_cmake ]]; then cmake-format -i "${cmake_names[@]}" fi ;; esac