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;