import argparse, re, sys
from pathlib import Path
from typing import Any, List, Optional, Pattern, Set, TextIO
roots: Set[Path] = set()
excludes: Set[Path] = set()
keeps: Set[Path] = set()
keep_pragma: bool = False
destn: TextIO = sys.stdout
found: Set[Path] = set()
pragma_regex: Pattern = re.compile(r'^\s*#\s*pragma\s*once\s*')
include_regex: Pattern = re.compile(r'^\s*#\s*include\s*"(.+?)"')
def test_match_include() -> bool:
if (include_regex.match('#include "file"') and
include_regex.match(' #include "file"') and
include_regex.match('# include "file"') and
include_regex.match('#include "file"') and
include_regex.match('#include "file" // comment')):
if (not include_regex.match('#include <file>') and
not include_regex.match('//#include "file"') and
not include_regex.match('/*#include "file"*/')):
found = include_regex.match('#include "file" // "')
if (found and found.group(1) == 'file'):
print('#include match valid')
return True
return False
def test_match_pragma() -> bool:
if (pragma_regex.match('#pragma once') and
pragma_regex.match(' #pragma once') and
pragma_regex.match('# pragma once') and
pragma_regex.match('#pragma once') and
pragma_regex.match('#pragma once // comment')):
if (not pragma_regex.match('//#pragma once') and
not pragma_regex.match('/*#pragma once*/')):
print('#pragma once match valid')
return True
return False
def resolve_include(file: str, parent: Optional[Path] = None) -> Optional[Path]:
for root in roots:
found = root.joinpath(file).resolve()
if (found.is_file()):
return found
if (parent):
found = parent.joinpath(file).resolve();
else:
found = Path(file)
if (found.is_file()):
return found
return None
def resolve_excluded_files(file_list: Optional[List[str]], resolved: Set[Path], parent: Optional[Path] = None) -> None:
if (file_list):
for filename in file_list:
found = resolve_include(filename, parent)
if (found):
resolved.add(found)
else:
error_line(f'Warning: excluded file not found: {filename}')
def write_line(line: str) -> None:
print(line, file=destn)
def error_line(line: Any) -> None:
print(line, file=sys.stderr)
def add_file(file: Path, file_name: str = None) -> None:
if (file.is_file()):
if (not file_name):
file_name = file.name
error_line(f'Processing: {file_name}')
with file.open('r', errors='replace') as opened:
for line in opened:
line = line.rstrip('\n')
match_include = include_regex.match(line);
if (match_include):
inc_name = match_include.group(1)
resolved = resolve_include(inc_name, file.parent)
if (resolved):
if (resolved in excludes):
write_line(f'#error Using excluded file: {inc_name} (re-amalgamate source to fix)')
error_line(f'Excluding: {inc_name}')
else:
if (resolved not in found):
found.add(resolved)
if (resolved in keeps):
write_line(f'/**** *NOT* inlining {inc_name} ****/')
write_line(line)
error_line(f'Not inlining: {inc_name}')
else:
write_line(f'/**** start inlining {inc_name} ****/')
add_file(resolved, inc_name)
write_line(f'/**** ended inlining {inc_name} ****/')
else:
write_line(f'/**** skipping file: {inc_name} ****/')
else:
write_line(f'#error Unable to find: {inc_name}')
error_line(f'Error: Unable to find: {inc_name}')
else:
if (keep_pragma or not pragma_regex.match(line)):
write_line(line)
else:
error_line(f'Error: Invalid file: {file}')
parser = argparse.ArgumentParser(description='Amalgamate Tool', epilog=f'example: {sys.argv[0]} -r ../my/path -r ../other/path -o out.c in.c')
parser.add_argument('-r', '--root', action='append', type=Path, help='file root search path')
parser.add_argument('-x', '--exclude', action='append', help='file to completely exclude from inlining')
parser.add_argument('-k', '--keep', action='append', help='file to exclude from inlining but keep the include directive')
parser.add_argument('-p', '--pragma', action='store_true', default=False, help='keep any "#pragma once" directives (removed by default)')
parser.add_argument('-o', '--output', type=argparse.FileType('w'), help='output file (otherwise stdout)')
parser.add_argument('input', type=Path, help='input file')
args = parser.parse_args()
args.input = args.input.resolve(strict=True)
found.add(args.input)
if (args.root):
for path in args.root:
roots.add(path.resolve(strict=True))
resolve_excluded_files(args.exclude, excludes, args.input.parent)
resolve_excluded_files(args.keep, keeps, args.input.parent)
keep_pragma = args.pragma;
try:
if (args.output):
destn = args.output
add_file(args.input)
finally:
if (destn):
destn.close()