[PATCH 1/2] get_maintainer: rewrite in Python

Guido De Rossi posted 2 patches 5 hours ago
[PATCH 1/2] get_maintainer: rewrite in Python
Posted by Guido De Rossi 5 hours ago
Add scripts/get_maintainer.py, a full-parity Python 3.6+ rewrite of
scripts/get_maintainer.pl. This is the first step toward deprecating
Perl in the kernel scripts, following the precedent set by
scripts/kernel-doc.py.

The Python version is a drop-in replacement that produces identical
output across all major operation modes: file lookups, patch parsing,
section listing, interactive mode, and self-test validation.

Benchmark comparison (10 iterations, wall time):

  Mode                          Perl     Python
  Single file (--nogit)         7.96s     8.00s
  Default (with git-fallback)   7.93s     8.09s
  Patch (--nogit)               2.60s     2.22s
  Self-test (sections, 1x)      2.69s     1.52s

Performance optimizations over a naive port:
- Pre-parsed (type, value) tuples avoid repeated regex matching on the
  ~24,000 typevalue entries during section traversal
- Literal prefix extraction from F:/X: glob patterns enables fast
  string-based short-circuit before regex compilation, reducing regex
  compilations from ~9,500 to ~170 per invocation (98% reduction)
- Lazy regex compilation ensures only patterns that survive the prefix
  check are compiled
- Pre-compiled regex for the hot-path type:value line format
- Self-test pattern matching uses pre-compiled regexes

Key implementation details:
- All 45+ CLI options supported with Perl-compatible --foo/--nofoo
  negation via argparse
- .get_maintainer.conf and .get_maintainer.ignore file loading
- Full VCS integration (git/hg) with command templates using
  str.format() instead of Perl eval-based interpolation
- RFC822 email validation ported from the Perl regex builder
- Mailmap support with all 4 entry formats
- Interactive terminal menu with all commands
- stdlib only (no external dependencies), single self-contained file

Signed-off-by: Guido De Rossi <guido.derossi91@gmail.com>
---
 scripts/get_maintainer.py | 2393 +++++++++++++++++++++++++++++++++++++
 1 file changed, 2393 insertions(+)
 create mode 100755 scripts/get_maintainer.py

