#!/usr/bin/python3 # Small bash markdown documentation generation for bash for my libraries. # Copyright (C) 2022 Ad5001 # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # This file generates a markdown reference for bash scripts based on publicly declared variables and functions. from sys import argv, exit from os import path from re import compile as RegEx, IGNORECASE copy_reg = RegEx("^#\s*(Copyright \\(C\\) .+)$") # Check for default copyright declaration. func_reg = RegEx("^([^|()=&^$_#\s][^|()=&$ #\\s.]*(\\.[^|()=&^$_#\s][^|()=&$ #\\s]*)?)\\s*\\(\\)(\\s*{)?", IGNORECASE) # Function declaration regex. var_reg = RegEx("^([a-z][\\w]*)=(.+)$", IGNORECASE) # Variable declaration regex. class_reg = RegEx("(static_)?class\\s+([a-z][\\w]*)\\s+([\"'][a-z][\\w.]*\\.shc['\"]|[a-z][\\w.]*\\.shc)", IGNORECASE) # Class declaration regex. property_reg = RegEx("^property\\s+[^|()=&^$_#\s][^|()=&$ #\\s]*\\.([^|()=&^$_#\s][^|()=&$ #\\s]*)(\\s+[^\\s].*)?$", IGNORECASE) if len(argv) != 2: print(f"Usage: {argv[0]} ") exit(1) if not path.exists(argv[1]): print(f"File {argv[1]} does not exist.") exit(1) LICENSES_TEXT = { "GNU Lesser General Public License": "Under [LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.en.html)", "GNU General Public License": "Under [GPLv3](https://www.gnu.org/licenses/gpl-3.0.en.html)", "GNU Affero General Public License": "Under [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.en.html)" } class FileType: LIBRARY = 0 NAMESPACE = 1 CLASS = 2 class Variable: def __init__(self, name: str, var_type: str, comment: str, default_value: str): self.name = name self.type = var_type self.comment = comment self.default_value = default_value class Function: def __init__(self, name: str, signature: str, comment: str): self.name = name self.signature = signature self.comment = comment class Class: def __init__(self, name: str, filename: str, comment: str, static: bool, signature: str): self.name = name self.filename = filename self.comment = comment self.static = static self.signature = signature def run(): f = open(argv[1]) filename = path.basename(argv[1]) lines = f.readlines() title = filename + " Library Reference." subtitle = "" legal_text = "" if filename[-3:] == ".sh": # For libraries copy_match = copy_reg.match(lines[2].strip()) if copy_match is not None: # Copyright declaration legal_text = copy_match.group(1) for text in LICENSES_TEXT: if text in lines[5]: legal_text = LICENSES_TEXT[text] + " - " + legal_text # Basic info line info_line = lines[1].strip()[1:].strip().split(" - ") project_name = info_line[0] project_desc = info_line[1:] title = f"{project_name} Library Reference ({filename})" subtitle = " - ".join(project_desc) elif filename[-4:] == ".shc": # For classes title = f"{filename.split('.')[0]} Class Reference ({filename})" namespace, properties, methods, variables, classes = parse_data(filename) if namespace != "" and filename[-4:] == ".shn": title = f"{namespace} Namespace Reference ({filename})" # Printing result. print(f"# {title}") if subtitle != "": print(f"*{subtitle}* ") if legal_text != "": print(f"{legal_text}\n") if subtitle != "" or legal_text != "": print( "---\n\n") # Table of contents. print( "|Contents |") print( "|-------------------------------------------|") if len(variables) != 0: print("| |") if len(classes) != 0: print("| |") if len(properties) != 0: print("||") if len(methods) != 0: print("| |") print("\n\n") # Skip a few liness # Declare variables. if len(variables) != 0: print("## Variables\n") print("### Reference\n") for var in variables: print(f"- [`{var.type} {var.name}`](#var-{var.name})") print("\n### Detailed Documentation\n") for var in variables: print(f'\n\n') print("\n ".join(var.comment.replace("NOTE:", "**Note**:").split("\n")) + "\n") # Declare classes. if len(classes) != 0: print("## Classes\n") print("**Note**: Classes requires the [BashOOP](https://git.ad5001.eu/Ad5001/BashOOP) library to be imported. ") print("### Reference\n") for cls in classes: print(f"- [`{'static ' if cls.static else ''}class {cls.name}`](#class-{cls.name})") print("\n### Detailed Documentation\n") for cls in classes: print(f'\n\n') print(f'*Class imported from file `{cls.filename}`.* ') print("\n ".join(cls.comment.replace("NOTE:", "**Note**:").split("\n")) + "\n") if not cls.static: print(f'An instance of {cls.name} can be created using `{cls.name} {cls.signature}`.') # Declare properties. if len(properties) != 0: print("## Properties\n") print("**Note**: Properties requires the [BashOOP](https://git.ad5001.eu/Ad5001/BashOOP) library to be imported. ") print("**Note**: Properties can be accessed using `$(.)` and set with `. = `.\n") print("### Reference\n") for prop in properties: print(f"- [`{prop.type} {prop.name}`](#prop-{prop.name})") print("\n### Detailed Documentation\n") for prop in properties: print(f'\n
  • {prop.type} {prop.name}{" = " + prop.default_value if prop.default_value != "" else ""}
