Source code for skaff.manualtools

#!/usr/bin/env python3

"""
A suite of manual-page processing tools.
"""

# ------------------------------- MODULE INFO ---------------------------------
__all__ = [
    "manual_check",
    "manuals_install",
    "manuals_probe",
    "manpath_select"
]
# ------------------------------- MODULE INFO ---------------------------------

# --------------------------------- MODULES -----------------------------------
import gzip
import os
import shutil
import subprocess
import sys

from filecmp import dircmp
from tempfile import TemporaryDirectory
from typing import (
    List,
    Union
)
# --------------------------------- MODULES -----------------------------------


# -------------------------------- FUNCTIONS ----------------------------------
[docs]def manual_check(manual: str) -> bool: """ Checks whether 'manual' actually is a valid unix man-page format manual; returns boolean value True if so, False otherwise. Files that are considered to be unix manual page format must end with file extensions that are solely consists of digits 1 to 9 excluding 0. NOTE: Man-page section 9 is a non-POSIX standard (convention adopted by both Linux and FreeBSD) commonly used for documenting "Kernel Routines". """ if not isinstance(manual, str): raise TypeError("'manual' argument must be of 'str' type") # The "lazy evaluation" property of the builtin 'all' function ensures that # the second lambda would NEVER be evaluated just in case 'x' is a string # of zero length qualifiers = [ lambda x: True if 2 == len(x) else False, lambda x: True if x[-1] in map(str, range(1, 10)) else False] file_extension = os.path.splitext(manual)[-1] if all(qualifier(file_extension) for qualifier in qualifiers): return True else: return False
[docs]def manuals_install(directory: str, rebuild: bool=True, *manuals: str) -> None: """ Installs the gzipped manual page(s) in 'manuals' to the subdirectory of 'directory' that ends with an extra manual section number if it exists; for example, if one of the basename of manual in 'manuals' is named 'git.1' and a subdirectory '/usr/share/man/man1/' exists, it will be installed to that subdirectory instead; otherwise falls back to '/usr/share/man/'. If 'rebuild' is set to True, also invokes the 'mandb' program to rebuild the manual page index cache. If '--record' flag exists in sys.argv, writes the list of installed manual pages to the file following the '--record' flag (setuptools compatibility). NOTE: It is callers' responsibility to ensure the proper write permission is satisfied for the 'directory' (affected by the real UID of the current process) and all the subdirectories of it that ends with an extra manual section number (see the example above); both 'directory' and all the 'manuals' must already exist. """ if 0 == len(manuals): return if not directory.endswith(os.sep): directory += os.sep if not os.path.isdir(directory): raise NotADirectoryError("{0} is not a directory".format(directory)) if not os.access(directory, os.W_OK): raise PermissionError(("The real UID of the current process does not " " permit write operation for " "directory {0}".format(directory))) if "--record" in sys.argv: with open(sys.argv[sys.argv.index("--record") + 1], "a") as log_output: _manuals_copy(directory, log_output, *manuals) else: _manuals_copy(directory, None, *manuals) if rebuild: # Finally rebuild the manual page index cache with open(os.devnull, "w") as dump: return_code = subprocess.call(["mandb"], stdout=dump, stderr=dump) if 0 != return_code: raise RuntimeError("'mandb' program does not exit properly")
def _manuals_copy(directory, log=None, *manuals): """ Copies the gzipped manual page(s) in 'manuals' to the subdirectory of 'directory' that ends with an extra manual section number if it exists; for example, if one of the basename of manual in 'manuals' is named 'git.1' and a subdirectory '/usr/share/man/man1/' exists, it will be installed to that subdirectory instead; otherwise falls back to '/usr/share/man/'. Also invokes the 'write' method of 'log' to append the file names of gzipped manual page(s) copied if 'log' is not None. """ # Based on example from # https://docs.python.org/3/library/gzip.html for manual in manuals: if not manual_check(manual): raise TypeError(("The manual page {0} ".format(manual) + "is not in unix 'manpage' format")) if not os.path.isfile(manual): raise FileNotFoundError(("The manual page {0} ".format(manual) + "does not exist")) # Test whether there exists extra manual subdirectory ends with # manual section number; for example, # '/usr/share/man/man1/' # if so, use this subdirectory instead; otherwise fall back to # '/usr/share/man/' path_postfix = os.path.splitext(manual)[-1][-1] man_subdir = directory + "man" + path_postfix + os.sep if os.path.isdir(man_subdir) and os.access(man_subdir, os.W_OK): target_manpage = man_subdir + os.path.basename(manual) + ".gz" else: target_manpage = directory + os.path.basename(manual) + ".gz" with open(manual, "rb") as input_manpage: with gzip.open(target_manpage, "wb") as output_manpage: shutil.copyfileobj(input_manpage, output_manpage) if log: log.write(target_manpage)
[docs]def manuals_probe(*directories: str) -> List[str]: """ Probes all directory specified in 'directories' and returns a sorted list containing all the manual page(s) found. All the manual page(s) returned are prefixed by absolute path(s). NOTE: This function does not recurse more than one level deep into the 'directories' specified; the order in the result does not necessarily correspond to the order the directory appears in 'directories', the results are sorted by the builtin 'sorted' function. """ result_manuals = set() for directory in directories: if not os.path.isdir(directory): raise NotADirectoryError("Directory '{0}' does not exist".format( directory)) if not os.path.isabs(directory): directory = os.path.abspath(directory) if not directory.endswith(os.sep): directory += os.sep # Here only 'file_list' is used; an alternative would be 'os.listdir'; # however, that function is not able to distinguish between files and # directories, so an extra filter is required, which is less efficient for directory_name, subdir_list, file_list in os.walk(directory): for candidate in file_list: if manual_check(candidate): result_manuals.add(directory + candidate) break return sorted(result_manuals)
[docs]def manpath_select(select: bool=True) -> Union[str, List[str]]: """ Parses the output of the 'manpath' program and returns one of its non-empty results (non-empty directory) if 'select' is set to True; otherwise returns all the results un-altered. NOTE: A platform-dependent path separator will be appended to the result. """ paths = None result_manpath = None with os.popen("manpath") as proc, TemporaryDirectory() as tmpdir: paths = proc.read().strip().split(os.pathsep) if select: for candidate in paths: # "Elect" the candidate directory with "rich" non-empty status if dircmp(candidate, tmpdir).left_only: result_manpath = candidate break if not paths: raise RuntimeError("Output of the 'manpath' program cannot be parsed") if select and not result_manpath: raise RuntimeError("All the directories in 'manpath' is empty") if select: if result_manpath.endswith(os.sep): return result_manpath else: return result_manpath + os.sep else: return [path + os.sep for path in paths if not path.endswith(os.sep)]
# -------------------------------- FUNCTIONS ----------------------------------