First commit.
This commit is contained in:
commit
0784aa543e
2 changed files with 348 additions and 0 deletions
116
README.md
Normal file
116
README.md
Normal 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 <string namespaceFile> → 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
232
generator.py
Executable 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("<", "<").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()
|
Loading…
Reference in a new issue