\n') print("\n ".join(prop.comment.replace("NOTE:", "**Note**:").split("\n")) + "\n") # Declare methods if len(methods) != 0: print("\n## Methods") print("**Note**: Arguments between <> are considered mandatory, while the ones within [] are considered optionnal.\n") print("### Reference\n") for func in methods: print(f"- [`{func.name} {func.signature}`](#method-{func.name})") print("\n### Detailed Documentation\n") for func in methods: print(f'\n
  • {func.name}{func.signature.replace("<", "<").replace(">", ">")}
\n') print(" \n".join(func.comment.replace("NOTE:", "**Note**:").split("\n")) + "\n") def parse_data(filename: str) -> tuple: """ Parses doc data from a file. """ methods = [] properties = [] variables = [] classes = [] namespace = "" f = open(filename) folder = path.dirname(filename) lines = f.readlines() for i in range(len(lines)): line = lines[i] # Check for namespace if filename[-4:] == ".shn" and line[:10] == "namespace ": namespace = line[10:].strip() # Check for properties prop_match = property_reg.match(line) if prop_match is not None: comment, found_data = find_full_comment(lines, i-1) default = prop_match.group(2) properties.append(Variable(prop_match.group(1), found_data["type"], comment, default.strip() if default is not None else "")) # Check for function func_match = func_reg.match(line) if func_match is not None: comment, found_data = find_full_comment(lines, i-1) methods.append(Function(func_match.group(1).split(".")[-1], found_data["signature"], comment)) # Check for variable var_match = var_reg.match(line) if var_match is not None: comment, found_data = find_full_comment(lines, i-1) variables.append(Variable(var_match.group(1), found_data["type"], comment, var_match.group(2).strip())) # Check for classes class_match = class_reg.match(line) if class_match is not None: comment, found_data = find_full_comment(lines, i-1) class_file = class_match.group(3) if class_file[0] == '"' or class_file[0] == "'": class_file = class_file[1:-1] # Get constructor from parsed file data. n, p, m, v, c = parse_data(path.join(folder, class_file)) constructor = "" for method in m: if method.name == "constructor": constructor = method.signature classes.append(Class(class_match.group(2), class_file, comment, class_match.group(1) is not None, constructor)) return namespace, properties, methods, variables, classes def find_full_comment(lines: list, line_index: int) -> tuple: """ Finds and returns a comment string without the comment begin, with index starting at the last line of the comment. It also searches for signature and type, and if not found returns it's default values. """ cmt = " " variables = { "signature": "() → void", "type": "string", } while line_index >= 0 and lines[line_index][0] == "#": line = lines[line_index][1:].strip() if line[:10] == "Signature:": variables["signature"] = line[10:].strip().replace("->", "→").replace("(", "").replace(")", "").replace(", ", " ") elif line[:5] == "Type:": variables["type"] = line[5:].strip() elif len(line) > 0: cmt = line + ("\n" if line[-1] == "." or cmt[0] == "-" else " ") + cmt # Only add newline if the comment finishes by a dot. line_index-=1 return cmt, variables run()