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