Created
February 16, 2026 21:23
-
-
Save dgelessus/d20e744dca85b35c798c9131b18ce2e2 to your computer and use it in GitHub Desktop.
Simple script to find potentially interesting conditional macro names in a C/C++/etc. codebase
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import collections | |
| import os | |
| import pathlib | |
| import re | |
| import sys | |
| c_header_suffixes = { | |
| ".h", | |
| ".hpp", | |
| } | |
| c_suffixes = { | |
| *c_header_suffixes, | |
| ".c", | |
| ".cpp", | |
| ".inl", | |
| ".m", | |
| ".mm", | |
| ".rc", | |
| } | |
| cmake_suffix = ".cmake" | |
| c_header_suffixes_combined = {(suffix,) for suffix in c_header_suffixes} | |
| c_header_suffixes_combined |= {(suffix, cmake_suffix) for suffix in c_header_suffixes} | |
| c_suffixes_combined = {(suffix,) for suffix in c_suffixes} | |
| c_suffixes_combined |= {(suffix, cmake_suffix) for suffix in c_suffixes} | |
| ignored_dirs = { | |
| "cmake-build", | |
| "gtest", | |
| "Scripts", | |
| "vcpkg", | |
| } | |
| def find_ifdefs_in_file(file_path, stream, is_header): | |
| include_guard_name = None | |
| directive_count = 0 | |
| for line_number, line in enumerate(stream, 1): | |
| line = line.lstrip() | |
| # Intentionally also find commented out directives. | |
| is_comment = False | |
| if line.startswith("//"): | |
| is_comment = True | |
| line = line.lstrip("/").lstrip() | |
| if not line.startswith("#"): | |
| continue | |
| line = line.removeprefix("#").lstrip() | |
| split = line.split(maxsplit=1) | |
| if not split: | |
| continue | |
| directive_name = split[0] | |
| arg = split[1] if len(split) > 1 else "" | |
| arg = re.sub(r"//.*$|/\*.*?\*/", " ", arg) | |
| arg = arg.strip() | |
| if not is_comment: | |
| if is_header and directive_count == 0 and directive_name == "ifndef": | |
| include_guard_name = arg | |
| directive_count += 1 | |
| continue | |
| elif include_guard_name is not None and directive_count == 1: | |
| if directive_name != "define" or arg != include_guard_name: | |
| include_guard_name = None | |
| directive_count += 1 | |
| continue | |
| if directive_name in {"ifdef", "ifndef", "elifdef", "elifndef"}: | |
| yield line_number, arg | |
| elif directive_name in {"if", "elif"}: | |
| for match in re.finditer(r"(?:^|\W)([A-Za-z_]\w*)", arg): | |
| macro = match[1] | |
| if macro == "defined": | |
| continue | |
| yield line_number, macro | |
| elif directive_name in {"cmakedefine", "define"}: | |
| split = arg.split() | |
| if split: | |
| macro = split[0] | |
| value = split[1] if len(split) > 1 else "" | |
| if "(" not in macro and value in {"", "0", "1", "false", "true"}: | |
| yield line_number, macro | |
| if not is_comment: | |
| directive_count += 1 | |
| if is_header and include_guard_name is None: | |
| print(f"WARNING: Header {file_path} seems to have no include guard") | |
| def find_ifdefs_in_dir(sources_root): | |
| ifdefs = collections.defaultdict(lambda: collections.defaultdict(list)) | |
| for dir_path, dir_names, file_names in os.walk(sources_root): | |
| for i in reversed(range(len(dir_names))): | |
| dir_name = dir_names[i] | |
| if dir_name.startswith(".") or dir_name in ignored_dirs: | |
| del dir_names[i] | |
| dir_path = pathlib.Path(dir_path) | |
| for file_name in file_names: | |
| file_path = dir_path / file_name | |
| suffixes = tuple(file_path.suffixes) | |
| if suffixes not in c_suffixes_combined: | |
| continue | |
| print(f".", end="", file=sys.stderr, flush=True) | |
| with open(file_path, "r", encoding="utf-8", errors="replace") as stream: | |
| for line_number, macro in find_ifdefs_in_file(file_path, stream, is_header=suffixes in c_header_suffixes_combined): | |
| ifdefs[macro][file_path].append(line_number) | |
| return ifdefs | |
| def format_file_lines(file, line_numbers): | |
| if len(line_numbers) == 1: | |
| lines_desc = f"line {line_numbers[0]}" | |
| else: | |
| lines_desc = f"lines {line_numbers}" | |
| return f"{file} at {lines_desc}" | |
| def main(): | |
| _, sources_root = sys.argv | |
| ifdefs = find_ifdefs_in_dir(sources_root) | |
| print(file=sys.stderr, flush=True) # Newline to terminate the sequence of ...s | |
| for macro, files in sorted(ifdefs.items()): | |
| if len(files) == 1: | |
| file, line_numbers = next(iter(files.items())) | |
| print(f"macro {macro} in 1 file: {format_file_lines(file, line_numbers)}") | |
| else: | |
| print(f"macro {macro} in {len(files)} files:") | |
| for file, line_numbers in sorted(files.items()): | |
| print(f"\t{format_file_lines(file, line_numbers)}") | |
| sys.exit(0) | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment