#!/bin/python3 """ 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 """ from os import path, listdir, chdir from sys import argv, exit import re # 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) 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 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]): 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) 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_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: # 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(): 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__": input_files = [] 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) elif argv[i] == "-r": recurse = True elif argv[i] == "-i": 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 len(input_files) == 0: error("Missing input files/paths.") for f in input_files: process_path(path.abspath(f), recurse=recurse, interactive=interactive)