github.com/hanks177/podman/v4@v4.1.3-0.20220613032544-16d90015bc83/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 In the spirit of shoehorning functionality where it wasn't intended, 58 $ME also checks the SEE ALSO section of each man page 59 to ensure that references and links are properly formatted 60 and valid. 61 62 Exit status is zero if no inconsistencies found, one otherwise 63 64 OPTIONS: 65 66 -v, --verbose show verbose progress indicators 67 -n, --dry-run make no actual changes 68 69 --help display this message 70 --version display program name and version 71 END_USAGE 72 73 exit; 74 } 75 76 # Command-line options. Note that this operates directly on @ARGV ! 77 our $debug = 0; 78 our $verbose = 0; 79 sub handle_opts { 80 use Getopt::Long; 81 GetOptions( 82 'debug!' => \$debug, 83 'verbose|v' => \$verbose, 84 85 help => \&usage, 86 version => sub { print "$ME version $VERSION\n"; exit 0 }, 87 ) or die "Try `$ME --help' for help\n"; 88 } 89 90 # END boilerplate args checking, usage messages 91 ############################################################################### 92 93 ############################## CODE BEGINS HERE ############################### 94 95 # The term is "modulino". 96 __PACKAGE__->main() unless caller(); 97 98 # Main code. 99 sub main { 100 # Note that we operate directly on @ARGV, not on function parameters. 101 # This is deliberate: it's because Getopt::Long only operates on @ARGV 102 # and there's no clean way to make it use @_. 103 handle_opts(); # will set package globals 104 105 # Fetch command-line arguments. Barf if too many. 106 die "$ME: Too many arguments; try $ME --help\n" if @ARGV; 107 108 my $help = podman_help(); 109 my $man = podman_man('podman'); 110 my $rst = podman_rst(); 111 112 xref_by_help($help, $man); 113 xref_by_man($help, $man); 114 115 xref_rst($help, $rst); 116 117 exit !!$Errs; 118 } 119 120 ############################################################################### 121 # BEGIN cross-referencing 122 123 ################## 124 # xref_by_help # Find keys in '--help' but not in man 125 ################## 126 sub xref_by_help { 127 my ($help, $man, @subcommand) = @_; 128 129 for my $k (sort keys %$help) { 130 if (exists $man->{$k}) { 131 if (ref $help->{$k}) { 132 xref_by_help($help->{$k}, $man->{$k}, @subcommand, $k); 133 } 134 # Otherwise, non-ref is leaf node such as a --option 135 } 136 else { 137 my $man = $man->{_path} || 'man'; 138 warn "$ME: 'podman @subcommand --help' lists '$k', which is not in $man\n"; 139 ++$Errs; 140 } 141 } 142 } 143 144 ################# 145 # xref_by_man # Find keys in man pages but not in --help 146 ################# 147 # 148 # In an ideal world we could share the functionality in one function; but 149 # there are just too many special cases in man pages. 150 # 151 sub xref_by_man { 152 my ($help, $man, @subcommand) = @_; 153 154 # FIXME: this generates way too much output 155 for my $k (grep { $_ ne '_path' } sort keys %$man) { 156 if (exists $help->{$k}) { 157 if (ref $man->{$k}) { 158 xref_by_man($help->{$k}, $man->{$k}, @subcommand, $k); 159 } 160 } 161 elsif ($k ne '--help' && $k ne '-h') { 162 my $man = $man->{_path} || 'man'; 163 164 # Special case: podman-inspect serves dual purpose (image, ctr) 165 my %ignore = map { $_ => 1 } qw(-l -s -t --latest --size --type); 166 next if $man =~ /-inspect/ && $ignore{$k}; 167 168 # Special case: podman-diff serves dual purpose (image, ctr) 169 my %diffignore = map { $_ => 1 } qw(-l --latest ); 170 next if $man =~ /-diff/ && $diffignore{$k}; 171 172 # Special case: the 'trust' man page is a mess 173 next if $man =~ /-trust/; 174 175 # Special case: '--net' is an undocumented shortcut 176 next if $k eq '--net' && $help->{'--network'}; 177 178 # Special case: these are actually global options 179 next if $k =~ /^--(cni-config-dir|runtime)$/ && $man =~ /-build/; 180 181 # Special case: weirdness with Cobra and global/local options 182 next if $k eq '--namespace' && $man =~ /-ps/; 183 184 next if "@subcommand" eq 'system' && $k eq 'service'; 185 186 # Special case: podman completion is a hidden command 187 next if $k eq 'completion'; 188 189 warn "$ME: 'podman @subcommand': $k in $man, but not --help\n"; 190 ++$Errs; 191 } 192 } 193 } 194 195 ############## 196 # xref_rst # Cross-check *.rst files against help 197 ############## 198 sub xref_rst { 199 my ($help, $rst, @subcommand) = @_; 200 201 # Cross-check against rst (but only subcommands, not options). 202 # We key on $help because that is Absolute Truth: anything in podman --help 203 # must be referenced in an rst (the converse is not true). 204 for my $k (sort grep { $_ !~ /^-/ } keys %$help) { 205 # Check for subcommands, if any (eg podman system -> connection -> add) 206 if (ref $help->{$k}) { 207 xref_rst($help->{$k}, $rst->{$k}, @subcommand, $k); 208 } 209 } 210 } 211 212 # END cross-referencing 213 ############################################################################### 214 # BEGIN data gathering 215 216 ################# 217 # podman_help # Parse output of 'podman [subcommand] --help' 218 ################# 219 sub podman_help { 220 my %help; 221 open my $fh, '-|', $PODMAN, @_, '--help' 222 or die "$ME: Cannot fork: $!\n"; 223 my $section = ''; 224 while (my $line = <$fh>) { 225 # Cobra is blessedly consistent in its output: 226 # Usage: ... 227 # Available Commands: 228 # .... 229 # Options: 230 # .... 231 # 232 # Start by identifying the section we're in... 233 if ($line =~ /^Available\s+(Commands):/) { 234 $section = lc $1; 235 } 236 elsif ($line =~ /^(Options):/) { 237 $section = lc $1; 238 } 239 240 # ...then track commands and options. For subcommands, recurse. 241 elsif ($section eq 'commands') { 242 if ($line =~ /^\s{1,4}(\S+)\s/) { 243 my $subcommand = $1; 244 print "> podman @_ $subcommand\n" if $debug; 245 246 # check that the same subcommand is not listed twice (#12356) 247 if (exists $help{$subcommand}) { 248 warn "$ME: 'podman @_ help' lists '$subcommand' twice\n"; 249 ++$Errs; 250 } 251 252 $help{$subcommand} = podman_help(@_, $subcommand) 253 unless $subcommand eq 'help'; # 'help' not in man 254 } 255 } 256 elsif ($section eq 'options') { 257 # Handle '--foo' or '-f, --foo' 258 if ($line =~ /^\s{1,10}(--\S+)\s/) { 259 print "> podman @_ $1\n" if $debug; 260 $help{$1} = 1; 261 } 262 elsif ($line =~ /^\s{1,10}(-\S),\s+(--\S+)\s/) { 263 print "> podman @_ $1, $2\n" if $debug; 264 $help{$1} = $help{$2} = 1; 265 } 266 } 267 } 268 close $fh 269 or die "$ME: Error running 'podman @_ --help'\n"; 270 271 return \%help; 272 } 273 274 275 ################ 276 # podman_man # Parse contents of podman-*.1.md 277 ################ 278 sub podman_man { 279 my $command = shift; 280 my $subpath = "$Markdown_Path/$command.1.md"; 281 my $manpath = "$FindBin::Bin/../$subpath"; 282 print "** $subpath \n" if $debug; 283 284 my %man = (_path => $subpath); 285 open my $fh, '<', $manpath 286 or die "$ME: Cannot read $manpath: $!\n"; 287 my $section = ''; 288 my @most_recent_flags; 289 my $previous_subcmd = ''; 290 my $previous_flag = ''; 291 while (my $line = <$fh>) { 292 chomp $line; 293 next unless $line; # skip empty lines 294 295 # .md files designate sections with leading double hash 296 if ($line =~ /^##\s*(GLOBAL\s+)?OPTIONS/) { 297 $section = 'flags'; 298 $previous_flag = ''; 299 } 300 elsif ($line =~ /^###\s+\w+\s+OPTIONS/) { 301 # podman image trust has sections for set & show 302 $section = 'flags'; 303 $previous_flag = ''; 304 } 305 elsif ($line =~ /^\#\#\s+(SUB)?COMMANDS/) { 306 $section = 'commands'; 307 } 308 elsif ($line =~ /^\#\#\s+SEE\s+ALSO/) { 309 $section = 'see-also'; 310 } 311 elsif ($line =~ /^\#\#[^#]/) { 312 $section = ''; 313 } 314 315 # This will be a table containing subcommand names, links to man pages. 316 # The format is slightly different between podman.1.md and subcommands. 317 elsif ($section eq 'commands') { 318 # In podman.1.md 319 if ($line =~ /^\|\s*\[podman-(\S+?)\(\d\)\]/) { 320 # $1 will be changed by recursion _*BEFORE*_ left-hand assignment 321 my $subcmd = $1; 322 $man{$subcmd} = podman_man("podman-$1"); 323 } 324 325 # In podman-<subcommand>.1.md 326 elsif ($line =~ /^\|\s+(\S+)\s+\|\s+\[\S+\]\((\S+)\.1\.md\)/) { 327 # $1 will be changed by recursion _*BEFORE*_ left-hand assignment 328 my $subcmd = $1; 329 if ($previous_subcmd gt $subcmd) { 330 warn "$ME: $subpath:$.: '$previous_subcmd' and '$subcmd' are out of order\n"; 331 ++$Errs; 332 } 333 $previous_subcmd = $subcmd; 334 $man{$subcmd} = podman_man($2); 335 } 336 } 337 338 # Options should always be of the form '**-f**' or '**\-\-flag**', 339 # possibly separated by comma-space. 340 elsif ($section eq 'flags') { 341 # e.g. 'podman run --ip6', documented in man page, but nonexistent 342 if ($line =~ /^not\s+implemented/i) { 343 delete $man{$_} for @most_recent_flags; 344 } 345 346 @most_recent_flags = (); 347 # As of PR #8292, all options are <h4> and anchored 348 if ($line =~ s/^\#{4}\s+//) { 349 # If option has long and short form, long must come first. 350 # This is a while-loop because there may be multiple long 351 # option names, e.g. --net/--network 352 my $is_first = 1; 353 while ($line =~ s/^\*\*(--[a-z0-9-]+)\*\*(=\*[a-zA-Z0-9-]+\*)?(,\s+)?//g) { 354 my $flag = $1; 355 $man{$flag} = 1; 356 if ($flag lt $previous_flag && $is_first) { 357 warn "$ME: $subpath:$.: $flag should precede $previous_flag\n"; 358 ++$Errs; 359 } 360 $previous_flag = $flag if $is_first; 361 push @most_recent_flags, $flag; 362 363 # Further iterations of /g are allowed to be out of order, 364 # e.g., it's OK for "--namespace, -ns" to precede --nohead 365 $is_first = 0; 366 } 367 # Short form 368 if ($line =~ s/^\*\*(-[a-zA-Z0-9])\*\*(=\*[a-zA-Z0-9-]+\*)?//g) { 369 $man{$1} = 1; 370 371 # Keep track of them, in case we see 'Not implemented' below 372 push @most_recent_flags, $1; 373 } 374 } 375 } 376 377 # It's easy to make mistakes in the SEE ALSO elements. 378 elsif ($section eq 'see-also') { 379 _check_seealso_links( "$subpath:$.", $line ); 380 } 381 } 382 close $fh; 383 384 # Special case: the 'image trust' man page tries hard to cover both set 385 # and show, which means it ends up not being machine-readable. 386 if ($command eq 'podman-image-trust') { 387 my %set = %man; 388 my %show = %man; 389 $show{$_} = 1 for qw(--raw -j --json); 390 return +{ set => \%set, show => \%show } 391 } 392 393 return \%man; 394 } 395 396 397 ################ 398 # podman_rst # Parse contents of docs/source/*.rst 399 ################ 400 sub podman_rst { 401 my %rst; 402 403 # Read all .rst files, looking for ":doc:`subcmd <target>` description" 404 for my $rst (glob "$Docs_Path/*.rst") { 405 open my $fh, '<', $rst 406 or die "$ME: Cannot read $rst: $!\n"; 407 408 # The basename of foo.rst is usually, but not always, the name of 409 # a podman subcommand. There are a few special cases: 410 (my $command = $rst) =~ s!^.*/(.*)\.rst!$1!; 411 412 my $subcommand_href = \%rst; 413 if ($command eq 'Commands') { 414 ; 415 } 416 elsif ($command eq 'managecontainers') { 417 $subcommand_href = $rst{container} //= { }; 418 } 419 elsif ($command eq 'connection') { 420 $subcommand_href = $rst{system}{connection} //= { }; 421 } 422 else { 423 $subcommand_href = $rst{$command} //= { }; 424 } 425 426 my $previous_subcommand = ''; 427 while (my $line = <$fh>) { 428 if ($line =~ /^:doc:`(\S+)\s+<(.*?)>`\s+(.*)/) { 429 my ($subcommand, $target, $desc) = ($1, $2, $3); 430 431 # Check that entries are in alphabetical order 432 if ($subcommand lt $previous_subcommand) { 433 warn "$ME: $rst:$.: '$previous_subcommand' and '$subcommand' are out of order\n"; 434 ++$Errs; 435 } 436 $previous_subcommand = $subcommand; 437 438 # Mark this subcommand as documented. 439 $subcommand_href->{$subcommand}{_desc} = $desc; 440 441 # Check for invalid links. These will be one of two forms: 442 # <markdown/foo.1> -> markdown/foo.1.md 443 # <foo> -> foo.rst 444 if ($target =~ m!^markdown/!) { 445 if (! -e "$Docs_Path/$target.md") { 446 warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target\n"; 447 ++$Errs; 448 } 449 } 450 else { 451 if (! -e "$Docs_Path/$target.rst") { 452 warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target.rst\n"; 453 } 454 } 455 } 456 } 457 close $fh; 458 } 459 460 # Special case: 'image trust set/show' are documented in image-trust.1 461 $rst{image}{trust}{$_} = { _desc => 'ok' } for (qw(set show)); 462 463 return \%rst; 464 } 465 466 # END data gathering 467 ############################################################################### 468 # BEGIN sanity checking of SEE ALSO links 469 470 ########################## 471 # _check_seealso_links # Check formatting and link validity. 472 ########################## 473 sub _check_seealso_links { 474 my $path = shift; 475 my $line = shift; 476 477 return if ! $line; 478 479 # Line must be a comma-separated list of man page references, e.g. 480 # **foo(1)**, **[podman-bar(1)](podman-bar.1.md)**, **[xxx(8)](http...)** 481 TOKEN: 482 for my $token (split /,\s+/, $line) { 483 # Elements must be separated by comma and space. (We don't do further 484 # checks here, so it's possible for the dev to add the space and then 485 # have us fail on the next iteration. I choose not to address that.) 486 if ($token =~ /,/) { 487 warn "$ME: $path: please add space after comma: '$token'\n"; 488 ++$Errs; 489 next TOKEN; 490 } 491 492 # Each token must be of the form '**something**' 493 if ($token !~ s/^\*\*(.*)\*\*$/$1/) { 494 if ($token =~ /\*\*/) { 495 warn "$ME: $path: '$token' has asterisks in the wrong place\n"; 496 } 497 else { 498 warn "$ME: $path: '$token' should be bracketed by '**'\n"; 499 } 500 ++$Errs; 501 next TOKEN; 502 } 503 504 # Is it a markdown link? 505 if ($token =~ /^\[(\S+)\]\((\S+)\)$/) { 506 my ($name, $link) = ($1, $2); 507 if ($name =~ /^(.*)\((\d)\)$/) { 508 my ($base, $section) = ($1, $2); 509 if (-e "$Markdown_Path/$base.$section.md" || -e "$Markdown_Path/links/$base.$section") { 510 if ($link ne "$base.$section.md") { 511 warn "$ME: $path: inconsistent link $name -> $link, expected $base.$section.md\n"; 512 ++$Errs; 513 } 514 } 515 else { 516 if (! _is_valid_external_link($base, $section, $link)) { 517 warn "$ME: $path: invalid link $name -> $link\n"; 518 ++$Errs; 519 } 520 } 521 } 522 else { 523 warn "$ME: $path: could not parse '$name' as 'manpage(N)'\n"; 524 ++$Errs; 525 } 526 } 527 528 # Not a markdown link; it must be a plain man reference, e.g. 'foo(5)' 529 elsif ($token =~ m!^(\S+)\((\d+)\)$!) { 530 my ($base, $section) = ($1, $2); 531 532 # Unadorned 'podman-foo(1)' must be a link. 533 if (-e "$Markdown_Path/$base.$section.md" || -e "$Markdown_Path/links/$base.$section") { 534 warn "$ME: $path: '$token' should be '[$token]($base.$section.md)'\n"; 535 ++$Errs; 536 } 537 538 # Link to man page foo(5) but without a link. This is not an error 539 # but Ed may sometimes want to see those on a manual test run. 540 warn "$ME: $path: plain '$token' would be so much nicer as a link\n" 541 if $verbose; 542 } 543 else { 544 warn "$ME: $path: invalid token '$token'\n"; 545 ++$Errs; 546 } 547 } 548 } 549 550 ############################# 551 # _is_valid_external_link # Tries to validate links to external man pages 552 ############################# 553 # 554 # This performs no actual fetches, so we can't actually check for 404. 555 # All we do is ensure that links conform to standard patterns. This is 556 # good for catching things like 'conmon(8)' pointing to a .5 URL, or 557 # linking to .md instead of .html. 558 # 559 # FIXME: we could actually rewrite this so as to offer hints on what to fix. 560 # That's a lot of work, and a lot of convoluted code, for questionable ROI. 561 # 562 sub _is_valid_external_link { 563 my ($base, $section, $link) = @_; 564 565 return 1 if $link =~ m!^https://github\.com/\S+/blob/(main|master)(/.*)?/\Q$base\E\.$section\.md!; 566 567 return 1 if $link =~ m!^https://.*unix\.com/man-page/(linux|redhat)/$section/$base$!; 568 return 1 if $link eq "https://man7\.org/linux/man-pages/man$section/$base\.$section\.html"; 569 570 if ($base =~ /systemd/) { 571 return 1 if $link eq "https://www.freedesktop.org/software/systemd/man/$base.html"; 572 } 573 574 return; 575 } 576 577 578 579 580 # END sanity checking of SEE ALSO links 581 ############################################################################### 582 583 1;