github.com/containers/podman/v5@v5.1.0-rc1/hack/markdown-preprocess (about)

     1  #!/usr/bin/env python3
     2  #
     3  # markdown-preprocess - filter *.md.in files, convert to .md
     4  #
     5  """
     6  Simpleminded include mechanism for podman man pages.
     7  """
     8  
     9  import filecmp
    10  import glob
    11  import os
    12  import re
    13  import sys
    14  
    15  class Preprocessor():
    16      """
    17      Doesn't really merit a whole OO approach, except we have a lot
    18      of state variables to pass around, and self is a convenient
    19      way to do that. Better than globals, anyway.
    20      """
    21      def __init__(self):
    22          self.infile = ''
    23          self.pod_or_container = ''
    24          self.used_by = {}
    25  
    26      def process(self, infile:str):
    27          """
    28          Main calling point: preprocesses one file
    29          """
    30          self.infile = infile
    31          # Some options are the same between containers and pods; determine
    32          # which description to use from the name of the source man page.
    33          self.pod_or_container = 'container'
    34          if '-pod-' in infile or '-kube-' in infile:
    35              self.pod_or_container = 'pod'
    36  
    37          # foo.md.in -> foo.md -- but always write to a tmpfile
    38          outfile = os.path.splitext(infile)[0]
    39          outfile_tmp = outfile + '.tmp.' + str(os.getpid())
    40  
    41          with open(infile, 'r', encoding='utf-8') as fh_in, open(outfile_tmp, 'w', encoding='utf-8', newline='\n') as fh_out:
    42              for line in fh_in:
    43                  # '@@option foo' -> include file options/foo.md
    44                  if line.startswith('@@option '):
    45                      _, optionname = line.strip().split(" ")
    46                      optionfile = os.path.join("options", optionname + '.md')
    47                      self.track_optionfile(optionfile)
    48                      self.insert_file(fh_out, optionfile)
    49                  # '@@include relative-path/must-exist.md'
    50                  elif line.startswith('@@include '):
    51                      _, path = line.strip().split(" ")
    52                      self.insert_file(fh_out, path)
    53                  else:
    54                      fh_out.write(line)
    55  
    56          os.chmod(outfile_tmp, 0o444)
    57          os.rename(outfile_tmp, outfile)
    58  
    59      def track_optionfile(self, optionfile: str):
    60          """
    61          Keep track of which man pages use which option files
    62          """
    63          if optionfile not in self.used_by:
    64              self.used_by[optionfile] = []
    65          self.used_by[optionfile].append(self.podman_subcommand('full'))
    66  
    67      def rewrite_optionfiles(self):
    68          """
    69          Rewrite all option files, such that they include header comments
    70          cross-referencing all the man pages in which they're used.
    71          """
    72          for optionfile in self.used_by:
    73              tmpfile = optionfile + '.tmp.' + str(os.getpid())
    74              with open(optionfile, 'r', encoding='utf-8') as fh_in, open(tmpfile, 'w', encoding='utf-8', newline='\n') as fh_out:
    75                  fh_out.write("####> This option file is used in:\n")
    76                  used_by = ', '.join(x for x in self.used_by[optionfile])
    77                  fh_out.write(f"####>   podman {used_by}\n")
    78                  fh_out.write("####> If file is edited, make sure the changes\n")
    79                  fh_out.write("####> are applicable to all of those.\n")
    80                  for line in fh_in:
    81                      if not line.startswith('####>'):
    82                          fh_out.write(line)
    83              # Compare files; only rewrite if the new one differs
    84              if not filecmp.cmp(optionfile, tmpfile):
    85                  os.unlink(optionfile)
    86                  os.rename(tmpfile, optionfile)
    87              else:
    88                  os.unlink(tmpfile)
    89  
    90      def insert_file(self, fh_out, path: str):
    91          """
    92          Reads one option file, writes it out to the given output filehandle
    93          """
    94          # Comment intended to help someone viewing the .md file.
    95          # Leading newline is important because if two lines are
    96          # consecutive without a break, sphinx (but not go-md2man)
    97          # treats them as one line and will unwantedly render the
    98          # comment in its output.
    99          fh_out.write("\n[//]: # (BEGIN included file " + path + ")\n")
   100          with open(path, 'r', encoding='utf-8') as fh_included:
   101              for opt_line in fh_included:
   102                  if opt_line.startswith('####>'):
   103                      continue
   104                  opt_line = self.replace_type(opt_line)
   105                  opt_line = opt_line.replace('<<subcommand>>', self.podman_subcommand())
   106                  opt_line = opt_line.replace('<<fullsubcommand>>', self.podman_subcommand('full'))
   107                  fh_out.write(opt_line)
   108              fh_out.write("\n[//]: # (END   included file " + path + ")\n")
   109  
   110      def podman_subcommand(self, full=None) -> str:
   111          """
   112          Returns the string form of the podman command, based on man page name;
   113          e.g., 'foo bar' for podman-foo-bar.1.md.in
   114          """
   115          subcommand = self.infile
   116          # Special case: 'podman-pod-start' becomes just 'start'
   117          if not full:
   118              if subcommand.startswith("podman-pod-"):
   119                  subcommand = subcommand[len("podman-pod-"):]
   120          if subcommand.startswith("podman-"):
   121              subcommand = subcommand[len("podman-"):]
   122          if subcommand.endswith(".1.md.in"):
   123              subcommand = subcommand[:-len(".1.md.in")]
   124          return subcommand.replace("-", " ")
   125  
   126      def replace_type(self, line: str) -> str:
   127          """
   128          Replace instances of '<<pod string|container string>>' with the
   129          appropriate one based on whether this is a pod-related man page
   130          or not.
   131          """
   132          # Internal helper function: determines the desired half of the <a|b> string
   133          def replwith(matchobj):
   134              lhs, rhs = matchobj[0].split('|')
   135              # Strip off '<<' and '>>'
   136              lhs = lhs[2:]
   137              rhs = rhs[:len(rhs)-2]
   138  
   139              # Check both sides for 'pod' followed by (non-"m" or end-of-string).
   140              # The non-m prevents us from triggering on 'podman', which could
   141              # conceivably be present in both sides. And we check for 'pod',
   142              # not 'container', because it's possible to have something like
   143              # <<container in pod|container>>.
   144              if re.match('.*pod([^m]|$)', lhs, re.IGNORECASE):
   145                  if re.match('.*pod([^m]|$)', rhs, re.IGNORECASE):
   146                      raise Exception(f"'{matchobj[0]}' matches 'pod' in both left and right sides")
   147                  # Only left-hand side has "pod"
   148                  if self.pod_or_container == 'pod':
   149                      return lhs
   150                  return rhs
   151  
   152              # 'pod' not in lhs, must be in rhs
   153              if not re.match('.*pod([^m]|$)', rhs, re.IGNORECASE):
   154                  raise Exception(f"'{matchobj[0]}' does not match 'pod' in either side")
   155              if self.pod_or_container == 'pod':
   156                  return rhs
   157              return lhs
   158  
   159          return re.sub(r'<<[^\|>]*\|[^\|>]*>>', replwith, line)
   160  
   161  
   162  def main():
   163      "script entry point"
   164      script_dir = os.path.abspath(os.path.dirname(__file__))
   165      man_dir = os.path.join(script_dir,"../docs/source/markdown")
   166  
   167      try:
   168          os.chdir(man_dir)
   169      except FileNotFoundError as ex:
   170          raise Exception("Please invoke me from the base repo dir") from ex
   171  
   172      # No longer possible to invoke us with args: reject any such invocation
   173      if len(sys.argv) > 1:
   174          raise Exception("This script accepts no arguments")
   175  
   176      preprocessor = Preprocessor()
   177      for infile in sorted(glob.glob('*.md.in')):
   178          preprocessor.process(infile)
   179  
   180      # Now rewrite all option files
   181      preprocessor.rewrite_optionfiles()
   182  
   183  if __name__ == "__main__":
   184      main()