233 lines
9.8 KiB
Python
Executable file
233 lines
9.8 KiB
Python
Executable file
#!/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]} <bash source file>")
|
|
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("|<ul><li>[Variables](#variables)</li></ul> |")
|
|
if len(properties) != 0:
|
|
print("|<ul><li>[Properties](#properties)</li></ul>|")
|
|
if len(classes) != 0:
|
|
print("|<ul><li>[Classes](#classes)</li></ul> |")
|
|
if len(methods) != 0:
|
|
print("|<ul><li>[Methods](#methods)</li></ul> |")
|
|
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<ul id="var-{var.name}"><li><code>{var.type} {var.name} = {var.default_value}</code></li></ul>\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<ul id="class-{cls.name}"><li><code>{"static" if cls.static else ""} class {cls.name}</code></li></ul>\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} <varName> {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 `$(<varName>.<propertyName>)` and set with `<varName>.<propertyName> = <value>`.\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<ul id="prop-{prop.name}"><li><code>{prop.type} {prop.name}{" = " + prop.default_value if prop.default_value != "" else ""}</code></li></ul>\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<ul id="method-{func.name}"><li><code>{func.name}{func.signature.replace("<", "<").replace(">", ">")}</code></li></ul>\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()
|