# @file DebugMacroCheck.py | |
# | |
# A script that checks if DEBUG macros are formatted properly. | |
# | |
# In particular, that print format specifiers are defined | |
# with the expected number of arguments in the variable | |
# argument list. | |
# | |
# Copyright (c) Microsoft Corporation. All rights reserved. | |
# SPDX-License-Identifier: BSD-2-Clause-Patent | |
## | |
from argparse import RawTextHelpFormatter | |
import logging | |
import os | |
import re | |
import regex | |
import sys | |
import shutil | |
import timeit | |
import yaml | |
from edk2toollib.utility_functions import RunCmd | |
from io import StringIO | |
from pathlib import Path, PurePath | |
from typing import Dict, Iterable, List, Optional, Tuple | |
PROGRAM_NAME = "Debug Macro Checker" | |
class GitHelpers: | |
""" | |
Collection of Git helpers. | |
Will be moved to a more generic module and imported in the future. | |
""" | |
@staticmethod | |
def get_git_ignored_paths(directory_path: PurePath) -> List[Path]: | |
"""Returns ignored files in this git repository. | |
Args: | |
directory_path (PurePath): Path to the git directory. | |
Returns: | |
List[Path]: List of file absolute paths to all files ignored | |
in this git repository. If git is not found, an empty | |
list will be returned. | |
""" | |
if not shutil.which("git"): | |
logging.warn( | |
"Git is not found on this system. Git submodule paths will " | |
"not be considered.") | |
return [] | |
out_stream_buffer = StringIO() | |
exit_code = RunCmd("git", "ls-files --other", | |
workingdir=str(directory_path), | |
outstream=out_stream_buffer, | |
logging_level=logging.NOTSET) | |
if exit_code != 0: | |
return [] | |
rel_paths = out_stream_buffer.getvalue().strip().splitlines() | |
abs_paths = [] | |
for path in rel_paths: | |
abs_paths.append(Path(directory_path, path)) | |
return abs_paths | |
@staticmethod | |
def get_git_submodule_paths(directory_path: PurePath) -> List[Path]: | |
"""Returns submodules in the given workspace directory. | |
Args: | |
directory_path (PurePath): Path to the git directory. | |
Returns: | |
List[Path]: List of directory absolute paths to the root of | |
each submodule found from this folder. If submodules are not | |
found, an empty list will be returned. | |
""" | |
if not shutil.which("git"): | |
return [] | |
if os.path.isfile(directory_path.joinpath(".gitmodules")): | |
out_stream_buffer = StringIO() | |
exit_code = RunCmd( | |
"git", "config --file .gitmodules --get-regexp path", | |
workingdir=str(directory_path), | |
outstream=out_stream_buffer, | |
logging_level=logging.NOTSET) | |
if exit_code != 0: | |
return [] | |
submodule_paths = [] | |
for line in out_stream_buffer.getvalue().strip().splitlines(): | |
submodule_paths.append( | |
Path(directory_path, line.split()[1])) | |
return submodule_paths | |
else: | |
return [] | |
class QuietFilter(logging.Filter): | |
"""A logging filter that temporarily suppresses message output.""" | |
def __init__(self, quiet: bool = False): | |
"""Class constructor method. | |
Args: | |
quiet (bool, optional): Indicates if messages are currently being | |
printed (False) or not (True). Defaults to False. | |
""" | |
self._quiet = quiet | |
def filter(self, record: logging.LogRecord) -> bool: | |
"""Quiet filter method. | |
Args: | |
record (logging.LogRecord): A log record object that the filter is | |
applied to. | |
Returns: | |
bool: True if messages are being suppressed. Otherwise, False. | |
""" | |
return not self._quiet | |
class ProgressFilter(logging.Filter): | |
"""A logging filter that suppresses 'Progress' messages.""" | |
def filter(self, record: logging.LogRecord) -> bool: | |
"""Progress filter method. | |
Args: | |
record (logging.LogRecord): A log record object that the filter is | |
applied to. | |
Returns: | |
bool: True if the message is not a 'Progress' message. Otherwise, | |
False. | |
""" | |
return not record.getMessage().startswith("\rProgress") | |
class CacheDuringProgressFilter(logging.Filter): | |
"""A logging filter that suppresses messages during progress operations.""" | |
_message_cache = [] | |
@property | |
def message_cache(self) -> List[logging.LogRecord]: | |
"""Contains a cache of messages accumulated during time of operation. | |
Returns: | |
List[logging.LogRecord]: List of log records stored while the | |
filter was active. | |
""" | |
return self._message_cache | |
def filter(self, record: logging.LogRecord): | |
"""Cache progress filter that suppresses messages during progress | |
display output. | |
Args: | |
record (logging.LogRecord): A log record to cache. | |
""" | |
self._message_cache.append(record) | |
def check_debug_macros(macros: Iterable[Dict[str, str]], | |
file_dbg_path: str, | |
**macro_subs: str | |
) -> Tuple[int, int, int]: | |
"""Checks if debug macros contain formatting errors. | |
Args: | |
macros (Iterable[Dict[str, str]]): : A groupdict of macro matches. | |
This is an iterable of dictionaries with group names from the regex | |
match as the key and the matched string as the value for the key. | |
file_dbg_path (str): The file path (or other custom string) to display | |
in debug messages. | |
macro_subs (Dict[str,str]): Variable-length keyword and replacement | |
value string pairs to substitute during debug macro checks. | |
Returns: | |
Tuple[int, int, int]: A tuple of the number of formatting errors, | |
number of print specifiers, and number of arguments for the macros | |
given. | |
""" | |
macro_subs = {k.lower(): v for k, v in macro_subs.items()} | |
arg_cnt, failure_cnt, print_spec_cnt = 0, 0, 0 | |
for macro in macros: | |
# Special Specifier Handling | |
processed_dbg_str = macro['dbg_str'].strip().lower() | |
logging.debug(f"Inspecting macro: {macro}") | |
# Make any macro substitutions so further processing is applied | |
# to the substituted value. | |
for k in macro_subs.keys(): | |
processed_dbg_str = processed_dbg_str.replace(k, macro_subs[k]) | |
logging.debug("Debug macro string after replacements: " | |
f"{processed_dbg_str}") | |
# These are very rarely used in debug strings. They are somewhat | |
# more common in HII code to control text displayed on the | |
# console. Due to the rarity and likelihood usage is a mistake, | |
# a warning is shown if found. | |
specifier_display_replacements = ['%n', '%h', '%e', '%b', '%v'] | |
for s in specifier_display_replacements: | |
if s in processed_dbg_str: | |
logging.warning(f"File: {file_dbg_path}") | |
logging.warning(f" {s} found in string and ignored:") | |
logging.warning(f" \"{processed_dbg_str}\"") | |
processed_dbg_str = processed_dbg_str.replace(s, '') | |
# These are miscellaneous print specifiers that do not require | |
# special parsing and simply need to be replaced since they do | |
# have a corresponding argument associated with them. | |
specifier_other_replacements = ['%%', '\r', '\n'] | |
for s in specifier_other_replacements: | |
if s in processed_dbg_str: | |
processed_dbg_str = processed_dbg_str.replace(s, '') | |
processed_dbg_str = re.sub( | |
r'%[.\-+ ,Ll0-9]*\*[.\-+ ,Ll0-9]*[a-zA-Z]', '%_%_', | |
processed_dbg_str) | |
logging.debug(f"Final macro before print specifier scan: " | |
f"{processed_dbg_str}") | |
print_spec_cnt = processed_dbg_str.count('%') | |
# Need to take into account parentheses between args in function | |
# calls that might be in the args list. Use regex module for | |
# this one since the recursive pattern match helps simplify | |
# only matching commas outside nested call groups. | |
if macro['dbg_args'] is None: | |
processed_arg_str = "" | |
else: | |
processed_arg_str = macro['dbg_args'].strip() | |
argument_other_replacements = ['\r', '\n'] | |
for r in argument_other_replacements: | |
if s in processed_arg_str: | |
processed_arg_str = processed_arg_str.replace(s, '') | |
processed_arg_str = re.sub(r' +', ' ', processed_arg_str) | |
# Handle special case of commas in arg strings - remove them for | |
# final count to pick up correct number of argument separating | |
# commas. | |
processed_arg_str = re.sub( | |
r'([\"\'])(?:|\\.|[^\\])*?(\1)', | |
'', | |
processed_arg_str) | |
arg_matches = regex.findall( | |
r'(?:\((?:[^)(]+|(?R))*+\))|(,)', | |
processed_arg_str, | |
regex.MULTILINE) | |
arg_cnt = 0 | |
if processed_arg_str != '': | |
arg_cnt = arg_matches.count(',') | |
if print_spec_cnt != arg_cnt: | |
logging.error(f"File: {file_dbg_path}") | |
logging.error(f" Message = {macro['dbg_str']}") | |
logging.error(f" Arguments = \"{processed_arg_str}\"") | |
logging.error(f" Specifier Count = {print_spec_cnt}") | |
logging.error(f" Argument Count = {arg_cnt}") | |
failure_cnt += 1 | |
return failure_cnt, print_spec_cnt, arg_cnt | |
def get_debug_macros(file_contents: str) -> List[Dict[str, str]]: | |
"""Extract debug macros from the given file contents. | |
Args: | |
file_contents (str): A string of source file contents that may | |
contain debug macros. | |
Returns: | |
List[Dict[str, str]]: A groupdict of debug macro regex matches | |
within the file contents provided. | |
""" | |
# This is the main regular expression that is responsible for identifying | |
# DEBUG macros within source files and grouping the macro message string | |
# and macro arguments strings so they can be further processed. | |
r = regex.compile( | |
r'(?>(?P<prologue>DEBUG\s*\(\s*\((?:.*?,))(?:\s*))(?P<dbg_str>.*?(?:\"' | |
r'(?:[^\"\\]|\\.)*\".*?)*)(?:(?(?=,)(?<dbg_args>.*?(?=(?:\s*\)){2}\s*;' | |
r'))))(?:\s*\)){2,};?', | |
regex.MULTILINE | regex.DOTALL) | |
return [m.groupdict() for m in r.finditer(file_contents)] | |
def check_macros_in_string(src_str: str, | |
file_dbg_path: str, | |
**macro_subs: str) -> Tuple[int, int, int]: | |
"""Checks for debug macro formatting errors in a string. | |
Args: | |
src_str (str): Contents of the string with debug macros. | |
file_dbg_path (str): The file path (or other custom string) to display | |
in debug messages. | |
macro_subs (Dict[str,str]): Variable-length keyword and replacement | |
value string pairs to substitute during debug macro checks. | |
Returns: | |
Tuple[int, int, int]: A tuple of the number of formatting errors, | |
number of print specifiers, and number of arguments for the macros | |
in the string given. | |
""" | |
return check_debug_macros( | |
get_debug_macros(src_str), file_dbg_path, **macro_subs) | |
def check_macros_in_file(file: PurePath, | |
file_dbg_path: str, | |
show_utf8_decode_warning: bool = False, | |
**macro_subs: str) -> Tuple[int, int, int]: | |
"""Checks for debug macro formatting errors in a file. | |
Args: | |
file (PurePath): The file path to check. | |
file_dbg_path (str): The file path (or other custom string) to display | |
in debug messages. | |
show_utf8_decode_warning (bool, optional): Indicates whether to show | |
warnings if UTF-8 files fail to decode. Defaults to False. | |
macro_subs (Dict[str,str]): Variable-length keyword and replacement | |
value string pairs to substitute during debug macro checks. | |
Returns: | |
Tuple[int, int, int]: A tuple of the number of formatting errors, | |
number of print specifiers, and number of arguments for the macros | |
in the file given. | |
""" | |
try: | |
return check_macros_in_string( | |
file.read_text(encoding='utf-8'), file_dbg_path, | |
**macro_subs) | |
except UnicodeDecodeError as e: | |
if show_utf8_decode_warning: | |
logging.warning( | |
f"{file_dbg_path} UTF-8 decode error.\n" | |
" Debug macro code check skipped!\n" | |
f" -> {str(e)}") | |
return 0, 0, 0 | |
def check_macros_in_directory(directory: PurePath, | |
file_extensions: Iterable[str] = ('.c',), | |
ignore_git_ignore_files: Optional[bool] = True, | |
ignore_git_submodules: Optional[bool] = True, | |
show_progress_bar: Optional[bool] = True, | |
show_utf8_decode_warning: bool = False, | |
**macro_subs: str | |
) -> int: | |
"""Checks files with the given extension in the given directory for debug | |
macro formatting errors. | |
Args: | |
directory (PurePath): The path to the directory to check. | |
file_extensions (Iterable[str], optional): An iterable of strings | |
representing file extensions to check. Defaults to ('.c',). | |
ignore_git_ignore_files (Optional[bool], optional): Indicates whether | |
files ignored by git should be ignored for the debug macro check. | |
Defaults to True. | |
ignore_git_submodules (Optional[bool], optional): Indicates whether | |
files located in git submodules should not be checked. Defaults to | |
True. | |
show_progress_bar (Optional[bool], optional): Indicates whether to | |
show a progress bar to show progress status while checking macros. | |
This is more useful on a very large directories. Defaults to True. | |
show_utf8_decode_warning (bool, optional): Indicates whether to show | |
warnings if UTF-8 files fail to decode. Defaults to False. | |
macro_subs (Dict[str,str]): Variable-length keyword and replacement | |
value string pairs to substitute during debug macro checks. | |
Returns: | |
int: Count of debug macro errors in the directory. | |
""" | |
def _get_file_list(root_directory: PurePath, | |
extensions: Iterable[str]) -> List[Path]: | |
"""Returns a list of files recursively located within the path. | |
Args: | |
root_directory (PurePath): A directory Path object to the root | |
folder. | |
extensions (Iterable[str]): An iterable of strings that | |
represent file extensions to recursively search for within | |
root_directory. | |
Returns: | |
List[Path]: List of file Path objects to files found in the | |
given directory with the given extensions. | |
""" | |
def _show_file_discovered_message(file_count: int, | |
elapsed_time: float) -> None: | |
print(f"\rDiscovered {file_count:,} files in", | |
f"{current_start_delta:-.0f}s" | |
f"{'.' * min(int(current_start_delta), 40)}", end="\r") | |
start_time = timeit.default_timer() | |
previous_indicator_time = start_time | |
files = [] | |
for file in root_directory.rglob('*'): | |
if file.suffix in extensions: | |
files.append(Path(file)) | |
# Give an indicator progress is being made | |
# This has a negligible impact on overall performance | |
# with print emission limited to half second intervals. | |
current_time = timeit.default_timer() | |
current_start_delta = current_time - start_time | |
if current_time - previous_indicator_time >= 0.5: | |
# Since this rewrites the line, it can be considered a form | |
# of progress bar | |
if show_progress_bar: | |
_show_file_discovered_message(len(files), | |
current_start_delta) | |
previous_indicator_time = current_time | |
if show_progress_bar: | |
_show_file_discovered_message(len(files), current_start_delta) | |
print() | |
return files | |
logging.info(f"Checking Debug Macros in directory: " | |
f"{directory.resolve()}\n") | |
logging.info("Gathering the overall file list. This might take a" | |
"while.\n") | |
start_time = timeit.default_timer() | |
file_list = set(_get_file_list(directory, file_extensions)) | |
end_time = timeit.default_timer() - start_time | |
logging.debug(f"[PERF] File search found {len(file_list):,} files in " | |
f"{end_time:.2f} seconds.") | |
if ignore_git_ignore_files: | |
logging.info("Getting git ignore files...") | |
start_time = timeit.default_timer() | |
ignored_file_paths = GitHelpers.get_git_ignored_paths(directory) | |
end_time = timeit.default_timer() - start_time | |
logging.debug(f"[PERF] File ignore gathering took {end_time:.2f} " | |
f"seconds.") | |
logging.info("Ignoring git ignore files...") | |
logging.debug(f"File list count before git ignore {len(file_list):,}") | |
start_time = timeit.default_timer() | |
file_list = file_list.difference(ignored_file_paths) | |
end_time = timeit.default_timer() - start_time | |
logging.info(f" {len(ignored_file_paths):,} files are ignored by git") | |
logging.info(f" {len(file_list):,} files after removing " | |
f"ignored files") | |
logging.debug(f"[PERF] File ignore calculation took {end_time:.2f} " | |
f"seconds.") | |
if ignore_git_submodules: | |
logging.info("Ignoring git submodules...") | |
submodule_paths = GitHelpers.get_git_submodule_paths(directory) | |
if submodule_paths: | |
logging.debug(f"File list count before git submodule exclusion " | |
f"{len(file_list):,}") | |
start_time = timeit.default_timer() | |
file_list = [f for f in file_list | |
if not f.is_relative_to(*submodule_paths)] | |
end_time = timeit.default_timer() - start_time | |
for path in enumerate(submodule_paths): | |
logging.debug(" {0}. {1}".format(*path)) | |
logging.info(f" {len(submodule_paths):,} submodules found") | |
logging.info(f" {len(file_list):,} files will be examined after " | |
f"excluding files in submodules") | |
logging.debug(f"[PERF] Submodule exclusion calculation took " | |
f"{end_time:.2f} seconds.") | |
else: | |
logging.warning("No submodules found") | |
logging.info(f"\nStarting macro check on {len(file_list):,} files.") | |
cache_progress_filter = CacheDuringProgressFilter() | |
handler = next((h for h in logging.getLogger().handlers if h.get_name() == | |
'stdout_logger_handler'), None) | |
if handler is not None: | |
handler.addFilter(cache_progress_filter) | |
start_time = timeit.default_timer() | |
failure_cnt, file_cnt = 0, 0 | |
for file_cnt, file in enumerate(file_list): | |
file_rel_path = str(file.relative_to(directory)) | |
failure_cnt += check_macros_in_file( | |
file, file_rel_path, show_utf8_decode_warning, | |
**macro_subs)[0] | |
if show_progress_bar: | |
_show_progress(file_cnt, len(file_list), | |
f" {failure_cnt} errors" if failure_cnt > 0 else "") | |
if show_progress_bar: | |
_show_progress(len(file_list), len(file_list), | |
f" {failure_cnt} errors" if failure_cnt > 0 else "") | |
print("\n", flush=True) | |
end_time = timeit.default_timer() - start_time | |
if handler is not None: | |
handler.removeFilter(cache_progress_filter) | |
for record in cache_progress_filter.message_cache: | |
handler.emit(record) | |
logging.debug(f"[PERF] The macro check operation took {end_time:.2f} " | |
f"seconds.") | |
_log_failure_count(failure_cnt, file_cnt) | |
return failure_cnt | |
def _log_failure_count(failure_count: int, file_count: int) -> None: | |
"""Logs the failure count. | |
Args: | |
failure_count (int): Count of failures to log. | |
file_count (int): Count of files with failures. | |
""" | |
if failure_count > 0: | |
logging.error("\n") | |
logging.error(f"{failure_count:,} debug macro errors in " | |
f"{file_count:,} files") | |
def _show_progress(step: int, total: int, suffix: str = '') -> None: | |
"""Print progress of tick to total. | |
Args: | |
step (int): The current step count. | |
total (int): The total step count. | |
suffix (str): String to print at the end of the progress bar. | |
""" | |
global _progress_start_time | |
if step == 0: | |
_progress_start_time = timeit.default_timer() | |
terminal_col = shutil.get_terminal_size().columns | |
var_consume_len = (len("Progress|\u2588| 000.0% Complete 000s") + | |
len(suffix)) | |
avail_len = terminal_col - var_consume_len | |
percent = f"{100 * (step / float(total)):3.1f}" | |
filled = int(avail_len * step // total) | |
bar = '\u2588' * filled + '-' * (avail_len - filled) | |
step_time = timeit.default_timer() - _progress_start_time | |
print(f'\rProgress|{bar}| {percent}% Complete {step_time:-3.0f}s' | |
f'{suffix}', end='\r') | |
def _module_invocation_check_macros_in_directory_wrapper() -> int: | |
"""Provides an command-line argument wrapper for checking debug macros. | |
Returns: | |
int: The system exit code value. | |
""" | |
import argparse | |
import builtins | |
def _check_dir_path(dir_path: str) -> bool: | |
"""Returns the absolute path if the path is a directory." | |
Args: | |
dir_path (str): A directory file system path. | |
Raises: | |
NotADirectoryError: The directory path given is not a directory. | |
Returns: | |
bool: True if the path is a directory else False. | |
""" | |
abs_dir_path = os.path.abspath(dir_path) | |
if os.path.isdir(dir_path): | |
return abs_dir_path | |
else: | |
raise NotADirectoryError(abs_dir_path) | |
def _check_file_path(file_path: str) -> bool: | |
"""Returns the absolute path if the path is a file." | |
Args: | |
file_path (str): A file path. | |
Raises: | |
FileExistsError: The path is not a valid file. | |
Returns: | |
bool: True if the path is a valid file else False. | |
""" | |
abs_file_path = os.path.abspath(file_path) | |
if os.path.isfile(file_path): | |
return abs_file_path | |
else: | |
raise FileExistsError(file_path) | |
def _quiet_print(*args, **kwargs): | |
"""Replaces print when quiet is requested to prevent printing messages. | |
""" | |
pass | |
root_logger = logging.getLogger() | |
root_logger.setLevel(logging.DEBUG) | |
stdout_logger_handler = logging.StreamHandler(sys.stdout) | |
stdout_logger_handler.set_name('stdout_logger_handler') | |
stdout_logger_handler.setLevel(logging.INFO) | |
stdout_logger_handler.setFormatter(logging.Formatter('%(message)s')) | |
root_logger.addHandler(stdout_logger_handler) | |
parser = argparse.ArgumentParser( | |
prog=PROGRAM_NAME, | |
description=( | |
"Checks for debug macro formatting " | |
"errors within files recursively located within " | |
"a given directory."), | |
formatter_class=RawTextHelpFormatter) | |
io_req_group = parser.add_mutually_exclusive_group(required=True) | |
io_opt_group = parser.add_argument_group( | |
"Optional input and output") | |
git_group = parser.add_argument_group("Optional git control") | |
io_req_group.add_argument('-w', '--workspace-directory', | |
type=_check_dir_path, | |
help="Directory of source files to check.\n\n") | |
io_req_group.add_argument('-i', '--input-file', nargs='?', | |
type=_check_file_path, | |
help="File path for an input file to check.\n\n" | |
"Note that some other options do not apply " | |
"if a single file is specified such as " | |
"the\ngit options and file extensions.\n\n") | |
io_opt_group.add_argument('-l', '--log-file', | |
nargs='?', | |
default=None, | |
const='debug_macro_check.log', | |
help="File path for log output.\n" | |
"(default: if the flag is given with no " | |
"file path then a file called\n" | |
"debug_macro_check.log is created and used " | |
"in the current directory)\n\n") | |
io_opt_group.add_argument('-s', '--substitution-file', | |
type=_check_file_path, | |
help="A substitution YAML file specifies string " | |
"substitutions to perform within the debug " | |
"macro.\n\nThis is intended to be a simple " | |
"mechanism to expand the rare cases of pre-" | |
"processor\nmacros without directly " | |
"involving the pre-processor. The file " | |
"consists of one or more\nstring value " | |
"pairs where the key is the identifier to " | |
"replace and the value is the value\nto " | |
"replace it with.\n\nThis can also be used " | |
"as a method to ignore results by " | |
"replacing the problematic string\nwith a " | |
"different string.\n\n") | |
io_opt_group.add_argument('-v', '--verbose-log-file', | |
action='count', | |
default=0, | |
help="Set file logging verbosity level.\n" | |
" - None: Info & > level messages\n" | |
" - '-v': + Debug level messages\n" | |
" - '-vv': + File name and function\n" | |
" - '-vvv': + Line number\n" | |
" - '-vvvv': + Timestamp\n" | |
"(default: verbose logging is not enabled)" | |
"\n\n") | |
io_opt_group.add_argument('-n', '--no-progress-bar', action='store_true', | |
help="Disables progress bars.\n" | |
"(default: progress bars are used in some" | |
"places to show progress)\n\n") | |
io_opt_group.add_argument('-q', '--quiet', action='store_true', | |
help="Disables console output.\n" | |
"(default: console output is enabled)\n\n") | |
io_opt_group.add_argument('-u', '--utf8w', action='store_true', | |
help="Shows warnings for file UTF-8 decode " | |
"errors.\n" | |
"(default: UTF-8 decode errors are not " | |
"shown)\n\n") | |
git_group.add_argument('-df', '--do-not-ignore-git-ignore-files', | |
action='store_true', | |
help="Do not ignore git ignored files.\n" | |
"(default: files in git ignore files are " | |
"ignored)\n\n") | |
git_group.add_argument('-ds', '--do-not-ignore-git_submodules', | |
action='store_true', | |
help="Do not ignore files in git submodules.\n" | |
"(default: files in git submodules are " | |
"ignored)\n\n") | |
parser.add_argument('-e', '--extensions', nargs='*', default=['.c'], | |
help="List of file extensions to include.\n" | |
"(default: %(default)s)") | |
args = parser.parse_args() | |
if args.quiet: | |
# Don't print in the few places that directly print | |
builtins.print = _quiet_print | |
stdout_logger_handler.addFilter(QuietFilter(args.quiet)) | |
if args.log_file: | |
file_logger_handler = logging.FileHandler(filename=args.log_file, | |
mode='w', encoding='utf-8') | |
# In an ideal world, everyone would update to the latest Python | |
# minor version (3.10) after a few weeks/months. Since that's not the | |
# case, resist from using structural pattern matching in Python 3.10. | |
# https://peps.python.org/pep-0636/ | |
if args.verbose_log_file == 0: | |
file_logger_handler.setLevel(logging.INFO) | |
file_logger_formatter = logging.Formatter( | |
'%(levelname)-8s %(message)s') | |
elif args.verbose_log_file == 1: | |
file_logger_handler.setLevel(logging.DEBUG) | |
file_logger_formatter = logging.Formatter( | |
'%(levelname)-8s %(message)s') | |
elif args.verbose_log_file == 2: | |
file_logger_handler.setLevel(logging.DEBUG) | |
file_logger_formatter = logging.Formatter( | |
'[%(filename)s - %(funcName)20s() ] %(levelname)-8s ' | |
'%(message)s') | |
elif args.verbose_log_file == 3: | |
file_logger_handler.setLevel(logging.DEBUG) | |
file_logger_formatter = logging.Formatter( | |
'[%(filename)s:%(lineno)s - %(funcName)20s() ] ' | |
'%(levelname)-8s %(message)s') | |
elif args.verbose_log_file == 4: | |
file_logger_handler.setLevel(logging.DEBUG) | |
file_logger_formatter = logging.Formatter( | |
'%(asctime)s [%(filename)s:%(lineno)s - %(funcName)20s() ]' | |
' %(levelname)-8s %(message)s') | |
else: | |
file_logger_handler.setLevel(logging.DEBUG) | |
file_logger_formatter = logging.Formatter( | |
'%(asctime)s [%(filename)s:%(lineno)s - %(funcName)20s() ]' | |
' %(levelname)-8s %(message)s') | |
file_logger_handler.addFilter(ProgressFilter()) | |
file_logger_handler.setFormatter(file_logger_formatter) | |
root_logger.addHandler(file_logger_handler) | |
logging.info(PROGRAM_NAME + "\n") | |
substitution_data = {} | |
if args.substitution_file: | |
logging.info(f"Loading substitution file {args.substitution_file}") | |
with open(args.substitution_file, 'r') as sf: | |
substitution_data = yaml.safe_load(sf) | |
if args.workspace_directory: | |
return check_macros_in_directory( | |
Path(args.workspace_directory), | |
args.extensions, | |
not args.do_not_ignore_git_ignore_files, | |
not args.do_not_ignore_git_submodules, | |
not args.no_progress_bar, | |
args.utf8w, | |
**substitution_data) | |
else: | |
curr_dir = Path(__file__).parent | |
input_file = Path(args.input_file) | |
rel_path = str(input_file) | |
if input_file.is_relative_to(curr_dir): | |
rel_path = str(input_file.relative_to(curr_dir)) | |
logging.info(f"Checking Debug Macros in File: " | |
f"{input_file.resolve()}\n") | |
start_time = timeit.default_timer() | |
failure_cnt = check_macros_in_file( | |
input_file, | |
rel_path, | |
args.utf8w, | |
**substitution_data)[0] | |
end_time = timeit.default_timer() - start_time | |
logging.debug(f"[PERF] The file macro check operation took " | |
f"{end_time:.2f} seconds.") | |
_log_failure_count(failure_cnt, 1) | |
return failure_cnt | |
if __name__ == '__main__': | |
# The exit status value is the number of macro formatting errors found. | |
# Therefore, if no macro formatting errors are found, 0 is returned. | |
# Some systems require the return value to be in the range 0-127, so | |
# a lower maximum of 100 is enforced to allow a wide range of potential | |
# values with a reasonably large maximum. | |
try: | |
sys.exit(max(_module_invocation_check_macros_in_directory_wrapper(), | |
100)) | |
except KeyboardInterrupt: | |
logging.warning("Exiting due to keyboard interrupt.") | |
# Actual formatting errors are only allowed to reach 100. | |
# 101 signals a keyboard interrupt. | |
sys.exit(101) | |
except FileExistsError as e: | |
# 102 signals a file not found error. | |
logging.critical(f"Input file {e.args[0]} does not exist.") | |
sys.exit(102) |