This python script is more configurable than the shell script that it replaces, and makes it clearer when looking at the makefile what it's acting on. Signed-off-by: Michael Richters <gedankenexperimenter@gmail.com>pull/1157/head
parent
3c3ff1efc8
commit
da03cbeccb
@ -0,0 +1,271 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# ------------------------------------------------------------------------------
|
||||
# Copyright (c) 2022 Michael Richters <gedankenexperimenter@gmail.com>
|
||||
|
||||
# This is free and unencumbered software released into the public domain.
|
||||
|
||||
# Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
# distribute this software, either in source code form or as a compiled
|
||||
# binary, for any purpose, commercial or non-commercial, and by any
|
||||
# means.
|
||||
|
||||
# In jurisdictions that recognize copyright laws, the author or authors
|
||||
# of this software dedicate any and all copyright interest in the
|
||||
# software to the public domain. We make this dedication for the benefit
|
||||
# of the public at large and to the detriment of our heirs and
|
||||
# successors. We intend this dedication to be an overt act of
|
||||
# relinquishment in perpetuity of all present and future rights to this
|
||||
# software under copyright law.
|
||||
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
# OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
# For more information, please refer to <http://unlicense.org/>
|
||||
# ------------------------------------------------------------------------------
|
||||
"""This script runs clang-format on Kaleidoscope's codebase."""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
def parse_args(args):
|
||||
"""Parse command line parameters
|
||||
|
||||
Args:
|
||||
args (List[str]): command line parameters as list of strings
|
||||
(for example ``["--help"]``).
|
||||
|
||||
Returns:
|
||||
:obj:`argparse.Namespace`: command line parameters namespace
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="""
|
||||
Recursively search specified directories and format source files with
|
||||
clang-format. By default, it operates on Arduino C++ source files with
|
||||
extensions: *.{cpp,h,hpp,inc,ino}.""")
|
||||
parser.add_argument(
|
||||
'-v',
|
||||
'--verbose',
|
||||
dest='loglevel',
|
||||
help="Verbose output",
|
||||
action='store_const',
|
||||
const=logging.INFO,
|
||||
)
|
||||
parser.add_argument(
|
||||
'-q',
|
||||
'--quiet',
|
||||
dest='loglevel',
|
||||
help="Suppress all non-error output",
|
||||
action='store_const',
|
||||
const=logging.ERROR,
|
||||
)
|
||||
parser.add_argument(
|
||||
'-X',
|
||||
'--exclude-dir',
|
||||
metavar="<path>",
|
||||
dest='exclude_dirs',
|
||||
help="Exclude dir from search (path relative to the pwd)",
|
||||
action='append',
|
||||
default=[],
|
||||
)
|
||||
parser.add_argument(
|
||||
'-x',
|
||||
'--exclude-file',
|
||||
metavar="<file>",
|
||||
dest='exclude_files',
|
||||
help="Exclude <file> (base name only, not a full path) from formatting",
|
||||
action='append',
|
||||
default=[],
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e',
|
||||
'--regex',
|
||||
metavar="<regex>",
|
||||
dest='src_re_str',
|
||||
help="Regular expression for matching source file names",
|
||||
default=r'\.(cpp|h|hpp|inc|ino)$',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-f',
|
||||
'--force',
|
||||
action='store_true',
|
||||
help="Format code even if there are unstaged changes",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--check',
|
||||
action='store_true',
|
||||
help="Check for changes after formatting",
|
||||
)
|
||||
parser.add_argument(
|
||||
'targets',
|
||||
metavar="<search_dir>",
|
||||
nargs='+',
|
||||
help="""A list of files and/or directories to search for source files to format""",
|
||||
)
|
||||
return parser.parse_args(args)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
def setup_logging(loglevel):
|
||||
"""Setup basic logging
|
||||
|
||||
Args:
|
||||
loglevel (int): minimum loglevel for emitting messages
|
||||
"""
|
||||
logformat = "%(message)s"
|
||||
logging.basicConfig(
|
||||
level=loglevel,
|
||||
stream=sys.stdout,
|
||||
format=logformat,
|
||||
datefmt="",
|
||||
)
|
||||
return logging.getLogger()
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
def format_code(path, opts, clang_format_cmd):
|
||||
"""Run clang-format on a directory."""
|
||||
logging.info("Formatting code in %s...", path)
|
||||
|
||||
src_regex = re.compile(opts.src_re_str)
|
||||
|
||||
src_files = []
|
||||
|
||||
for root, dirs, files in os.walk(path):
|
||||
for exclude_path in opts.exclude_dirs:
|
||||
exclude_path = exclude_path.rstrip(os.path.sep)
|
||||
if os.path.dirname(exclude_path) == root:
|
||||
exclude_dir = os.path.basename(exclude_path)
|
||||
if exclude_dir in dirs:
|
||||
dirs.remove(exclude_dir)
|
||||
|
||||
for name in files:
|
||||
if name in opts.exclude_files:
|
||||
continue
|
||||
if src_regex.search(name):
|
||||
src_files.append(os.path.join(root, name))
|
||||
|
||||
proc = subprocess.run(clang_format_cmd + src_files)
|
||||
if proc.returncode != 0:
|
||||
logging.error("Error: clang-format returned non-zero status: %s", proc.returncode)
|
||||
return
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
def build_file_list(path, src_regex):
|
||||
"""Docstring"""
|
||||
|
||||
# If the specified path is a filename, return it (as a list), regardless of
|
||||
# whether or not it matches the regex.
|
||||
if os.path.isfile(path):
|
||||
return [path]
|
||||
|
||||
# If the specified path is not valid, just return an empty list.
|
||||
if not os.path.isdir(path):
|
||||
return []
|
||||
|
||||
# The specified path is a directory, so we search recursively for files
|
||||
# contained therein that match the specified regular expression.
|
||||
source_files = []
|
||||
for root, dirs, files in os.walk(path):
|
||||
# First, ignore all dotfiles (and directories).
|
||||
dotfiles = set(glob.glob('.*'))
|
||||
dirs = set(dirs) - dotfiles
|
||||
files = set(dirs) - dotfiles
|
||||
|
||||
# Check for a list of file glob patterns that should be excluded.
|
||||
if IWYU_IGNORE_FILE in files:
|
||||
with open(os.path.join(root, IWYU_IGNORE_FILE)) as f:
|
||||
for pattern in f.read().splitlines():
|
||||
matches = set(glob.glob(os.path.join(root, pattern)))
|
||||
dirs = set(dirs) - matches
|
||||
files = set(files) - matches
|
||||
|
||||
# Add all matching files to the list of source files to be formatted.
|
||||
for f in filter(src_regex.search, files):
|
||||
source_files.append(os.path.join(root, f))
|
||||
|
||||
return source_files
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
def warn_if_output(byte_str, msg):
|
||||
"""Convert a string of bytes to a UTF-8 string, break it on newlines. If
|
||||
there is any output, print a warning message, followed by each line,
|
||||
indented by four spaces."""
|
||||
lines = byte_str.decode('utf-8').splitlines()
|
||||
if '' in lines:
|
||||
lines.remove('')
|
||||
if len(lines) > 0:
|
||||
logging.warning('%s', msg)
|
||||
for line in lines:
|
||||
logging.warning(' %s', line)
|
||||
return
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
def main(cli_args):
|
||||
"""Parse command-line arguments and format source files."""
|
||||
args = parse_args(cli_args)
|
||||
if args.loglevel is None:
|
||||
args.loglevel = logging.WARNING
|
||||
setup_logging(args.loglevel)
|
||||
|
||||
clang_format = os.getenv('CLANG_FORMAT_CMD')
|
||||
if clang_format is None:
|
||||
clang_format = 'clang-format'
|
||||
|
||||
clang_format_cmd = [clang_format, '-i']
|
||||
|
||||
git_diff_cmd = ['git', 'diff', '--exit-code']
|
||||
|
||||
proc = subprocess.run(git_diff_cmd + ['--name-only'], capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
warn_if_output(proc.stdout, 'Warning: you have unstaged changes to these files:')
|
||||
if not args.force:
|
||||
logging.warning(
|
||||
'Formatting aborted. Stage your changes or use --force to override.')
|
||||
sys.exit(proc.returncode)
|
||||
|
||||
if args.loglevel >= logging.WARNING:
|
||||
git_diff_cmd.append('--quiet')
|
||||
elif args.loglevel <= logging.INFO:
|
||||
git_diff_cmd.append('--name-only')
|
||||
|
||||
for path in args.targets:
|
||||
format_code(path, args, clang_format_cmd)
|
||||
|
||||
if args.check:
|
||||
logging.warning('Checking for changes made by the formatter...')
|
||||
|
||||
proc = subprocess.run(git_diff_cmd + ['--cached'], capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
logging.warning(
|
||||
'Warning: Your working tree has staged changes. ' +
|
||||
'Committed changes might not pass this check.')
|
||||
warn_if_output(proc.stdout, 'The following files have unstaged changes:')
|
||||
|
||||
proc = subprocess.run(git_diff_cmd, capture_output=True)
|
||||
if proc.returncode != 0:
|
||||
warn_if_output(proc.stdout, 'The following files have changes:')
|
||||
logging.error(
|
||||
'Check failed: Please commit formatting changes before submitting.')
|
||||
sys.exit(proc.returncode)
|
||||
return
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
@ -1,25 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Allow the caller to specify a particular version of clang-format to use:
|
||||
: "${CLANG_FORMAT_CMD:=clang-format}"
|
||||
: "${KALEIDOSCOPE_DIR:=$(pwd)}"
|
||||
cd "${KALEIDOSCOPE_DIR}" || exit 1
|
||||
|
||||
# Find all *.cpp and *.h files, except those in `testing/googletest/` and files
|
||||
# generated by testcase scripts, and run `clang-format` on them:
|
||||
find ./* -type f \( -name '*.h' -o -name '*.cpp' \) \
|
||||
-not \( -path './testing/googletest/*' -o -name 'generated-testcase.cpp' \) \
|
||||
-print0 | \
|
||||
xargs -0 "${CLANG_FORMAT_CMD}" -i
|
||||
: "${VERBOSE:=}"
|
||||
|
||||
# If we get the `--check` option, return an error if there are any changes to
|
||||
# the git working tree after running `clang-format`:
|
||||
if [[ $1 == '--check' ]]; then
|
||||
|
||||
if ! git diff --quiet; then
|
||||
cat >&2 <<EOF
|
||||
Differences found between git head and working tree. Either 'clang-format' made
|
||||
formatting changes to your code, or you had uncommitted changes in your working
|
||||
tree. Please remember to run 'bin/format-code.sh' before committing changes.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
"${KALEIDOSCOPE_DIR}/bin/format-code.py" \
|
||||
--exclude-dir 'testing/googletest' \
|
||||
--exclude-file 'generated-testcase.cpp' \
|
||||
--verbose \
|
||||
src plugins testing
|
||||
|
Loading…
Reference in new issue