This is an automated email from the ASF dual-hosted git repository.
aw pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/yetus.git
The following commit(s) were added to refs/heads/main by this push:
new e341681 YETUS-1121. rewrite shelldocs (#219)
e341681 is described below
commit e341681e706690ee13e63b80a86153ed12bb960f
Author: Allen Wittenauer <[email protected]>
AuthorDate: Tue Sep 28 07:52:03 2021 -0700
YETUS-1121. rewrite shelldocs (#219)
---
shelldocs/src/main/python/shelldocs.py | 590 +++++++++++++++------------------
1 file changed, 265 insertions(+), 325 deletions(-)
diff --git a/shelldocs/src/main/python/shelldocs.py
b/shelldocs/src/main/python/shelldocs.py
index 3660e3b..7248125 100755
--- a/shelldocs/src/main/python/shelldocs.py
+++ b/shelldocs/src/main/python/shelldocs.py
@@ -17,10 +17,13 @@
""" process bash scripts and generate documentation from them """
# Do this immediately to prevent compiled forms
-import sys
+
+import logging
import os
+import pathlib
import re
-import errno
+import sys
+
from argparse import ArgumentParser
sys.dont_write_bytecode = True
@@ -45,54 +48,30 @@ ASFLICENSE = '''
-->
'''
-FUNCTIONRE = re.compile(r"^(\w+) *\(\) *{")
-
-def docstrip(key, dstr):
- '''remove extra spaces from shelldoc phrase'''
- dstr = re.sub("^## @%s " % key, "", dstr)
- dstr = dstr.lstrip()
- dstr = dstr.rstrip()
- return dstr
-
-
-def toc(tlist):
- '''build a table of contents'''
- tocout = []
- header = ()
- for i in tlist:
- if header != i.getinter():
- header = i.getinter()
- line = " * %s\n" % (i.headerbuild())
- tocout.append(line)
- line = " * [%s](#%s)\n" % (i.getname().replace("_",
- r"\_"), i.getname())
- tocout.append(line)
- return tocout
-
-
-class ShellFunction: # pylint: disable=too-many-public-methods,
too-many-instance-attributes
+class ShellFunction: # pylint: disable=too-many-instance-attributes
"""a shell function"""
- def __init__(self, filename):
+ def __init__(self, filename='Unknown'):
'''Initializer'''
- self.name = None
- self.audience = None
- self.stability = None
- self.replacetext = None
- self.replaceb = False
- self.returnt = None
- self.desc = None
- self.params = None
+ self.audience = ''
+ self.description = []
self.filename = filename
self.linenum = 0
+ self.name = ''
+ self.params = []
+ self.replacebool = False
+ self.replacerawtext = ''
+ self.replacetext = 'Not Replaceable'
+ self.returnt = []
+ self.stability = ''
def __lt__(self, other):
'''comparison'''
if self.audience == other.audience:
if self.stability == other.stability:
- if self.replaceb == other.replaceb:
+ if self.replacebool == other.replacebool:
return self.name < other.name
- if self.replaceb:
+ if self.replacebool:
return True
else:
if self.stability == "Stable":
@@ -102,320 +81,272 @@ class ShellFunction: # pylint:
disable=too-many-public-methods, too-many-instan
return True
return False
- def reset(self):
- '''empties current function'''
- self.name = None
- self.audience = None
- self.stability = None
- self.replacetext = None
- self.replaceb = False
- self.returnt = None
- self.desc = None
- self.params = None
- self.linenum = 0
- self.filename = None
-
- def getfilename(self):
- '''get the name of the function'''
- if self.filename is None:
- return "undefined"
- return self.filename
-
- def setname(self, text):
- '''set the name of the function'''
- if FUNCTIONRE.match(text):
- definition = FUNCTIONRE.match(text).groups()[0]
- else:
- definition = text.split()[1]
- self.name = definition.replace("(", "").replace(")", "")
-
- def getname(self):
- '''get the name of the function'''
- if self.name is None:
- return "None"
- return self.name
-
- def setlinenum(self, linenum):
- '''set the line number of the function'''
- self.linenum = linenum
-
- def getlinenum(self):
- '''get the line number of the function'''
- return self.linenum
-
- def setaudience(self, text):
- '''set the audience of the function'''
- self.audience = docstrip("audience", text)
- self.audience = self.audience.capitalize()
-
- def getaudience(self):
- '''get the audience of the function'''
- if self.audience is None:
- return "None"
- return self.audience
-
- def setstability(self, text):
- '''set the stability of the function'''
- self.stability = docstrip("stability", text)
- self.stability = self.stability.capitalize()
-
- def getstability(self):
- '''get the stability of the function'''
- if self.stability is None:
- return "None"
- return self.stability
-
- def setreplace(self, text):
- '''set the replacement state'''
- self.replacetext = docstrip("replaceable", text)
- if self.replacetext.capitalize() == "Yes":
- self.replaceb = True
-
- def getreplace(self):
- '''get the replacement state'''
- if self.replaceb:
- return "Yes"
- return "No"
-
- def getreplacetext(self):
- '''get the replacement state text'''
- return self.replacetext
-
- def getinter(self):
- '''get the function state'''
- return self.getaudience(), self.getstability(), self.getreplace()
-
- def addreturn(self, text):
- '''add a return state'''
- if self.returnt is None:
- self.returnt = []
- self.returnt.append(docstrip("return", text))
-
- def getreturn(self):
- '''get the complete return state'''
- if self.returnt is None:
- return "Nothing"
- return "\n\n".join(self.returnt)
-
- def adddesc(self, text):
- '''add to the description'''
- if self.desc is None:
- self.desc = []
- self.desc.append(docstrip("description", text))
-
- def getdesc(self):
- '''get the description'''
- if self.desc is None:
- return "None"
- return " ".join(self.desc)
-
- def addparam(self, text):
- '''add a parameter'''
- if self.params is None:
- self.params = []
- self.params.append(docstrip("param", text))
-
- def getparams(self):
- '''get all of the parameters'''
- if self.params is None:
- return ""
- return " ".join(self.params)
-
- def getusage(self):
- '''get the usage string'''
- line = "%s %s" % (self.name, self.getparams())
- return line.rstrip()
-
- def headerbuild(self):
+ def header(self):
'''get the header for this function'''
- if self.getreplace() == "Yes":
- replacetext = "Replaceable"
- else:
- replacetext = "Not Replaceable"
- line = "%s/%s/%s" % (self.getaudience(), self.getstability(),
- replacetext)
- return line
+ return f"{self.audience}/{self.stability}/{self.replacetext}"
def getdocpage(self):
'''get the built document page for this function'''
- line = "### `%s`\n\n"\
- "* Synopsis\n\n"\
- "```\n%s\n"\
- "```\n\n" \
- "* Description\n\n" \
- "%s\n\n" \
- "* Returns\n\n" \
- "%s\n\n" \
- "| Classification | Level |\n" \
- "| :--- | :--- |\n" \
- "| Audience | %s |\n" \
- "| Stability | %s |\n" \
- "| Replaceable | %s |\n\n" \
- % (self.getname(),
- self.getusage(),
- self.getdesc(),
- self.getreturn(),
- self.getaudience(),
- self.getstability(),
- self.getreplace())
- return line
+ params = " ".join(self.params)
+ usage = f"{self.name} {params}"
+ description = "\n".join(self.description)
+ if not self.returnt:
+ returntext = 'Nothing'
+ else:
+ returntext = "\n".join(self.returnt)
+ return (f"### `{self.name}`\n\n"
+ "* Synopsis\n\n"
+ f"```\n{usage}\n"
+ "```\n\n"
+ "* Description\n\n"
+ f"{description}\n\n"
+ "* Returns\n\n"
+ f"{returntext}\n\n"
+ "| Classification | Level |\n"
+ "| :--- | :--- |\n"
+ f"| Audience | {self.audience} |\n"
+ f"| Stability | {self.stability} |\n"
+ f"| Replaceable | {self.replacebool} |\n\n")
+
+ def isprivateandnotreplaceable(self):
+ ''' is this function Private and not replaceable? '''
+ return self.audience == "Private" and not self.replacebool
def lint(self):
'''Lint this function'''
- getfuncs = {
- "audience": self.getaudience,
- "stability": self.getstability,
- "replaceable": self.getreplacetext,
- }
validvalues = {
"audience": ("Public", "Private"),
"stability": ("Stable", "Evolving"),
- "replaceable": ("yes", "no"),
+ "replacerawtext": ("yes", "no"),
}
- messages = []
- for attr in ("audience", "stability", "replaceable"):
- value = getfuncs[attr]()
- if value == "None" and attr != 'replaceable':
- messages.append("%s:%u: ERROR: function %s has no @%s" %
- (self.getfilename(), self.getlinenum(),
- self.getname(), attr.lower()))
- elif value not in validvalues[attr]:
- validvalue = "|".join(v.lower() for v in validvalues[attr])
- messages.append(
- "%s:%u: ERROR: function %s has invalid value (%s) for @%s
(%s)"
- % (self.getfilename(), self.getlinenum(), self.getname(),
- value.lower(), attr.lower(), validvalue))
- return "\n".join(messages)
+ for attribute in validvalues:
+ value = getattr(self, attribute)
+ if (not value or value == ''):
+ logging.error("%s:%u:ERROR: function %s has no @%s",
+ self.filename, self.linenum, self.name,
+ attribute.lower().replace('rawtext', 'able'))
+ elif value not in validvalues[attribute]:
+ validvalue = "|".join(v.lower()
+ for v in validvalues[attribute])
+ logging.error(
+ "%s:%d:ERROR: function %s has invalid value (%s) for @%s
(%s)",
+ self.filename, self.linenum, self.name, value.lower(),
+ attribute.lower().replace('rawtext', 'able'), validvalue)
def __str__(self):
'''Generate a string for this function'''
- line = "{%s %s %s %s}" \
- % (self.getname(),
- self.getaudience(),
- self.getstability(),
- self.getreplace())
- return line
+ return f"{{{self.name} {self.audience} {self.stability}
{self.replacebool}}}"
-def marked_as_ignored(file_path):
- """Checks for the presence of the marker(SHELLDOC-IGNORE) to ignore the
file.
+class ProcessFile:
+ ''' shell file processor '''
- Marker needs to be in a line of its own and can not
- be an inline comment.
+ FUNCTIONRE = re.compile(r"^(\w+) *\(\) *{")
- A leading '#' and white-spaces(leading or trailing)
- are trimmed before checking equality.
+ def __init__(self, filename=None, skipsuperprivate=False):
+ self.filename = filename
+ self.functions = []
+ self.skipsuperprivate = skipsuperprivate
- Comparison is case sensitive and the comment must be in
- UPPERCASE.
- """
- with open(file_path) as input_file:
- for line in input_file:
- if line.startswith("#") and line[1:].strip() == "SHELLDOC-IGNORE":
- return True
- return False
+ def isignored(self):
+ """Checks for the presence of the marker(SHELLDOC-IGNORE) to ignore
the file.
+ Marker needs to be in a line of its own and can not
+ be an inline comment.
-def process_file(filename, skipprnorep):
- """ stuff all of the functions into an array """
- allfuncs = []
- try:
- with open(filename, "r") as shellcode:
- # if the file contains a comment containing
- # only "SHELLDOC-IGNORE" then skip that file
- if marked_as_ignored(filename):
- return None
- funcdef = ShellFunction(filename)
- linenum = 0
- for line in shellcode:
- linenum = linenum + 1
- if line.startswith('## @description'):
- funcdef.adddesc(line)
- elif line.startswith('## @audience'):
- funcdef.setaudience(line)
- elif line.startswith('## @stability'):
- funcdef.setstability(line)
- elif line.startswith('## @replaceable'):
- funcdef.setreplace(line)
- elif line.startswith('## @param'):
- funcdef.addparam(line)
- elif line.startswith('## @return'):
- funcdef.addreturn(line)
- elif line.startswith('function') or FUNCTIONRE.match(line):
- funcdef.setname(line)
- funcdef.setlinenum(linenum)
- if skipprnorep and \
- funcdef.getaudience() == "Private" and \
- funcdef.getreplace() == "No":
- pass
- else:
- allfuncs.append(funcdef)
- funcdef = ShellFunction(filename)
- except IOError as err:
- print("ERROR: Failed to read from file: %s. Skipping." % err.filename,
- file=sys.stderr)
- return None
- return allfuncs
+ A leading '#' and white-spaces(leading or trailing)
+ are trimmed before checking equality.
+
+ Comparison is case sensitive and the comment must be in
+ UPPERCASE.
+ """
+ with open(self.filename) as input_file:
+ for line in input_file:
+ if line.startswith(
+ "#") and line[1:].strip() == "SHELLDOC-IGNORE":
+ return True
+ return False
+
+ def _docstrip(self, key, dstr): #pylint: disable=no-self-use
+ '''remove extra spaces from shelldoc phrase'''
+ dstr = re.sub(f"^## @{key} ", "", dstr)
+ dstr = dstr.strip()
+ return dstr
+
+ def _process_description(self, funcdef, text=None):
+ if not text:
+ funcdef.description = []
+ return
+ funcdef.description.append(self._docstrip('description', text))
+
+ def _process_audience(self, funcdef, text=None):
+ '''set the audience of the function'''
+ if not text:
+ return
+ funcdef.audience = self._docstrip('audience', text)
+ funcdef.audience = funcdef.audience.capitalize()
+
+ def _process_stability(self, funcdef, text=None):
+ '''set the stability of the function'''
+ if not text:
+ return
+ funcdef.stability = self._docstrip('stability', text)
+ funcdef.stability = funcdef.stability.capitalize()
+
+ def _process_replaceable(self, funcdef, text=None):
+ '''set the replacement state'''
+ if not text:
+ return
+ funcdef.replacerawtext = self._docstrip("replaceable", text)
+ if funcdef.replacerawtext in ['yes', 'Yes', 'true', 'True']:
+ funcdef.replacebool = True
+ else:
+ funcdef.replacebool = False
+ if funcdef.replacebool:
+ funcdef.replacetext = 'Replaceable'
+ else:
+ funcdef.replacetext = 'Not Replaceable'
+
+ def _process_param(self, funcdef, text=None):
+ '''add a parameter'''
+ if not text:
+ funcdef.params = []
+ return
+ funcdef.params.append(self._docstrip('param', text))
+
+ def _process_return(self, funcdef, text=None):
+ '''add a return value'''
+ if not text:
+ funcdef.returnt = []
+ return
+ funcdef.returnt.append(self._docstrip('return', text))
+
+ def _process_function(self, funcdef, text=None, linenum=1): # pylint:
disable=no-self-use
+ '''set the name of the function'''
+ if ProcessFile.FUNCTIONRE.match(text):
+ definition = ProcessFile.FUNCTIONRE.match(text).groups()[0]
+ else:
+ definition = text.split()[1]
+ funcdef.name = definition.replace("(", "").replace(")", "")
+ funcdef.linenum = linenum
+
+ def process_file(self):
+ """ stuff all of the functions into an array """
+ self.functions = []
+
+ mapping = {
+ '## @description': '_process_description',
+ '## @audience': '_process_audience',
+ '## @stability': '_process_stability',
+ '## @replaceable': '_process_replaceable',
+ '## @param': '_process_param',
+ '## @return': '_process_return',
+ }
+
+ if self.isignored():
+ return
+
+ try:
+ with open(self.filename, "r") as shellcode:
+ # if the file contains a comment containing
+ # only "SHELLDOC-IGNORE" then skip that file
+
+ funcdef = ShellFunction(self.filename)
+ linenum = 0
+ for line in shellcode:
+ linenum = linenum + 1
+ for text, method in mapping.items():
+ if line.startswith(text):
+ getattr(self, method)(funcdef, text=line)
+
+ if line.startswith(
+ 'function') or ProcessFile.FUNCTIONRE.match(line):
+ self._process_function(funcdef,
+ text=line,
+ linenum=linenum)
+
+ if self.skipsuperprivate and
funcdef.isprivateandnotreplaceable(
+ ):
+ pass
+ else:
+ self.functions.append(funcdef)
+ funcdef = ShellFunction(self.filename)
+
+ except OSError as err:
+ logging.error("ERROR: Failed to read from file: %s. Skipping.",
+ err.filename)
+ self.functions = []
+
+
+class MarkdownReport:
+ ''' generate a markdown report '''
+ def __init__(self, functions, filename=None):
+ self.filename = filename
+ self.filepath = pathlib.Path(self.filename)
+ if functions:
+ self.functions = sorted(functions)
+ else:
+ self.functions = None
+
+ def write_tableofcontents(self, fhout):
+ '''build a table of contents'''
+ header = None
+ for function in self.functions:
+ if header != function.header():
+ header = function.header()
+ fhout.write(f" * {header}\n")
+ markdownsafename = function.name.replace("_", r"\_")
+ fhout.write(f" * [{markdownsafename}](#{function.name})\n")
+
+ def write_output(self):
+ """ write the markdown file """
+
+ self.filepath.parent.mkdir(parents=True, exist_ok=True)
+
+ with open(self.filename, "w", encoding='utf-8') as outfile:
+ outfile.write(ASFLICENSE)
+ self.write_tableofcontents(outfile)
+ outfile.write("\n------\n\n")
+
+ header = []
+ for function in self.functions:
+ if header != function.header():
+ header = function.header()
+ outfile.write(f"## {header}\n")
+ outfile.write(function.getdocpage())
def process_input(inputlist, skipprnorep):
""" take the input and loop around it """
+ def call_process_file(filename, skipsuperprivate):
+ ''' handle building a ProcessFile '''
+ fileprocessor = ProcessFile(filename=filename,
+ skipsuperprivate=skipsuperprivate)
+ fileprocessor.process_file()
+ return fileprocessor.functions
+
allfuncs = []
- for filename in inputlist: #pylint: disable=too-many-nested-blocks
- if os.path.isdir(filename):
- for root, dirs, files in os.walk(filename): #pylint:
disable=unused-variable
- for fname in files:
+ for inputname in inputlist:
+ if pathlib.Path(inputname).is_dir():
+ for dirpath, dirnames, filenames in os.walk(inputname): #pylint:
disable=unused-variable
+ for fname in filenames:
if fname.endswith('sh'):
- newfuncs = process_file(filename=os.path.join(
- root, fname),
- skipprnorep=skipprnorep)
- if newfuncs:
- allfuncs = allfuncs + newfuncs
+ allfuncs = allfuncs + call_process_file(
+ filename=pathlib.Path(dirpath).joinpath(fname),
+ skipsuperprivate=skipprnorep)
else:
- newfuncs = process_file(filename=filename, skipprnorep=skipprnorep)
- if newfuncs:
- allfuncs = allfuncs + newfuncs
-
+ allfuncs = allfuncs + call_process_file(
+ filename=inputname, skipsuperprivate=skipprnorep)
if allfuncs is None:
- print("ERROR: no functions found.", file=sys.stderr)
+ logging.error("ERROR: no functions found.")
sys.exit(1)
allfuncs = sorted(allfuncs)
return allfuncs
-def write_output(filename, functions):
- """ write the markdown file """
- try:
- directory = os.path.dirname(filename)
- if directory:
- if not os.path.exists(directory):
- os.makedirs(directory)
- except OSError as exc:
- if exc.errno == errno.EEXIST and os.path.isdir(directory):
- pass
- else:
- print("Unable to create output directory %s: %u, %s" % \
- (directory, exc.errno, exc.strerror))
- sys.exit(1)
-
- with open(filename, "w") as outfile:
- outfile.write(ASFLICENSE)
- for line in toc(functions):
- outfile.write(line)
- outfile.write("\n------\n\n")
-
- header = []
- for funcs in functions:
- if header != funcs.getinter():
- header = funcs.getinter()
- line = "## %s\n" % (funcs.headerbuild())
- outfile.write(line)
- outfile.write(funcs.getdocpage())
-
-
-def main():
- '''main entry point'''
+def process_arguments():
+ ''' deal with parameters '''
parser = ArgumentParser(
prog='shelldocs',
epilog="You can mark a file to be ignored by shelldocs by adding"
@@ -454,8 +385,8 @@ def main():
options = parser.parse_args()
if options.release_version:
- with open(os.path.join(os.path.dirname(__file__), "../VERSION"),
- 'r') as ver_file:
+ verfile = pathlib.Path(__file__).parent.joinpath('VERSION')
+ with open(verfile, encoding='utf-8') as ver_file:
print(ver_file.read())
sys.exit(0)
@@ -465,16 +396,25 @@ def main():
parser.error(
"At least one of output file and lint mode needs to be specified")
+ return options
+
+
+def main():
+ '''main entry point'''
+
+ logging.basicConfig(format='%(message)s')
+
+ options = process_arguments()
+
allfuncs = process_input(options.infile, options.skipprnorep)
if options.lint:
for funcs in allfuncs:
- message = funcs.lint()
- if message:
- print(message)
+ funcs.lint()
- if options.outfile is not None:
- write_output(options.outfile, allfuncs)
+ if options.outfile:
+ mdreport = MarkdownReport(allfuncs, filename=options.outfile)
+ mdreport.write_output()
if __name__ == "__main__":