diff --git a/scripts/get_maintainer.py b/scripts/get_maintainer.py
new file mode 100755
index 000000000000..cc1dab4d4f64
--- /dev/null
+++ b/scripts/get_maintainer.py
@@ -0,0 +1,2393 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Print selected MAINTAINERS information for
+# the files modified in a patch or for a file
+#
+# usage: get_maintainer.py [OPTIONS] <patch>
+#        get_maintainer.py [OPTIONS] -f <file>
+#
+# Python rewrite of get_maintainer.pl
+
+import argparse
+import os
+import re
+import shlex
+import shutil
+import subprocess
+import sys
+
+V = '0.26'
+
+# ---- constants ----
+
+penguin_chief = [
+    "Linus Torvalds:torvalds@linux-foundation.org",
+]
+
+penguin_chief_names = []
+for chief in penguin_chief:
+    m = re.match(r'^(.*?):(.*)', chief)
+    if m:
+        penguin_chief_names.append(m.group(1))
+
+penguin_chiefs = "(" + "|".join(re.escape(n) for n in penguin_chief_names) + ")"
+
+signature_tags = [
+    "Signed-off-by:",
+    "Reviewed-by:",
+    "Acked-by:",
+]
+
+signature_pattern = "(" + "|".join(re.escape(t) for t in signature_tags) + ")"
+
+rfc822_lwsp = r"(?:(?:\r\n)?[ \t])"
+rfc822_char = r'[\000-\377]'
+
+# Pre-compiled patterns for hot paths
+_re_type_value = re.compile(r'^([A-Z]):\s*(.*)')
+_re_blank_line = re.compile(r'^\s*$')
+_re_comment_line = re.compile(r'^\s*#')
+
+# ---- VCS command templates ----
+
+VCS_cmds_git = {
+    "available": "git",
+    "find_signers_cmd":
+        'git log --no-color --follow --since={email_git_since} '
+        '--numstat --no-merges '
+        '--format="GitCommit: %H%n'
+                  'GitAuthor: %an <%ae>%n'
+                  'GitDate: %aD%n'
+                  'GitSubject: %s%n'
+                  '%b%n"'
+        ' -- {file}',
+    "find_commit_signers_cmd":
+        'git log --no-color '
+        '--numstat '
+        '--format="GitCommit: %H%n'
+                  'GitAuthor: %an <%ae>%n'
+                  'GitDate: %aD%n'
+                  'GitSubject: %s%n'
+                  '%b%n"'
+        ' -1 {commit}',
+    "find_commit_author_cmd":
+        'git log --no-color '
+        '--numstat '
+        '--format="GitCommit: %H%n'
+                  'GitAuthor: %an <%ae>%n'
+                  'GitDate: %aD%n'
+                  'GitSubject: %s%n"'
+        ' -1 {commit}',
+    "blame_range_cmd": "git blame -l -L {diff_start},+{diff_length} {file}",
+    "blame_file_cmd": "git blame -l {file}",
+    "commit_pattern": r"^GitCommit: ([0-9a-f]{40})",
+    "blame_commit_pattern": r"^([0-9a-f]+) ",
+    "author_pattern": r"^GitAuthor: (.*)",
+    "subject_pattern": r"^GitSubject: (.*)",
+    "stat_pattern": r"^(\d+)\t(\d+)\t{file}$",
+    "file_exists_cmd": "git ls-files {file}",
+    "list_files_cmd": "git ls-files {file}",
+}
+
+VCS_cmds_hg = {
+    "available": "hg",
+    "find_signers_cmd":
+        "hg log --date={email_hg_since} "
+        "--template='HgCommit: {{node}}\\n"
+                    "HgAuthor: {{author}}\\n"
+                    "HgSubject: {{desc}}\\n'"
+        " -- {file}",
+    "find_commit_signers_cmd":
+        "hg log "
+        "--template='HgSubject: {{desc}}\\n'"
+        " -r {commit}",
+    "find_commit_author_cmd":
+        "hg log "
+        "--template='HgCommit: {{node}}\\n"
+                    "HgAuthor: {{author}}\\n"
+                    "HgSubject: {{desc|firstline}}\\n'"
+        " -r {commit}",
+    "blame_range_cmd": "",
+    "blame_file_cmd": "hg blame -n {file}",
+    "commit_pattern": r"^HgCommit: ([0-9a-f]{40})",
+    "blame_commit_pattern": r"^([ 0-9a-f]+):",
+    "author_pattern": r"^HgAuthor: (.*)",
+    "subject_pattern": r"^HgSubject: (.*)",
+    "stat_pattern": r"^(\d+)\t(\d+)\t{file}$",
+    "file_exists_cmd": "hg files {file}",
+    "list_files_cmd": "hg manifest -R {file}",
+}
+
+# ---- global state ----
+
+P = ""
+cur_path = ""
+lk_path = "./"
+
+# Options (with defaults matching the Perl script)
+email = 1
+email_usename = 1
+email_maintainer = 1
+email_reviewer = 1
+email_fixes = 1
+email_list = 1
+email_moderated_list = 1
+email_subscriber_list = 0
+email_git_penguin_chiefs = 0
+email_git = 0
+email_git_all_signature_types = 0
+email_git_blame = 0
+email_git_blame_signatures = 1
+email_git_fallback = 1
+email_git_min_signatures = 1
+email_git_max_maintainers = 5
+email_git_min_percent = 5
+email_git_since = "1-year-ago"
+email_hg_since = "-365"
+interactive = 0
+email_remove_duplicates = 1
+email_use_mailmap = 1
+output_multiline = 1
+output_separator = ", "
+output_roles = 0
+output_rolestats = 1
+output_substatus = None
+output_section_maxlen = 50
+scm = 0
+tree = 1
+web = 0
+bug = 0
+subsystem_opt = 0
+status_opt = 0
+letters = ""
+keywords = 1
+keywords_in_file = 0
+sections = 0
+email_file_emails = 0
+from_filename = 0
+pattern_depth = 0
+self_test = None
+version = 0
+find_maintainer_files = 0
+maintainer_path = None
+vcs_used = 0
+
+# Mutable state
+files = []
+fixes = []
+range_list = []
+keyword_tvi = []
+file_emails = []
+
+commit_author_hash = {}
+commit_signer_hash = {}
+
+typevalue = []
+# Pre-parsed (type_char, value_string) tuples parallel to typevalue.
+# For non-type lines, type_char is None.
+typevalue_parsed = []
+# Lazily compiled regexes for F:/X: patterns, keyed by typevalue index.
+# Sentinel value _UNCOMPILED means the pattern has not been compiled yet.
+_UNCOMPILED = object()
+typevalue_compiled = {}
+# Literal prefix for each F:/X: pattern -- fast string check before regex.
+typevalue_prefix = {}
+keyword_hash = {}
+mfiles = []
+self_test_info = []
+
+mailmap_data = None
+
+email_hash_name = {}
+email_hash_address = {}
+email_to = []
+hash_list_to = {}
+list_to = []
+scm_list = []
+web_list = []
+bug_list = []
+subsystem_list = []
+status_list = []
+substatus_list = []
+deduplicate_name_hash = {}
+deduplicate_address_hash = {}
+
+ignore_emails = []
+
+VCS_cmds = {}
+
+printed_novcs = False
+
+# ---- utility functions ----
+
+def which(bin_name):
+    path = shutil.which(bin_name)
+    return path if path else ""
+
+def which_conf(conf):
+    for path_dir in [".", os.environ.get("HOME", ""), ".scripts"]:
+        p = os.path.join(path_dir, conf)
+        if os.path.exists(p):
+            return p
+    return ""
+
+def top_of_kernel_tree(lk):
+    if lk and not lk.endswith("/"):
+        lk += "/"
+    checks_f = ["COPYING", "CREDITS", "Kbuild", "Makefile", "README"]
+    checks_e = ["MAINTAINERS"]
+    checks_d = ["Documentation", "arch", "include", "drivers", "fs",
+                 "init", "ipc", "kernel", "lib", "scripts"]
+    for f in checks_f:
+        if not os.path.isfile(lk + f):
+            return False
+    for f in checks_e:
+        if not os.path.exists(lk + f):
+            return False
+    for d in checks_d:
+        if not os.path.isdir(lk + d):
+            return False
+    return True
+
+def uniq(lst):
+    seen = set()
+    result = []
+    for x in lst:
+        if x not in seen:
+            seen.add(x)
+            result.append(x)
+    return result
+
+def sort_and_uniq(lst):
+    seen = set()
+    result = []
+    for x in sorted(lst):
+        if x not in seen:
+            seen.add(x)
+            result.append(x)
+    return result
+
+# ---- email functions ----
+
+def escape_name(name):
+    if re.search(r'[^\w \-]', name, re.ASCII | re.IGNORECASE):
+        name = name.replace('\\', '\\\\')
+        name = name.replace('"', '\\"')
+        name = '"' + name + '"'
+    return name
+
+def parse_email(formatted_email):
+    name = ""
+    address = ""
+    m = re.match(r'^([^<]+)<(.+@.*)>.*$', formatted_email)
+    if m:
+        name = m.group(1)
+        address = m.group(2)
+    else:
+        m = re.match(r'^\s*<(.+@\S*)>.*$', formatted_email)
+        if m:
+            address = m.group(1)
+        else:
+            m = re.match(r'^(.+@\S*).*$', formatted_email)
+            if m:
+                address = m.group(1)
+    name = name.strip()
+    name = name.strip('"')
+    name = escape_name(name)
+    address = address.strip()
+    return (name, address)
+
+def format_email(name, address, usename):
+    name = name.strip().strip('"')
+    name = escape_name(name)
+    address = address.strip()
+    if usename:
+        if name == "":
+            return address
+        else:
+            return "{} <{}>".format(name, address)
+    else:
+        return address
+
+# ---- RFC822 validation ----
+
+_rfc822re = None
+
+def make_rfc822re():
+    specials = r'()<>@,;:\\".\\[\\]'
+    controls = r'\000-\037\177'
+
+    dtext = r"[^\[\]\r\\]"
+    domain_literal = r"\[(?:" + dtext + r"|\\\\.)* \]" + rfc822_lwsp + "*"
+
+    quoted_string = r'"(?:[^"\r\\]|\\\\.|' + rfc822_lwsp + r')*"' + rfc822_lwsp + "*"
+
+    atom = r"[^" + specials + " " + controls + r"]+(?:" + rfc822_lwsp + r"+|\Z|(?=[\\[\"" + specials + r"]))"
+    word = r"(?:" + atom + r"|" + quoted_string + r")"
+    localpart = word + r"(?:\." + rfc822_lwsp + r"*" + word + r")*"
+
+    sub_domain = r"(?:" + atom + r"|" + domain_literal + r")"
+    domain = sub_domain + r"(?:\." + rfc822_lwsp + r"*" + sub_domain + r")*"
+
+    addr_spec = localpart + r"@" + rfc822_lwsp + r"*" + domain
+
+    phrase = word + r"*"
+    route = r"(?:@" + domain + r"(?:,@" + rfc822_lwsp + r"*" + domain + r")*:" + rfc822_lwsp + r"*)"
+    route_addr = r"\<" + rfc822_lwsp + r"*" + route + r"?" + addr_spec + r"\>" + rfc822_lwsp + r"*"
+    mailbox = r"(?:" + addr_spec + r"|" + phrase + route_addr + r")"
+
+    group = phrase + r":" + rfc822_lwsp + r"*(?:" + mailbox + r"(?:,\s*" + mailbox + r")*)?;\s*"
+    address = r"(?:" + mailbox + r"|" + group + r")"
+
+    return rfc822_lwsp + r"*" + address
+
+def rfc822_strip_comments(s):
+    while True:
+        new_s = re.sub(
+            r'^((?:[^"\\]|\\.)*(?:"(?:[^"\\]|\\.)*"(?:[^"\\]|\\.)*)*)\((?:[^()\\]|\\.)*\)',
+            r'\1 ', s, count=1, flags=re.DOTALL)
+        if new_s == s:
+            break
+        s = new_s
+    return s
+
+def rfc822_valid(s):
+    global _rfc822re
+    s = rfc822_strip_comments(s)
+    if _rfc822re is None:
+        _rfc822re = make_rfc822re()
+    if re.match(r'^' + _rfc822re + r'$', s) and re.match(r'^' + rfc822_char + r'*$', s):
+        return True
+    return False
+
+def rfc822_validlist(s):
+    global _rfc822re
+    s = rfc822_strip_comments(s)
+    if _rfc822re is None:
+        _rfc822re = make_rfc822re()
+    if re.match(r'^(?:' + _rfc822re + r')?(?:,(?:' + _rfc822re + r')?)*$', s) and \
+       re.match(r'^' + rfc822_char + r'*$', s):
+        result = []
+        for m in re.finditer(r'(?:^|,' + rfc822_lwsp + r'*)(' + _rfc822re + r')', s):
+            result.append(m.group(1))
+        return result
+    return []
+
+# ---- mailmap functions ----
+
+def read_mailmap():
+    global mailmap_data, email_use_mailmap, lk_path
+    mailmap_data = {"names": {}, "addresses": {}}
+
+    if not email_use_mailmap or not os.path.isfile(lk_path + ".mailmap"):
+        return
+
+    try:
+        with open(lk_path + ".mailmap", "r", encoding="utf-8") as f:
+            for line in f:
+                line = re.sub(r'#.*$', '', line)  # strip comments
+                line = line.strip()
+                if not line:
+                    continue
+
+                # name1 <mail1> name2 <mail2>
+                m = re.match(r'^(.+)<([^>]+)>\s*(.+)\s*<([^>]+)>$', line)
+                if m:
+                    real_name = m.group(1).rstrip()
+                    real_address = m.group(2)
+                    wrong_name = m.group(3).rstrip()
+                    wrong_address = m.group(4)
+                    real_name, real_address = parse_email("{} <{}>".format(real_name, real_address))
+                    wrong_name, wrong_address = parse_email("{} <{}>".format(wrong_name, wrong_address))
+                    wrong_email = format_email(wrong_name, wrong_address, 1)
+                    mailmap_data["names"][wrong_email] = real_name
+                    mailmap_data["addresses"][wrong_email] = real_address
+                    continue
+
+                # name1 <mail1> <mail2>
+                m = re.match(r'^(.+)<([^>]+)>\s*<([^>]+)>$', line)
+                if m:
+                    real_name = m.group(1).rstrip()
+                    real_address = m.group(2)
+                    wrong_address = m.group(3)
+                    real_name, real_address = parse_email("{} <{}>".format(real_name, real_address))
+                    mailmap_data["names"][wrong_address] = real_name
+                    mailmap_data["addresses"][wrong_address] = real_address
+                    continue
+
+                # <mail1> <mail2>
+                m = re.match(r'^<([^>]+)>\s*<([^>]+)>$', line)
+                if m:
+                    real_address = m.group(1)
+                    wrong_address = m.group(2)
+                    mailmap_data["addresses"][wrong_address] = real_address
+                    continue
+
+                # name1 <mail1>
+                m = re.match(r'^([^<]+)<([^>]+)>$', line)
+                if m:
+                    real_name = m.group(1).rstrip()
+                    address = m.group(2)
+                    real_name, address = parse_email("{} <{}>".format(real_name, address))
+                    mailmap_data["names"][address] = real_name
+                    continue
+    except IOError:
+        print("{}:  Can't open .mailmap".format(P), file=sys.stderr)
+
+def mailmap_email(line):
+    name, address = parse_email(line)
+    email_str = format_email(name, address, 1)
+    real_name = name
+    real_address = address
+
+    if email_str in mailmap_data["names"] or email_str in mailmap_data["addresses"]:
+        if email_str in mailmap_data["names"]:
+            real_name = mailmap_data["names"][email_str]
+        if email_str in mailmap_data["addresses"]:
+            real_address = mailmap_data["addresses"][email_str]
+    else:
+        if address in mailmap_data["names"]:
+            real_name = mailmap_data["names"][address]
+        if address in mailmap_data["addresses"]:
+            real_address = mailmap_data["addresses"][address]
+
+    return format_email(real_name, real_address, 1)
+
+def mailmap(addresses):
+    mapped = []
+    for line in addresses:
+        mapped.append(mailmap_email(line))
+    if email_use_mailmap:
+        merge_by_realname(mapped)
+    return mapped
+
+def merge_by_realname(emails):
+    address_map = {}
+    for i in range(len(emails)):
+        name, address = parse_email(emails[i])
+        if name in address_map:
+            address = address_map[name]
+            emails[i] = format_email(name, address, 1)
+        else:
+            address_map[name] = address
+
+# ---- MAINTAINERS parsing ----
+
+def read_maintainer_file(filepath):
+    global typevalue, typevalue_parsed, typevalue_compiled
+    global keyword_hash, self_test_info, self_test
+    try:
+        with open(filepath, "r", encoding="utf-8") as f:
+            i = 1
+            for raw_line in f:
+                line = raw_line.rstrip('\n')
+
+                m = _re_type_value.match(line)
+                if m:
+                    typ = m.group(1)
+                    value = m.group(2)
+
+                    if typ in ("F", "X"):
+                        value = value.replace('.', '\\.')
+                        value = value.replace('**', '\x00')
+                        value = value.replace('*', '.*')
+                        value = value.replace('?', '.')
+                        value = value.replace('\x00', '(?:.*)')
+                        if os.path.isdir(value):
+                            if not value.endswith('/'):
+                                value += '/'
+                        # Mark for lazy compilation and extract literal prefix
+                        idx = len(typevalue)
+                        typevalue_compiled[idx] = _UNCOMPILED
+                        # Extract the literal prefix before any regex metachar
+                        prefix = []
+                        for ch in value:
+                            if ch in r'.*+?()[]{}|\\^$':
+                                break
+                            prefix.append(ch)
+                        typevalue_prefix[idx] = ''.join(prefix)
+                    elif typ == "K":
+                        keyword_hash[len(typevalue)] = value
+
+                    typevalue.append("{}:{}".format(typ, value))
+                    typevalue_parsed.append((typ, value))
+                elif not _re_blank_line.match(line) and not _re_comment_line.match(line):
+                    typevalue.append(line)
+                    typevalue_parsed.append((None, line))
+
+                if self_test is not None:
+                    self_test_info.append({"file": filepath, "linenr": i, "line": line})
+                i += 1
+    except IOError:
+        print("{}: Can't open MAINTAINERS file '{}'" .format(P, filepath), file=sys.stderr)
+        sys.exit(1)
+
+def read_all_maintainer_files():
+    global mfiles, lk_path, maintainer_path, find_maintainer_files
+    path = lk_path + "MAINTAINERS"
+    if maintainer_path is not None:
+        path = maintainer_path
+        path = os.path.expanduser(path)
+
+    if os.path.isdir(path):
+        if not path.endswith('/'):
+            path += '/'
+        if find_maintainer_files:
+            for root, dirs, fnames in os.walk(path):
+                # skip .git
+                dirs[:] = [d for d in dirs if d != '.git']
+                for fname in fnames:
+                    if fname == "MAINTAINERS":
+                        mfiles.append(os.path.join(root, fname))
+        else:
+            for fname in os.listdir(path):
+                if not fname.startswith('.'):
+                    mfiles.append(path + fname)
+    elif os.path.isfile(path):
+        mfiles.append(path)
+    else:
+        print("{}: MAINTAINER file not found '{}'".format(P, path), file=sys.stderr)
+        sys.exit(1)
+
+    if len(mfiles) == 0:
+        print("{}: No MAINTAINER files found in '{}'".format(P, path), file=sys.stderr)
+        sys.exit(1)
+
+    for filepath in mfiles:
+        read_maintainer_file(filepath)
+
+def maintainers_in_file(filepath):
+    global file_emails, email_file_emails
+    if re.search(r'\bMAINTAINERS$', filepath):
+        return
+    if os.path.isfile(filepath) and (email_file_emails or filepath.endswith('.yaml')):
+        try:
+            with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
+                text = f.read()
+            poss_addr = re.findall(
+                r"[\w\"\' \,\.\+-]*\s*[\,]*\s*[\(\<\{]?[A-Za-z0-9_\.\+-]+@[A-Za-z0-9\.\-]+\.[A-Za-z0-9]+[\)\>\}]?",
+                text)
+            file_emails.extend(clean_file_emails(poss_addr))
+        except IOError:
+            pass
+
+# ---- section navigation ----
+
+def find_first_section():
+    index = 0
+    while index < len(typevalue_parsed):
+        if typevalue_parsed[index][0] is not None:
+            break
+        index += 1
+    return index
+
+def find_starting_index(index):
+    while index > 0:
+        if typevalue_parsed[index][0] is None:
+            break
+        index -= 1
+    return index
+
+def find_ending_index(index):
+    while index < len(typevalue_parsed):
+        if typevalue_parsed[index][0] is None:
+            break
+        index += 1
+    return index
+
+def get_subsystem_name(index):
+    start = find_starting_index(index)
+    sub = typevalue[start]
+    if output_section_maxlen and len(sub) > output_section_maxlen:
+        sub = sub[:output_section_maxlen - 3].rstrip() + "..."
+    return sub
+
+# ---- file matching ----
+
+def _get_compiled(idx):
+    """Lazily compile and cache the regex for typevalue entry at idx."""
+    compiled = typevalue_compiled.get(idx)
+    if compiled is _UNCOMPILED:
+        value = typevalue_parsed[idx][1]
+        try:
+            compiled = re.compile(r'^' + value)
+        except re.error:
+            compiled = None
+        typevalue_compiled[idx] = compiled
+    return compiled
+
+def file_match_pattern(filepath, pattern, compiled=None):
+    if compiled is None:
+        try:
+            compiled = re.compile(r'^' + pattern)
+        except re.error:
+            return False
+    if pattern.endswith("/"):
+        if compiled.search(filepath):
+            return True
+    else:
+        if compiled.search(filepath):
+            s1 = filepath.count('/')
+            s2 = pattern.count('/')
+            if s1 == s2 or '(?:' in pattern:
+                return True
+    return False
+
+# ---- category/role functions ----
+
+def get_maintainer_role(index):
+    start = find_starting_index(index)
+    end = find_ending_index(index)
+    role = "maintainer"
+    sub = get_subsystem_name(index)
+    sts = "unknown"
+
+    for i in range(start + 1, end):
+        typ, value = typevalue_parsed[i]
+        if typ == "S":
+            sts = value
+
+    sts = sts.lower()
+    if sts == "buried alive in reporters":
+        role = "chief penguin"
+
+    return role + ":" + sub
+
+def get_list_role(index):
+    sub = get_subsystem_name(index)
+    if sub == "THE REST":
+        sub = ""
+    return sub
+
+def add_categories(index, suffix):
+    global email_to, hash_list_to, list_to, scm_list, web_list, bug_list
+    global subsystem_list, status_list, substatus_list
+
+    start = find_starting_index(index)
+    end = find_ending_index(index)
+
+    sub = typevalue[start]
+    subsystem_list.append(sub)
+    sts = "Unknown"
+
+    for i in range(start + 1, end):
+        ptype, pvalue = typevalue_parsed[i]
+        if ptype is not None:
+            if ptype == "L":
+                list_address = pvalue
+                list_additional = ""
+                list_role = get_list_role(i)
+
+                if list_role:
+                    list_role = ":" + list_role
+
+                m2 = re.match(r'^(\S+)\s+(.*)$', list_address)
+                if m2:
+                    list_address = m2.group(1)
+                    list_additional = m2.group(2)
+
+                if re.search(r'subscribers-only', list_additional):
+                    if email_subscriber_list:
+                        if list_address.lower() not in hash_list_to:
+                            hash_list_to[list_address.lower()] = 1
+                            list_to.append([list_address,
+                                            "subscriber list{}{}".format(list_role, suffix)])
+                else:
+                    if email_list:
+                        if list_address.lower() not in hash_list_to:
+                            if re.search(r'moderated', list_additional):
+                                if email_moderated_list:
+                                    hash_list_to[list_address.lower()] = 1
+                                    list_to.append([list_address,
+                                                    "moderated list{}{}".format(list_role, suffix)])
+                            else:
+                                hash_list_to[list_address.lower()] = 1
+                                list_to.append([list_address,
+                                                "open list{}{}".format(list_role, suffix)])
+
+            elif ptype == "M":
+                if email_maintainer:
+                    role = get_maintainer_role(i)
+                    push_email_addresses(pvalue, role + suffix)
+            elif ptype == "R":
+                if email_reviewer:
+                    subs = get_subsystem_name(i)
+                    push_email_addresses(pvalue, "reviewer:" + subs + suffix)
+            elif ptype == "T":
+                scm_list.append(pvalue + suffix)
+            elif ptype == "W":
+                web_list.append(pvalue + suffix)
+            elif ptype == "B":
+                bug_list.append(pvalue + suffix)
+            elif ptype == "S":
+                status_list.append(pvalue + suffix)
+                sts = pvalue
+
+    if sub != "THE REST" and sts != "Maintained":
+        substatus_list.append("{} status: {}{}".format(sub, sts, suffix))
+
+# ---- email management ----
+
+def email_inuse(name, address):
+    if name == "" and address == "":
+        return True
+    if name != "" and name.lower() in email_hash_name:
+        return True
+    if address != "" and address.lower() in email_hash_address:
+        return True
+    return False
+
+def push_email_address(line, role):
+    global email_to, email_hash_name, email_hash_address
+    name, address = parse_email(line)
+    if address == "":
+        return False
+
+    if not email_remove_duplicates:
+        email_to.append([format_email(name, address, email_usename), role])
+    elif not email_inuse(name, address):
+        email_to.append([format_email(name, address, email_usename), role])
+        if name != "":
+            email_hash_name[name.lower()] = email_hash_name.get(name.lower(), 0) + 1
+        email_hash_address[address.lower()] = email_hash_address.get(address.lower(), 0) + 1
+
+    return True
+
+def push_email_addresses(address, role):
+    if rfc822_valid(address):
+        push_email_address(address, role)
+    else:
+        addr_list = rfc822_validlist(address)
+        if addr_list:
+            for entry in addr_list:
+                push_email_address(entry, role)
+        else:
+            if not push_email_address(address, role):
+                print("Invalid MAINTAINERS address: '{}'".format(address), file=sys.stderr)
+
+def add_role(line, role):
+    global email_to
+    name, address = parse_email(line)
+    email_str = format_email(name, address, email_usename)
+
+    for entry in email_to:
+        if email_remove_duplicates:
+            entry_name, entry_address = parse_email(entry[0])
+            if (name == entry_name or address == entry_address) and \
+               (role == "" or role not in entry[1]):
+                if entry[1] == "":
+                    entry[1] = role
+                else:
+                    entry[1] = "{},{}".format(entry[1], role)
+        else:
+            if email_str == entry[0] and \
+               (role == "" or role not in entry[1]):
+                if entry[1] == "":
+                    entry[1] = role
+                else:
+                    entry[1] = "{},{}".format(entry[1], role)
+
+def deduplicate_email(email_addr):
+    global deduplicate_name_hash, deduplicate_address_hash
+    name, address = parse_email(email_addr)
+    email_addr = format_email(name, address, 1)
+    email_addr = mailmap_email(email_addr)
+
+    if not email_remove_duplicates:
+        return email_addr
+
+    name, address = parse_email(email_addr)
+    matched = False
+
+    if name != "" and name.lower() in deduplicate_name_hash:
+        name = deduplicate_name_hash[name.lower()][0]
+        address = deduplicate_name_hash[name.lower()][1]
+        matched = True
+    elif address.lower() in deduplicate_address_hash:
+        name = deduplicate_address_hash[address.lower()][0]
+        address = deduplicate_address_hash[address.lower()][1]
+        matched = True
+
+    if not matched:
+        deduplicate_name_hash[name.lower()] = [name, address]
+        deduplicate_address_hash[address.lower()] = [name, address]
+
+    email_addr = format_email(name, address, 1)
+    email_addr = mailmap_email(email_addr)
+    return email_addr
+
+def ignore_email_address(address):
+    for ig in ignore_emails:
+        if ig == address:
+            return True
+    return False
+
+def clean_file_emails(raw_emails):
+    fmt_emails = []
+    for em in raw_emails:
+        em = re.sub(
+            r'[\(\<\{]?([A-Za-z0-9_\.\+-]+@[A-Za-z0-9\.\-]+)[\)\>\}]?',
+            r'<\1>', em)
+        name, address = parse_email(em)
+
+        # Strip quotes
+        if name.startswith('"') and name.endswith('"'):
+            name = name[1:-1]
+
+        # Split into name-like parts
+        nw = re.split(r"[^\w\'\,\.\+\-]", name)
+        nw = [w for w in nw if w and not re.match(r"^[\'\,\.\+\-]$", w)]
+
+        if len(nw) > 2:
+            first = nw[-3]
+            middle = nw[-2]
+            last = nw[-1]
+            if ((len(first) == 1 and re.match(r'\w', first)) or
+                (len(first) == 2 and first.endswith("."))) or \
+               (len(middle) == 1 or
+                (len(middle) == 2 and middle.endswith("."))):
+                name = "{} {} {}".format(first, middle, last)
+            else:
+                name = "{} {}".format(middle, last)
+        else:
+            name = " ".join(nw)
+
+        if name and name[-1] in (',', '.'):
+            name = name[:-1]
+        if name and name[0] in (',', '.'):
+            name = name[1:]
+
+        fmt_emails.append(format_email(name, address, email_usename))
+    return fmt_emails
+
+# ---- VCS integration ----
+
+def git_execute_cmd(cmd):
+    try:
+        output = subprocess.run(cmd, shell=True, capture_output=True,
+                                text=True, encoding='utf-8', errors='replace')
+        lines = output.stdout.lstrip().split('\n')
+        # strip leading whitespace from each line (matching Perl)
+        lines = [l.lstrip() for l in lines]
+        return lines
+    except Exception:
+        return []
+
+def hg_execute_cmd(cmd):
+    try:
+        output = subprocess.run(cmd, shell=True, capture_output=True,
+                                text=True, encoding='utf-8', errors='replace')
+        return output.stdout.split('\n')
+    except Exception:
+        return []
+
+def vcs_exists():
+    global VCS_cmds, vcs_used, printed_novcs
+    # Try git
+    if which("git") and os.path.exists(".git"):
+        VCS_cmds = VCS_cmds_git
+        vcs_used = 1
+        return 1
+    # Try hg
+    if which("hg") and os.path.isdir(".hg"):
+        VCS_cmds = VCS_cmds_hg
+        vcs_used = 2
+        return 2
+    VCS_cmds = {}
+    if not printed_novcs and email_git:
+        print("{}: No supported VCS found.  Add --nogit to options?".format(P), file=sys.stderr)
+        print("Using a git repository produces better results.", file=sys.stderr)
+        print("Try Linus Torvalds' latest git repository using:", file=sys.stderr)
+        print("git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git", file=sys.stderr)
+        printed_novcs = True
+    return 0
+
+def vcs_is_git():
+    vcs_exists()
+    return vcs_used == 1
+
+def vcs_is_hg():
+    return vcs_used == 2
+
+def _vcs_exec(cmd):
+    if vcs_used == 1:
+        return git_execute_cmd(cmd)
+    elif vcs_used == 2:
+        return hg_execute_cmd(cmd)
+    return []
+
+def _interpolate_cmd(cmd_template, **kwargs):
+    """Substitute variables in a VCS command template."""
+    result = cmd_template
+    for key, val in kwargs.items():
+        result = result.replace('{' + key + '}', str(val))
+    return result
+
+def extract_formatted_signatures(signature_lines):
+    types = []
+    signers = []
+    for line in signature_lines:
+        # extract type (everything before first ':')
+        m = re.match(r'\s*(.*?):.*', line)
+        typ = m.group(1) if m else ""
+        types.append(typ)
+
+        # extract signer (everything after first ':')
+        m = re.match(r'\s*.*?:\s*(.+)\s*', line)
+        signer = m.group(1).strip() if m else ""
+        signer = deduplicate_email(signer)
+        signers.append(signer)
+
+    return (types, signers)
+
+def vcs_find_signers(cmd, filepath):
+    global signature_pattern
+    lines = _vcs_exec(cmd)
+
+    pattern = VCS_cmds["commit_pattern"]
+    author_pattern = VCS_cmds["author_pattern"]
+    stat_pattern = VCS_cmds["stat_pattern"]
+
+    if filepath:
+        stat_pattern = stat_pattern.replace('{file}', re.escape(filepath))
+    else:
+        stat_pattern = stat_pattern.replace('{file}', '')
+
+    commits = sum(1 for l in lines if re.search(pattern, l))
+    authors = [l for l in lines if re.search(author_pattern, l)]
+    signatures = [l for l in lines if re.search(
+        r'^[ \t]*' + signature_pattern + r'.*@.*$', l)]
+    stats = [l for l in lines if re.search(stat_pattern, l)]
+
+    if not signatures:
+        return (0, signatures, authors, stats)
+
+    if interactive:
+        save_commits_by_author(lines)
+        save_commits_by_signer(lines)
+
+    if not email_git_penguin_chiefs:
+        signatures = [s for s in signatures if not re.search(penguin_chiefs, s, re.IGNORECASE)]
+
+    _, authors_list = extract_formatted_signatures(authors)
+    _, signers_list = extract_formatted_signatures(signatures)
+
+    return (commits, signers_list, authors_list, stats)
+
+def vcs_find_author(cmd):
+    lines = _vcs_exec(cmd)
+
+    if not email_git_penguin_chiefs:
+        lines = [l for l in lines if not re.search(penguin_chiefs, l, re.IGNORECASE)]
+
+    if not lines:
+        return []
+
+    authors = []
+    author_pattern = VCS_cmds["author_pattern"]
+    for line in lines:
+        m = re.search(author_pattern, line)
+        if m:
+            author = m.group(1)
+            name, address = parse_email(author)
+            author = format_email(name, address, 1)
+            authors.append(author)
+
+    if interactive:
+        save_commits_by_author(lines)
+        save_commits_by_signer(lines)
+
+    return authors
+
+def vcs_save_commits(cmd):
+    lines = _vcs_exec(cmd)
+    commits = []
+    blame_pattern = VCS_cmds["blame_commit_pattern"]
+    for line in lines:
+        m = re.search(blame_pattern, line)
+        if m:
+            commits.append(m.group(1))
+    return commits
+
+def vcs_blame(filepath):
+    global range_list
+    commits = []
+    if not os.path.isfile(filepath):
+        return commits
+
+    if range_list and VCS_cmds.get("blame_range_cmd", "") == "":
+        cmd = _interpolate_cmd(VCS_cmds["blame_file_cmd"], file=shlex.quote(filepath))
+        all_commits = vcs_save_commits(cmd)
+
+        for file_range_diff in range_list:
+            m = re.match(r'(.+):(.+):(.+)', file_range_diff)
+            if not m:
+                continue
+            diff_file = m.group(1)
+            diff_start = int(m.group(2))
+            diff_length = int(m.group(3))
+            if filepath != diff_file:
+                continue
+            for i in range(diff_start, diff_start + diff_length):
+                if i < len(all_commits):
+                    commits.append(all_commits[i])
+    elif range_list:
+        for file_range_diff in range_list:
+            m = re.match(r'(.+):(.+):(.+)', file_range_diff)
+            if not m:
+                continue
+            diff_file = m.group(1)
+            diff_start = m.group(2)
+            diff_length = m.group(3)
+            if filepath != diff_file:
+                continue
+            cmd = _interpolate_cmd(VCS_cmds["blame_range_cmd"],
+                                   file=shlex.quote(filepath),
+                                   diff_start=diff_start,
+                                   diff_length=diff_length)
+            commits.extend(vcs_save_commits(cmd))
+    else:
+        cmd = _interpolate_cmd(VCS_cmds["blame_file_cmd"], file=shlex.quote(filepath))
+        commits = vcs_save_commits(cmd)
+
+    commits = [c.lstrip('^') for c in commits]
+    return commits
+
+def vcs_file_exists(filepath):
+    v = vcs_exists()
+    if not v:
+        return False
+    cmd = _interpolate_cmd(VCS_cmds["file_exists_cmd"], file=shlex.quote(filepath))
+    cmd += " 2>&1"
+    result = _vcs_exec(cmd)
+    # Check if any non-empty output
+    return any(line.strip() for line in result) if result else False
+
+def vcs_list_files(filepath):
+    v = vcs_exists()
+    if not v:
+        return []
+    cmd = _interpolate_cmd(VCS_cmds["list_files_cmd"], file=shlex.quote(filepath))
+    return _vcs_exec(cmd)
+
+def vcs_assign(role, divisor, lines):
+    if not lines:
+        return
+
+    if divisor <= 0:
+        print("Bad divisor in vcs_assign: {}".format(divisor), file=sys.stderr)
+        divisor = 1
+
+    lines = mailmap(lines)
+
+    if not lines:
+        return
+
+    lines.sort()
+
+    # uniq -c
+    counts = {}
+    for l in lines:
+        counts[l] = counts.get(l, 0) + 1
+
+    count = 0
+    for line in sorted(counts.keys(), key=lambda x: counts[x], reverse=True):
+        sign_offs = counts[line]
+        percent = sign_offs * 100 / divisor
+        if percent > 100:
+            percent = 100
+        if ignore_email_address(line):
+            continue
+        count += 1
+        if sign_offs < email_git_min_signatures or \
+           count > email_git_max_maintainers or \
+           percent < email_git_min_percent:
+            break
+        push_email_address(line, '')
+        if output_rolestats:
+            fmt_percent = "{:.0f}".format(percent)
+            add_role(line, "{}:{}/{}={}%".format(role, sign_offs, divisor, fmt_percent))
+        else:
+            add_role(line, role)
+
+def vcs_file_signoffs(filepath):
+    global vcs_used
+
+    vcs_used = vcs_exists()
+    if not vcs_used:
+        return
+
+    cmd = _interpolate_cmd(VCS_cmds["find_signers_cmd"],
+                           email_git_since=email_git_since,
+                           email_hg_since=email_hg_since,
+                           file=shlex.quote(filepath))
+
+    commits, signers, authors, stats = vcs_find_signers(cmd, filepath)
+
+    for i in range(len(signers)):
+        signers[i] = deduplicate_email(signers[i])
+
+    vcs_assign("commit_signer", commits, signers)
+    vcs_assign("authored", commits, authors)
+
+    if len(authors) == len(stats):
+        stat_pattern = VCS_cmds["stat_pattern"]
+        stat_pattern = stat_pattern.replace('{file}', re.escape(filepath))
+
+        added = 0
+        deleted = 0
+        for i in range(len(stats)):
+            m = re.search(stat_pattern, stats[i])
+            if m:
+                added += int(m.group(1))
+                deleted += int(m.group(2))
+
+        tmp_authors = uniq(authors)
+        tmp_authors = [deduplicate_email(a) for a in tmp_authors]
+        tmp_authors = uniq(tmp_authors)
+
+        list_added = []
+        list_deleted = []
+        for author in tmp_authors:
+            auth_added = 0
+            auth_deleted = 0
+            for i in range(len(stats)):
+                if author == deduplicate_email(authors[i]):
+                    m = re.search(stat_pattern, stats[i])
+                    if m:
+                        auth_added += int(m.group(1))
+                        auth_deleted += int(m.group(2))
+            list_added.extend([author] * auth_added)
+            list_deleted.extend([author] * auth_deleted)
+
+        vcs_assign("added_lines", added, list_added)
+        vcs_assign("removed_lines", deleted, list_deleted)
+
+def vcs_file_blame(filepath):
+    global vcs_used
+    vcs_used = vcs_exists()
+    if not vcs_used:
+        return
+
+    all_commits = vcs_blame(filepath)
+    commits = uniq(all_commits)
+    total_commits = len(commits)
+    total_lines = len(all_commits)
+
+    signers = []
+
+    if email_git_blame_signatures:
+        if vcs_is_hg():
+            commit = " -r ".join(commits)
+            cmd = _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], commit=commit)
+            _, commit_signers, _, _ = vcs_find_signers(cmd, filepath)
+            signers.extend(commit_signers)
+        else:
+            for commit in commits:
+                cmd = _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], commit=commit)
+                _, commit_signers, _, _ = vcs_find_signers(cmd, filepath)
+                signers.extend(commit_signers)
+
+    if from_filename:
+        if output_rolestats:
+            blame_signers = []
+            if vcs_is_hg():
+                u_commits = sorted(uniq(commits))
+                commit = " -r ".join(u_commits)
+                cmd = _interpolate_cmd(VCS_cmds["find_commit_author_cmd"], commit=commit)
+                lines = _vcs_exec(cmd)
+                if not email_git_penguin_chiefs:
+                    lines = [l for l in lines if not re.search(penguin_chiefs, l, re.IGNORECASE)]
+                if lines:
+                    author_pattern = VCS_cmds["author_pattern"]
+                    for line in lines:
+                        m = re.search(author_pattern, line)
+                        if m:
+                            author = deduplicate_email(m.group(1))
+                            signers.append(author)
+                    if interactive:
+                        save_commits_by_author(lines)
+                        save_commits_by_signer(lines)
+            else:
+                for commit in commits:
+                    cmd = _interpolate_cmd(VCS_cmds["find_commit_author_cmd"], commit=commit)
+                    author_list = vcs_find_author(cmd)
+                    if not author_list:
+                        continue
+                    formatted_author = deduplicate_email(author_list[0])
+                    cnt = sum(1 for c in all_commits if commit in c)
+                    blame_signers.extend([formatted_author] * cnt)
+
+            if blame_signers:
+                vcs_assign("authored lines", total_lines, blame_signers)
+
+        signers = [deduplicate_email(s) for s in signers]
+        vcs_assign("commits", total_commits, signers)
+    else:
+        signers = [deduplicate_email(s) for s in signers]
+        vcs_assign("modified commits", total_commits, signers)
+
+def vcs_add_commit_signers(commit, desc):
+    if not vcs_exists():
+        return
+
+    cmd = _interpolate_cmd(VCS_cmds["find_commit_signers_cmd"], commit=commit)
+    commit_count, commit_signers, commit_authors, stats = vcs_find_signers(cmd, "")
+
+    for i in range(len(commit_signers)):
+        commit_signers[i] = deduplicate_email(commit_signers[i])
+
+    vcs_assign(desc, 1, commit_signers)
+
+# ---- interactive commit tracking ----
+
+def save_commits_by_author(lines):
+    global commit_author_hash
+    authors = []
+    commits = []
+    subjects = []
+    author_pattern = VCS_cmds["author_pattern"]
+    commit_pattern = VCS_cmds["commit_pattern"]
+    subject_pattern = VCS_cmds["subject_pattern"]
+
+    for line in lines:
+        m = re.search(author_pattern, line)
+        if m:
+            author = deduplicate_email(m.group(1))
+            authors.append(author)
+        m = re.search(commit_pattern, line)
+        if m:
+            commits.append(m.group(1))
+        m = re.search(subject_pattern, line)
+        if m:
+            subjects.append(m.group(1))
+
+    for i in range(len(authors)):
+        if i >= len(commits) or i >= len(subjects):
+            break
+        if authors[i] not in commit_author_hash:
+            commit_author_hash[authors[i]] = []
+        exists = False
+        for ref in commit_author_hash[authors[i]]:
+            if ref[0] == commits[i] and ref[1] == subjects[i]:
+                exists = True
+                break
+        if not exists:
+            commit_author_hash[authors[i]].append([commits[i], subjects[i]])
+
+def save_commits_by_signer(lines):
+    global commit_signer_hash, signature_pattern
+    commit = ""
+    subject = ""
+    commit_pattern = VCS_cmds["commit_pattern"]
+    subject_pattern = VCS_cmds["subject_pattern"]
+
+    for line in lines:
+        m = re.search(commit_pattern, line)
+        if m:
+            commit = m.group(1)
+        m = re.search(subject_pattern, line)
+        if m:
+            subject = m.group(1)
+        if re.search(r'^[ \t]*' + signature_pattern + r'.*@.*$', line):
+            sig_types, sig_signers = extract_formatted_signatures([line])
+            if sig_signers:
+                typ = sig_types[0]
+                signer = deduplicate_email(sig_signers[0])
+                if signer not in commit_signer_hash:
+                    commit_signer_hash[signer] = []
+                exists = False
+                for ref in commit_signer_hash[signer]:
+                    if ref[0] == commit and ref[1] == subject and ref[2] == typ:
+                        exists = True
+                        break
+                if not exists:
+                    commit_signer_hash[signer].append([commit, subject, typ])
+
+# ---- range checks ----
+
+def range_is_maintained(start, end):
+    for i in range(start, end):
+        typ, value = typevalue_parsed[i]
+        if typ == 'S':
+            if re.search(r'maintain|support', value, re.IGNORECASE):
+                return True
+    return False
+
+def range_has_maintainer(start, end):
+    for i in range(start, end):
+        typ, value = typevalue_parsed[i]
+        if typ == 'M':
+            return True
+    return False
+
+# ---- core orchestration ----
+
+def get_maintainers():
+    global email_hash_name, email_hash_address, commit_author_hash, commit_signer_hash
+    global email_to, hash_list_to, list_to, scm_list, web_list, bug_list
+    global subsystem_list, status_list, substatus_list
+    global deduplicate_name_hash, deduplicate_address_hash
+    global signature_pattern, keyword_tvi, file_emails
+
+    email_hash_name = {}
+    email_hash_address = {}
+    commit_author_hash = {}
+    commit_signer_hash = {}
+    email_to = []
+    hash_list_to = {}
+    list_to = []
+    scm_list = []
+    web_list = []
+    bug_list = []
+    subsystem_list = []
+    status_list = []
+    substatus_list = []
+    deduplicate_name_hash = {}
+    deduplicate_address_hash = {}
+
+    if email_git_all_signature_types:
+        signature_pattern = r"(.+?)[Bb][Yy]:"
+    else:
+        signature_pattern = "(" + "|".join(re.escape(t) for t in signature_tags) + ")"
+
+    exact_pattern_match_hash = {}
+
+    for filepath in files:
+        hash_map = {}
+        tvi = find_first_section()
+        while tvi < len(typevalue):
+            start = find_starting_index(tvi)
+            end = find_ending_index(tvi)
+            exclude = False
+
+            # Check excluded patterns
+            for i in range(start, end):
+                typ, value = typevalue_parsed[i]
+                if typ == 'X':
+                    prefix = typevalue_prefix.get(i, '')
+                    if prefix and not filepath.startswith(prefix):
+                        continue
+                    compiled = _get_compiled(i)
+                    if compiled is not None and file_match_pattern(filepath, value, compiled):
+                        exclude = True
+                        break
+
+            if not exclude:
+                for i in range(start, end):
+                    typ, value = typevalue_parsed[i]
+                    if typ is None:
+                        continue
+                    if typ == 'F':
+                        prefix = typevalue_prefix.get(i, '')
+                        if prefix and not filepath.startswith(prefix):
+                            continue
+                        compiled = _get_compiled(i)
+                        if compiled is not None and file_match_pattern(filepath, value, compiled):
+                            value_pd = value.count('/')
+                            file_pd = filepath.count('/')
+                            if not value.endswith('/'):
+                                value_pd += 1
+                            if re.match(r'^(\.\*|\(\?:\.\*\))', value):
+                                value_pd = -1
+                            if value_pd >= file_pd and \
+                               range_is_maintained(start, end) and \
+                               range_has_maintainer(start, end):
+                                exact_pattern_match_hash[filepath] = 1
+                            if pattern_depth == 0 or \
+                               (file_pd - value_pd) < pattern_depth:
+                                hash_map[tvi] = value_pd
+                    elif typ == 'N':
+                        try:
+                            if re.search(value, filepath, re.VERBOSE):
+                                hash_map[tvi] = 0
+                        except re.error:
+                            pass
+
+            tvi = end + 1
+
+        for line_idx in sorted(hash_map.keys(), key=lambda x: hash_map[x], reverse=True):
+            add_categories(line_idx, "")
+            if sections:
+                start = find_starting_index(line_idx)
+                end = find_ending_index(line_idx)
+                for i in range(start, end):
+                    line = typevalue[i]
+                    if re.match(r'^[FX]:', line):
+                        # Restore file patterns
+                        line = re.sub(r'([^\\])\.([^\*])', r'\1?\2', line)
+                        line = re.sub(r'([^\\])\.$', r'\1?', line)
+                        line = line.replace('\\.', '.')
+                        line = line.replace('(?:.*)', '**')
+                        line = line.replace('.*', '*')
+                    m2 = re.match(r'^([A-Z]):(.*)', line)
+                    if m2:
+                        line = "{}:\t{}".format(m2.group(1), m2.group(2))
+                        if letters == "" or re.search(m2.group(1), letters, re.IGNORECASE):
+                            print(line)
+                    else:
+                        # Section header lines are always printed
+                        print(line)
+                print()
+
+        maintainers_in_file(filepath)
+
+    if keywords:
+        kw_tvi = sort_and_uniq(keyword_tvi)
+        for line_idx in kw_tvi:
+            if line_idx in keyword_hash:
+                add_categories(line_idx, ":Keyword:" + keyword_hash[line_idx])
+
+    for em in email_to + list_to:
+        em[0] = deduplicate_email(em[0])
+
+    for filepath in files:
+        if email and \
+           (email_git or
+            (email_git_fallback and
+             not filepath.endswith('MAINTAINERS') and
+             filepath not in exact_pattern_match_hash)):
+            vcs_file_signoffs(filepath)
+        if email and email_git_blame:
+            vcs_file_blame(filepath)
+
+    if email:
+        for chief in penguin_chief:
+            m = re.match(r'^(.*?):(.*)', chief)
+            if m:
+                email_address = format_email(m.group(1), m.group(2), email_usename)
+                if email_git_penguin_chiefs:
+                    email_to.append([email_address, 'chief penguin'])
+                else:
+                    email_to[:] = [e for e in email_to
+                                   if not re.search(re.escape(email_address), e[0])]
+
+        for em in file_emails:
+            em = mailmap_email(em)
+            name, address = parse_email(em)
+            tmp_email = format_email(name, address, email_usename)
+            push_email_address(tmp_email, '')
+            add_role(tmp_email, 'in file')
+
+    for fix in fixes:
+        vcs_add_commit_signers(fix, "blamed_fixes")
+
+    to = []
+    if email or email_list:
+        if email:
+            to.extend(email_to)
+        if email_list:
+            to.extend(list_to)
+
+    if interactive:
+        to = interactive_get_maintainers(to)
+
+    return to
+
+# ---- output functions ----
+
+def merge_email(entries):
+    lines = []
+    saw = {}
+    for entry in entries:
+        address = entry[0]
+        role = entry[1]
+        if address not in saw:
+            if output_roles:
+                lines.append("{} ({})".format(address, role))
+            else:
+                lines.append(address)
+            saw[address] = 1
+    return lines
+
+def output(parms):
+    if output_multiline:
+        for line in parms:
+            print(line)
+    else:
+        print(output_separator.join(parms))
+
+# ---- interactive mode ----
+
+def interactive_get_maintainers(to_list):
+    global interactive, output_rolestats, output_roles, output_substatus
+    global email_git, email_git_fallback, email_git_blame
+    global email_git_blame_signatures, email_git_min_signatures
+    global email_git_max_maintainers, email_git_min_percent
+    global email_git_since, email_hg_since, email_git_all_signature_types
+    global email_file_emails, email_remove_duplicates, email_use_mailmap
+    global keywords, pattern_depth
+
+    vcs_exists()
+
+    selected = {}
+    authored = {}
+    signed = {}
+    count = 0
+    maintained = False
+
+    for entry in to_list:
+        if re.match(r'^(maintainer|supporter)', entry[1], re.IGNORECASE):
+            maintained = True
+        selected[count] = True
+        authored[count] = False
+        signed[count] = False
+        count += 1
+
+    done = False
+    print_options = False
+    redraw = True
+
+    while not done:
+        count = len(to_list)
+        if redraw:
+            sys.stderr.write("\n{:1s} {:2s} {:65s}".format("*", "#", "email/list and role:stats"))
+            if email_git or (email_git_fallback and not maintained) or email_git_blame:
+                sys.stderr.write("auth sign")
+            sys.stderr.write("\n")
+            for idx, entry in enumerate(to_list):
+                em = entry[0]
+                role = entry[1]
+                sel = "*" if selected.get(idx, False) else ""
+                ca = commit_author_hash.get(em, [])
+                cs = commit_signer_hash.get(em, [])
+                auth_count = len(ca)
+                sign_count = len(cs)
+                sys.stderr.write("{:1s} {:2d} {:65s}".format(sel, idx + 1, em))
+                if auth_count > 0 or sign_count > 0:
+                    sys.stderr.write("{:4d} {:4d}".format(auth_count, sign_count))
+                sys.stderr.write("\n     {}\n".format(role))
+                if authored.get(idx, False):
+                    for ref in ca:
+                        sys.stderr.write("     Author: {}\n".format(ref[1]))
+                if signed.get(idx, False):
+                    for ref in cs:
+                        sys.stderr.write("     {}: {}\n".format(ref[2], ref[1]))
+
+        if print_options:
+            print_options = False
+            if vcs_exists():
+                date_ref = email_hg_since if vcs_is_hg() else email_git_since
+                sys.stderr.write("""
+Version Control options:
+g  use git history      [{}]
+gf use git-fallback     [{}]
+b  use git blame        [{}]
+bs use blame signatures [{}]
+c# minimum commits      [{}]
+%# min percent          [{}]
+d# history to use       [{}]
+x# max maintainers      [{}]
+t  all signature types  [{}]
+m  use .mailmap         [{}]
+""".format(email_git, email_git_fallback, email_git_blame,
+           email_git_blame_signatures, email_git_min_signatures,
+           email_git_min_percent, date_ref,
+           email_git_max_maintainers, email_git_all_signature_types,
+           email_use_mailmap))
+
+            sys.stderr.write("""
+Additional options:
+0  toggle all
+tm toggle maintainers
+tg toggle git entries
+tl toggle open list entries
+ts toggle subscriber list entries
+f  emails in file       [{}]
+k  keywords in file     [{}]
+r  remove duplicates    [{}]
+p# pattern match depth  [{}]
+""".format(email_file_emails, keywords, email_remove_duplicates, pattern_depth))
+
+        sys.stderr.write(
+            "\n#(toggle), A#(author), S#(signed) *(all), ^(none), O(options), Y(approve): ")
+
+        try:
+            inp = input()
+        except EOFError:
+            break
+
+        redraw = True
+        rerun = False
+        wishes = re.split(r'[, ]+', inp)
+
+        for nr in wishes:
+            nr = nr.lower().strip()
+            if not nr:
+                continue
+            sel = nr[0]
+            rest = nr[1:]
+            val = 0
+            m = re.match(r'^(\d+)$', rest)
+            if m:
+                val = int(m.group(1))
+
+            if sel == "y":
+                interactive = 0
+                done = True
+                output_rolestats = 0
+                output_roles = 0
+                output_substatus = 0
+                break
+            elif re.match(r'^\d+$', nr):
+                num = int(nr)
+                if 0 < num <= count:
+                    selected[num - 1] = not selected.get(num - 1, False)
+            elif sel in ('*', '^'):
+                toggle = (sel == '*')
+                for i in range(count):
+                    selected[i] = toggle
+            elif sel == '0':
+                for i in range(count):
+                    selected[i] = not selected.get(i, False)
+            elif sel == 't':
+                if rest == 'm':
+                    for i in range(count):
+                        if re.match(r'^(maintainer|supporter)', to_list[i][1], re.IGNORECASE):
+                            selected[i] = not selected.get(i, False)
+                elif rest == 'g':
+                    for i in range(count):
+                        if re.match(r'^(author|commit|signer)', to_list[i][1], re.IGNORECASE):
+                            selected[i] = not selected.get(i, False)
+                elif rest == 'l':
+                    for i in range(count):
+                        if re.match(r'^(open list)', to_list[i][1], re.IGNORECASE):
+                            selected[i] = not selected.get(i, False)
+                elif rest == 's':
+                    for i in range(count):
+                        if re.match(r'^(subscriber list)', to_list[i][1], re.IGNORECASE):
+                            selected[i] = not selected.get(i, False)
+                else:
+                    # 't' alone toggles all signature types
+                    email_git_all_signature_types = not email_git_all_signature_types
+                    rerun = True
+            elif sel == 'a':
+                if val > 0 and val <= count:
+                    authored[val - 1] = not authored.get(val - 1, False)
+                elif rest in ('*', '^'):
+                    toggle = (rest == '*')
+                    for i in range(count):
+                        authored[i] = toggle
+            elif sel == 's':
+                if val > 0 and val <= count:
+                    signed[val - 1] = not signed.get(val - 1, False)
+                elif rest in ('*', '^'):
+                    toggle = (rest == '*')
+                    for i in range(count):
+                        signed[i] = toggle
+            elif sel == 'o':
+                print_options = True
+                redraw = True
+            elif sel == 'g':
+                if rest == 'f':
+                    email_git_fallback = not email_git_fallback
+                else:
+                    email_git = not email_git
+                rerun = True
+            elif sel == 'b':
+                if rest == 's':
+                    email_git_blame_signatures = not email_git_blame_signatures
+                else:
+                    email_git_blame = not email_git_blame
+                rerun = True
+            elif sel == 'c':
+                if val > 0:
+                    email_git_min_signatures = val
+                    rerun = True
+            elif sel == 'x':
+                if val > 0:
+                    email_git_max_maintainers = val
+                    rerun = True
+            elif sel == '%':
+                if rest != "" and val >= 0:
+                    email_git_min_percent = val
+                    rerun = True
+            elif sel == 'd':
+                if vcs_is_git():
+                    email_git_since = rest
+                elif vcs_is_hg():
+                    email_hg_since = rest
+                rerun = True
+            elif sel == 'f':
+                email_file_emails = not email_file_emails
+                rerun = True
+            elif sel == 'r':
+                email_remove_duplicates = not email_remove_duplicates
+                rerun = True
+            elif sel == 'm':
+                email_use_mailmap = not email_use_mailmap
+                read_mailmap()
+                rerun = True
+            elif sel == 'k':
+                keywords = not keywords
+                rerun = True
+            elif sel == 'p':
+                if rest != "" and val >= 0:
+                    pattern_depth = val
+                    rerun = True
+            elif sel in ('h', '?'):
+                sys.stderr.write("""
+Interactive mode allows you to select the various maintainers, submitters,
+commit signers and mailing lists that could be CC'd on a patch.
+
+Any *'d entry is selected.
+
+If you have git or hg installed, you can choose to summarize the commit
+history of files in the patch.  Also, each line of the current file can
+be matched to its commit author and that commits signers with blame.
+
+Various knobs exist to control the length of time for active commit
+tracking, the maximum number of commit authors and signers to add,
+and such.
+
+Enter selections at the prompt until you are satisfied that the selected
+maintainers are appropriate.  You may enter multiple selections separated
+by either commas or spaces.
+
+""")
+            else:
+                sys.stderr.write("invalid option: '{}'\n".format(nr))
+                redraw = False
+
+        if rerun:
+            if email_git_blame:
+                sys.stderr.write("git-blame can be very slow, please have patience...")
+            return get_maintainers()
+
+    # drop not selected entries
+    new_emailto = []
+    for i in range(len(to_list)):
+        if selected.get(i, False):
+            new_emailto.append(to_list[i])
+    return new_emailto
+
+# ---- self-test mode ----
+
+def do_self_test():
+    lsfiles = vcs_list_files(lk_path)
+    good_links = []
+    bad_links = []
+    section_headers = []
+    index = 0
+
+    for x in self_test_info:
+        index += 1
+
+        # Section header duplication and missing section content
+        if (self_test == "" or "sections" in self_test) and \
+           re.match(r'^\S[^:]', x["line"]) and \
+           index < len(self_test_info) and \
+           re.match(r'^([A-Z]):\s*\S', self_test_info[index]["line"]):
+            has_S = False
+            has_F = False
+            has_ML = False
+            sts = ""
+            if any(h.startswith(x["line"]) for h in section_headers):
+                print("{}:{}: warning: duplicate section header\t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+            else:
+                section_headers.append(x["line"])
+            nextline = index
+            while nextline < len(self_test_info):
+                m = re.match(r'^([A-Z]):\s*(\S.*)', self_test_info[nextline]["line"])
+                if not m:
+                    break
+                typ = m.group(1)
+                val = m.group(2)
+                if typ == "S":
+                    has_S = True
+                    sts = val
+                elif typ in ("F", "N"):
+                    has_F = True
+                elif typ in ("M", "R", "L"):
+                    has_ML = True
+                nextline += 1
+            if not has_ML and not re.search(r'orphan|obsolete', sts, re.IGNORECASE):
+                print("{}:{}: warning: section without email address\t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+            if not has_S:
+                print("{}:{}: warning: section without status \t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+            if not has_F:
+                print("{}:{}: warning: section without file pattern\t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+
+        m = _re_type_value.match(x["line"])
+        if not m:
+            continue
+
+        typ = m.group(1)
+        value = m.group(2)
+
+        # Filename pattern matching
+        if typ in ("F", "X") and (self_test == "" or "patterns" in self_test):
+            val = value
+            val = val.replace('.', '\\.')
+            val = val.replace('**', '\x00')
+            val = val.replace('*', '.*')
+            val = val.replace('?', '.')
+            val = val.replace('\x00', '(?:.*)')
+            if os.path.isdir(val):
+                if not val.endswith('/'):
+                    val += '/'
+            try:
+                pat = re.compile(r'^' + val)
+            except re.error:
+                continue
+            if not any(pat.search(f) for f in lsfiles):
+                print("{}:{}: warning: no file matches\t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+
+        # Link reachability
+        elif typ in ("W", "Q", "B") and \
+             re.match(r'^https?:', value) and \
+             (self_test == "" or "links" in self_test):
+            if value in good_links:
+                continue
+            isbad = False
+            if value in bad_links:
+                isbad = True
+            else:
+                ret = subprocess.run(
+                    ["wget", "--spider", "-q", "--no-check-certificate",
+                     "--timeout", "10", "--tries", "1", value],
+                    capture_output=True)
+                if ret.returncode == 0:
+                    good_links.append(value)
+                else:
+                    bad_links.append(value)
+                    isbad = True
+            if isbad:
+                print("{}:{}: warning: possible bad link\t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+
+        # SCM reachability
+        elif typ == "T" and (self_test == "" or "scm" in self_test):
+            if value in good_links:
+                continue
+            isbad = False
+            if value in bad_links:
+                isbad = True
+            elif not re.match(r'^(?:git|quilt|hg)\s+\S', value):
+                print("{}:{}: warning: malformed entry\t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+            else:
+                m2 = re.match(r'^git\s+(\S+)(\s+([^\(]+\S+))?', value)
+                if m2:
+                    url = m2.group(1)
+                    branch = m2.group(3) if m2.group(3) else ""
+                    ret = subprocess.run(
+                        'git ls-remote --exit-code -h "{}" {} > /dev/null 2>&1'.format(url, branch),
+                        shell=True, capture_output=True)
+                    if ret.returncode == 0:
+                        good_links.append(value)
+                    else:
+                        bad_links.append(value)
+                        isbad = True
+                else:
+                    m2 = re.match(r'^(?:quilt|hg)\s+(https?:\S+)', value)
+                    if m2:
+                        url = m2.group(1)
+                        ret = subprocess.run(
+                            ["wget", "--spider", "-q", "--no-check-certificate",
+                             "--timeout", "10", "--tries", "1", url],
+                            capture_output=True)
+                        if ret.returncode == 0:
+                            good_links.append(value)
+                        else:
+                            bad_links.append(value)
+                            isbad = True
+            if isbad:
+                print("{}:{}: warning: possible bad link\t{}".format(
+                    x["file"], x["linenr"], x["line"]))
+
+# ---- usage ----
+
+def usage():
+    print("""usage: {} [options] patchfile
+       {} [options] -f file|directory
+version: {}
+
+MAINTAINER field selection options:
+  --email => print email address(es) if any
+    --git => include recent git *-by: signers
+    --git-all-signature-types => include signers regardless of signature type
+        or use only {} signers (default: {})
+    --git-fallback => use git when no exact MAINTAINERS pattern (default: {})
+    --git-chief-penguins => include {}
+    --git-min-signatures => number of signatures required (default: {})
+    --git-max-maintainers => maximum maintainers to add (default: {})
+    --git-min-percent => minimum percentage of commits required (default: {})
+    --git-blame => use git blame to find modified commits for patch or file
+    --git-blame-signatures => when used with --git-blame, also include all commit signers
+    --git-since => git history to use (default: {})
+    --hg-since => hg history to use (default: {})
+    --interactive => display a menu (mostly useful if used with the --git option)
+    --m => include maintainer(s) if any
+    --r => include reviewer(s) if any
+    --n => include name 'Full Name <addr@domain.tld>'
+    --l => include list(s) if any
+    --moderated => include moderated lists(s) if any (default: true)
+    --s => include subscriber only list(s) if any (default: false)
+    --remove-duplicates => minimize duplicate email names/addresses
+    --roles => show roles (role:subsystem, git-signer, list, etc...)
+    --rolestats => show roles and statistics (commits/total_commits, %)
+    --substatus => show subsystem status if not Maintained (default: match --roles when output is tty)"
+    --file-emails => add email addresses found in -f file (default: 0 (off))
+    --fixes => for patches, add signatures of commits with 'Fixes: <commit>' (default: 1 (on))
+  --scm => print SCM tree(s) if any
+  --status => print status if any
+  --subsystem => print subsystem name if any
+  --web => print website(s) if any
+  --bug => print bug reporting info if any
+
+Output type options:
+  --separator [, ] => separator for multiple entries on 1 line
+    using --separator also sets --nomultiline if --separator is not [, ]
+  --multiline => print 1 entry per line
+
+Other options:
+  --pattern-depth => Number of pattern directory traversals (default: 0 (all))
+  --keywords => scan patch for keywords (default: {})
+  --keywords-in-file => scan file for keywords (default: {})
+  --sections => print all of the subsystem sections with pattern matches
+  --letters => print all matching 'letter' types from all matching sections
+  --mailmap => use .mailmap file (default: {})
+  --no-tree => run without a kernel tree
+  --self-test => show potential issues with MAINTAINERS file content
+  --version => show version
+  --help => show this help information
+
+Default options:
+  [--email --tree --nogit --git-fallback --m --r --n --l --multiline
+   --pattern-depth=0 --remove-duplicates --rolestats --keywords]
+
+Notes:
+  Using "-f directory" may give unexpected results:
+      Used with "--git", git signators for _all_ files in and below
+          directory are examined as git recurses directories.
+          Any specified X: (exclude) pattern matches are _not_ ignored.
+      Used with "--nogit", directory is used as a pattern match,
+          no individual file within the directory or subdirectory
+          is matched.
+      Used with "--git-blame", does not iterate all files in directory
+  Using "--git-blame" is slow and may add old committers and authors
+      that are no longer active maintainers to the output.
+  Using "--roles" or "--rolestats" with git send-email --cc-cmd or any
+      other automated tools that expect only ["name"] <email address>
+      may not work because of additional output after <email address>.
+  Using "--rolestats" and "--git-blame" shows the #/total=% commits,
+      not the percentage of the entire file authored.  # of commits is
+      not a good measure of amount of code authored.  1 major commit may
+      contain a thousand lines, 5 trivial commits may modify a single line.
+  If git is not installed, but mercurial (hg) is installed and an .hg
+      repository exists, the following options apply to mercurial:
+          --git,
+          --git-min-signatures, --git-max-maintainers, --git-min-percent, and
+          --git-blame
+      Use --hg-since not --git-since to control date selection
+  File ".get_maintainer.conf", if it exists in the linux kernel source root
+      directory, can change whatever get_maintainer defaults are desired.
+      Entries in this file can be any command line argument.
+      This file is prepended to any additional command line arguments.
+      Multiple lines and # comments are allowed.
+  Most options have both positive and negative forms.
+      The negative forms for --<foo> are --no<foo> and --no-<foo>.
+""".format(P, P, V, signature_pattern, email_git_all_signature_types,
+           email_git_fallback, penguin_chiefs,
+           email_git_min_signatures, email_git_max_maintainers,
+           email_git_min_percent, email_git_since, email_hg_since,
+           keywords, keywords_in_file, email_use_mailmap))
+
+# ---- argument parsing helpers ----
+
+def add_negatable_flag(parser, name, dest, default, short=None):
+    """Add --foo/--nofoo/--no-foo flags (Perl Getopt::Long style negation)."""
+    names = ['--' + name]
+    if short:
+        names.insert(0, short)
+    parser.add_argument(*names, dest=dest, action='store_true', default=default)
+    parser.add_argument('--no' + name, '--no-' + name,
+                        dest=dest, action='store_false')
+
+# ---- main ----
+
+def main():
+    global P, cur_path, lk_path
+    global email, email_usename, email_maintainer, email_reviewer, email_fixes
+    global email_list, email_moderated_list, email_subscriber_list
+    global email_git_penguin_chiefs, email_git, email_git_all_signature_types
+    global email_git_blame, email_git_blame_signatures, email_git_fallback
+    global email_git_min_signatures, email_git_max_maintainers, email_git_min_percent
+    global email_git_since, email_hg_since, interactive
+    global email_remove_duplicates, email_use_mailmap
+    global output_multiline, output_separator, output_roles, output_rolestats
+    global output_substatus, output_section_maxlen
+    global scm, tree, web, bug, subsystem_opt, status_opt
+    global letters, keywords, keywords_in_file, sections
+    global email_file_emails, from_filename, pattern_depth
+    global self_test, version, find_maintainer_files, maintainer_path
+    global files, fixes, range_list, keyword_tvi, file_emails
+    global ignore_emails
+
+    P = sys.argv[0]
+    cur_path = os.getcwd() + '/'
+
+    # Load config file
+    conf = which_conf(".get_maintainer.conf")
+    conf_args = []
+    if conf and os.path.isfile(conf):
+        try:
+            with open(conf, 'r', encoding='utf-8') as f:
+                for line in f:
+                    line = line.rstrip('\n').strip()
+                    line = re.sub(r'\s+', ' ', line)
+                    if not line or line.startswith('#'):
+                        continue
+                    for word in line.split():
+                        if word.startswith('#'):
+                            break
+                        conf_args.append(word)
+        except IOError:
+            print("{}: Can't find a readable .get_maintainer.conf file".format(P), file=sys.stderr)
+
+    # Load ignore file
+    ignore_file = which_conf(".get_maintainer.ignore")
+    if ignore_file and os.path.isfile(ignore_file):
+        try:
+            with open(ignore_file, 'r', encoding='utf-8') as f:
+                for line in f:
+                    line = line.strip()
+                    line = re.sub(r'#.*$', '', line).strip()
+                    if not line:
+                        continue
+                    if rfc822_valid(line):
+                        ignore_emails.append(line)
+        except IOError:
+            pass
+
+    # Check for --self-test exclusivity
+    all_args = conf_args + sys.argv[1:]
+    if len(all_args) > 1:
+        for arg in all_args:
+            if re.match(r'^-{1,2}self-test', arg):
+                print("{}: using --self-test does not allow any other option or argument".format(P),
+                      file=sys.stderr)
+                sys.exit(1)
+
+    # Build argument parser
+    parser = argparse.ArgumentParser(add_help=False)
+
+    add_negatable_flag(parser, 'email', 'email', True)
+    add_negatable_flag(parser, 'git', 'email_git', False)
+    add_negatable_flag(parser, 'git-all-signature-types', 'email_git_all_signature_types', False)
+    add_negatable_flag(parser, 'git-blame', 'email_git_blame', False)
+    add_negatable_flag(parser, 'git-blame-signatures', 'email_git_blame_signatures', True)
+    add_negatable_flag(parser, 'git-fallback', 'email_git_fallback', True)
+    add_negatable_flag(parser, 'git-chief-penguins', 'email_git_penguin_chiefs', False)
+    parser.add_argument('--git-min-signatures', type=int, default=1)
+    parser.add_argument('--git-max-maintainers', type=int, default=5)
+    parser.add_argument('--git-min-percent', type=int, default=5)
+    parser.add_argument('--git-since', default="1-year-ago")
+    parser.add_argument('--hg-since', default="-365")
+    add_negatable_flag(parser, 'interactive', 'interactive', False, short='-i')
+    add_negatable_flag(parser, 'remove-duplicates', 'email_remove_duplicates', True)
+    add_negatable_flag(parser, 'mailmap', 'email_use_mailmap', True)
+    add_negatable_flag(parser, 'm', 'email_maintainer', True)
+    add_negatable_flag(parser, 'r', 'email_reviewer', True)
+    add_negatable_flag(parser, 'n', 'email_usename', True)
+    add_negatable_flag(parser, 'l', 'email_list', True)
+    add_negatable_flag(parser, 'fixes', 'email_fixes', True)
+    add_negatable_flag(parser, 'moderated', 'email_moderated_list', True)
+    add_negatable_flag(parser, 's', 'email_subscriber_list', False)
+    add_negatable_flag(parser, 'multiline', 'output_multiline', True)
+    add_negatable_flag(parser, 'roles', 'output_roles', False)
+    add_negatable_flag(parser, 'rolestats', 'output_rolestats', True)
+    parser.add_argument('--separator', default=", ")
+    add_negatable_flag(parser, 'subsystem', 'subsystem', False)
+    add_negatable_flag(parser, 'status', 'status', False)
+    add_negatable_flag(parser, 'substatus', 'output_substatus_flag', None)
+    add_negatable_flag(parser, 'scm', 'scm', False)
+    add_negatable_flag(parser, 'tree', 'tree', True)
+    add_negatable_flag(parser, 'web', 'web', False)
+    add_negatable_flag(parser, 'bug', 'bug', False)
+    parser.add_argument('--letters', default="")
+    parser.add_argument('--pattern-depth', type=int, default=0)
+    add_negatable_flag(parser, 'keywords', 'keywords', True, short='-k')
+    add_negatable_flag(parser, 'keywords-in-file', 'keywords_in_file', False)
+    add_negatable_flag(parser, 'sections', 'sections', False)
+    add_negatable_flag(parser, 'file-emails', 'email_file_emails', False)
+    parser.add_argument('-f', '--file', dest='from_filename', action='store_true', default=False)
+    parser.add_argument('--find-maintainer-files', action='store_true', default=False)
+    parser.add_argument('--mpath', '--maintainer-path', dest='maintainer_path', default=None)
+    parser.add_argument('--self-test', dest='self_test', nargs='?', const='', default=None)
+    parser.add_argument('-v', '--version', dest='show_version', action='store_true', default=False)
+    parser.add_argument('-h', '--help', '--usage', dest='show_help', action='store_true', default=False)
+    parser.add_argument('files', nargs='*')
+
+    # Handle negatable --fe alias
+    parser.add_argument('--fe', dest='email_file_emails', action='store_true')
+    parser.add_argument('--kf', dest='keywords_in_file', action='store_true')
+
+    try:
+        args = parser.parse_args(conf_args + sys.argv[1:])
+    except SystemExit:
+        sys.exit(1)
+
+    if args.show_help:
+        usage()
+        sys.exit(0)
+
+    if args.show_version:
+        print("{} {}".format(P, V))
+        sys.exit(0)
+
+    # Apply parsed args to globals
+    email = 1 if args.email else 0
+    email_git = 1 if args.email_git else 0
+    email_git_all_signature_types = 1 if args.email_git_all_signature_types else 0
+    email_git_blame = 1 if args.email_git_blame else 0
+    email_git_blame_signatures = 1 if args.email_git_blame_signatures else 0
+    email_git_fallback = 1 if args.email_git_fallback else 0
+    email_git_penguin_chiefs = 1 if args.email_git_penguin_chiefs else 0
+    email_git_min_signatures = args.git_min_signatures
+    email_git_max_maintainers = args.git_max_maintainers
+    email_git_min_percent = args.git_min_percent
+    email_git_since = args.git_since
+    email_hg_since = args.hg_since
+    interactive = 1 if args.interactive else 0
+    email_remove_duplicates = 1 if args.email_remove_duplicates else 0
+    email_use_mailmap = 1 if args.email_use_mailmap else 0
+    email_maintainer = 1 if args.email_maintainer else 0
+    email_reviewer = 1 if args.email_reviewer else 0
+    email_usename = 1 if args.email_usename else 0
+    email_list = 1 if args.email_list else 0
+    email_fixes = 1 if args.email_fixes else 0
+    email_moderated_list = 1 if args.email_moderated_list else 0
+    email_subscriber_list = 1 if args.email_subscriber_list else 0
+    output_multiline = 1 if args.output_multiline else 0
+    output_roles = 1 if args.output_roles else 0
+    output_rolestats = 1 if args.output_rolestats else 0
+    output_separator = args.separator
+    subsystem_opt = 1 if args.subsystem else 0
+    status_opt = 1 if args.status else 0
+    scm = 1 if args.scm else 0
+    tree = 1 if args.tree else 0
+    web = 1 if args.web else 0
+    bug = 1 if args.bug else 0
+    letters = args.letters
+    pattern_depth = args.pattern_depth
+    keywords = 1 if args.keywords else 0
+    keywords_in_file = 1 if args.keywords_in_file else 0
+    sections = 1 if args.sections else 0
+    email_file_emails = 1 if args.email_file_emails else 0
+    from_filename = 1 if args.from_filename else 0
+    find_maintainer_files = 1 if args.find_maintainer_files else 0
+    maintainer_path = args.maintainer_path
+    self_test = args.self_test
+
+    # Handle substatus special logic
+    if args.output_substatus_flag is None:
+        output_substatus = None
+    else:
+        output_substatus = 1 if args.output_substatus_flag else 0
+
+    if self_test is not None:
+        read_all_maintainer_files()
+        do_self_test()
+        sys.exit(0)
+
+    if output_separator != ", ":
+        output_multiline = 0
+    if interactive:
+        output_rolestats = 1
+    if output_rolestats:
+        output_roles = 1
+
+    if output_substatus is None:
+        output_substatus = 1 if (email and output_roles and sys.stdout.isatty()) else 0
+
+    if sections or letters != "":
+        sections = 1
+        email = 0
+        email_list = 0
+        scm = 0
+        status_opt = 0
+        subsystem_opt = 0
+        web = 0
+        bug = 0
+        keywords = 0
+        keywords_in_file = 0
+        interactive = 0
+    else:
+        selections = email + scm + status_opt + subsystem_opt + web + bug
+        if selections == 0:
+            print("{}: Missing required option: email, scm, status, subsystem, web or bug".format(P),
+                  file=sys.stderr)
+            sys.exit(1)
+
+    if email and \
+       (email_maintainer + email_reviewer +
+        email_list + email_subscriber_list +
+        email_git + email_git_penguin_chiefs + email_git_blame) == 0:
+        print("{}: Please select at least 1 email option".format(P), file=sys.stderr)
+        sys.exit(1)
+
+    if tree and not top_of_kernel_tree(lk_path):
+        print("{}: The current directory does not appear to be "
+              "a linux kernel source tree.".format(P), file=sys.stderr)
+        sys.exit(1)
+
+    # Read MAINTAINERS
+    read_all_maintainer_files()
+
+    # Read mailmap
+    read_mailmap()
+
+    # Process input files
+    input_files = args.files if args.files else []
+    if not input_files and not sys.stdin.isatty():
+        input_files = ["&STDIN"]
+    elif not input_files:
+        print("{}: missing patchfile or -f file - use --help if necessary".format(P),
+              file=sys.stderr)
+        sys.exit(1)
+
+    for filepath in input_files:
+        if filepath != "&STDIN":
+            filepath = os.path.normpath(filepath)
+            if os.path.isdir(filepath):
+                if not filepath.endswith('/'):
+                    filepath += '/'
+            elif not os.path.isfile(filepath):
+                print("{}: file '{}' not found".format(P, filepath), file=sys.stderr)
+                sys.exit(1)
+
+        file_in_vcs = None
+        if from_filename and vcs_exists():
+            file_in_vcs = vcs_file_exists(filepath)
+            if not file_in_vcs:
+                print("{}: file '{}' not found in version control".format(P, filepath), file=sys.stderr)
+
+        if from_filename or (filepath != "&STDIN" and
+                             (file_in_vcs if file_in_vcs is not None else vcs_file_exists(filepath))):
+            # strip absolute or lk_path prefix
+            if filepath.startswith(cur_path):
+                filepath = filepath[len(cur_path):]
+            if filepath.startswith(lk_path) and lk_path != "./":
+                filepath = filepath[len(lk_path):]
+            files.append(filepath)
+            if filepath != "MAINTAINERS" and os.path.isfile(filepath) and keywords and keywords_in_file:
+                try:
+                    with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
+                        text = f.read()
+                    for line_idx in keyword_hash:
+                        try:
+                            if re.search(keyword_hash[line_idx], text, re.VERBOSE):
+                                keyword_tvi.append(line_idx)
+                        except re.error:
+                            pass
+                except IOError:
+                    pass
+        else:
+            file_cnt = len(files)
+            lastfile = None
+
+            if filepath == "&STDIN":
+                patch = sys.stdin
+            else:
+                try:
+                    patch = open(filepath, 'r', encoding='utf-8', errors='replace')
+                except IOError:
+                    print("{}: Can't open {}: ".format(P, filepath), file=sys.stderr)
+                    sys.exit(1)
+
+            patch_prefix = ""  # Parsing the intro
+
+            for patch_line in patch:
+                m = re.match(r'^ mode change [0-7]+ => [0-7]+ (\S+)\s*$', patch_line)
+                if m:
+                    files.append(m.group(1))
+                    continue
+                m = re.match(r'^rename (?:from|to) (\S+)\s*$', patch_line)
+                if m:
+                    files.append(m.group(1))
+                    continue
+                m = re.match(r'^diff --git a/(\S+) b/(\S+)\s*$', patch_line)
+                if m:
+                    files.append(m.group(1))
+                    files.append(m.group(2))
+                    continue
+                m = re.match(r'^Fixes:\s+([0-9a-fA-F]{6,40})', patch_line)
+                if m:
+                    if email_fixes:
+                        fixes.append(m.group(1))
+                    continue
+                m = re.match(r'^\+\+\+\s+(\S+)', patch_line) or \
+                    re.match(r'^---\s+(\S+)', patch_line)
+                if m:
+                    filename = m.group(1)
+                    filename = re.sub(r'^[^/]*/', '', filename)
+                    filename = filename.rstrip('\n')
+                    lastfile = filename
+                    files.append(filename)
+                    patch_prefix = "^[+-].*"
+                    continue
+                m = re.match(r'^@@ -(\d+),(\d+)', patch_line)
+                if m:
+                    if email_git_blame and lastfile:
+                        range_list.append("{}:{}:{}".format(lastfile, m.group(1), m.group(2)))
+                    continue
+                if keywords:
+                    for line_idx in keyword_hash:
+                        try:
+                            if re.search(patch_prefix + keyword_hash[line_idx],
+                                         patch_line, re.VERBOSE):
+                                keyword_tvi.append(line_idx)
+                        except re.error:
+                            pass
+
+            if filepath != "&STDIN":
+                patch.close()
+
+            if file_cnt == len(files):
+                print("{}: file '{}' doesn't appear to be a patch.  "
+                      "Add -f to options?".format(P, filepath), file=sys.stderr)
+
+            files[:] = sort_and_uniq(files)
+
+    file_emails[:] = uniq(file_emails)
+    fixes[:] = uniq(fixes)
+
+    maintainers = get_maintainers()
+    if maintainers:
+        maintainers = merge_email(maintainers)
+        output(maintainers)
+
+    if scm:
+        scm_out = uniq(scm_list)
+        output(scm_out)
+
+    if output_substatus:
+        ss = uniq(substatus_list)
+        output(ss)
+
+    if status_opt:
+        st = uniq(status_list)
+        output(st)
+
+    if subsystem_opt:
+        ss = uniq(subsystem_list)
+        output(ss)
+
+    if web:
+        w = uniq(web_list)
+        output(w)
+
+    if bug:
+        b = uniq(bug_list)
+        output(b)
+
+if __name__ == "__main__":
+    main()
-- 
2.52.0