diff --git a/gen_enum_str.py b/gen_enum_str.py index e9332ce..f6458f5 100755 --- a/gen_enum_str.py +++ b/gen_enum_str.py @@ -1,6 +1,6 @@ #!/bin/python3 """ -A python script to generate to_string functions for all enumerations in a cpp file. +A python script to generate toString functions for all enumerations in a cpp file. Copyright © 2022 Matthias Quintern. This software comes with no warranty. This software is licensed under the GPL3 @@ -10,40 +10,280 @@ from sys import argv, exit import re -filetypes = [".hpp", ".cpp", ".tpp"] +# search in header files for enums +header_filetypes = [".hpp"] +# definition are written to source file having the same name as the header +source_filetype = ".cpp" def error(s): print(s) exit(1) -def getEnumStrF(name: str, enum: dict, spaces: int): - s = " " * spaces - s += f"const std::map<{name}, std::string> {name}ToStringMap" + " { // generated by gen_enum_str.py\n" - for name_, val in enum.items(): - s += " " * (spaces + 4) - s += "{ " + name_ + ", \"" + name_ + "\" },\n" - s += " " * spaces + "};\n" - s += " " * spaces + "const std::string& to_string(" + name + " v) { return " + f"{name}ToStringMap.at(v);" + " }; // generated by gen_enum_str.py\n\n" - return s +class Enum: + # settings + include_docstrings = True + """ + include a doxygen docstring before the declarations + """ + docstring_include_generated = True + """ + include a list of the enum values in @details + """ + docstring_include_names = True + """ + throw exceptions on invalid arguments + if false: + toString: return empty string + fromString: return the last enum value + """ + throw_exceptions = True + # generate fromString for string_view + string_view = True + def __init__(self, startLine:int, name:str, numbers:list[int], names:list[str], namespace:str=""): + self.startLine = startLine + self.name = name + self.numbers = numbers + self.names = names + self.namespace = namespace + + def is_range(self) -> tuple[bool, int, int]: + """ + check if the enum is a continous range + returns: bool, min, max + """ + sorted_numbers = self.numbers.copy() + sorted_numbers.sort() + if sorted_numbers == range(sorted_numbers[0], sorted_numbers[-1]+1, 1): + return True, sorted_numbers[0], sorted_numbers[-1] + else: + return False, -1, -1 + + def get_name(self): + if self.namespace: + return self.namespace + "::" + self.name + else: + return self.name + + def get_struct_name(self) -> str: + return "EnumStringConversion_" + self.name + + def get_names_for_doc(self, qoutes="") -> str: + """ + returns all names in quotes, separated with a , + """ + s = "" + for name in self.names: + s += qoutes + name + qoutes + ", " + if len(s) > 2: return s[:-1] + else: return s + + def get_dec_toString(self) -> str: + """ + return the declaration for the toString method + """ + s = "" + if Enum.include_docstrings: + s += "/**\n" + s += f' * @brief Convert @ref {self.get_name()} "an enumeration value" to std::string\n' + s += f' * @details\n' + if Enum.docstring_include_generated: + s += f' * This function was generated by gen_enum_str.py\\n\n' + if Enum.throw_exceptions: + s += f' * Throws gz::InvalidArgument if v is invalid.\n' + s += f' * @throws gz::InvalidArgument if v is invalid.\n' + else: + s += f' * Returns an empty string if v is invalid.\n' + s += f' */\n' + s += f"std::string toString(const {self.get_name()}& v);\n" + return s + + def get_dec_fromString(self) -> str: + """ + return the declaration(s) for the fromString method(s) + """ + s = f"template T>\n" + s += f"{self.get_name()} fromString(const std::string& s);\n" + + if Enum.string_view: + s += f"template T>\n" + s += f"{self.get_name()} fromString(const std::string_view& sv);\n" + + if Enum.include_docstrings: + s += "/**\n" + s += f' * @brief Convert a std::string to @ref {self.get_name()} "an enumeration value"\n' + s += f' * @details\n' + if Enum.docstring_include_generated: + s += f' * This function was generated by gen_enum_str.py\\n\n' + if Enum.throw_exceptions: + s += f' * Throws gz::InvalidArgument if s is invalid.\n' + s += f' * @throws gz::InvalidArgument if s is invalid.\n' + else: + s += f' * Returns {self.get_name()}::{self.names[-1]} if s is invalid.\n' + if Enum.docstring_include_names: + s += f' * @param v one of: {self.get_names_for_doc()}\n' + s += f' */\n' + s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string& s);\n" + + if Enum.string_view: + if Enum.include_docstrings: + s += '/// @brief Convert a std::string_view to @ref {self.get_name()} "an enumeration value"\n' + s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string_view& sv);\n" + return s + + def get_def_struct(self) -> str: + """ + return the definition of the EnumStringConversion struct + """ + s = "" + s += f"// Holds maps used by fromString and toString for conversion of {self.name} values\n" + s += f"struct {self.get_struct_name()}" + " {\n" + s += f" static gz::util::unordered_string_map<{self.get_name()}> name2type;\n" + s += f" static std::map<{self.get_name()}, std::string> type2name;\n" + s += "}; // generated by gen_enum_str\n\n" + + # name2type + s += f"gz::util::unordered_string_map<{self.get_name()}> {self.get_struct_name()}::name2type " + "{\n" + for i in range(len(self.names)): + # { "ENUM_VAL", namespace::ENUM_NAME::ENUM_VAL }, + s += '\t{ "' + self.names[i] + '", ' + self.get_name() + "::" + self.names[i] + " },\n" + s += "}; // generated by gen_enum_str\n\n" + + # type2name + s += f"std::map<{self.get_name()}, std::string> {self.get_struct_name()}::type2name " + "{\n" + for i in range(len(self.names)): + # { namespace::ENUM_NAME::ENUM_VAL, "ENUM_VAL", }, + s += '\t{ ' + self.get_name() + "::" + self.names[i] + ', "' + self.names[i] + '" },\n' + s += "}; // generated by gen_enum_str\n\n" + return s + + def get_def_toString(self) -> str: + s = "" + s += f"std::string toString(const {self.get_name()}& v) " + "{\n" + s += f"\tif ({self.get_struct_name()}::type2name.contains(v)) " + "{\n" + s += f"\t\treturn {self.get_struct_name()}::type2name.at(v);\n" + s += "\t}\n" + s += "\telse {\n" + if Enum.throw_exceptions: + s += f'\t\tthrow gz::InvalidArgument("InvalidArgument: \'" + std::to_string(static_cast(v)) + "\'", "toString({self.name})");\n' + else: + s += f'\t\treturn "";\n' + s += "\t}\n" + s += "} // generated by gen_enum_str\n\n" + return s + + def get_def_fromString(self) -> str: + s = "" + # string_view + s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string_view& sv) " + "{\n" + s += f"\tif ({self.get_struct_name()}::name2type.contains(sv)) " + "{\n" + s += f"\t\treturn {self.get_struct_name()}::name2type.find(sv)->second;\n" + s += "\t}\n" + s += "\telse {\n" + if Enum.throw_exceptions: + s += f'\t\tthrow gz::InvalidArgument("InvalidArgument: \'" + std::string(sv) + "\'", "fromString<{self.name}>");\n' + else: + s += f'\t\treturn {self.get_name()}::{self.names[-1]};\n' + s += "\t}\n" + s += "} // generated by gen_enum_str\n\n" + # string + s += f"template<> {self.get_name()} fromString<{self.get_name()}>(const std::string& s) " + "{\n" + s += f"\treturn fromString<{self.get_name()}>(std::string_view(s));\n" + s += "} // generated by gen_enum_str\n\n" + return s -def process_file(inputfile: str, outputfile: str): - print("process_file:", inputfile) - if not path.isfile(inputfile): error("File does not exist:" + inputfile) - with open(inputfile, "r") as file: +def append_enums_to_files(header_file:str, source_file:str, enums: list[Enum], interactive:bool=False): + """ + put declaration at the end of the source + put definition at the end of the header + """ + if len(enums) == 0: + return + + print("append_enums_to_files:", header_file, source_file) + if not path.isfile(header_file): error("File does not exist:" + header_file) + if not path.isfile(source_file): + print("Creating source_file: " + source_file) + with open(source_file, "w") as file: + file.write(f'// generated by gen_enum_str\n#include "{header_file}"\n\n#include \n\n') + + with open(header_file, "r") as file: + header = file.read() + with open(source_file, "r") as file: + source = file.read() + + # delete everything between the two comments + comment_gen_begin = "// ENUM - STRING CONVERSION BEGIN\n" + comment_gen_end = "// ENUM - STRING CONVERSION END\n" + + gen_begin = header.find(comment_gen_begin) + gen_end = header.find(comment_gen_end) + if gen_begin > 0 and gen_end > gen_begin: + header = header[:gen_begin - 1] + header[gen_end + len(comment_gen_end):] + + gen_begin = source.find(comment_gen_begin) + gen_end = source.find(comment_gen_end) + if gen_begin > 0 and gen_end > gen_begin: + source = source[:gen_begin - 1] + source[gen_end + len(comment_gen_end):] + + header += "\n" + comment_gen_begin + header += "// do not write your own code between these comment blocks - it will be overwritten when you run gen_enum_str.py again\n" + source += "\n" + comment_gen_begin + source += "// do not write your own code between these comment blocks - it will be overwritten when you run gen_enum_str.py again\n" + for enum in enums: + if interactive: + answer = input(f"Generate conversion for: {header_file}:{enum.startLine} - enum {enum.name}? (y/n): ") + if answer not in "yY": + continue + header += f"//\n// {enum.name}\n//\n" + header += enum.get_dec_toString() + header += enum.get_dec_fromString() + "\n" + + source += f"//\n// {enum.name}\n//\n" + source += enum.get_def_struct() + source += enum.get_def_toString() + source += enum.get_def_fromString() + header += "\n" + comment_gen_end + source += "\n" + comment_gen_end + + + with open(header_file, "w") as file: + file.write(header) + with open(source_file, "w") as file: + file.write(source) + + + +def get_enums(headerfile: str) -> list[Enum]: + """ + find enums in headerfile + namespaces are not properly supported, the script can not detect when a namespace is closed. + so it only works when you dont open namespaces in namespaces + """ + # print("get_enums:", headerfile) + if not path.isfile(headerfile): error("File does not exist:" + headerfile) + + with open(headerfile, "r") as file: lines = file.readlines() # FIND ENUMS enums_start = [] enums_stop = [] + enums_namespace = [] + current_namespace = "" in_enum = False for i in range(len(lines)): + match = re.match(r" *namespace ([a-zA-Z0-9_:]+) +{.*", lines[i]) + if match: + current_namespace = match.groups()[0] + if re.match(r" *enum [a-zA-Z0-9_-]+ {.*", lines[i]) and not "gen_enum_str.py" in lines[i]: if in_enum: error("process_file: could not find end of enum starting at line " + str(enums_start.pop())) in_enum = True enums_start.append(i) + enums_namespace.append(current_namespace) if in_enum: if re.match(r" *[^/*]?.*}.*", lines[i]): @@ -84,84 +324,149 @@ def process_file(inputfile: str, outputfile: str): var_end = len(m.group()) enum[m.group()[0:var_end]] = enum_val enum_val += 1 - print("enum:", enum_names[i], enum) + # print("enum:", enum_names[i], enum) enum_dicts.append(enum) - - for i in reversed(range(len(enum_names))): - lines.insert(enums_stop[i] + 1, getEnumStrF(enum_names[i], enum_dicts[i], spaces[i])) - lines[enums_start[i]] = lines[enums_start[i]].strip("\n") + " // processed by gen_enum_str.py\n" - - with open(outputfile, "w") as file: - file.writelines(lines) + enums = [] + for i in range(len(enum_names)): + enums.append(Enum(enums_start[i], enum_names[i], list(enum_dicts[i].values()), list(enum_dicts[i].keys()), enums_namespace[i])) + return enums -def process_path(path_): - if path.isfile(path_) and path.splitext(path_)[-1] in filetypes: - process_file(path_, path_) +# def process_file(inputfile: str, outputfile: str): +# print("process_file:", inputfile) +# if not path.isfile(inputfile): error("File does not exist:" + inputfile) - elif path.isdir(path_): - for p in listdir(path_): - if path.isfile(p) and path.splitext(p)[-1] in filetypes: - process_file(p, p) - elif path.isdir(p): - chdir(p) - process_path(p) - chdir("..") - else: - print("Invalid path:", path_) +# with open(inputfile, "r") as file: +# lines = file.readlines() + +# # FIND ENUMS +# enums_start = [] +# enums_stop = [] +# in_enum = False +# for i in range(len(lines)): +# if re.match(r" *enum [a-zA-Z0-9_-]+ {.*", lines[i]) and not "gen_enum_str.py" in lines[i]: +# if in_enum: error("process_file: could not find end of enum starting at line " + str(enums_start.pop())) +# in_enum = True +# enums_start.append(i) + +# if in_enum: +# if re.match(r" *[^/*]?.*}.*", lines[i]): +# enums_stop.append(i) +# in_enum = False +# if len(enums_start) != len(enums_stop): error("process_file: could not find end of enum starting at line " + str(enums_start.pop())) + +# # create a single string with the enum +# enums = [] +# for i in range(len(enums_start)): +# enums.append("") +# j = enums_start[i] +# while j <= enums_stop[i]: +# enums[i] += lines[j] +# j += 1 + +# # get name and vars +# enum_names = [] +# enum_dicts = [] +# spaces = [] +# var = "[a-zA-Z0-9_-]+" +# enumvar = "(" + var + r"(=*-?\d+)?)" +# restring = "enum (" + var + "){(" + enumvar + ",)*" + enumvar + ",?}" +# for i in range(len(enums)): +# enums[i] = enums[i].replace("\n", "").replace("enum ", "enumü").replace(" ", "").replace("enumü", "enum ") +# match = re.match(restring, enums[i]) +# if not match: error("Invalid enum at line " + str(enums_start[i])) + +# spaces.append(lines[enums_start[i]].find("e")) +# enum_names.append(match.groups()[0]) +# enum_val = 0 +# enum = {} +# for m in re.finditer(enumvar, enums[i][len("enum " + enum_names[i]):]): +# if "=" in m.group(): +# enum_val = int(m.group()[m.group().find("=")+1:]) +# var_end = m.group().find("=") +# else: +# var_end = len(m.group()) +# enum[m.group()[0:var_end]] = enum_val +# enum_val += 1 +# print("enum:", enum_names[i], enum) +# enum_dicts.append(enum) + +# for i in reversed(range(len(enum_names))): +# lines.insert(enums_stop[i] + 1, getEnumStrF(enum_names[i], enum_dicts[i], spaces[i])) +# lines[enums_start[i]] = lines[enums_start[i]].strip("\n") + " // processed by gen_enum_str.py\n" + +# with open(outputfile, "w") as file: +# file.writelines(lines) + + +def process_path(path_, interactive:bool=False, recurse:bool=False): + # print("Processing path:", path_) + if path.isfile(path_) and path.splitext(path_)[-1] in header_filetypes: + enums = get_enums(path_) + append_enums_to_files(path_, path_.split('.')[0] + source_filetype, enums, interactive) + elif recurse: + if path.isdir(path_): + for p in listdir(path_): + process_path(path_ + "/" + p, interactive, recurse) def print_help(): - """ -Synposis: get_enum_str.py ... ... --o output: file to path. Only when 1 input file. --h --help help --r recurse: Recurse through the directory and process all cpp/hpp files. Implies -i --i in place: Edit the file in place -""" + print(""" +Synposis: get_enum_str.py ... ... + get_enum_str.py ... -r ... +-h --help help +-r recurse: Recurse through given paths process all files. +-i interactive: prompt for every enumeration +--no-docs turn off docstring generation +--docs-no-names do not list names of enumeration values in docstring +--docs-no-gen do not put "generated by gen_enum_str" in docstring +--no-throw return empty string/last enum value if the argument for to/fromString is invalid. + This would normaly throw gz::InvalidArgument + This option does not make the functions noexcept! + +If the generated code produces errors: +- check that necessary headers are included in the source file: + - gz-util/string.hpp + - gz-util/exceptions.hpp (unless you use --no-throw) +- check the namespaces of the enumerations, the generated code should be in global namespace + nested namespace are not supported, you will have to correct that if you use them +""") def missing_arg(arg): print("Missing argument for", arg) exit(1) if __name__ == "__main__": - output_file = None input_files = [] - in_place = False recurse = False + interactive = False i = 1 while i in range(1, len(argv)): if argv[i] == "--help" or argv[i] == "-h": print_help() exit(0) - if argv[i] == "-o": - if len(argv) > i + 1: output_file = argv[i+1] - else: missing_arg(argv[i]) - i += 1 elif argv[i] == "-r": recurse = True elif argv[i] == "-i": - in_place = True + interactive = True + elif argv[i] == "--no-throw": + Enum.throw_exceptions = False + elif argv[i] == "--no-docs": + Enum.include_docstrings = False + elif argv[i] == "--docs-no-names": + Enum.docstring_include_names = False + elif argv[i] == "--docs-no-gen": + Enum.docstring_include_generated = False else: input_files.append(argv[i]) i += 1 - if output_file is None and not in_place and not recurse: - error("Missing output path or -i") - if output_file and (recurse or in_place or len(input_files) > 1): - error("Error: -o does not work with -r, -i or multiple input paths.") if len(input_files) == 0: - error("Missing input files.") - if in_place: - output_file = input_files[0] - - if (recurse): - for f in input_files: - process_path(f) - else: - process_file(input_files[0], output_file) + error("Missing input files/paths.") + for f in input_files: + process_path(path.abspath(f), recurse=recurse, interactive=interactive)