commit 0784aa543e608c149696c30fa8677db93850d62b Author: Ad5001 Date: Sun Jan 2 00:31:44 2022 +0100 First commit. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3444a12 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# BashDocGenerator + Small bash markdown documentation generation for bash for my libraries. + +## Usage + +```bash +./generator.py file.sh > output.md +``` + +## Example + +### Bash source code: +```bash +# +# BashOOP - Simple OOP implementation for bash. +# Copyright (C) 2021 Ad5001 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# Namespace related variables. Due to beginning with a _, it's considered internal. +_namespace="" + +# Example test variable. +exampleVariable="test" + +# Other variable +# Type: int +exampleVariable2=0 + +# Declares the current namespace. +# Signature: ([string namespaceName]) -> void +namespace() { + _namespace=$1; +} + +# Imports a namespace into the current shell. +# It saves the path of the file so that relative paths can be +# properly resolved. +# For example, if the object Object exists within namespace Example, it +# will be accessible with "Example.Object". +# Signature: () -> void +importNamespace() { + namespaceFile=$1 + # Save the path in order to get the absolute path of the file. + _namespacePath=$(realpath $(dirname $namespaceFile)) + . $namespaceFile +} +``` + +# Markdown equivalent + +```markdown + +# BashOOP Library Reference (oop.sh) +*Simple OOP implementation for bash.* +Under [LGPLv3](https://www.gnu.org/licenses/lgpl-3.0.en.html) - Copyright (C) 2021 Ad5001 + +--- + +|Contents | +|----------------------------------| +|
    [Variables](#variables)
| +|
    [Classes](#classes)
| +|
    [Properties](#properties)
| +|
    [Methods](#methods)
| + +

+ +## Variables + +### Reference + +- `string ` [`exampleVariable`](#var-exampleVariable) +- `int ` [`exampleVariable2`](#var-exampleVariable2) + +### Detailed Documentation + +
  • string exampleVariable
+ +Example test variable. + +
  • int exampleVariable2
+ +Other variable. + + +## Methods +**Note**: Arguments between <> are considered mandatory, while the ones within [] are considered optionnal. + +### Reference + +- [`namespace`](#method-namespace)` [string namespaceName] → void` +- [`importNamespace`](#method-importNamespace)` → void` + +### Detailed Documentation + +
  • namespace [string namespaceName] → void
+Declares the current namespace. + +
  • importNamespace <string namespaceFile> → void
+Imports a namespace into the current shell. +It saves the path of the file so that relative paths can be properly resolved. +For example, if the object Object exists within namespace Example, it will be accessible with "Example.Object". +``` diff --git a/generator.py b/generator.py new file mode 100755 index 0000000..1337eb1 --- /dev/null +++ b/generator.py @@ -0,0 +1,232 @@ +#!/usr/bin/python3 +# 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("|
  • [Variables](#variables)
|") + if len(properties) != 0: + print("|
  • [Properties](#properties)
|") + if len(classes) != 0: + print("|
  • [Classes](#classes)
|") + if len(methods) != 0: + print("|
  • [Methods](#methods)
|") + 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
  • {var.type} {var.name} = {var.default_value}
\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
  • {"static" if cls.static else ""} class {cls.name}
\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()