github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/hack/xref-helpmsgs-manpages (about) 1 #!/usr/bin/perl 2 # 3 # xref-helpmsgs-manpages - cross-reference --help options against man pages 4 # 5 package LibPod::CI::XrefHelpmsgsManpages; 6 7 use v5.14; 8 use utf8; 9 10 use strict; 11 use warnings; 12 13 (our $ME = $0) =~ s|.*/||; 14 our $VERSION = '0.1'; 15 16 # For debugging, show data structures using DumpTree($var) 17 #use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0; 18 19 # unbuffer output 20 $| = 1; 21 22 ############################################################################### 23 # BEGIN user-customizable section 24 25 # Path to podman executable 26 my $Default_Podman = './bin/podman'; 27 my $PODMAN = $ENV{PODMAN} || $Default_Podman; 28 29 # Path to all doc files, including .rst and (down one level) markdown 30 my $Docs_Path = 'docs/source'; 31 32 # Path to podman markdown source files (of the form podman-*.1.md) 33 my $Markdown_Path = "$Docs_Path/markdown"; 34 35 # Global error count 36 my $Errs = 0; 37 38 # END user-customizable section 39 ############################################################################### 40 41 use FindBin; 42 43 ############################################################################### 44 # BEGIN boilerplate args checking, usage messages 45 46 sub usage { 47 print <<"END_USAGE"; 48 Usage: $ME [OPTIONS] 49 50 $ME recursively runs 'podman --help' against 51 all subcommands; and recursively reads podman-*.1.md files 52 in $Markdown_Path, then cross-references that each --help 53 option is listed in the appropriate man page and vice-versa. 54 55 $ME invokes '\$PODMAN' (default: $Default_Podman). 56 57 Exit status is zero if no inconsistencies found, one otherwise 58 59 OPTIONS: 60 61 -v, --verbose show verbose progress indicators 62 -n, --dry-run make no actual changes 63 64 --help display this message 65 --version display program name and version 66 END_USAGE 67 68 exit; 69 } 70 71 # Command-line options. Note that this operates directly on @ARGV ! 72 our $debug = 0; 73 our $verbose = 0; 74 sub handle_opts { 75 use Getopt::Long; 76 GetOptions( 77 'debug!' => \$debug, 78 'verbose|v' => \$verbose, 79 80 help => \&usage, 81 version => sub { print "$ME version $VERSION\n"; exit 0 }, 82 ) or die "Try `$ME --help' for help\n"; 83 } 84 85 # END boilerplate args checking, usage messages 86 ############################################################################### 87 88 ############################## CODE BEGINS HERE ############################### 89 90 # The term is "modulino". 91 __PACKAGE__->main() unless caller(); 92 93 # Main code. 94 sub main { 95 # Note that we operate directly on @ARGV, not on function parameters. 96 # This is deliberate: it's because Getopt::Long only operates on @ARGV 97 # and there's no clean way to make it use @_. 98 handle_opts(); # will set package globals 99 100 # Fetch command-line arguments. Barf if too many. 101 die "$ME: Too many arguments; try $ME --help\n" if @ARGV; 102 103 my $help = podman_help(); 104 my $man = podman_man('podman'); 105 my $rst = podman_rst(); 106 107 xref_by_help($help, $man); 108 xref_by_man($help, $man); 109 110 xref_rst($help, $rst); 111 112 exit !!$Errs; 113 } 114 115 ############################################################################### 116 # BEGIN cross-referencing 117 118 ################## 119 # xref_by_help # Find keys in '--help' but not in man 120 ################## 121 sub xref_by_help { 122 my ($help, $man, @subcommand) = @_; 123 124 for my $k (sort keys %$help) { 125 if (exists $man->{$k}) { 126 if (ref $help->{$k}) { 127 xref_by_help($help->{$k}, $man->{$k}, @subcommand, $k); 128 } 129 # Otherwise, non-ref is leaf node such as a --option 130 } 131 else { 132 my $man = $man->{_path} || 'man'; 133 warn "$ME: podman @subcommand --help lists $k, but $k not in $man\n"; 134 ++$Errs; 135 } 136 } 137 } 138 139 ################# 140 # xref_by_man # Find keys in man pages but not in --help 141 ################# 142 # 143 # In an ideal world we could share the functionality in one function; but 144 # there are just too many special cases in man pages. 145 # 146 sub xref_by_man { 147 my ($help, $man, @subcommand) = @_; 148 149 # FIXME: this generates way too much output 150 for my $k (grep { $_ ne '_path' } sort keys %$man) { 151 if (exists $help->{$k}) { 152 if (ref $man->{$k}) { 153 xref_by_man($help->{$k}, $man->{$k}, @subcommand, $k); 154 } 155 } 156 elsif ($k ne '--help' && $k ne '-h') { 157 my $man = $man->{_path} || 'man'; 158 159 # Special case: podman-inspect serves dual purpose (image, ctr) 160 my %ignore = map { $_ => 1 } qw(-l -s -t --latest --size --type); 161 next if $man =~ /-inspect/ && $ignore{$k}; 162 163 # Special case: podman-diff serves dual purpose (image, ctr) 164 my %diffignore = map { $_ => 1 } qw(-l --latest ); 165 next if $man =~ /-diff/ && $diffignore{$k}; 166 167 # Special case: the 'trust' man page is a mess 168 next if $man =~ /-trust/; 169 170 # Special case: '--net' is an undocumented shortcut 171 next if $k eq '--net' && $help->{'--network'}; 172 173 # Special case: these are actually global options 174 next if $k =~ /^--(cni-config-dir|runtime)$/ && $man =~ /-build/; 175 176 # Special case: weirdness with Cobra and global/local options 177 next if $k eq '--namespace' && $man =~ /-ps/; 178 179 # Special case: these require compiling with 'varlink' tag, 180 # which doesn't happen in CI gating task. 181 next if $k eq 'varlink'; 182 next if "@subcommand" eq 'system' && $k eq 'service'; 183 184 # Special case: podman completion is a hidden command 185 next if $k eq 'completion'; 186 187 warn "$ME: podman @subcommand: $k in $man, but not --help\n"; 188 ++$Errs; 189 } 190 } 191 } 192 193 ############## 194 # xref_rst # Cross-check *.rst files against help 195 ############## 196 sub xref_rst { 197 my ($help, $rst, @subcommand) = @_; 198 199 # Cross-check against rst (but only subcommands, not options). 200 # We key on $help because that is Absolute Truth: anything in podman --help 201 # must be referenced in an rst (the converse is not true). 202 for my $k (sort grep { $_ !~ /^-/ } keys %$help) { 203 # Check for subcommands, if any (eg podman system -> connection -> add) 204 if (ref $help->{$k}) { 205 xref_rst($help->{$k}, $rst->{$k}, @subcommand, $k); 206 } 207 208 # Check that command is mentioned in at least one .rst file 209 if (! exists $rst->{$k}{_desc}) { 210 my @podman = ("podman", @subcommand, $k); 211 warn "$ME: no link in *.rst for @podman\n"; 212 ++$Errs; 213 } 214 } 215 } 216 217 # END cross-referencing 218 ############################################################################### 219 # BEGIN data gathering 220 221 ################# 222 # podman_help # Parse output of 'podman [subcommand] --help' 223 ################# 224 sub podman_help { 225 my %help; 226 open my $fh, '-|', $PODMAN, @_, '--help' 227 or die "$ME: Cannot fork: $!\n"; 228 my $section = ''; 229 while (my $line = <$fh>) { 230 # Cobra is blessedly consistent in its output: 231 # Usage: ... 232 # Available Commands: 233 # .... 234 # Options: 235 # .... 236 # 237 # Start by identifying the section we're in... 238 if ($line =~ /^Available\s+(Commands):/) { 239 $section = lc $1; 240 } 241 elsif ($line =~ /^(Options):/) { 242 $section = lc $1; 243 } 244 245 # ...then track commands and options. For subcommands, recurse. 246 elsif ($section eq 'commands') { 247 if ($line =~ /^\s{1,4}(\S+)\s/) { 248 my $subcommand = $1; 249 print "> podman @_ $subcommand\n" if $debug; 250 $help{$subcommand} = podman_help(@_, $subcommand) 251 unless $subcommand eq 'help'; # 'help' not in man 252 } 253 } 254 elsif ($section eq 'options') { 255 # Handle '--foo' or '-f, --foo' 256 if ($line =~ /^\s{1,10}(--\S+)\s/) { 257 print "> podman @_ $1\n" if $debug; 258 $help{$1} = 1; 259 } 260 elsif ($line =~ /^\s{1,10}(-\S),\s+(--\S+)\s/) { 261 print "> podman @_ $1, $2\n" if $debug; 262 $help{$1} = $help{$2} = 1; 263 } 264 } 265 } 266 close $fh 267 or die "$ME: Error running 'podman @_ --help'\n"; 268 269 return \%help; 270 } 271 272 273 ################ 274 # podman_man # Parse contents of podman-*.1.md 275 ################ 276 sub podman_man { 277 my $command = shift; 278 my $subpath = "$Markdown_Path/$command.1.md"; 279 my $manpath = "$FindBin::Bin/../$subpath"; 280 print "** $subpath \n" if $debug; 281 282 my %man = (_path => $subpath); 283 open my $fh, '<', $manpath 284 or die "$ME: Cannot read $manpath: $!\n"; 285 my $section = ''; 286 my @most_recent_flags; 287 my $previous_subcmd = ''; 288 while (my $line = <$fh>) { 289 chomp $line; 290 next unless $line; # skip empty lines 291 292 # .md files designate sections with leading double hash 293 if ($line =~ /^##\s*(GLOBAL\s+)?OPTIONS/) { 294 $section = 'flags'; 295 } 296 elsif ($line =~ /^\#\#\s+(SUB)?COMMANDS/) { 297 $section = 'commands'; 298 } 299 elsif ($line =~ /^\#\#[^#]/) { 300 $section = ''; 301 } 302 303 # This will be a table containing subcommand names, links to man pages. 304 # The format is slightly different between podman.1.md and subcommands. 305 elsif ($section eq 'commands') { 306 # In podman.1.md 307 if ($line =~ /^\|\s*\[podman-(\S+?)\(\d\)\]/) { 308 # $1 will be changed by recursion _*BEFORE*_ left-hand assignment 309 my $subcmd = $1; 310 $man{$subcmd} = podman_man("podman-$1"); 311 } 312 313 # In podman-<subcommand>.1.md 314 elsif ($line =~ /^\|\s+(\S+)\s+\|\s+\[\S+\]\((\S+)\.1\.md\)/) { 315 # $1 will be changed by recursion _*BEFORE*_ left-hand assignment 316 my $subcmd = $1; 317 if ($previous_subcmd gt $subcmd) { 318 warn "$ME: $subpath: '$previous_subcmd' and '$subcmd' are out of order\n"; 319 ++$Errs; 320 } 321 $previous_subcmd = $subcmd; 322 $man{$subcmd} = podman_man($2); 323 } 324 } 325 326 # Options should always be of the form '**-f**' or '**--flag**', 327 # possibly separated by comma-space. 328 elsif ($section eq 'flags') { 329 # e.g. 'podman run --ip6', documented in man page, but nonexistent 330 if ($line =~ /^not\s+implemented/i) { 331 delete $man{$_} for @most_recent_flags; 332 } 333 334 @most_recent_flags = (); 335 # As of PR #8292, all options are <h4> and anchored 336 if ($line =~ s/^\#{4}\s+//) { 337 # If option has long and short form, long must come first. 338 # This is a while-loop because there may be multiple long 339 # option names, e.g. --net/--network 340 while ($line =~ s/^\*\*(--[a-z0-9-]+)\*\*(=\*[a-zA-Z0-9-]+\*)?(,\s+)?//g) { 341 $man{$1} = 1; 342 push @most_recent_flags, $1; 343 } 344 # Short form 345 if ($line =~ s/^\*\*(-[a-zA-Z0-9])\*\*(=\*[a-zA-Z0-9-]+\*)?//g) { 346 $man{$1} = 1; 347 348 # Keep track of them, in case we see 'Not implemented' below 349 push @most_recent_flags, $1; 350 } 351 } 352 } 353 } 354 close $fh; 355 356 # Special case: the 'image trust' man page tries hard to cover both set 357 # and show, which means it ends up not being machine-readable. 358 if ($command eq 'podman-image-trust') { 359 my %set = %man; 360 my %show = %man; 361 $show{$_} = 1 for qw(--raw -j --json); 362 return +{ set => \%set, show => \%show } 363 } 364 365 return \%man; 366 } 367 368 369 ################ 370 # podman_rst # Parse contents of docs/source/*.rst 371 ################ 372 sub podman_rst { 373 my %rst; 374 375 # Read all .rst files, looking for ":doc:`subcmd <target>` description" 376 for my $rst (glob "$Docs_Path/*.rst") { 377 open my $fh, '<', $rst 378 or die "$ME: Cannot read $rst: $!\n"; 379 380 # The basename of foo.rst is usually, but not always, the name of 381 # a podman subcommand. There are a few special cases: 382 (my $command = $rst) =~ s!^.*/(.*)\.rst!$1!; 383 384 my $subcommand_href = \%rst; 385 if ($command eq 'Commands') { 386 ; 387 } 388 elsif ($command eq 'managecontainers') { 389 $subcommand_href = $rst{container} //= { }; 390 } 391 elsif ($command eq 'connection') { 392 $subcommand_href = $rst{system}{connection} //= { }; 393 } 394 else { 395 $subcommand_href = $rst{$command} //= { }; 396 } 397 398 my $previous_subcommand = ''; 399 while (my $line = <$fh>) { 400 if ($line =~ /^:doc:`(\S+)\s+<(.*?)>`\s+(.*)/) { 401 my ($subcommand, $target, $desc) = ($1, $2, $3); 402 403 # Check that entries are in alphabetical order 404 if ($subcommand lt $previous_subcommand) { 405 warn "$ME: $rst:$.: '$previous_subcommand' and '$subcommand' are out of order\n"; 406 ++$Errs; 407 } 408 $previous_subcommand = $subcommand; 409 410 # Mark this subcommand as documented. 411 $subcommand_href->{$subcommand}{_desc} = $desc; 412 413 # Check for invalid links. These will be one of two forms: 414 # <markdown/foo.1> -> markdown/foo.1.md 415 # <foo> -> foo.rst 416 if ($target =~ m!^markdown/!) { 417 if (! -e "$Docs_Path/$target.md") { 418 warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target\n"; 419 ++$Errs; 420 } 421 } 422 else { 423 if (! -e "$Docs_Path/$target.rst") { 424 warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target.rst\n"; 425 } 426 } 427 } 428 } 429 close $fh; 430 } 431 432 # Special case: 'image trust set/show' are documented in image-trust.1 433 $rst{image}{trust}{$_} = { _desc => 'ok' } for (qw(set show)); 434 435 return \%rst; 436 } 437 438 # END data gathering 439 ############################################################################### 440 441 1;