diff --git a/src/bundler.py b/src/bundler.py index fa3e163..08e4a70 100644 --- a/src/bundler.py +++ b/src/bundler.py @@ -1,18 +1,20 @@ #!/usr/bin/python3 +import argparse import os import re -import argparse -###define some defaults +# define some defaults DEFAULT_INPUT_FILEPATH = 'main.cpp' DEFAULT_INCLUDE_GUARD_PREFIX = '__INCLUDE_GUARD_' -###launch the argument parser for a proper cli experience -parser = argparse.ArgumentParser(description='A script for bundling small C++ projects into a single file, to upload to CodinGame by resolving includes, include guards, and accompanying .cpp files') +# launch the argument parser for a proper cli experience +parser = argparse.ArgumentParser( + description='A script for bundling small C++ projects into a single file, to upload to CodinGame by resolving includes, include guards, and accompanying .cpp files') parser.add_argument( '-i', '--input', - help='A path to the file to start reading from (this should typically be the file containing your main() method). Defaults to \'main.cpp\' if omitted' + help='A path to a file to start reading from (this should typically be the file containing your main() method). Defaults to \'main.cpp\' if omitted. May be specified multiple times', + action='append' ) parser.add_argument( '-o', @@ -43,21 +45,34 @@ help='A flag indicating that the script should not attempt to detect source files that accompany any header file included in the input', action='store_true' ) +parser.add_argument( + '-p', + '--path', + help='A flag indicating an additional path that the script should search for include files on (beyond the directory of the input file). May be specified multiple times', + action='append' +) + arguments = parser.parse_args() -inputFilePath = arguments.input or DEFAULT_INPUT_FILEPATH +inputFilePaths = arguments.input or [DEFAULT_INPUT_FILEPATH] +for i in range(len(inputFilePaths)): + inputFilePaths[i] = os.path.abspath(inputFilePaths[i]) outputFilePath = arguments.output if arguments.no_guard: includeGuardPrefix = None else: includeGuardPrefix = arguments.guard_prefix or '__INCLUDE_GUARD_' - +includePath = arguments.path or [] +for i in range(len(includePath)): + includePath[i] = os.path.abspath(includePath[i]) alwaysOnce = arguments.always_once noSource = arguments.no_source -###set some values, etc to use in the processing +# set some values, etc to use in the processing definesFound = {} -ifStack=[] +ifStack = [] + +# processing section + -###processing section def processInclude(line): macro = re.search('^#include ".+"', line) if macro is not None: @@ -66,7 +81,8 @@ def processInclude(line): includeFilePath = macro.group(0).split('#include "')[1].split('"')[0] if alwaysOnce: - associatedDefine = ('ALWAYS ONCE: ' + os.path.abspath(includeFilePath)) + associatedDefine = ( + 'ALWAYS ONCE: ' + os.path.abspath(includeFilePath)) if associatedDefine in definesFound: return '' else: @@ -76,14 +92,17 @@ def processInclude(line): if lastDirSepPos == -1: lastDirSepPos = includeFilePath.rfind('\\') if lastDirSepPos != -1: - includePath, includeFileName = includeFilePath[0:lastDirSepPos], includeFilePath[lastDirSepPos+1:] + includePath, includeFileName = includeFilePath[0: + lastDirSepPos], includeFilePath[lastDirSepPos+1:] os.chdir(includePath) else: includeFileName = includeFilePath + includeFileName = resolveIncludeFile(includeFileName) with open(includeFileName, 'r') as subFile: subFileContents = scan(subFile) - fileContents += '%s%s' % (subFileContents, '\n' if len(subFileContents) > 0 else '') - #now get the corresponding .cpp, unless it was for some reason the actual include above, or the cli flag --no-source is set + fileContents = mergeSubFile( + fileContents, subFileContents) + # now get the corresponding .cpp, unless it was for some reason the actual include above, or the cli flag --no-source is set if not noSource: splitFileName = includeFileName.rsplit('.', 1) if len(splitFileName) == 2: @@ -91,35 +110,60 @@ def processInclude(line): if ext.lower() != 'cpp': srcFileName = name + '.cpp' if not os.path.isfile(srcFileName): - #try a .c file instead + # try a .c file instead srcFileName = name + '.c' if os.path.isfile(srcFileName): with open(srcFileName, 'r') as subFile: - associatedDefine = ('DETECT SOURCE ONCE:' + os.path.abspath(srcFileName)) + associatedDefine = ( + 'DETECT SOURCE ONCE:' + os.path.abspath(srcFileName)) if not associatedDefine in definesFound: definesFound[associatedDefine] = True subFileContents = scan(subFile) - fileContents += '%s%s' % (subFileContents, '\n' if len(subFileContents) > 0 else '') + fileContents = mergeSubFile( + fileContents, subFileContents) + os.chdir(oldWorkingDirectory) fileContents = fileContents.strip() return '%s%s' % (fileContents, '\n' if len(fileContents) > 0 else '') else: return None + +def mergeSubFile(fileContents, subFileContents): + fileContents += '%s%s' % (subFileContents, + '\n' if len(subFileContents) > 0 else '') + return fileContents + + +def resolveIncludeFile(path): + if os.path.isfile(path): + return path + + for includeDir in includePath: + testFileName = os.path.join(includeDir, path) + if os.path.isfile(testFileName): + return testFileName + + raise FileNotFoundError('Fail to find %s on include path' % path) + + def processDefine(line): macro = re.search('^#define %s(_|\w|[0-9])+' % includeGuardPrefix, line) if macro is not None: - definedThing = macro.group(0).split('#define %s' % includeGuardPrefix, 1)[1] + definedThing = macro.group(0).split( + '#define %s' % includeGuardPrefix, 1)[1] definesFound[definedThing] = True return line else: return None + def processUndef(line): macro = re.search('^#undef %s(_|\w|[0-9])+' % includeGuardPrefix, line) if macro is not None: - definedThing = macro.group(0).split('#undef %s' % includeGuardPrefix, 1)[1] + definedThing = macro.group(0).split( + '#undef %s' % includeGuardPrefix, 1)[1] if definedThing in definesFound: del definesFound[definedThing] return line @@ -134,6 +178,7 @@ def processIf(line): else: return None + def processIfDef(line): macro = re.search('^#ifdef (_|\w|[0-9])+', line) if macro is not None: @@ -146,10 +191,12 @@ def processIfDef(line): else: return None + def processIfNDef(line): macro = re.search('^#ifndef (_|\w|[0-9])+', line) if macro is not None: - targetThing = macro.group(0).split('#ifndef %s' % includeGuardPrefix, 1) + targetThing = macro.group(0).split( + '#ifndef %s' % includeGuardPrefix, 1) if len(targetThing) > 1: targetThing = targetThing[1] return 1 if not (targetThing in definesFound) else 0 @@ -158,6 +205,7 @@ def processIfNDef(line): else: return None + def processEndif(line): macro = re.search('^#endif', line) if macro is not None: @@ -165,7 +213,9 @@ def processEndif(line): else: return None -#if the macro was found, this returns whether or not associated file already has been pragma-onced, otherwise returns None +# if the macro was found, this returns whether or not associated file already has been pragma-onced, otherwise returns None + + def processPragmaOnce(line, file): macro = re.search('^#pragma once', line) if macro is not None: @@ -178,6 +228,7 @@ def processPragmaOnce(line, file): else: return None + def scan(file): fileContents = "" for line in file.readlines(): @@ -201,13 +252,13 @@ def scan(file): temp = processIfDef(line) - #handle ifdef and ifndef with the same main thing below this one + # handle ifdef and ifndef with the same main thing below this one if temp is None: temp = processIfNDef(line) if temp is not None: - #ignore any macros in an #if we're not fully handling, other than includes - #this means that the contents of #ifs cannot take advantage of the size reduction, but won't produce invalid code + # ignore any macros in an #if we're not fully handling, other than includes + # this means that the contents of #ifs cannot take advantage of the size reduction, but won't produce invalid code if len(ifStack) == 0 or ifStack[-1] == 1: nextIfValue = temp else: @@ -216,56 +267,60 @@ def scan(file): fileContents += line ifStack.append(nextIfValue) continue - #only try to deal with macros if not nested in a macro we are skipping. + # only try to deal with macros if not nested in a macro we are skipping. if len(ifStack) == 0 or ifStack[-1] == 1: if includeGuardPrefix is not None: temp = processDefine(line) if temp is not None: -# fileContents += temp + # fileContents += temp continue temp = processUndef(line) if temp is not None: fileContents += temp continue - #don't include anything in this file if we see #pragma once + # don't include anything in this file if we see #pragma once temp = processPragmaOnce(line, file) if temp is not None: if temp == True: return "" else: continue - #include process includes if we are not ommitting this entire block + # include process includes if we are not ommitting this entire block if len(ifStack) == 0 or ifStack[-1] != 0: temp = processInclude(line) if temp is not None: fileContents += temp continue - #if nothing else happened, just add the line to the file + # if nothing else happened, just add the line to the file fileContents += line return fileContents.strip() -#process the initial input file path, moving to to the appropriate path if neccessary +# process the initial input file path, moving to to the appropriate path if neccessary oldWorkingDirectory = os.getcwd() -lastDirSepPos = inputFilePath.rfind('/') -if lastDirSepPos == -1: - lastDirSepPos = inputFilePath.rfind('\\') -if lastDirSepPos != -1: - inputPath, inputFileName = inputFilePath[0:lastDirSepPos], inputFilePath[lastDirSepPos+1:] - os.chdir(inputPath) -else: - inputFileName = inputFilePath -#now scan the file -with open(inputFileName, 'r') as inputFile: - result = scan(inputFile) -#now jump back to the initial directory +result = '' +for inputFilePath in inputFilePaths: + lastDirSepPos = inputFilePath.rfind('/') + if lastDirSepPos == -1: + lastDirSepPos = inputFilePath.rfind('\\') + if lastDirSepPos != -1: + inputPath, inputFileName = inputFilePath[0: + lastDirSepPos], inputFilePath[lastDirSepPos+1:] + os.chdir(inputPath) + else: + inputFileName = inputFilePath + # now scan the file + with open(inputFileName, 'r') as inputFile: + result = mergeSubFile(result, scan(inputFile)) + +# now jump back to the initial directory os.chdir(oldWorkingDirectory) if outputFilePath is not None: with open(outputFilePath, 'w') as outFile: outFile.write(result) else: - print(result) \ No newline at end of file + print(result)