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()