import argparse import fileinput import os import pathlib import re import subprocess match_expressions = [] valid_extensions = [] root_dir = '' use_batching = True def is_header_missing(f): with open(f) as reader: lines = reader.read().lstrip().splitlines() if len(lines) > 0: return not lines[0].startswith("// ") return True def add_headers(files, header): for line in fileinput.input(files, inplace=True): if fileinput.isfirstline(): [ print(h) for h in header.splitlines() ] print(line, end="") def scan_tree(root): files = [] header_files = [] with os.scandir(root) as dirs: for entry in dirs: if entry.is_dir(): scan_tree(os.path.join(root, entry.name)) continue full_path = os.path.join(root, entry.name) relative_root_path = os.path.relpath(full_path, start=root_dir) if is_matching_filename(relative_root_path): print("... formatting: {}".format(relative_root_path)) files.append(full_path) if is_header_missing(full_path): header_files.append(full_path) args = "" if files: if use_batching: os.system("clang-format -i " + " ".join(files)) else: for file in files: os.system("clang-format -i " + file) if header_files: add_headers(header_files, "// Copyright Epic Games, Inc. All Rights Reserved.\n\n") def scan_zen(root): with os.scandir(root) as dirs: for entry in dirs: if entry.is_dir() and entry.name.startswith("zen"): scan_tree(os.path.join(root, entry.name)) def is_matching_filename(relative_root_path): global match_expressions global root_dir global valid_extensions if os.path.splitext(relative_root_path)[1].lower() not in valid_extensions: return False if not match_expressions: return True relative_root_path = relative_root_path.replace('\\', '/') for regex in match_expressions: if regex.fullmatch(relative_root_path): return True return False def parse_match_expressions(wildcards, matches): global match_expressions global valid_extensions valid_extensions = ['.cpp', '.h'] for wildcard in wildcards: regex = wildcard.replace('*', '%FORMAT_STAR%').replace('\\', '/') regex = re.escape(regex) regex = '.*' + regex.replace('%FORMAT_STAR%', '.*') + '.*' try: match_expressions.append(re.compile(regex, re.IGNORECASE)) except Exception as ex: print(f'Could not parse input filename expression \'{wildcard}\': {str(ex)}') quit() for regex in matches: try: match_expressions.append(re.compile(regex, re.IGNORECASE)) except Exception as ex: print(f'Could not parse input --match expression \'{regex}\': {str(ex)}') quit() def validate_clang_format(): vstring = subprocess.check_output("clang-format --version", shell=True).decode().rstrip() match = re.search(r'(\d+)\.(\d+)(\.(\d+))?$', vstring) if not match: raise ValueError("invalid version number '%s'" % vstring) (major, minor, patch) = match.group(1, 2, 4) if int(major) < 13: if int(minor) == 0: if int(patch) < 1: raise ValueError(f'invalid clang-format version -- we require at least v12.0.1') def _main(): global root_dir, use_batching parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='*', help="Match text for filenames. If fullpath contains text it is a match, " +\ "* is a wildcard. Directory separators are matched by either / or \\. Case insensitive.") parser.add_argument('--match', action='append', default=[], help="Match regular expression for filenames. " +\ "Relative path from the root zen directory must be a complete match. Directory separators are matched only by /. Case insensitive.") parser.add_argument('--batch', dest='use_batching', action='store_true', help="Enable batching calls to clang-format.") parser.add_argument('--no-batch', dest='use_batching', action='store_false', help="Disable batching calls to clang-format.") parser.set_defaults(use_batching=True) options = parser.parse_args() parse_match_expressions(options.filenames, options.match) root_dir = pathlib.Path(__file__).parent.parent.resolve() use_batching = options.use_batching validate_clang_format() while True: if (os.path.isfile(".clang-format")): scan_zen(".") quit() else: cwd = os.getcwd() if os.path.dirname(cwd) == cwd: quit() os.chdir("..") if __name__ == '__main__': _main()