First commit.

This commit is contained in:
Adsooi 2022-01-02 00:31:44 +01:00
commit 0784aa543e
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
2 changed files with 348 additions and 0 deletions

116
README.md Normal file
View file

@ -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 <mail@ad5001.eu>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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: (<string namespaceFile>) -> 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 <mail@ad5001.eu>
---
|Contents |
|----------------------------------|
|<ul>[Variables](#variables)</ul> |
|<ul>[Classes](#classes)</ul> |
|<ul>[Properties](#properties)</ul>|
|<ul>[Methods](#methods)</ul> |
<br><br>
## Variables
### Reference
- `string ` [`exampleVariable`](#var-exampleVariable)
- `int ` [`exampleVariable2`](#var-exampleVariable2)
### Detailed Documentation
<ul id="var-exampleVariable"><li><code>string exampleVariable</code></li></ul>
Example test variable.
<ul id="var-exampleVariable"><li><code>int exampleVariable2</code></li></ul>
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)` <string namespaceFile> → void`
### Detailed Documentation
<ul id="method-namespace"><li><code>namespace [string namespaceName] → void</code></li></ul>
Declares the current namespace.
<ul id="method-importNamespace"><li><code>importNamespace &lt;string namespaceFile&gt; → void</code></li></ul>
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".
```

232
generator.py Executable file
View file

@ -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]} <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("<", "&lt;").replace(">", "&gt;")}</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()