diff --git a/scripts/format-code b/scripts/format-code new file mode 100644 index 000000000..476406f2a --- /dev/null +++ b/scripts/format-code @@ -0,0 +1,304 @@ +#!/usr/bin/env bash + +# Copyright (c) Prevail Verifier contributors. +# SPDX-License-Identifier: MIT + +##============================================================================== +## +## 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 + mapfile -t userFiles <<< "$(git diff --cached --name-only --diff-filter=ACMR)" + ;; + + --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< /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='17.0.3' + # 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" diff --git a/scripts/format-code.ps1 b/scripts/format-code.ps1 new file mode 100644 index 000000000..2995473cc --- /dev/null +++ b/scripts/format-code.ps1 @@ -0,0 +1,337 @@ +# Copyright (c) Prevail Verifier contributors. +# SPDX-License-Identifier: MIT + +$usage=$false; +$quiet=$false; +$verbose=$false; +$whatif=$false; +[System.Collections.ArrayList]$userExcludeDirs=@(); +[System.Collections.ArrayList]$userIncludeExts=@(); +[System.Collections.ArrayList]$excludeDirs=@(); +[System.Collections.ArrayList]$includeExts=@(); +[System.Collections.ArrayList]$userFiles=@(); + + +##============================================================================== +## +## Echo if verbose flag (ignores quiet flag) +## +##============================================================================== +function log_verbose() +{ + if ($verbose) { + Write-Host "$args" + } +} + +##============================================================================== +## +## Echo if whatif flag is specified but not quiet flag +## +##============================================================================== +function log_whatif() +{ + if ( $whatif -and -not $quiet) + { + Write-Host "$args" + } +} + +##============================================================================== +## +## Process command-line options +## +## Note that in Powershell syntax, fallthrough does not work as such, +## instead one must compare against multiple values. For a discussion, see +## https://stackoverflow.com/questions/3493731/whats-the-powershell-syntax-for-multiple-values-in-a-switch-statement +## +##============================================================================== + +foreach ($opt in $args) +{ + switch -regex ($opt) { + + { @("-h", "--help") -contains $_ } + { + $usage=$true; + break; + } + + { @("-q", "--quiet") -contains $_ } + { + $quiet=$true; + break; + } + + { @("-s", "--staged") -contains $_ } + { + $userFiles=@(git diff --cached --name-only --diff-filter=ACMR); + break; + } + + { @("-v", "--verbose") -contains $_ } + { + $verbose=$true; + break; + } + + { @("-w", "--whatif") -contains $_ } + { + $whatif=$true; + break; + } + + "--exclude-dirs=*" { + $userExcludeDirs=($opt -split "=")[1]; + break; + } + + "--include-exts=*" { + $userIncludeExts=($opt -split "=")[1]; + break; + } + + "--files=*" { + $userFiles=($opt -split "=")[1]; + break; + } + default { + Write-Error "$PSCommandPath unknown option: $opt" + exit 1 + break; + } + } +} + +##============================================================================== +## +## Display help +## +##============================================================================== + +if ( $usage ) { + $usageMessage = @' + +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" + +'@ + Write-Host "$usageMessage" + exit 0 +} + +##============================================================================== +## +## Determine parameters for finding files to format +## +##============================================================================== +function get_find_args() +{ + $defaultExcludeDirs=@( ".git", "external", "packages", "x64" ); + $defaultIncludeExts=@( "h", "hpp", "c", "cpp", "idl", "acf" ) + + $findargs='get-childitem -Recurse -Name "*" -Path "." ' + if ( !($userIncludeExts) ) { + # not local as this is used in get_file_list() too + $includeExts.AddRange($defaultIncludeExts) + } + else + { + log_verbose "Using user extension inclusions: $userIncludeExts" + $includeExts.AddRange($userIncludeExts) + } + + $findargs+=" -Include @( " + foreach ($ext in $includeExts) + { + $findargs+=("'*."+"$ext'") + if ($includeExts.IndexOf($ext) -lt $includeExts.count-1) + { + $findargs+=", " + } + } + $findargs+=") " + + if ( !($userExcludeDirs) ) { + $excludeDirs.AddRange($defaultExcludeDirs) + } + else { + log_verbose "Using user directory exclusions: $userExcludeDirs" + $excludeDirs.AddRange($userExcludeDirs) + } + + $findargs+=" | where { " + foreach ($dir in $excludeDirs) + { + $findargs+='$_ -notlike ' + $findargs+= "'$dir"+"\*'" + if ($excludeDirs.IndexOf($dir) -lt $excludeDirs.count-1) + { + $findargs+=" -and " + } + } + $findargs+="} " + + return $findargs +} + +function get_file_list() +{ + if ( !($userFiles) ) { + $file_list = Invoke-Expression($findargs) + if ( $file_list.count -eq 0 ) { + Write-Host "No files were found to format!" + exit 1 + } + } + else { + log_verbose "Using user files: $userfiles" + $file_list=@() + foreach ( $file_name in $userfiles ) { + $user_file_name = get-ChildItem -Path '.' -Name $file_name + $file = New-Object System.IO.FileInfo($user_file_name) + foreach ( $ext in $includeExts ) { + if ( $file.Extension -eq ".$ext" ) { + $file_list += $file_name + log_verbose "Checking user file: $file_name" + break; + } + } + } + } + return $file_list +} + +$global:cf="" + +##============================================================================== +## +## Check for installed clang-format tool +## +##============================================================================== +function check_clang-format() +{ + # Windows does not have a clang-format-7 executable + + + $required_cfver='17.0.3' + + try { + $cfver=(( Invoke-Expression "clang-format --version" 2> $null ) -split " ")[2] + } + catch { + Write-Host "clang-format not installed" + return $false + } + + $req_ver = $required_cfver -split '.' + $cf_ver = $cfver -split '.' + + for ($i = 0; $i -lt 3; $i++) + { + if ( $cf_ver[$i] -gt $req_ver[$i]) + { + return $true + } + + if ( $cf_ver[$i] -lt $req_ver[$i]) + { + Write-Host "Required version of clang-format is $required_cfver. Current version is $cfver" + return $false + } + # Equal just keeps going + } + $global:cf="clang-format" + return $true +} + + +##============================================================================== +## +## Mainline: Call clang-format for each file to be formatted +## +##============================================================================== + +if (!(check_clang-format)) # getting the filelist takes a few seconds. If we cant format we may as well exit now. +{ + exit -1 +} + +$findargs = get_find_args; +$filelist = get_file_list; +$filecount=0 +$changecount=0 + +$cfargs="$global:cf -style=file" +if ( !$whatif ) { + $cfargs="$cfargs -i" +} + +foreach ( $file in $filelist ) { + $filecount+=1; + $cf="$cfargs $file" + + + if ( $whatif ) { + log_whatif "Formatting $file ..." + ( Invoke-Expression ($cf) ) | Compare-Object (get-content $file) + } + else { + if ( $verbose ) { + log_verbose "Formatting $file ..." + Invoke-Expression $cf + } + else { + Invoke-Expression $cf > $null + } + } + if ( $? ) { + if ( $whatif ) { + $changecount++ + } + } + else { + Write-Host "clang-format failed on file: $file." + } +} + +log_whatif "$filecount files processed, $changecount changed." + +# If files are being edited, this count is zero so we exit with success. +exit $changecount diff --git a/scripts/pre-commit b/scripts/pre-commit index e558ec8fb..a10ce5124 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -37,6 +37,10 @@ fi scripts=$(git rev-parse --show-toplevel)/scripts +if ! "$scripts/format-code" --quiet --whatif --files="${files[*]}"; then + exit_ "Commit failed: to fix the formatting please run './scripts/format-code --staged' in bash or '.\\scripts\\format-code.ps1 --staged' in powershell" +fi + if ! "$scripts/check-license.sh" "${files[@]}"; then exit_ "Commit failed: please add license headers to the above files" fi