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 89f15d8  YETUS-452. Remove python2; rewrite python bits for python3 
(#201)
89f15d8 is described below

commit 89f15d8c998575535bba1f4b1a3bc312cb155c9f
Author: Allen Wittenauer <[email protected]>
AuthorDate: Mon Nov 16 23:24:55 2020 -0800

    YETUS-452. Remove python2; rewrite python bits for python3 (#201)
    
    Signed-off-by: Akira Ajisaka <[email protected]>
---
 .circleci/config.yml                               |   3 +-
 .cirrus.yml                                        |   3 +-
 .github/workflows/action-test.yml                  |   4 +-
 .github/workflows/yetus.yml                        |   1 -
 .gitlab-ci.yml                                     |   3 +-
 .semaphore/semaphore-build.sh                      |   3 +-
 .travis.yml                                        |   3 +-
 Jenkinsfile                                        |   5 +-
 asf-site-src/source/contribute/website.html.md     |   4 +-
 .../in-progress/precommit/plugins/pylint.html.md   |   3 +-
 .../in-progress/releasedocmaker.html.md            |   4 +-
 .../documentation/in-progress/shelldocs.html.md    |   2 +-
 precommit/src/main/python/jenkins-admin.py         | 191 ++++----
 .../src/main/shell/test-patch-docker/Dockerfile    |  35 +-
 releasedocmaker/src/main/python/releasedocmaker.py |   3 +-
 .../src/main/python/releasedocmaker/__init__.py    | 538 +++++++++++----------
 .../src/main/python/releasedocmaker/utils.py       |  61 ++-
 shelldocs/src/main/python/shelldocs.py             | 464 +++++++++++++++++-
 shelldocs/src/main/python/shelldocs/__init__.py    | 472 ------------------
 19 files changed, 881 insertions(+), 921 deletions(-)

diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1dc6731..a291f46 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -40,12 +40,11 @@ jobs:
              --plugins=all
              --java-home=/usr/lib/jvm/java-8-openjdk-amd64
              --patch-dir=/tmp/yetus-out
-             --pylint=pylint2
              --html-report-file=/tmp/yetus-out/report.html
              --console-report-file=/tmp/yetus-out/console.txt
              --brief-report-file=/tmp/yetus-out/brief.txt
              --bugcomments=briefreport,htmlout,junit
-             --tests-filter=checkstyle,javadoc,rubocop,test4tests
+             --tests-filter=checkstyle,test4tests
              --junit-report-xml=/tmp/yetus-out/junit-report.xml
 
       - store_test_results:
diff --git a/.cirrus.yml b/.cirrus.yml
index fe84b31..e54fc9b 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -28,8 +28,7 @@ yetus_task:
              --java-home=/usr/lib/jvm/java-8-openjdk-amd64
              --junit-report-xml=/tmp/yetus-out/junit.xml
              --plugins=all
-             --pylint=pylint2
-             --tests-filter=checkstyle,javadoc,rubocop,test4tests
+             --tests-filter=checkstyle,test4tests
   always:
     junit_artifacts:
       path: "yetus-out/junit.xml"
diff --git a/.github/workflows/action-test.yml 
b/.github/workflows/action-test.yml
index 17861f3..262458e 100644
--- a/.github/workflows/action-test.yml
+++ b/.github/workflows/action-test.yml
@@ -41,9 +41,7 @@ jobs:
           patchdir: ./out
           buildtool: maven
           githubtoken: ${{ secrets.GITHUB_TOKEN }}
-          pylint: pylint2
-          pip: pip2
-          testsfilter: checkstyle,javadoc,test4tests
+          testsfilter: checkstyle,test4tests
       - name: Artifact output
         if: ${{ always() }}
         uses: actions/upload-artifact@v2
diff --git a/.github/workflows/yetus.yml b/.github/workflows/yetus.yml
index 8a6fbea..8a6b908 100644
--- a/.github/workflows/yetus.yml
+++ b/.github/workflows/yetus.yml
@@ -52,7 +52,6 @@ jobs:
           --plugins=all
           --proclimit=2000
           --project=yetus
-          --pylint=pylint2
           --tests-filter=checkstyle,javadoc,rubocop,test4tests
       - name: Artifact output
         if: ${{ always() }}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 91b71a1..bec0520 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -23,13 +23,12 @@ buretoolbox-job:
       precommit/src/main/shell/test-patch.sh
       --patch-dir=/tmp/yetus-out
       --plugins=all
-      --pylint=pylint2
       --java-home=/usr/lib/jvm/java-8-openjdk-amd64
       --html-report-file=/tmp/yetus-out/report.html
       --console-report-file=/tmp/yetus-out/console.txt
       --brief-report-file=/tmp/yetus-out/brief.txt
       --bugcomments=briefreport,htmlout,gitlab,junit
-      --tests-filter=checkstyle,javadoc,rubocop,test4tests
+      --tests-filter=checkstyle,test4tests
       --junit-report-xml=/tmp/yetus-out/junit-report.xml
 
   artifacts:
diff --git a/.semaphore/semaphore-build.sh b/.semaphore/semaphore-build.sh
index e861676..879c661 100755
--- a/.semaphore/semaphore-build.sh
+++ b/.semaphore/semaphore-build.sh
@@ -22,8 +22,7 @@ PRECOMMITDIR=precommit/src/main/shell
   --mvn-custom-repos \
   --mvn-custom-repos-dir=/tmp/yetus-m2 \
   --patch-dir=/tmp/yetus-out \
-  --tests-filter=checkstyle,javadoc,rubocop,test4tests \
+  --tests-filter=checkstyle,test4tests \
   --docker \
-  --pylint=pylint2 \
   --dockerfile="${PRECOMMITDIR}/test-patch-docker/Dockerfile" \
   --docker-cache-from=apache/yetus-base:main,ubuntu:focal
diff --git a/.travis.yml b/.travis.yml
index 8afd242..28fab1f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -32,10 +32,9 @@ script:
     --patch-dir=/tmp/yetus-out
     --java-home=/usr/lib/jvm/java-8-openjdk-amd64
     --plugins=all
-    --pylint=pylint2
     --docker-cache-from=apache/yetus:main
     --html-report-file=/tmp/yetus-out/report.html
     --console-report-file=/tmp/yetus-out/console.txt
     --brief-report-file=/tmp/yetus-out/brief.txt
     --bugcomments=briefreport,htmlout
-    --tests-filter=checkstyle,javadoc,rubocop,test4tests
+    --tests-filter=checkstyle,test4tests
diff --git a/Jenkinsfile b/Jenkinsfile
index 02b7059..9acb826 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -101,9 +101,6 @@ pipeline {
                 YETUS_ARGS+=(--jira-password="${JIRA_PASSWORD}")
                 YETUS_ARGS+=(--jira-user="${JIRA_USER}")
 
-                # pylint settings
-                YETUS_ARGS+=('--pylint=pylint2')
-
                 # auto-kill any surefire stragglers during unit test runs
                 YETUS_ARGS+=(--reapermode=report)
 
@@ -122,7 +119,7 @@ pipeline {
 
                 # don't let these tests cause -1s because we aren't really 
paying that
                 # much attention to them
-                
YETUS_ARGS+=("--tests-filter=checkstyle,javadoc,rubocop,test4tests")
+                YETUS_ARGS+=("--tests-filter=checkstyle,test4tests")
 
                 if [[ "${USE_DEBUG_FLAG}" == true ]]; then
                   YETUS_ARGS+=("--debug")
diff --git a/asf-site-src/source/contribute/website.html.md 
b/asf-site-src/source/contribute/website.html.md
index bca25ed..1edf8d4 100644
--- a/asf-site-src/source/contribute/website.html.md
+++ b/asf-site-src/source/contribute/website.html.md
@@ -37,7 +37,7 @@ by reading [Middleman's excellent 
documentation](https://middlemanapp.com/basics
 
     NOTE: You MUST have run `mvn install` at least once prior to running `mvn 
site`.
 
-The following steps assume you have a working ruby 2.3+ environment setup:
+The following steps assume you have a working ruby 2.7+ environment setup:
 
 ```bash
 $ sudo gem install bundler
@@ -45,7 +45,7 @@ $ cd asf-site-src
 $ bundle install
 ```
 
-and a working Python 2.7 environment for 
[releasedocmaker](/documentation/in-progress/releasedocmaker/).
+and a working Python 3.8 environment for 
[releasedocmaker](/documentation/in-progress/releasedocmaker/).
 
 ## Make changes in asf-site-src/source
 
diff --git 
a/asf-site-src/source/documentation/in-progress/precommit/plugins/pylint.html.md
 
b/asf-site-src/source/documentation/in-progress/precommit/plugins/pylint.html.md
index 1195d73..44b8b6c 100644
--- 
a/asf-site-src/source/documentation/in-progress/precommit/plugins/pylint.html.md
+++ 
b/asf-site-src/source/documentation/in-progress/precommit/plugins/pylint.html.md
@@ -45,8 +45,7 @@ None
 
 # Docker Notes
 
-The default Apache Yetus image comes with `pip2`/`pylint2` for Python 2.x 
support and `pip3`/`pylint3` for Python 3.x support.
-The `pip` and `pylint` point to the Python 3.x versions.
+None
 
 # Developer Notes
 
diff --git 
a/asf-site-src/source/documentation/in-progress/releasedocmaker.html.md 
b/asf-site-src/source/documentation/in-progress/releasedocmaker.html.md
index c946c3b..f399e86 100644
--- a/asf-site-src/source/documentation/in-progress/releasedocmaker.html.md
+++ b/asf-site-src/source/documentation/in-progress/releasedocmaker.html.md
@@ -53,9 +53,9 @@ In order to solve these problems, `releasedocmaker` was 
written to automatically
 
 # Requirements
 
-* Python 2.7 with dateutil extension
+* Python 3.8 with dateutil extension
 
-dateutil may be installed via pip:  `pip2 install python-dateutil`
+dateutil may be installed via pip:  `pip3 install python-dateutil`
 
 # Basic Usage
 
diff --git a/asf-site-src/source/documentation/in-progress/shelldocs.html.md 
b/asf-site-src/source/documentation/in-progress/shelldocs.html.md
index 90038ea..4cd0e6a 100644
--- a/asf-site-src/source/documentation/in-progress/shelldocs.html.md
+++ b/asf-site-src/source/documentation/in-progress/shelldocs.html.md
@@ -41,7 +41,7 @@ Some projects have complex shell functions that act as APIs. 
`shelldocs` provide
 
 # Requirements
 
-* Python 2.7
+* Python 3.8
 
 # Function Annotations
 
diff --git a/precommit/src/main/python/jenkins-admin.py 
b/precommit/src/main/python/jenkins-admin.py
index 0786cf4..18679fd 100755
--- a/precommit/src/main/python/jenkins-admin.py
+++ b/precommit/src/main/python/jenkins-admin.py
@@ -1,5 +1,5 @@
-#!/usr/bin/env python2
-#pylint: disable=invalid-name
+#!/usr/bin/env python3
+# pylint: disable=invalid-name
 #
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -18,48 +18,37 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-
 """ Process patch file attachments from JIRA using a query """
 
-from optparse import OptionParser
+from argparse import ArgumentParser
 from tempfile import NamedTemporaryFile
 from xml.etree import ElementTree
-import base64
-import httplib
 import os
 import re
 import sys
-import urllib2
+import requests
+
 
 def http_get(resource, ignore_error=False, username=None, password=None):
     """ get the contents of a URL """
-    request = urllib2.Request(resource)
-    if username and password:
-        base64string = base64.b64encode('%s:%s' % (username, password))
-        request.add_header("Authorization", "Basic %s" % base64string)
+
     try:
-        response = urllib2.urlopen(request)
-    except urllib2.HTTPError, http_err:
-        code = http_err.code
-        print '%s returns HTTP error %d: %s' \
-              % (resource, code, http_err.reason)
-        if ignore_error:
-            return ''
-        else:
-            print 'Aborting.'
-            sys.exit(1)
-    except urllib2.URLError, url_err:
-        print 'Error contacting %s: %s' % (resource, url_err.reason)
-        if ignore_error:
-            return ''
+        if username and password:
+            response = requests.get(resource, auth=(username, password))
         else:
-            raise url_err
-    except httplib.BadStatusLine, err:
+            response = requests.get(resource)
+        response.raise_for_status()
+    except requests.exceptions.HTTPError as http_err:
+        errstr = str(http_err)
+        print(
+            f'%{resource} returns HTTP error %{response.status_code}: 
%{errstr}\n'
+        )
         if ignore_error:
             return ''
-        else:
-            raise err
-    return response.read()
+        print('Aborting.')
+        sys.exit(1)
+    return response.text
+
 
 def parse_jira_data(filename):
     """ returns a map of (project, issue) => attachment id """
@@ -88,9 +77,10 @@ def parse_jira_data(filename):
             result[jiraissue] = attachmentids[-1]
     return result
 
-def main(): #pylint: disable=too-many-branches, too-many-statements, 
too-many-locals
+
+def main():  #pylint: disable=too-many-branches, too-many-statements, 
too-many-locals
     """ main program """
-    parser = OptionParser(prog='jenkins-admin')
+    parser = ArgumentParser(prog='jenkins-admin')
     if os.getenv('JENKINS_URL'):
         parser.set_defaults(jenkinsurl=os.getenv('JENKINS_URL'))
     if os.getenv('JOB_NAME'):
@@ -99,40 +89,61 @@ def main(): #pylint: disable=too-many-branches, 
too-many-statements, too-many-lo
         parser.set_defaults(jenkinsJobName='PreCommit-Admin')
 
     parser.set_defaults(jenkinsJobTemplate='PreCommit-{project}')
-    parser.add_option('--initialize', action='store_true',
-                      dest='jenkinsInit',
-                      help='Start a new patch_tested.txt file')
-    parser.add_option('--jenkins-jobname', type='string',
-                      dest='jenkinsJobName',
-                      help='PreCommit-Admin JobName', metavar='JOB_NAME')
-    parser.add_option('--jenkins-project-template', type='string',
-                      dest='jenkinsJobTemplate',
-                      help='Template for project jobs',
-                      metavar='TEMPLATE')
-    parser.add_option('--jenkins-token', type='string',
-                      dest='jenkinsToken', help='Jenkins Token',
-                      metavar='TOKEN')
-    parser.add_option('--jenkins-url', type='string', dest='jenkinsurl'
-                      , help='Jenkins base URL', metavar='URL')
-    parser.add_option(
+    parser.add_argument('--initialize',
+                        action='store_true',
+                        dest='jenkinsInit',
+                        help='Start a new patch_tested.txt file')
+    parser.add_argument('--jenkins-jobname',
+                        type=str,
+                        dest='jenkinsJobName',
+                        help='PreCommit-Admin JobName',
+                        metavar='JOB_NAME')
+    parser.add_argument('--jenkins-project-template',
+                        type=str,
+                        dest='jenkinsJobTemplate',
+                        help='Template for project jobs',
+                        metavar='TEMPLATE')
+    parser.add_argument('--jenkins-token',
+                        type=str,
+                        dest='jenkinsToken',
+                        help='Jenkins Token',
+                        metavar='TOKEN')
+    parser.add_argument('--jenkins-url',
+                        type=str,
+                        dest='jenkinsurl',
+                        help='Jenkins base URL',
+                        metavar='URL')
+    parser.add_argument(
         '--jenkins-url-override',
-        type='string',
+        type=str,
         dest='jenkinsurloverrides',
         action='append',
         help='Project specific Jenkins base URL',
         metavar='PROJECT=URL',
-        )
-    parser.add_option('--jira-filter', type='string', dest='jiraFilter',
-                      help='JIRA filter URL', metavar='URL')
-    parser.add_option('--jira-user', type='string', dest='jiraUser',
-                      help='JIRA username')
-    parser.add_option('--jira-password', type='string', dest='jiraPassword',
-                      help='JIRA password')
-    parser.add_option('--live', dest='live', action='store_true',
-                      help='Submit Job to jenkins')
-    parser.add_option('--max-history', dest='history', type='int',
-                      help='Maximum history to store', default=5000)
-    parser.add_option(
+    )
+    parser.add_argument('--jira-filter',
+                        type=str,
+                        dest='jiraFilter',
+                        help='JIRA filter URL',
+                        metavar='URL')
+    parser.add_argument('--jira-user',
+                        type=str,
+                        dest='jiraUser',
+                        help='JIRA username')
+    parser.add_argument('--jira-password',
+                        type=str,
+                        dest='jiraPassword',
+                        help='JIRA password')
+    parser.add_argument('--live',
+                        dest='live',
+                        action='store_true',
+                        help='Submit Job to jenkins')
+    parser.add_argument('--max-history',
+                        dest='history',
+                        type=int,
+                        help='Maximum history to store',
+                        default=5000)
+    parser.add_argument(
         '-V',
         '--version',
         dest='release_version',
@@ -140,14 +151,13 @@ def main(): #pylint: disable=too-many-branches, 
too-many-statements, too-many-lo
         default=False,
         help="display version information for jenkins-admin and exit.")
 
-    (options, args) = parser.parse_args() #pylint: disable=unused-variable
+    options = parser.parse_args()
 
     # Handle the version string right away and exit
     if options.release_version:
-        with open(
-            os.path.join(
-                os.path.dirname(__file__), "../../VERSION"), 'r') as ver_file:
-            print ver_file.read()
+        with open(os.path.join(os.path.dirname(__file__), "../../VERSION"),
+                  'r') as ver_file:
+            print(ver_file.read())
         sys.exit(0)
 
     token_frag = ''
@@ -158,36 +168,39 @@ def main(): #pylint: disable=too-many-branches, 
too-many-statements, too-many-lo
     if not options.jiraFilter:
         parser.error('ERROR: --jira-filter is a required argument.')
     if not options.jenkinsurl:
-        parser.error('ERROR: --jenkins-url or the JENKINS_URL environment 
variable is required.'
-                    )
+        parser.error(
+            'ERROR: --jenkins-url or the JENKINS_URL environment variable is 
required.'
+        )
     if options.history < 0:
-        parser.error('ERROR: --max-history must be 0 or a positive integer.'
-                    )
+        parser.error('ERROR: --max-history must be 0 or a positive integer.')
     jenkinsurloverrides = {}
     if options.jenkinsurloverrides:
         for override in options.jenkinsurloverrides:
             if '=' not in override:
-                parser.error('Invalid Jenkins Url Override: '
-                             + override)
+                parser.error('Invalid Jenkins Url Override: ' + override)
             (project, url) = override.split('=', 1)
             jenkinsurloverrides[project.upper()] = url
     tempfile = NamedTemporaryFile(delete=False)
     try:
         jobloghistory = None
         if not options.jenkinsInit:
-            jobloghistory = http_get(options.jenkinsurl
-                                     + 
'/job/%s/lastSuccessfulBuild/artifact/patch_tested.txt'
-                                     % options.jenkinsJobName, True)
+            jobloghistory = http_get(
+                options.jenkinsurl +
+                '/job/%s/lastSuccessfulBuild/artifact/patch_tested.txt' %
+                options.jenkinsJobName, True)
 
             # if we don't have a successful build available try the last build
 
             if not jobloghistory:
-                jobloghistory = http_get(options.jenkinsurl
-                                         + 
'/job/%s/lastCompletedBuild/artifact/patch_tested.txt'
-                                         % options.jenkinsJobName)
+                jobloghistory = http_get(
+                    options.jenkinsurl +
+                    '/job/%s/lastCompletedBuild/artifact/patch_tested.txt' %
+                    options.jenkinsJobName)
             jobloghistory = jobloghistory.strip().split('\n')
             if 'TESTED ISSUES' not in jobloghistory[0]:
-                print 'Downloaded patch_tested.txt control file may be 
corrupted. Failing.'
+                print(
+                    'Downloaded patch_tested.txt control file may be 
corrupted. Failing.'
+                )
                 sys.exit(1)
 
         # we are either going to write a new one or rewrite the old one
@@ -204,12 +217,13 @@ def main(): #pylint: disable=too-many-branches, 
too-many-statements, too-many-lo
         else:
             joblog.write('TESTED ISSUES\n')
         joblog.flush()
-        rssdata = http_get(options.jiraFilter, False, options.jiraUser, 
options.jiraPassword)
-        tempfile.write(rssdata)
+        rssdata = http_get(options.jiraFilter, False, options.jiraUser,
+                           options.jiraPassword)
+        tempfile.write(rssdata.encode('utf-8'))
         tempfile.flush()
-        for (key, attachment) in parse_jira_data(tempfile.name).items():
+        for (key, attachment) in list(parse_jira_data(tempfile.name).items()):
             (project, issue) = key
-            if jenkinsurloverrides.has_key(project):
+            if jenkinsurloverrides.get(project):
                 url = jenkinsurloverrides[project]
             else:
                 url = options.jenkinsurl
@@ -223,28 +237,29 @@ def main(): #pylint: disable=too-many-branches, 
too-many-statements, too-many-lo
                 'project': project,
                 'issue': issue,
                 'attachment': attachment,
-                }
+            }
             jenkinsurl = jenkinsurltemplate.format(**url_args)
 
             # submit job
 
             jobname = '%s-%s,%s' % (project, issue, attachment)
             if not jobloghistory or jobname not in jobloghistory:
-                print jobname + ' has not been processed, submitting'
+                print(jobname + ' has not been processed, submitting')
                 joblog.write(jobname + '\n')
                 joblog.flush()
                 if options.live:
                     http_get(jenkinsurl, True)
                 else:
-                    print 'GET ' + jenkinsurl
+                    print('GET ' + jenkinsurl)
             else:
-                print jobname + ' has been processed, ignoring'
+                print(jobname + ' has been processed, ignoring')
         joblog.close()
     finally:
         if options.live:
             os.remove(tempfile.name)
         else:
-            print 'JIRA Data is located: ' + tempfile.name
+            print('JIRA Data is located: ' + tempfile.name)
+
 
 if __name__ == '__main__':
     main()
diff --git a/precommit/src/main/shell/test-patch-docker/Dockerfile 
b/precommit/src/main/shell/test-patch-docker/Dockerfile
index d874af2..ae0ccb8 100644
--- a/precommit/src/main/shell/test-patch-docker/Dockerfile
+++ b/precommit/src/main/shell/test-patch-docker/Dockerfile
@@ -324,41 +324,8 @@ RUN apt-get -q update && apt-get -q install 
--no-install-recommends -y \
         yamllint==1.24.2 \
     && rm -rf /root/.cache \
     && mv /usr/local/bin/pylint /usr/local/bin/pylint3
-
-######
-# Install python, pylint2, and yamllint
-######
-RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
-        python2 \
-        python-backports.functools-lru-cache \
-        python-bcrypt \
-        python-cffi \
-        python-cryptography \
-        python-dateutil \
-        python-dev \
-        python-enum34 \
-        python-pkg-resources \
-        python-setuptools \
-        python-singledispatch \
-        python-six \
-        python-yaml \
-    && apt-get clean \
-    && rm -rf /var/lib/apt/lists/* \
-    && curl -sSL https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py \
-    && python2 /tmp/get-pip.py \
-    && rm /tmp/get-pip.py /usr/local/bin/pip \
-    && pip2 install -v \
-        astroid==1.6.5 \
-        configparser==4.0.2 \
-        isort==4.3.21 \
-        pylint==1.9.2 \
-    && rm -rf /root/.cache \
-    && mv /usr/local/bin/pylint /usr/local/bin/pylint2
-
-#####
-# all of the world's python2 code stopped working, right?
-#####
 RUN ln -s /usr/local/bin/pylint3 /usr/local/bin/pylint
+RUN ln -s /usr/local/bin/pip3 /usr/local/bin/pip
 
 ####
 # Install ruby and associated bits
diff --git a/releasedocmaker/src/main/python/releasedocmaker.py 
b/releasedocmaker/src/main/python/releasedocmaker.py
index f4527c7..154abe9 100755
--- a/releasedocmaker/src/main/python/releasedocmaker.py
+++ b/releasedocmaker/src/main/python/releasedocmaker.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 #
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -15,7 +15,6 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
 """ wrapper to launch releasedocmaker from the CLI """
 
 import sys
diff --git a/releasedocmaker/src/main/python/releasedocmaker/__init__.py 
b/releasedocmaker/src/main/python/releasedocmaker/__init__.py
index 16fa7ed..e73ff8b 100755
--- a/releasedocmaker/src/main/python/releasedocmaker/__init__.py
+++ b/releasedocmaker/src/main/python/releasedocmaker/__init__.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -14,28 +14,29 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
 """ Generate releasenotes based upon JIRA """
 
 # pylint: disable=too-many-lines
 
-from __future__ import print_function
-import sys
-from glob import glob
-from optparse import OptionParser
-from time import gmtime, strftime, sleep
-from distutils.version import LooseVersion
 import errno
+import http.client
+import json
 import os
 import re
 import shutil
-import urllib
-import urllib2
-import httplib
-import json
+import sys
+import urllib.error
+import urllib.parse
+import urllib.request
+
+from pprint import pprint
+from glob import glob
+from argparse import ArgumentParser
+from time import gmtime, strftime, sleep
+
 sys.dont_write_bytecode = True
-# pylint: disable=wrong-import-position,relative-import
-from utils import get_jira, to_unicode, sanitize_text, processrelnote, Outputs
+# pylint: disable=wrong-import-position
+from .utils import get_jira, to_unicode, sanitize_text, processrelnote, Outputs
 # pylint: enable=wrong-import-position
 
 try:
@@ -45,6 +46,27 @@ except ImportError:
           "You can install it using:\n\t pip install python-dateutil")
     sys.exit(1)
 
+# These are done in order of preference as to which one seems to be
+# more up-to-date at any given point in time.  And yes, it is
+# ironic that packaging is usually the last one to be
+# correct.
+
+try:
+    from pip._vendor.packaging.version import LegacyVersion as PythonVersion
+except ImportError:
+    try:
+        from setuptools._vendor.packaging.version import LegacyVersion as 
PythonVersion
+    except ImportError:
+        try:
+            from pkg_resources._vendor.packaging.version import LegacyVersion 
as PythonVersion
+        except ImportError:
+            try:
+                from packaging.version import LegacyVersion as PythonVersion
+            except ImportError:
+                print(
+                    "This script requires a packaging module to be installed.")
+                sys.exit(1)
+
 RELEASE_VERSION = {}
 
 JIRA_BASE_URL = "https://issues.apache.org/jira";
@@ -77,19 +99,20 @@ ASF_LICENSE = '''
 -->
 '''
 
+
 def indexbuilder(title, asf_license, format_string):
     """Write an index file for later conversion using mvn site"""
     versions = glob("[0-9]*.[0-9]*")
-    versions.sort(key=LooseVersion, reverse=True)
+    versions = sorted(versions, reverse=True, key=PythonVersion)
     with open("index" + EXTENSION, "w") as indexfile:
         if asf_license is True:
             indexfile.write(ASF_LICENSE)
         for version in versions:
             indexfile.write("* %s v%s\n" % (title, version))
             for k in ("Changelog", "Release Notes"):
-                indexfile.write(format_string %
-                                (k, version, k.upper().replace(" ", ""),
-                                 version))
+                indexfile.write(
+                    format_string %
+                    (k, version, k.upper().replace(" ", ""), version))
 
 
 def buildprettyindex(title, asf_license):
@@ -105,7 +128,7 @@ def buildindex(title, asf_license):
 def buildreadme(title, asf_license):
     """Write an index file for Github using README.md"""
     versions = glob("[0-9]*.[0-9]*")
-    versions.sort(key=LooseVersion, reverse=True)
+    versions = sorted(versions, reverse=True, key=PythonVersion)
     with open("README.md", "w") as indexfile:
         if asf_license is True:
             indexfile.write(ASF_LICENSE)
@@ -113,16 +136,15 @@ def buildreadme(title, asf_license):
             indexfile.write("* %s v%s\n" % (title, version))
             for k in ("Changelog", "Release Notes"):
                 indexfile.write("    * [%s](%s/%s.%s%s)\n" %
-                                (k, version, k.upper().replace(" ", ""),
-                                 version, EXTENSION))
+                                (k, version, k.upper().replace(
+                                    " ", ""), version, EXTENSION))
 
 
-class GetVersions(object): # pylint: disable=too-few-public-methods
+class GetVersions:  # pylint: disable=too-few-public-methods
     """ List of version strings """
-
     def __init__(self, versions, projects):
         self.newversions = []
-        versions.sort(key=LooseVersion)
+        versions = sorted(versions, key=PythonVersion)
         print("Looking for %s through %s" % (versions[0], versions[-1]))
         newversions = set()
         for project in projects:
@@ -130,18 +152,20 @@ class GetVersions(object): # pylint: 
disable=too-few-public-methods
               "/rest/api/2/project/%s/versions" % project.upper()
             try:
                 resp = get_jira(url)
-            except (urllib2.HTTPError, urllib2.URLError, 
httplib.BadStatusLine):
+            except (urllib.error.HTTPError, urllib.error.URLError,
+                    http.client.BadStatusLine):
                 sys.exit(1)
 
             datum = json.loads(resp.read())
             for data in datum:
-                newversions.add(data['name'])
+                newversions.add(PythonVersion(data['name']))
         newlist = list(newversions.copy())
-        newlist.append(versions[0])
-        newlist.append(versions[-1])
-        newlist.sort(key=LooseVersion)
-        start_index = newlist.index(versions[0])
-        end_index = len(newlist) - 1 - newlist[::-1].index(versions[-1])
+        newlist.append(PythonVersion(versions[0]))
+        newlist.append(PythonVersion(versions[-1]))
+        newlist = sorted(newlist)
+        start_index = newlist.index(PythonVersion(versions[0]))
+        end_index = len(newlist) - 1 - newlist[::-1].index(
+            PythonVersion(versions[-1]))
         for newversion in newlist[start_index + 1:end_index]:
             if newversion in newversions:
                 print("Adding %s to the list" % newversion)
@@ -152,32 +176,8 @@ class GetVersions(object): # pylint: 
disable=too-few-public-methods
         return self.newversions
 
 
-class Version(object):
-    """Represents a version number"""
-
-    def __init__(self, data):
-        self.mod = False
-        self.data = data
-        found = re.match(r'^((\d+)(\.\d+)*).*$', data)
-        if found:
-            self.parts = [int(p) for p in found.group(1).split('.')]
-        else:
-            self.parts = []
-        # backfill version with zeros if missing parts
-        self.parts.extend((0,) * (3 - len(self.parts)))
-
-    def __str__(self):
-        if self.mod:
-            return '.'.join([str(p) for p in self.parts])
-        return self.data
-
-    def __cmp__(self, other):
-        return cmp(self.parts, other.parts)
-
-
-class Jira(object):
+class Jira:
     """A single JIRA"""
-
     def __init__(self, data, parent):
         self.key = data['key']
         self.fields = data['fields']
@@ -226,8 +226,8 @@ class Jira(object):
     def get_components(self):
         """ Get the component(s) """
         if self.fields['components']:
-            return ", ".join([comp['name'] for comp in 
self.fields['components']
-                             ])
+            return ", ".join(
+                [comp['name'] for comp in self.fields['components']])
         return ""
 
     def get_summary(self):
@@ -258,26 +258,25 @@ class Jira(object):
             ret = mid['key']
         return to_unicode(ret)
 
-    def __cmp__(self, other):
-        result = 0
+    def __lt__(self, other):
 
         if SORTTYPE == 'issueid':
             # compare by issue name-number
             selfsplit = self.get_id().split('-')
             othersplit = other.get_id().split('-')
-            result = cmp(selfsplit[0], othersplit[0])
-            if result == 0:
-                result = cmp(int(selfsplit[1]), int(othersplit[1]))
+            result = selfsplit[0] < othersplit[0]
+            if not result:
+                result = int(selfsplit[1]) < int(othersplit[1])
                 # dec is supported for backward compatibility
                 if SORTORDER in ['dec', 'desc']:
-                    result *= -1
+                    result = not result
 
         elif SORTTYPE == 'resolutiondate':
             dts = dateutil.parser.parse(self.fields['resolutiondate'])
             dto = dateutil.parser.parse(other.fields['resolutiondate'])
-            result = cmp(dts, dto)
+            result = dts < dto
             if SORTORDER == 'newer':
-                result *= -1
+                result = not result
 
         return result
 
@@ -317,16 +316,16 @@ class Jira(object):
         return self.important
 
 
-class JiraIter(object):
+class JiraIter:
     """An Iterator of JIRAs"""
-
     @staticmethod
     def collect_fields():
         """send a query to JIRA and collect field-id map"""
         try:
             resp = get_jira(JIRA_BASE_URL + "/rest/api/2/field")
             data = json.loads(resp.read())
-        except (urllib2.HTTPError, urllib2.URLError, httplib.BadStatusLine, 
ValueError):
+        except (urllib.error.HTTPError, urllib.error.URLError,
+                http.client.BadStatusLine, ValueError):
             sys.exit(1)
         field_id_map = {}
         for part in data:
@@ -342,9 +341,11 @@ class JiraIter(object):
         jql = "project in ('%s') and \
                fixVersion in ('%s') and \
                resolution = Fixed" % (pjs, ver)
-        params = urllib.urlencode({'jql': jql,
-                                   'startAt': pos,
-                                   'maxResults': count})
+        params = urllib.parse.urlencode({
+            'jql': jql,
+            'startAt': pos,
+            'maxResults': count
+        })
         return JiraIter.load_jira(params, 0)
 
     @staticmethod
@@ -352,12 +353,12 @@ class JiraIter(object):
         """send query to JIRA and collect with retries"""
         try:
             resp = get_jira(JIRA_BASE_URL + "/rest/api/2/search?%s" % params)
-        except (urllib2.URLError, httplib.BadStatusLine) as err:
+        except (urllib.error.URLError, http.client.BadStatusLine) as err:
             return JiraIter.retry_load(err, params, fail_count)
 
         try:
             data = json.loads(resp.read())
-        except httplib.IncompleteRead as err:
+        except http.client.IncompleteRead as err:
             return JiraIter.retry_load(err, params, fail_count)
         return data
 
@@ -370,9 +371,8 @@ class JiraIter(object):
             print("Connection failed %d times. Retrying." % (fail_count))
             sleep(1)
             return JiraIter.load_jira(params, fail_count)
-        else:
-            print("Connection failed %d times. Aborting." % (fail_count))
-            sys.exit(1)
+        print("Connection failed %d times. Aborting." % (fail_count))
+        sys.exit(1)
 
     @staticmethod
     def collect_jiras(ver, projects):
@@ -384,7 +384,8 @@ class JiraIter(object):
         while pos < end:
             data = JiraIter.query_jira(ver, projects, pos)
             if 'error_messages' in data:
-                print("JIRA returns error message: %s" % 
data['error_messages'])
+                print("JIRA returns error message: %s" %
+                      data['error_messages'])
                 sys.exit(1)
             pos = data['startAt'] + data['maxResults']
             end = data['total']
@@ -409,19 +410,20 @@ class JiraIter(object):
     def __iter__(self):
         return self
 
-    def next(self):
+    def __next__(self):
         """ get next """
-        data = self.iter.next()
+        data = next(self.iter)
         j = Jira(data, self)
         return j
 
 
-class Linter(object):
+class Linter:
     """Encapsulates lint-related functionality.
     Maintains running lint statistics about JIRAs."""
 
-    _valid_filters = ["incompatible", "important", "version", "component",
-                      "assignee"]
+    _valid_filters = [
+        "incompatible", "important", "version", "component", "assignee"
+    ]
 
     def __init__(self, version, options):
         self._warning_count = 0
@@ -429,8 +431,8 @@ class Linter(object):
         self._lint_message = ""
         self._version = version
 
-        self._filters = dict(zip(self._valid_filters, [False] * len(
-            self._valid_filters)))
+        self._filters = dict(
+            list(zip(self._valid_filters, [False] * len(self._valid_filters))))
 
         self.enabled = False
         self._parse_options(options)
@@ -439,12 +441,12 @@ class Linter(object):
     def add_parser_options(parser):
         """Add Linter options to passed optparse parser."""
         filter_string = ", ".join("'" + f + "'" for f in Linter._valid_filters)
-        parser.add_option(
+        parser.add_argument(
             "-n",
             "--lint",
             dest="lint",
             action="append",
-            type="string",
+            type=str,
             help="Specify lint filters. Valid filters are " + filter_string +
             ". " + "'all' enables all lint filters. " +
             "Multiple filters can be specified comma-delimited and " +
@@ -535,7 +537,8 @@ class Linter(object):
         if not self.enabled:
             return
         if not jira.get_release_note():
-            if self._filters["incompatible"] and 
jira.get_incompatible_change():
+            if self._filters["incompatible"] and jira.get_incompatible_change(
+            ):
                 self._warning_count += 1
                 self._lint_message += "\nWARNING: incompatible change %s lacks 
release notes." % \
                                 (sanitize_text(jira.get_id()))
@@ -561,67 +564,64 @@ class Linter(object):
                             % (" and ".join(error_message), jira.get_id())
 
 
-def parse_args(): # pylint: disable=too-many-branches
+def parse_args():  # pylint: disable=too-many-branches
     """Parse command-line arguments with optparse."""
-    usage = "usage: %prog [OPTIONS] " + \
-            "--project PROJECT [--project PROJECT] " + \
-            "--version VERSION [--version VERSION2 ...]"
-    parser = OptionParser(
-        usage=usage,
+    parser = ArgumentParser(
+        prog='releasedocmaker',
         epilog=
-        "Markdown-formatted CHANGELOG and RELEASENOTES files will be stored"
-        " in a directory named after the highest version provided.")
-    parser.add_option("--dirversions",
-                      dest="versiondirs",
-                      action="store_true",
-                      default=False,
-                      help="Put files in versioned directories")
-    parser.add_option("--empty",
-                      dest="empty",
-                      action="store_true",
-                      default=False,
-                      help="Create empty files when no issues")
-    parser.add_option("--extension",
-                      dest="extension",
-                      default=EXTENSION,
-                      type="string",
-                      help="Set the file extension of created Markdown files")
-    parser.add_option("--fileversions",
-                      dest="versionfiles",
-                      action="store_true",
-                      default=False,
-                      help="Write files with embedded versions")
-    parser.add_option("-i",
-                      "--index",
-                      dest="index",
-                      action="store_true",
-                      default=False,
-                      help="build an index file")
-    parser.add_option("-l",
-                      "--license",
-                      dest="license",
-                      action="store_true",
-                      default=False,
-                      help="Add an ASF license")
-    parser.add_option("-p",
-                      "--project",
-                      dest="projects",
-                      action="append",
-                      type="string",
-                      help="projects in JIRA to include in releasenotes",
-                      metavar="PROJECT")
-    parser.add_option("--prettyindex",
-                      dest="prettyindex",
-                      action="store_true",
-                      default=False,
-                      help="build an index file with pretty URLs")
-    parser.add_option("-r",
-                      "--range",
-                      dest="range",
-                      action="store_true",
-                      default=False,
-                      help="Given versions are a range")
-    parser.add_option(
+        "--project and --version may be given multiple times.")
+    parser.add_argument("--dirversions",
+                        dest="versiondirs",
+                        action="store_true",
+                        default=False,
+                        help="Put files in versioned directories")
+    parser.add_argument("--empty",
+                        dest="empty",
+                        action="store_true",
+                        default=False,
+                        help="Create empty files when no issues")
+    parser.add_argument(
+        "--extension",
+        dest="extension",
+        default=EXTENSION,
+        type=str,
+        help="Set the file extension of created Markdown files")
+    parser.add_argument("--fileversions",
+                        dest="versionfiles",
+                        action="store_true",
+                        default=False,
+                        help="Write files with embedded versions")
+    parser.add_argument("-i",
+                        "--index",
+                        dest="index",
+                        action="store_true",
+                        default=False,
+                        help="build an index file")
+    parser.add_argument("-l",
+                        "--license",
+                        dest="license",
+                        action="store_true",
+                        default=False,
+                        help="Add an ASF license")
+    parser.add_argument("-p",
+                        "--project",
+                        dest="projects",
+                        action="append",
+                        type=str,
+                        help="projects in JIRA to include in releasenotes",
+                        metavar="PROJECT")
+    parser.add_argument("--prettyindex",
+                        dest="prettyindex",
+                        action="store_true",
+                        default=False,
+                        help="build an index file with pretty URLs")
+    parser.add_argument("-r",
+                        "--range",
+                        dest="range",
+                        action="store_true",
+                        default=False,
+                        help="Given versions are a range")
+    parser.add_argument(
         "--sortorder",
         dest="sortorder",
         metavar="TYPE",
@@ -629,67 +629,72 @@ def parse_args(): # pylint: disable=too-many-branches
         # dec is supported for backward compatibility
         choices=["asc", "dec", "desc", "newer", "older"],
         help="Sorting order for sort type (default: %s)" % SORTORDER)
-    parser.add_option("--sorttype",
-                      dest="sorttype",
-                      metavar="TYPE",
-                      default=SORTTYPE,
-                      choices=["resolutiondate", "issueid"],
-                      help="Sorting type for issues (default: %s)" % SORTTYPE)
-    parser.add_option(
+    parser.add_argument("--sorttype",
+                        dest="sorttype",
+                        metavar="TYPE",
+                        default=SORTTYPE,
+                        choices=["resolutiondate", "issueid"],
+                        help="Sorting type for issues (default: %s)" %
+                        SORTTYPE)
+    parser.add_argument(
         "-t",
         "--projecttitle",
         dest="title",
-        type="string",
+        type=str,
         help="Title to use for the project (default is Apache PROJECT)")
-    parser.add_option("-u",
-                      "--usetoday",
-                      dest="usetoday",
-                      action="store_true",
-                      default=False,
-                      help="use current date for unreleased versions")
-    parser.add_option("-v",
-                      "--version",
-                      dest="versions",
-                      action="append",
-                      type="string",
-                      help="versions in JIRA to include in releasenotes",
-                      metavar="VERSION")
-    parser.add_option(
+    parser.add_argument("-u",
+                        "--usetoday",
+                        dest="usetoday",
+                        action="store_true",
+                        default=False,
+                        help="use current date for unreleased versions")
+    parser.add_argument("-v",
+                        "--version",
+                        dest="versions",
+                        action="append",
+                        type=str,
+                        help="versions in JIRA to include in releasenotes",
+                        metavar="VERSION")
+    parser.add_argument(
         "-V",
         dest="release_version",
         action="store_true",
         default=False,
         help="display version information for releasedocmaker and exit.")
-    parser.add_option("-O",
-                      "--outputdir",
-                      dest="output_directory",
-                      action="append",
-                      type="string",
-                      help="specify output directory to put release docs to.")
-    parser.add_option("-B",
-                      "--baseurl",
-                      dest="base_url",
-                      action="append",
-                      type="string",
-                      help="specify base URL of the JIRA instance.")
-    parser.add_option(
+    parser.add_argument(
+        "-O",
+        "--outputdir",
+        dest="output_directory",
+        action="append",
+        type=str,
+        help="specify output directory to put release docs to.")
+    parser.add_argument("-B",
+                        "--baseurl",
+                        dest="base_url",
+                        action="append",
+                        type=str,
+                        help="specify base URL of the JIRA instance.")
+    parser.add_argument(
         "--retries",
         dest="retries",
         action="append",
-        type="int",
+        type=int,
         help="Specify how many times to retry connection for each URL.")
-    parser.add_option(
+    parser.add_argument(
         "--skip-credits",
         dest="skip_credits",
         action="store_true",
         default=False,
-        help="While creating release notes skip the 'reporter' and 
'contributor' columns")
-    parser.add_option("-X",
-                      "--incompatiblelabel",
-                      dest="incompatible_label",
-                      default="backward-incompatible",
-                      type="string",
-                      help="Specify the label to indicate backward 
incompatibility.")
+        help=
+        "While creating release notes skip the 'reporter' and 'contributor' 
columns"
+    )
+    parser.add_argument(
+        "-X",
+        "--incompatiblelabel",
+        dest="incompatible_label",
+        default="backward-incompatible",
+        type=str,
+        help="Specify the label to indicate backward incompatibility.")
 
     Linter.add_parser_options(parser)
 
@@ -697,13 +702,12 @@ def parse_args(): # pylint: disable=too-many-branches
         parser.print_help()
         sys.exit(1)
 
-    (options, _) = parser.parse_args()
+    options = parser.parse_args()
 
     # Handle the version string right away and exit
     if options.release_version:
-        with open(
-            os.path.join(
-                os.path.dirname(__file__), "../VERSION"), 'r') as ver_file:
+        with open(os.path.join(os.path.dirname(__file__), "../VERSION"),
+                  'r') as ver_file:
             print(ver_file.read())
         sys.exit(0)
 
@@ -726,19 +730,21 @@ def parse_args(): # pylint: disable=too-many-branches
 
     if options.range or len(options.versions) > 1:
         if not options.versiondirs and not options.versionfiles:
-            parser.error("Multiple versions require either --fileversions or 
--dirversions")
+            parser.error(
+                "Multiple versions require either --fileversions or 
--dirversions"
+            )
 
     return options
 
 
-def main(): # pylint: disable=too-many-statements, too-many-branches, 
too-many-locals
+def main():  # pylint: disable=too-many-statements, too-many-branches, 
too-many-locals
     """ hey, it's main """
-    global JIRA_BASE_URL #pylint: disable=global-statement
-    global BACKWARD_INCOMPATIBLE_LABEL #pylint: disable=global-statement
-    global SORTTYPE #pylint: disable=global-statement
-    global SORTORDER #pylint: disable=global-statement
-    global NUM_RETRIES #pylint: disable=global-statement
-    global EXTENSION #pylint: disable=global-statement
+    global JIRA_BASE_URL  #pylint: disable=global-statement
+    global BACKWARD_INCOMPATIBLE_LABEL  #pylint: disable=global-statement
+    global SORTTYPE  #pylint: disable=global-statement
+    global SORTORDER  #pylint: disable=global-statement
+    global NUM_RETRIES  #pylint: disable=global-statement
+    global EXTENSION  #pylint: disable=global-statement
 
     options = parse_args()
 
@@ -753,7 +759,7 @@ def main(): # pylint: disable=too-many-statements, 
too-many-branches, too-many-l
                 pass
             else:
                 print("Unable to create output directory %s: %u, %s" % \
-                        (options.output_directory, exc.errno, exc.message))
+                        (options.output_directory, exc.errno, exc.strerror))
                 sys.exit(1)
         os.chdir(options.output_directory)
 
@@ -769,11 +775,10 @@ def main(): # pylint: disable=too-many-statements, 
too-many-branches, too-many-l
     projects = options.projects
 
     if options.range is True:
-        versions = [Version(v)
-                    for v in GetVersions(options.versions, projects).getlist()]
+        versions = GetVersions(options.versions, projects).getlist()
     else:
-        versions = [Version(v) for v in options.versions]
-    versions.sort()
+        versions = [PythonVersion(v) for v in options.versions]
+    versions = sorted(versions)
 
     SORTTYPE = options.sorttype
     SORTORDER = options.sortorder
@@ -793,7 +798,8 @@ def main(): # pylint: disable=too-many-statements, 
too-many-branches, too-many-l
         linter = Linter(vstr, options)
         jlist = sorted(JiraIter(vstr, projects))
         if not jlist and not options.empty:
-            print("There is no issue which has the specified version: %s" % 
version)
+            print("There is no issue which has the specified version: %s" %
+                  version)
             continue
 
         if vstr in RELEASE_VERSION:
@@ -807,57 +813,67 @@ def main(): # pylint: disable=too-many-statements, 
too-many-branches, too-many-l
             os.mkdir(vstr)
 
         if options.versionfiles and options.versiondirs:
-            reloutputs = Outputs("%(ver)s/RELEASENOTES.%(ver)s%(ext)s",
-                                 
"%(ver)s/RELEASENOTES.%(key)s.%(ver)s%(ext)s", [],
-                                 {"ver": version,
-                                  "date": reldate,
-                                  "title": title,
-                                  "ext": EXTENSION})
+            reloutputs = Outputs(
+                "%(ver)s/RELEASENOTES.%(ver)s%(ext)s",
+                "%(ver)s/RELEASENOTES.%(key)s.%(ver)s%(ext)s", [], {
+                    "ver": version,
+                    "date": reldate,
+                    "title": title,
+                    "ext": EXTENSION
+                })
             choutputs = Outputs("%(ver)s/CHANGELOG.%(ver)s%(ext)s",
                                 "%(ver)s/CHANGELOG.%(key)s.%(ver)s%(ext)s", [],
-                                {"ver": version,
-                                 "date": reldate,
-                                 "title": title,
-                                 "ext": EXTENSION})
+                                {
+                                    "ver": version,
+                                    "date": reldate,
+                                    "title": title,
+                                    "ext": EXTENSION
+                                })
         elif options.versiondirs:
             reloutputs = Outputs("%(ver)s/RELEASENOTES%(ext)s",
-                                 "%(ver)s/RELEASENOTES.%(key)s%(ext)s", [],
-                                 {"ver": version,
-                                  "date": reldate,
-                                  "title": title,
-                                  "ext": EXTENSION})
+                                 "%(ver)s/RELEASENOTES.%(key)s%(ext)s", [], {
+                                     "ver": version,
+                                     "date": reldate,
+                                     "title": title,
+                                     "ext": EXTENSION
+                                 })
             choutputs = Outputs("%(ver)s/CHANGELOG%(ext)s",
-                                "%(ver)s/CHANGELOG.%(key)s%(ext)s", [],
-                                {"ver": version,
-                                 "date": reldate,
-                                 "title": title,
-                                 "ext": EXTENSION})
+                                "%(ver)s/CHANGELOG.%(key)s%(ext)s", [], {
+                                    "ver": version,
+                                    "date": reldate,
+                                    "title": title,
+                                    "ext": EXTENSION
+                                })
         elif options.versionfiles:
             reloutputs = Outputs("RELEASENOTES.%(ver)s%(ext)s",
-                                 "RELEASENOTES.%(key)s.%(ver)s%(ext)s", [],
-                                 {"ver": version,
-                                  "date": reldate,
-                                  "title": title,
-                                  "ext": EXTENSION})
+                                 "RELEASENOTES.%(key)s.%(ver)s%(ext)s", [], {
+                                     "ver": version,
+                                     "date": reldate,
+                                     "title": title,
+                                     "ext": EXTENSION
+                                 })
             choutputs = Outputs("CHANGELOG.%(ver)s%(ext)s",
-                                "CHANGELOG.%(key)s.%(ver)s%(ext)s", [],
-                                {"ver": version,
-                                 "date": reldate,
-                                 "title": title,
-                                 "ext": EXTENSION})
+                                "CHANGELOG.%(key)s.%(ver)s%(ext)s", [], {
+                                    "ver": version,
+                                    "date": reldate,
+                                    "title": title,
+                                    "ext": EXTENSION
+                                })
         else:
             reloutputs = Outputs("RELEASENOTES%(ext)s",
-                                 "RELEASENOTES.%(key)s%(ext)s", [],
-                                 {"ver": version,
-                                  "date": reldate,
-                                  "title": title,
-                                  "ext": EXTENSION})
-            choutputs = Outputs("CHANGELOG%(ext)s",
-                                "CHANGELOG.%(key)s%(ext)s", [],
-                                {"ver": version,
-                                 "date": reldate,
-                                 "title": title,
-                                 "ext": EXTENSION})
+                                 "RELEASENOTES.%(key)s%(ext)s", [], {
+                                     "ver": version,
+                                     "date": reldate,
+                                     "title": title,
+                                     "ext": EXTENSION
+                                 })
+            choutputs = Outputs("CHANGELOG%(ext)s", "CHANGELOG.%(key)s%(ext)s",
+                                [], {
+                                    "ver": version,
+                                    "date": reldate,
+                                    "title": title,
+                                    "ext": EXTENSION
+                                })
 
         if options.license is True:
             reloutputs.write_all(ASF_LICENSE)
@@ -915,8 +931,8 @@ def main(): # pylint: disable=too-many-statements, 
too-many-branches, too-many-l
                 if not jira.get_release_note():
                     line = '\n**WARNING: No release note provided for this 
change.**\n\n'
                 else:
-                    line = '\n%s\n\n' % (
-                        processrelnote(jira.get_release_note()))
+                    line = '\n%s\n\n' % (processrelnote(
+                        jira.get_release_note()))
                 reloutputs.write_key_raw(jira.get_project(), line)
 
             linter.lint(jira)
@@ -945,25 +961,29 @@ def main(): # pylint: disable=too-many-statements, 
too-many-branches, too-many-l
             choutputs.write_all("### INCOMPATIBLE CHANGES:\n\n")
             choutputs.write_all(change_header21)
             choutputs.write_all(change_header22)
-            choutputs.write_list(incompatlist, options.skip_credits, 
JIRA_BASE_URL)
+            choutputs.write_list(incompatlist, options.skip_credits,
+                                 JIRA_BASE_URL)
 
         if importantlist:
             choutputs.write_all("\n\n### IMPORTANT ISSUES:\n\n")
             choutputs.write_all(change_header21)
             choutputs.write_all(change_header22)
-            choutputs.write_list(importantlist, options.skip_credits, 
JIRA_BASE_URL)
+            choutputs.write_list(importantlist, options.skip_credits,
+                                 JIRA_BASE_URL)
 
         if newfeaturelist:
             choutputs.write_all("\n\n### NEW FEATURES:\n\n")
             choutputs.write_all(change_header21)
             choutputs.write_all(change_header22)
-            choutputs.write_list(newfeaturelist, options.skip_credits, 
JIRA_BASE_URL)
+            choutputs.write_list(newfeaturelist, options.skip_credits,
+                                 JIRA_BASE_URL)
 
         if improvementlist:
             choutputs.write_all("\n\n### IMPROVEMENTS:\n\n")
             choutputs.write_all(change_header21)
             choutputs.write_all(change_header22)
-            choutputs.write_list(improvementlist, options.skip_credits, 
JIRA_BASE_URL)
+            choutputs.write_list(improvementlist, options.skip_credits,
+                                 JIRA_BASE_URL)
 
         if buglist:
             choutputs.write_all("\n\n### BUG FIXES:\n\n")
@@ -981,13 +1001,15 @@ def main(): # pylint: disable=too-many-statements, 
too-many-branches, too-many-l
             choutputs.write_all("\n\n### SUB-TASKS:\n\n")
             choutputs.write_all(change_header21)
             choutputs.write_all(change_header22)
-            choutputs.write_list(subtasklist, options.skip_credits, 
JIRA_BASE_URL)
+            choutputs.write_list(subtasklist, options.skip_credits,
+                                 JIRA_BASE_URL)
 
         if tasklist or otherlist:
             choutputs.write_all("\n\n### OTHER:\n\n")
             choutputs.write_all(change_header21)
             choutputs.write_all(change_header22)
-            choutputs.write_list(otherlist, options.skip_credits, 
JIRA_BASE_URL)
+            choutputs.write_list(otherlist, options.skip_credits,
+                                 JIRA_BASE_URL)
             choutputs.write_list(tasklist, options.skip_credits, JIRA_BASE_URL)
 
         choutputs.write_all("\n\n")
diff --git a/releasedocmaker/src/main/python/releasedocmaker/utils.py 
b/releasedocmaker/src/main/python/releasedocmaker/utils.py
index 3964671..f3f93ae 100755
--- a/releasedocmaker/src/main/python/releasedocmaker/utils.py
+++ b/releasedocmaker/src/main/python/releasedocmaker/utils.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 #
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -15,21 +15,22 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
 """ Utility methods used by releasedocmaker """
 
-from __future__ import print_function
 import base64
 import os
 import re
-import urllib2
+import urllib.request
+import urllib.error
+import urllib.parse
 import sys
 import json
-import httplib
+import http.client
 sys.dont_write_bytecode = True
 
 NAME_PATTERN = re.compile(r' \([0-9]+\)')
 
+
 def get_jira(jira_url):
     """ Provide standard method for fetching content from apache jira and
         handling of potential errors. Returns urllib2 response or
@@ -38,14 +39,15 @@ def get_jira(jira_url):
     username = os.environ.get('RDM_JIRA_USERNAME')
     password = os.environ.get('RDM_JIRA_PASSWORD')
 
-    req = urllib2.Request(jira_url)
+    req = urllib.request.Request(jira_url)
     if username and password:
-        basicauth = base64.encodestring("%s:%s" % (username, 
password)).replace('\n', '')
+        basicauth = base64.b64encode("%s:%s" % (username, password)).replace(
+            '\n', '')
         req.add_header('Authorization', 'Basic %s' % basicauth)
 
     try:
-        response = urllib2.urlopen(req)
-    except urllib2.HTTPError as http_err:
+        response = urllib.request.urlopen(req)
+    except urllib.error.HTTPError as http_err:
         code = http_err.code
         print("JIRA returns HTTP error %d: %s. Aborting." % \
               (code, http_err.msg))
@@ -59,11 +61,11 @@ def get_jira(jira_url):
         except ValueError:
             print("FATAL: Could not parse json response from server.")
         sys.exit(1)
-    except urllib2.URLError as url_err:
+    except urllib.error.URLError as url_err:
         print("Error contacting JIRA: %s\n" % jira_url)
         print("Reason: %s" % url_err.reason)
         raise url_err
-    except httplib.BadStatusLine as err:
+    except http.client.BadStatusLine as err:
         raise err
     return response
 
@@ -78,24 +80,17 @@ def format_components(input_string):
         ret = "."
     return sanitize_markdown(re.sub(NAME_PATTERN, "", ret))
 
-def encode_utf8(input_string):
-    """ Return the string encoded as UTF-8.
-        This is necessary for handling markdown in Python. """
-    return input_string.encode('utf-8')
-
 
 def sanitize_markdown(input_string):
     """ Sanitize Markdown input so it can be handled by Python.
 
         The expectation is that the input is already valid Markdown,
         so no additional escaping is required. """
-    input_string = encode_utf8(input_string)
-    input_string = input_string.replace("\r", "")
+    input_string = input_string.replace('\r', '')
     input_string = input_string.rstrip()
     return input_string
 
 
-
 def sanitize_text(input_string):
     """ Sanitize arbitrary text so it can be embedded in MultiMarkdown output.
 
@@ -122,7 +117,7 @@ def sanitize_text(input_string):
     output_string = ""
     for char in input_string:
         out = char
-        if char in escapes:
+        if escapes.get(char):
             out = escapes[char]
         output_string += out
 
@@ -144,12 +139,11 @@ def to_unicode(obj):
     """ convert string to unicode """
     if obj is None:
         return ""
-    return unicode(obj)
+    return str(obj)
 
 
-class Outputs(object):
+class Outputs:
     """Several different files to output to at the same time"""
-
     def __init__(self, base_file_name, file_name_pattern, keys, params=None):
         if params is None:
             params = {}
@@ -175,12 +169,12 @@ class Outputs(object):
         """ write everything without changes """
         self.base.write(input_string)
         if key in self.others:
-            self.others[key].write(input_string)
+            self.others[key].write(input_string.decode("utf-8"))
 
     def close(self):
         """ close all the outputs """
         self.base.close()
-        for value in self.others.values():
+        for value in list(self.others.values()):
             value.close()
 
     def write_list(self, mylist, skip_credits, base_url):
@@ -192,13 +186,14 @@ class Outputs(object):
             else:
                 line = '| [{id}]({base_url}/browse/{id}) | {summary} |  ' \
                        '{priority} | {component} | {reporter} | {assignee} |\n'
-            args = {'id': encode_utf8(jira.get_id()),
-                    'base_url': base_url,
-                    'summary': sanitize_text(jira.get_summary()),
-                    'priority': sanitize_text(jira.get_priority()),
-                    'component': format_components(jira.get_components()),
-                    'reporter': sanitize_text(jira.get_reporter()),
-                    'assignee': sanitize_text(jira.get_assignee())
-                   }
+            args = {
+                'id': jira.get_id(),
+                'base_url': base_url,
+                'summary': sanitize_text(jira.get_summary()),
+                'priority': sanitize_text(jira.get_priority()),
+                'component': format_components(jira.get_components()),
+                'reporter': sanitize_text(jira.get_reporter()),
+                'assignee': sanitize_text(jira.get_assignee())
+            }
             line = line.format(**args)
             self.write_key_raw(jira.get_project(), line)
diff --git a/shelldocs/src/main/python/shelldocs.py 
b/shelldocs/src/main/python/shelldocs.py
index f1c28fa..c953d1f 100755
--- a/shelldocs/src/main/python/shelldocs.py
+++ b/shelldocs/src/main/python/shelldocs.py
@@ -1,5 +1,32 @@
-#!/usr/bin/env python2
+#!/usr/bin/env python3
 #
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+""" process bash scripts and generate documentation from them """
+
+# Do this immediately to prevent compiled forms
+import sys
+import os
+import re
+import errno
+from argparse import ArgumentParser
+
+sys.dont_write_bytecode = True
+
+ASFLICENSE = '''
+<!---
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
 # distributed with this work for additional information
@@ -15,13 +42,432 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+-->
+'''
 
-""" wrapper to run shelldocs from the CLI """
+FUNCTIONRE = re.compile(r"^(\w+) *\(\) *{")
 
-import sys
-sys.dont_write_bytecode = True
-# pylint: disable=wrong-import-position,import-self
-import shelldocs
-# pylint: enable=wrong-import-position
-# pylint: disable=no-member
-shelldocs.main()
+
+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
+    """a shell function"""
+    def __init__(self, filename):
+        '''Initializer'''
+        self.name = None
+        self.audience = None
+        self.stability = None
+        self.replaceb = None
+        self.returnt = None
+        self.desc = None
+        self.params = None
+        self.filename = filename
+        self.linenum = 0
+
+    def __lt__(self, other):
+        '''comparison'''
+        if self.audience == other.audience:
+            if self.stability == other.stability:
+                if self.replaceb == other.replaceb:
+                    return self.name < other.name
+                if self.replaceb == "Yes":
+                    return True
+            else:
+                if self.stability == "Stable":
+                    return True
+        else:
+            if self.audience == "Public":
+                return True
+        return False
+
+    def reset(self):
+        '''empties current function'''
+        self.name = None
+        self.audience = None
+        self.stability = None
+        self.replaceb = None
+        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.replaceb = docstrip("replaceable", text)
+        self.replaceb = self.replaceb.capitalize()
+
+    def getreplace(self):
+        '''get the replacement state'''
+        if self.replaceb == "Yes":
+            return self.replaceb
+        return "No"
+
+    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):
+        '''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
+
+    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
+
+    def lint(self):
+        '''Lint this function'''
+        getfuncs = {
+            "audience": self.getaudience,
+            "stability": self.getstability,
+            "replaceable": self.getreplace,
+        }
+        validvalues = {
+            "audience": ("Public", "Private"),
+            "stability": ("Stable", "Evolving"),
+            "replaceable": ("Yes", "No"),
+        }
+        messages = []
+        for attr in ("audience", "stability", "replaceable"):
+            value = getfuncs[attr]()
+            if value == "None":
+                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)
+
+    def __str__(self):
+        '''Generate a string for this function'''
+        line = "{%s %s %s %s}" \
+          % (self.getname(),
+             self.getaudience(),
+             self.getstability(),
+             self.getreplace())
+        return line
+
+
+def marked_as_ignored(file_path):
+    """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.
+
+    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(file_path) as input_file:
+        for line in input_file:
+            if line.startswith("#") and line[1:].strip() == "SHELLDOC-IGNORE":
+                return True
+        return False
+
+
+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
+
+
+def process_input(inputlist, skipprnorep):
+    """ take the input and loop around it """
+    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:
+                    if fname.endswith('sh'):
+                        newfuncs = process_file(filename=os.path.join(
+                            root, fname),
+                                                skipprnorep=skipprnorep)
+                        if newfuncs:
+                            allfuncs = allfuncs + newfuncs
+        else:
+            newfuncs = process_file(filename=filename, skipprnorep=skipprnorep)
+            if newfuncs:
+                allfuncs = allfuncs + newfuncs
+
+    if allfuncs is None:
+        print("ERROR: no functions found.", file=sys.stderr)
+        sys.exit(1)
+
+    allfuncs = sorted(allfuncs)
+    return allfuncs
+
+
+def write_output(filename, functions):
+    """ write the markdown file """
+    try:
+        directory = os.path.dirname(filename)
+        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'''
+    parser = ArgumentParser(
+        prog='shelldocs',
+        epilog="You can mark a file to be ignored by shelldocs by adding"
+        " 'SHELLDOC-IGNORE' as comment in its own line. "+
+        "--input may be given multiple times.")
+    parser.add_argument("-o",
+                        "--output",
+                        dest="outfile",
+                        action="store",
+                        type=str,
+                        help="file to create",
+                        metavar="OUTFILE")
+    parser.add_argument("-i",
+                        "--input",
+                        dest="infile",
+                        action="append",
+                        type=str,
+                        help="file to read",
+                        metavar="INFILE")
+    parser.add_argument("--skipprnorep",
+                        dest="skipprnorep",
+                        action="store_true",
+                        help="Skip Private & Not Replaceable")
+    parser.add_argument("--lint",
+                        dest="lint",
+                        action="store_true",
+                        help="Enable lint mode")
+    parser.add_argument(
+        "-V",
+        "--version",
+        dest="release_version",
+        action="store_true",
+        default=False,
+        help="display version information for shelldocs and exit.")
+
+    options = parser.parse_args()
+
+    if options.release_version:
+        with open(os.path.join(os.path.dirname(__file__), "../VERSION"),
+                  'r') as ver_file:
+            print(ver_file.read())
+        sys.exit(0)
+
+    if options.infile is None:
+        parser.error("At least one input file needs to be supplied")
+    elif options.outfile is None and options.lint is None:
+        parser.error(
+            "At least one of output file and lint mode needs to be specified")
+
+    allfuncs = process_input(options.infile, options.skipprnorep)
+
+    if options.lint:
+        for funcs in allfuncs:
+            message = funcs.lint()
+            if message:
+                print(message)
+
+    if options.outfile is not None:
+        write_output(options.outfile, allfuncs)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/shelldocs/src/main/python/shelldocs/__init__.py 
b/shelldocs/src/main/python/shelldocs/__init__.py
deleted file mode 100755
index 29ed858..0000000
--- a/shelldocs/src/main/python/shelldocs/__init__.py
+++ /dev/null
@@ -1,472 +0,0 @@
-#!/usr/bin/env python2
-#
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-""" process bash scripts and generate documentation from them """
-
-# Do this immediately to prevent compiled forms
-import sys
-import os
-import re
-import errno
-from optparse import OptionParser
-
-sys.dont_write_bytecode = True
-
-ASFLICENSE = '''
-<!---
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
--->
-'''
-
-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(object): # pylint: disable=too-many-public-methods, 
too-many-instance-attributes
-    """a shell function"""
-
-    def __init__(self, filename):
-        '''Initializer'''
-        self.name = None
-        self.audience = None
-        self.stability = None
-        self.replaceb = None
-        self.returnt = None
-        self.desc = None
-        self.params = None
-        self.filename = filename
-        self.linenum = 0
-
-    def __cmp__(self, other):
-        '''comparison'''
-        if self.audience == other.audience:
-            if self.stability == other.stability:
-                if self.replaceb == other.replaceb:
-                    return cmp(self.name, other.name)
-                else:
-                    if self.replaceb == "Yes":
-                        return -1
-            else:
-                if self.stability == "Stable":
-                    return -1
-        else:
-            if self.audience == "Public":
-                return -1
-        return 1
-
-    def reset(self):
-        '''empties current function'''
-        self.name = None
-        self.audience = None
-        self.stability = None
-        self.replaceb = None
-        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.replaceb = docstrip("replaceable", text)
-        self.replaceb = self.replaceb.capitalize()
-
-    def getreplace(self):
-        '''get the replacement state'''
-        if self.replaceb == "Yes":
-            return self.replaceb
-        return "No"
-
-    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):
-        '''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
-
-    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
-
-    def lint(self):
-        '''Lint this function'''
-        getfuncs = {
-            "audience": self.getaudience,
-            "stability": self.getstability,
-            "replaceable": self.getreplace,
-        }
-        validvalues = {
-            "audience": ("Public", "Private"),
-            "stability": ("Stable", "Evolving"),
-            "replaceable": ("Yes", "No"),
-        }
-        messages = []
-        for attr in ("audience", "stability", "replaceable"):
-            value = getfuncs[attr]()
-            if value == "None":
-                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)
-
-    def __str__(self):
-        '''Generate a string for this function'''
-        line = "{%s %s %s %s}" \
-          % (self.getname(),
-             self.getaudience(),
-             self.getstability(),
-             self.getreplace())
-        return line
-
-
-def marked_as_ignored(file_path):
-    """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.
-
-    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(file_path) as input_file:
-        for line in input_file:
-            if line.startswith("#") and line[1:].strip() == "SHELLDOC-IGNORE":
-                return True
-        return False
-
-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, err:
-        print >> sys.stderr, "ERROR: Failed to read from file: %s. Skipping." 
% err.filename
-        return None
-    return allfuncs
-
-def process_input(inputlist, skipprnorep):
-    """ take the input and loop around it """
-    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:
-                    if fname.endswith('sh'):
-                        newfuncs = process_file(filename=os.path.join(root, 
fname),
-                                                skipprnorep=skipprnorep)
-                        if newfuncs:
-                            allfuncs = allfuncs + newfuncs
-        else:
-            newfuncs = process_file(filename=filename, skipprnorep=skipprnorep)
-            if newfuncs:
-                allfuncs = allfuncs + newfuncs
-
-    if allfuncs is None:
-        print >> sys.stderr, "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 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.message)
-            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'''
-    parser = OptionParser(
-        usage="usage: %prog [--skipprnorep] " + "[--output OUTFILE|--lint] " +
-        "--input INFILE " + "[--input INFILE ...]",
-        epilog=
-        "You can mark a file to be ignored by shelldocs by adding"
-        " 'SHELLDOC-IGNORE' as comment in its own line."
-        )
-    parser.add_option("-o",
-                      "--output",
-                      dest="outfile",
-                      action="store",
-                      type="string",
-                      help="file to create",
-                      metavar="OUTFILE")
-    parser.add_option("-i",
-                      "--input",
-                      dest="infile",
-                      action="append",
-                      type="string",
-                      help="file to read",
-                      metavar="INFILE")
-    parser.add_option("--skipprnorep",
-                      dest="skipprnorep",
-                      action="store_true",
-                      help="Skip Private & Not Replaceable")
-    parser.add_option("--lint",
-                      dest="lint",
-                      action="store_true",
-                      help="Enable lint mode")
-    parser.add_option(
-        "-V",
-        "--version",
-        dest="release_version",
-        action="store_true",
-        default=False,
-        help="display version information for shelldocs and exit.")
-
-    (options, dummy_args) = parser.parse_args()
-
-    if options.release_version:
-        with open(
-            os.path.join(
-                os.path.dirname(__file__), "../VERSION"), 'r') as ver_file:
-            print ver_file.read()
-        sys.exit(0)
-
-    if options.infile is None:
-        parser.error("At least one input file needs to be supplied")
-    elif options.outfile is None and options.lint is None:
-        parser.error(
-            "At least one of output file and lint mode needs to be specified")
-
-    allfuncs = process_input(options.infile, options.skipprnorep)
-
-    if options.lint:
-        for funcs in allfuncs:
-            message = funcs.lint()
-            if message:
-                print message
-
-    if options.outfile is not None:
-        write_output(options.outfile, allfuncs)
-
-if __name__ == "__main__":
-    main()

Reply via email to