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;