3
# xref-helpmsgs-manpages - cross-reference --help options against man pages
5
package LibPod::CI::XrefHelpmsgsManpages;
15
(our $ME = $0) =~ s|.*/||;
18
# For debugging, show data structures using DumpTree($var)
19
#use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0;
24
###############################################################################
25
# BEGIN user-customizable section
27
# Path to podman executable
28
my $Default_Podman = "$FindBin::Bin/../bin/podman";
29
my $PODMAN = $ENV{PODMAN} || $Default_Podman;
31
# Path to all doc files, including .rst and (down one level) markdown
32
our $Docs_Path = 'docs/source';
34
# Path to podman markdown source files (of the form podman-*.1.md)
35
our $Markdown_Path = "$Docs_Path/markdown";
40
# Table of exceptions for documenting fields in '--format {{.Foo}}'
42
# Autocomplete is wonderful, and it's even better when we document the
43
# existing options. Unfortunately, sometimes internal structures get
44
# exposed that are of no use to anyone and cannot be guaranteed. Avoid
45
# documenting those. This table lists those exceptions. Format is:
49
# ...such that "podman foo --format '{{.Bar}}'" will not be documented.
51
my $Format_Exceptions = <<'END_EXCEPTIONS';
52
# Deep internal structs; pretty sure these are permanent exceptions
54
history .ImageHistoryLayer
55
images .Arch .ImageSummary .Os .IsManifestList
58
# FIXME: this one, maybe? But someone needs to write the text
61
# No clue what these are. Some are just different-case dups of others.
62
pod-ps .Containers .Id .InfraId .ListPodsReport .Namespace
63
ps .Cgroup .CGROUPNS .IPC .ListContainer .MNT .Namespaces .NET .PIDNS .User .USERNS .UTS
65
# I think .Destination is an internal struct, but .IsMachine maybe needs doc?
66
system-connection-list .Destination .IsMachine
70
for my $line (split "\n", $Format_Exceptions) {
71
$line =~ s/#.*$//; # strip comments
72
next unless $line; # skip empty lines
73
my ($subcommand, @fields) = split(' ', $line);
74
$Format_Exceptions{"podman-$subcommand"} = \@fields;
77
# Hardcoded list of podman commands for which '--format' does NOT mean
80
# I realize that it looks stupid to hardcode these: I could instead
81
# check "--format ''" and look for completion strings, 'oci', 'json',
82
# doesn't matter: if anything shows up, we excuse the missing '{{.'.
83
# (We'd still have to make a special exception for 'podman inspect').
85
# My reason for hardcoding is that these should be rare exceptions
86
# and we want to account for every single one. If a new command gets
87
# added, with a --format option that does not autocomplete '{{.',
88
# let's make sure it gets extra eyeballs.
89
my %Format_Option_Is_Special = map { $_ => 1 } (
90
'build', 'farm build', 'image build', # oci | docker
91
'commit', 'container commit', # " " " "
92
'diff', 'container diff', 'image diff', # only supports "json"
93
'generate systemd', # " " " "
94
'mount', 'container mount', 'image mount', # " " " "
95
'push', 'image push', 'manifest push', # oci | v2s*
96
'save', 'image save', # image formats (oci-*, ...)
97
'inspect', # ambiguous (container/image)
100
# Hardcoded list of existing duplicate-except-for-case format codes,
101
# with their associated subcommands. Let's not add any more.
102
my %Format_Option_Dup_Allowed = (
103
'podman-images' => { '.id' => 1 },
104
'podman-stats' => { '.avgcpu' => 1, '.pids' => 1 },
107
# Do not cross-reference these.
108
my %Skip_Subcommand = map { $_ => 1 } (
109
"help", # has no man page
110
"completion", # internal (hidden) subcommand
111
"compose", # external tool, outside of our control
114
# END user-customizable section
115
###############################################################################
116
# BEGIN boilerplate args checking, usage messages
122
$ME recursively runs 'podman --help' against
123
all subcommands; and recursively reads podman-*.1.md files
124
in $Markdown_Path, then cross-references that each --help
125
option is listed in the appropriate man page and vice-versa.
127
$ME invokes '\$PODMAN' (default: $Default_Podman).
129
In the spirit of shoehorning functionality where it wasn't intended,
130
$ME also checks the SEE ALSO section of each man page
131
to ensure that references and links are properly formatted
134
Exit status is zero if no inconsistencies found, one otherwise
138
-v, --verbose show verbose progress indicators
139
-n, --dry-run make no actual changes
141
--help display this message
142
--version display program name and version
148
# Command-line options. Note that this operates directly on @ARGV !
155
'verbose|v' => \$verbose,
158
version => sub { print "$ME version $VERSION\n"; exit 0 },
159
) or die "Try `$ME --help' for help\n";
162
# END boilerplate args checking, usage messages
163
###############################################################################
165
############################## CODE BEGINS HERE ###############################
167
# The term is "modulino".
168
__PACKAGE__->main() unless caller();
172
# Note that we operate directly on @ARGV, not on function parameters.
173
# This is deliberate: it's because Getopt::Long only operates on @ARGV
174
# and there's no clean way to make it use @_.
175
handle_opts(); # will set package globals
177
# Fetch command-line arguments. Barf if too many.
178
die "$ME: Too many arguments; try $ME --help\n" if @ARGV;
180
chdir "$FindBin::Bin/.."
181
or die "$ME: FATAL: Cannot cd $FindBin::Bin/..: $!";
183
my $help = podman_help();
184
my $man = podman_man('podman');
185
my $rst = podman_rst();
187
xref_by_help($help, $man);
188
xref_by_man($help, $man);
190
xref_rst($help, $rst);
195
###############################################################################
196
# BEGIN cross-referencing
199
# xref_by_help # Find keys in '--help' but not in man
202
my ($help, $man, @subcommand) = @_;
205
for my $k (sort keys %$help) {
206
next if $k =~ /^_/; # metadata ("_desc"). Ignore.
209
# Super-unlikely but I've seen it
210
warn "$ME: 'podman @subcommand' is not documented in man pages!\n";
215
if (exists $man->{$k}) {
216
if (ref $help->{$k}) {
217
# This happens when 'podman foo --format' offers
218
# autocompletion that looks like a Go template, but those
219
# template options aren't documented in the man pages.
220
if ($k eq '--format' && ! ref($man->{$k})) {
221
# "podman inspect" tries to autodetect if it's being run
222
# on an image or container. It cannot sanely be documented.
223
unless ("@subcommand" eq "inspect") {
224
warn "$ME: 'podman @subcommand': --format options are available through autocomplete, but are not documented in $man->{_path}\n";
230
xref_by_help($help->{$k}, $man->{$k}, @subcommand, $k);
233
# Documenting --format fields is tricky! They can be scalars, structs,
234
# or functions. This is a complicated block because if help & man don't
235
# match, we want to give the most user-friendly message possible.
236
elsif (@subcommand && $subcommand[-1] eq '--format') {
237
# '!' is one of the Format_Exceptions defined at top
238
if (($man->{$k} ne '!') && ($man->{$k} ne $help->{$k})) {
240
my $msg = "TELL ED TO HANDLE THIS: man='$man->{$k}' help='$help->{$k}'";
242
# Many different permutations of mismatches.
243
my $combo = "$man->{$k}-$help->{$k}";
244
if ($combo eq '0-...') {
245
$msg = "is a nested structure. Please add '...' to man page.";
247
elsif ($combo =~ /^\d+-\.\.\.$/) {
248
$msg = "is a nested structure, but the man page documents it as a function?!?";
250
elsif ($combo eq '...-0') {
251
$msg = "is a simple value, not a nested structure. Please remove '...' from man page.";
253
elsif ($combo =~ /^0-[1-9]\d*$/) {
254
$msg = "is a function that calls for $help->{$k} args. Please investigate what those are, then add them to the man page. E.g., '$k *bool*' or '$k *path* *bool*'";
256
elsif ($combo =~ /^\d+-[1-9]\d*$/) {
257
$msg = "is a function that calls for $help->{$k} args; the man page lists $man->{$k}. Please fix the man page.";
260
warn "$ME: 'podman @subcommand {{$k' $msg\n";
266
# Not documented in man. However, handle '...' as a special case
267
# in formatting strings. E.g., 'podman info .Host' is documented
268
# in the man page as '.Host ...' to indicate that the subfields
269
# are way too many to list individually.
271
while ($k_copy =~ s/\.[^.]+$//) {
272
my $parent_man = $man->{$k_copy} // '';
273
if (($parent_man eq '...') || ($parent_man eq '!')) {
278
# Nope, it's not that case.
279
my $man = $man->{_path} || 'man';
280
# The usual case is "podman ... --help"...
282
# ...but for *options* (e.g. --filter), we're checking command completion
283
$what = '<TAB>' if @subcommand && $subcommand[-1] =~ /^--/;
284
warn "$ME: 'podman @subcommand $what' lists '$k', which is not in $man\n";
291
# xref_by_man # Find keys in man pages but not in --help
294
# In an ideal world we could share the functionality in one function; but
295
# there are just too many special cases in man pages.
298
my ($help, $man, @subcommand) = @_;
300
# FIXME: this generates way too much output
302
for my $k (grep { $_ ne '_path' } sort keys %$man) {
303
if ($k eq '--format' && ref($man->{$k}) && ! ref($help->{$k})) {
304
warn "$ME: 'podman @subcommand': --format options documented in man page, but not available via autocomplete\n";
308
if (exists $help->{$k}) {
309
if (ref $man->{$k}) {
310
xref_by_man($help->{$k}, $man->{$k}, @subcommand, $k);
313
# This is OK: we don't recurse into options
316
# FIXME: should never get here, but we do. Figure it out later.
319
elsif ($k ne '--help' && $k ne '-h') {
320
my $man = $man->{_path} || 'man';
322
# Special case: podman-inspect serves dual purpose (image, ctr)
323
my %ignore = map { $_ => 1 } qw(-l -s -t --latest --size --type);
324
next if $man =~ /-inspect/ && $ignore{$k};
326
# Special case: podman-diff serves dual purpose (image, ctr)
327
my %diffignore = map { $_ => 1 } qw(-l --latest );
328
next if $man =~ /-diff/ && $diffignore{$k};
330
# Special case: the 'trust' man page is a mess
331
next if $man =~ /-trust/;
333
# Special case: '--net' is an undocumented shortcut
334
next if $k eq '--net' && $help->{'--network'};
336
# Special case: these are actually global options
337
next if $k =~ /^--(cni-config-dir|runtime)$/ && $man =~ /-build/;
339
# Special case: weirdness with Cobra and global/local options
340
next if $k eq '--namespace' && $man =~ /-ps/;
342
next if "@subcommand" eq 'system' && $k eq 'service';
344
# Special case for hidden or external commands
345
next if $Skip_Subcommand{$k};
347
# It's not always --help, sometimes we check <TAB> completion
349
$what = 'command completion' if @subcommand && $subcommand[-1] =~ /^--/;
350
warn "$ME: 'podman @subcommand': '$k' in $man, but not in $what\n";
357
# xref_rst # Cross-check *.rst files against help
360
# This makes a pass over top-level commands only. There is no rst
361
# documentation for any podman subcommands.
364
my ($help, $rst) = @_;
367
# We key on $help because that is Absolute Truth: anything in podman --help
368
# must be referenced in an rst (the converse is not necessarily true)
369
for my $k (sort grep { $_ !~ /^[_-]/ } keys %$help) {
370
if (exists $rst->{$k}) {
371
# Descriptions must match
372
if ($rst->{$k}{_desc} ne $help->{$k}{_desc}) {
373
warn "$ME: podman $k: inconsistent description in $rst->{$k}{_source}:\n";
374
warn " help: '$help->{$k}{_desc}'\n";
375
warn " rst: '$rst->{$k}{_desc}'\n";
380
warn "$ME: Not found in rst: $k\n";
385
# Now the other way around: look for anything in Commands.rst that is
386
# not in podman --help
387
for my $k (sort grep { $rst->{$_}{_source} =~ /Commands.rst/ } keys %$rst) {
388
if ($k ne 'Podman' && ! exists $help->{$k}) {
389
warn "$ME: 'podman $k' found in $rst->{$k}{_source} but not 'podman help'\n";
395
# END cross-referencing
396
###############################################################################
397
# BEGIN data gathering
400
# podman_help # Parse output of 'podman [subcommand] --help'
404
open my $fh, '-|', $PODMAN, @_, '--help'
405
or die "$ME: Cannot fork: $!\n";
407
while (my $line = <$fh>) {
410
# First line of --help is a short command description. We compare it
411
# (in a later step) against the blurb in Commands.rst.
412
# FIXME: we should crossref against man pages, but as of 2024-03-18
413
# it would be way too much work to get those aligned.
414
$help{_desc} //= $line;
416
# Cobra is blessedly consistent in its output:
420
# Available Commands:
425
# Start by identifying the section we're in...
426
if ($line =~ /^Available\s+(Commands):/) {
429
elsif ($line =~ /^(Options):/) {
433
# ...then track commands and options. For subcommands, recurse.
434
elsif ($section eq 'commands') {
435
if ($line =~ /^\s{1,4}(\S+)\s/) {
437
print "> podman @_ $subcommand\n" if $debug;
439
# check that the same subcommand is not listed twice (#12356)
440
if (exists $help{$subcommand}) {
441
warn "$ME: 'podman @_ help' lists '$subcommand' twice\n";
445
$help{$subcommand} = podman_help(@_, $subcommand)
446
unless $Skip_Subcommand{$subcommand};
449
elsif ($section eq 'options') {
452
# Handle '--foo' or '-f, --foo'
453
if ($line =~ /^\s{1,10}(--\S+)\s/) {
454
print "> podman @_ $1\n" if $debug;
458
elsif ($line =~ /^\s{1,10}(-\S),\s+(--\S+)\s/) {
459
print "> podman @_ $1, $2\n" if $debug;
461
$help{$1} = $help{$opt} = 1;
464
# Special case for --format: run podman with autocomplete.
465
# If that lists one or more '{{.Foo<something>' entries,
466
# convert our option data structure from scalar (indicating
467
# that we just cross-check for existence in the man page)
468
# to hashref (indicating that we recurse down and cross-check
469
# each individual param).
471
# There are three possibilities for <something>:
472
# {{.Foo}} (end braces) | terminal node. Usual case.
473
# {{.Foo. (dot) | deeper struct. Hard to handle.
474
# {{.Foo This is a function... | function. Rare, and " " " "
476
if ($opt eq '--format') {
477
my @completions = _completions(@_, '--format', '{{.');
478
for my $c (@completions) {
479
if ($c =~ /^\{\{(\.\S+)(\s+This.*\s(\d+)\s+arg.*)?$/) {
480
my ($fmt, $n_args) = ($1, $3 || 0);
481
# Strip off braces/dot, leaving just the name
482
$fmt =~ s/(\.|\}\})$//;
485
# First time through: convert to a hashref
486
$help{$opt} = {} if ! ref($help{$opt});
488
# Remember this param
489
if ($stripped eq '}}') {
492
elsif ($stripped eq '.') {
495
$help{$opt}{$fmt} = $n_args;
499
# If subcommand supports '--format {{.x', it should also
500
# support '--format json'
501
if (ref $help{$opt}) {
502
my @json = _completions(@_, '--format', 'json');
503
if (! grep { $_ eq 'json' } @json) {
504
warn "$ME: podman @_ --format json is unimplemented\n";
510
# --format option for this subcommand does not support
511
# completion for Go templates. This is OK for an
512
# existing set of commands (see table at top of script)
513
# but is a fatal error for any others, presumably a
514
# new subcommand. Either the subcommand must be fixed
515
# to support autocompletion, or the subcommand must be
516
# added to our exclusion list at top.
517
unless ($Format_Option_Is_Special{"@_"}) {
518
warn "$ME: podman @_ --format '{{.' does not offer autocompletion\n";
523
# Same thing, for --filter
524
elsif ($opt eq '--filter') {
525
my @completions = _completions(@_, '--filter=');
526
for my $c (@completions) {
527
if ($c =~ /^(\S+)=/) {
528
$help{$opt} = {} if ! ref($help{$opt});
536
or die "$ME: Error running 'podman @_ --help'\n";
543
# podman_man # Parse contents of podman-*.1.md
548
my $subpath = "$Markdown_Path/$command.1.md";
549
print "** $subpath \n" if $debug;
551
my %man = (_path => $subpath);
553
# We often get called multiple times on the same man page,
554
# because (e.g.) podman-container-list == podman-ps. It's the
555
# same man page text, though, and we don't know which subcommand
556
# we're being called for, so there's nothing to be gained by
557
# rereading the man page or by dumping yet more warnings
558
# at the user. So, keep a cache of what we've done.
559
if (my $seen = $Man_Seen{$subpath}) {
562
$Man_Seen{$subpath} = \%man;
564
open my $fh, '<', $subpath
565
or die "$ME: Cannot read $subpath: $!\n";
567
my @most_recent_flags;
568
my $previous_subcmd = '';
569
my $previous_flag = '';
570
my $previous_format = '';
571
my $previous_filter = '';
573
while (my $line = <$fh>) {
575
next LINE unless $line; # skip empty lines
577
# First line (page title) must match the command name.
578
if ($line =~ /^%\s+/) {
579
my $expect = "% $command 1";
580
if ($line ne $expect) {
581
warn "$ME: $subpath:$.: wrong title line '$line'; should be '$expect'\n";
586
# .md files designate sections with leading double hash
587
if ($line =~ /^##\s*(GLOBAL\s+)?OPTIONS/) {
591
elsif ($line =~ /^###\s+\w+\s+OPTIONS/) {
592
# podman image trust has sections for set & show
596
elsif ($line =~ /^\#\#\s+(SUB)?COMMANDS/) {
597
$section = 'commands';
599
elsif ($line =~ /^\#\#\s+SEE\s+ALSO/) {
600
$section = 'see-also';
602
elsif ($line =~ /^\#\#[^#]/) {
606
# This will be a table containing subcommand names, links to man pages.
607
# The format is slightly different between podman.1.md and subcommands.
608
elsif ($section eq 'commands') {
610
if ($line =~ /^\|\s*\[podman-(\S+?)\(\d\)\]/) {
611
# $1 will be changed by recursion _*BEFORE*_ left-hand assignment
613
$man{$subcmd} = podman_man("podman-$subcmd");
616
# In podman-<subcommand>.1.md
618
elsif ($line =~ /^\|\s+(\S+)\s+\|\s+(\[(\S+)\]\((\S+)\.1\.md\))/) {
619
my ($subcmd, $blob, $shown_name, $link_name) = ($1, $2, $3, $4);
620
if ($previous_subcmd gt $subcmd) {
621
warn "$ME: $subpath:$.: '$previous_subcmd' and '$subcmd' are out of order\n";
624
if ($previous_subcmd eq $subcmd) {
625
warn "$ME: $subpath:$.: duplicate subcommand '$subcmd'\n";
628
$previous_subcmd = $subcmd;
629
$man{$subcmd} = podman_man($link_name);
631
# Check for inconsistencies between the displayed man page name
632
# and the actual man page name, e.g.
633
# '[podman-bar(1)](podman-baz.1.md)
634
$shown_name =~ s/\(\d\)$//;
635
$shown_name =~ s/\\//g; # backslashed hyphens
636
(my $should_be = $link_name) =~ s/\.1\.md$//;
637
if ($shown_name ne $should_be) {
638
warn "$ME: $subpath:$.: '$shown_name' should be '$should_be' in '$blob'\n";
644
# Options should always be of the form '**-f**' or '**\-\-flag**',
645
# possibly separated by comma-space.
646
elsif ($section eq 'flags') {
647
# e.g. 'podman run --ip6', documented in man page, but nonexistent
648
if ($line =~ /^not\s+implemented/i) {
649
delete $man{$_} for @most_recent_flags;
652
@most_recent_flags = ();
653
# As of PR #8292, all options are <h4> and anchored
654
if ($line =~ s/^\#{4}\s+//) {
655
# If option has long and short form, long must come first.
656
# This is a while-loop because there may be multiple long
657
# option names, e.g. --net/--network
659
while ($line =~ s/^\*\*(--[a-z0-9-]+)\*\*(,\s+)?//g) {
662
if ($flag lt $previous_flag && $is_first) {
663
warn "$ME: $subpath:$.: $flag should precede $previous_flag\n";
666
if ($flag eq $previous_flag) {
667
warn "$ME: $subpath:$.: flag '$flag' is a dup\n";
670
$previous_flag = $flag if $is_first;
671
push @most_recent_flags, $flag;
673
# Further iterations of /g are allowed to be out of order,
674
# e.g., it's OK for "--namespace, -ns" to precede --nohead
678
if ($line =~ s/^\*\*(-[a-zA-Z0-9])\*\*//) {
682
# Keep track of them, in case we see 'Not implemented' below
683
push @most_recent_flags, $flag;
686
# Options with no '=whatever'
689
# Anything remaining *must* be of the form '=<possibilities>'
691
warn "$ME: $subpath:$.: could not parse '$line' in option description\n";
695
# For some years it was traditional, albeit wrong, to write
696
# **--foo**=*bar*, **-f**
697
# The correct way is to add =*bar* at the end.
698
if ($line =~ s/,\s\*\*(-[a-zA-Z])\*\*//) {
700
warn "$ME: $subpath:$.: please rewrite as ', **$1**$line'\n";
704
# List of possibilities ('=*a* | *b*') must be space-separated
706
if ($line =~ /[^\s]\|[^\s]/) {
707
# Sigh, except for this one special case
708
if ($line !~ /SOURCE-VOLUME.*HOST-DIR.*CONTAINER-DIR/) {
709
warn "$ME: $subpath:$.: values must be space-separated: '$line'\n";
714
if ($copy =~ s/\**true\**//) {
715
if ($copy =~ s/\**false\**//) {
716
if ($copy !~ /[a-z]/) {
717
warn "$ME: $subpath:$.: Do not enumerate true/false for boolean-only options\n";
725
# --format does not always mean a Go format! E.g., push --format=oci
726
if ($previous_flag eq '--format') {
727
# ...but if there's a table like '| .Foo | blah blah |'
728
# then it's definitely a Go template. There are three cases:
729
# .Foo - Scalar field. The usual case.
730
# .Foo ... - Structure with subfields, e.g. .Foo.Xyz
731
# .Foo ARG(s) - Function requiring one or more arguments
734
if ($line =~ /^\|\s+(\.\S+)(\s+([^\|]+\S))?\s+\|/) {
735
my ($format, $etc) = ($1, $3);
737
# Confirmed: we have a table with '.Foo' strings, so
738
# this is a Go template. Override previous (scalar)
739
# setting of the --format flag with a hash, indicating
740
# that we will recursively cross-check each param.
741
if (! ref($man{$previous_flag})) {
742
$man{$previous_flag} = { _path => $subpath };
745
# ...and document this format option. $etc, if set,
746
# will indicate if this is a struct ("...") or a
749
if ($etc eq '...') { # ok
752
elsif ($etc =~ /^\*[a-z]+\*(\s+\*[a-z]+\*)*$/) {
753
# a function. Preserve only the arg COUNT, not
754
# their names. (command completion has no way
755
# to give us arg names or types).
756
$etc = scalar(split(' ', $etc));
759
warn "$ME: $subpath:$.: unknown args '$etc' for '$format'. Valid args are '...' for nested structs or, for functions, one or more asterisk-wrapped argument names.\n";
764
$man{$previous_flag}{$format} = $etc || 0;
766
# Sort order check, case-insensitive
767
if (lc($format) lt lc($previous_format)) {
768
warn "$ME: $subpath:$.: format specifier '$format' should precede '$previous_format'\n";
772
# Dup check, would've caught #19462.
773
if (lc($format) eq lc($previous_format)) {
774
# Sigh. Allow preexisting exceptions, but no new ones.
775
unless ($Format_Option_Dup_Allowed{$command}{lc $format}) {
776
warn "$ME: $subpath:$.: format specifier '$format' is a dup\n";
780
$previous_format = $format;
783
# Same as above, but with --filter
784
elsif ($previous_flag eq '--filter') {
785
if ($line =~ /^\|\s+(\S+)\s+\|/) {
788
# (Garbage: these are just table column titles & dividers)
789
next LINE if $filter =~ /^\**Filter\**$/;
790
next LINE if $filter =~ /---+/;
792
# Special case: treat slash-separated options
793
# ("after/since") as identical, and require that
794
# each be documented.
795
for my $f (split '/', $filter) {
796
# Special case for negated options ("label!="): allow,
797
# but only immediately after the positive case.
799
if ($f ne $previous_filter) {
800
warn "$ME: $subpath:$.: filter '$f!' only allowed immediately after its positive\n";
806
if (! ref($man{$previous_flag})) {
807
$man{$previous_flag} = { _path => $subpath };
809
$man{$previous_flag}{$f} = 1;
812
# Sort order check, case-insensitive
813
# FIXME FIXME! Disabled for now because it would make
814
# this PR completely impossible to review (as opposed to
815
# only mostly-impossible)
816
#if (lc($filter) lt lc($previous_filter)) {
817
# warn "$ME: $subpath:$.: filter specifier '$filter' should precede '$previous_filter'\n";
821
# Dup check. Yes, it happens.
822
if (lc($filter) eq lc($previous_filter)) {
823
warn "$ME: $subpath:$.: filter specifier '$filter' is a dup\n";
826
$previous_filter = $filter;
831
# It's easy to make mistakes in the SEE ALSO elements.
832
elsif ($section eq 'see-also') {
833
_check_seealso_links( "$subpath:$.", $line );
838
# Done reading man page. If there are any '--format' exceptions defined
839
# for this command, flag them as seen, and as '...' so we don't
840
# complain about any sub-fields.
841
if (my $fields = $Format_Exceptions{$command}) {
842
$man{"--format"}{$_} = '!' for @$fields;
845
# Special case: the 'image trust' man page tries hard to cover both set
846
# and show, which means it ends up not being machine-readable.
847
if ($command eq 'podman-image-trust') {
850
$show{$_} = 1 for qw(--raw -j --json);
851
return +{ set => \%set, show => \%show }
859
# podman_rst # Parse contents of docs/source/*.rst
864
# Read all .rst files, looking for ":doc:`subcmd <target>` description"
865
for my $rst (glob "$Docs_Path/*.rst") {
866
open my $fh, '<', $rst
867
or die "$ME: Cannot read $rst: $!\n";
869
# The basename of foo.rst is usually, but not always, the name of
870
# a podman subcommand. There are a few special cases:
871
(my $command = $rst) =~ s!^.*/(.*)\.rst!$1!;
873
my $subcommand_href = \%rst;
874
if ($command eq 'Commands') {
878
$subcommand_href = $rst{$command} //= { _source => $rst };
881
my $previous_subcommand = '';
882
while (my $line = <$fh>) {
883
if ($line =~ /^:doc:`(\S+)\s+<(.*?)>`\s+(.*)/) {
884
my ($subcommand, $target, $desc) = ($1, $2, $3);
886
# Check that entries are in alphabetical order, and not dups
887
if ($subcommand lt $previous_subcommand) {
888
warn "$ME: $rst:$.: '$previous_subcommand' and '$subcommand' are out of order\n";
891
if ($subcommand eq $previous_subcommand) {
892
warn "$ME: $rst:$.: duplicate '$subcommand'\n";
895
$previous_subcommand = $subcommand;
897
# Mark this subcommand as documented.
898
$subcommand_href->{$subcommand}{_desc} = $desc;
899
$subcommand_href->{$subcommand}{_source} = $rst;
901
# Check for invalid links. These will be one of two forms:
902
# <markdown/foo.1> -> markdown/foo.1.md
904
if ($target =~ m!^markdown/!) {
905
if (! -e "$Docs_Path/$target.md") {
906
warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target\n";
910
my $expect = "markdown/podman-$subcommand.1";
911
if ($subcommand eq 'Podman') {
912
$expect = "markdown/podman.1";
914
if ($target ne $expect) {
915
warn "$ME: $rst:$.: '$subcommand' links to $target (expected '$expect')\n";
920
if (! -e "$Docs_Path/$target.rst") {
921
warn "$ME: $rst:$.: '$subcommand' links to nonexistent $target.rst\n";
930
# Special case: 'image trust set/show' are documented in image-trust.1
931
$rst{image}{trust}{$_} = { _desc => 'ok' } for (qw(set show));
937
# _completions # run podman __complete, return list of completions
940
my $kidpid = open my $podman_fh, '-|';
941
if (! defined $kidpid) {
942
die "$ME: Could not fork: $!\n";
948
exec $PODMAN, '__complete', @_;
949
die "$ME: Could not exec: $!\n";
954
while (my $line = <$podman_fh>) {
956
push @completions, $line;
958
# Recursively expand Go templates, like '{{.Server.Os}}'
959
if ($line =~ /^\{\{\..*\.$/) {
960
my @cmd_copy = @_; # clone of podman subcommands...
961
pop @cmd_copy; # ...so we can recurse with new format
962
my @subcompletions = _completions(@cmd_copy, $line);
964
# A huge number of deep fields are time-related. Don't document them.
965
my @is_time = grep { /Nanosecond|UnixNano|YearDay/ } @subcompletions;
966
push @completions, @subcompletions
967
unless @is_time >= 3;
971
or warn "$ME: Error running podman __complete @_\n";
976
###############################################################################
977
# BEGIN sanity checking of SEE ALSO links
979
##########################
980
# _check_seealso_links # Check formatting and link validity.
981
##########################
982
sub _check_seealso_links {
988
# Line must be a comma-separated list of man page references, e.g.
989
# **foo(1)**, **[podman-bar(1)](podman-bar.1.md)**, **[xxx(8)](http...)**
991
for my $token (split /,\s+/, $line) {
992
# Elements must be separated by comma and space. (We don't do further
993
# checks here, so it's possible for the dev to add the space and then
994
# have us fail on the next iteration. I choose not to address that.)
996
warn "$ME: $path: please add space after comma: '$token'\n";
1001
# Each token must be of the form '**something**'
1002
if ($token !~ s/^\*\*(.*)\*\*$/$1/) {
1003
if ($token =~ /\*\*/) {
1004
warn "$ME: $path: '$token' has asterisks in the wrong place\n";
1007
warn "$ME: $path: '$token' should be bracketed by '**'\n";
1013
# Is it a markdown link?
1014
if ($token =~ /^\[(\S+)\]\((\S+)\)$/) {
1015
my ($name, $link) = ($1, $2);
1016
if ($name =~ /^(.*)\((\d)\)$/) {
1017
my ($base, $section) = ($1, $2);
1018
if (-e "$Markdown_Path/$base.$section.md") {
1019
if ($link ne "$base.$section.md") {
1020
warn "$ME: $path: inconsistent link $name -> $link, expected $base.$section.md\n";
1025
if (! _is_valid_external_link($base, $section, $link)) {
1026
warn "$ME: $path: invalid link $name -> $link\n";
1032
warn "$ME: $path: could not parse '$name' as 'manpage(N)'\n";
1037
# Not a markdown link; it must be a plain man reference, e.g. 'foo(5)'
1038
elsif ($token =~ m!^(\S+)\((\d+)\)$!) {
1039
my ($base, $section) = ($1, $2);
1041
# Unadorned 'podman-foo(1)' must be a link.
1042
if (-e "$Markdown_Path/$base.$section.md") {
1043
warn "$ME: $path: '$token' should be '[$token]($base.$section.md)'\n";
1047
# Aliases (non-canonical command names): never link to these
1048
if (-e "$Markdown_Path/links/$base.$section") {
1049
warn "$ME: $path: '$token' refers to a command alias; please use the canonical command name instead\n";
1053
# Link to man page foo(5) but without a link. This is not an error
1054
# but Ed may sometimes want to see those on a manual test run.
1055
warn "$ME: $path: plain '$token' would be so much nicer as a link\n"
1059
warn "$ME: $path: invalid token '$token'\n";
1065
#############################
1066
# _is_valid_external_link # Tries to validate links to external man pages
1067
#############################
1069
# This performs no actual fetches, so we can't actually check for 404.
1070
# All we do is ensure that links conform to standard patterns. This is
1071
# good for catching things like 'conmon(8)' pointing to a .5 URL, or
1072
# linking to .md instead of .html.
1074
# FIXME: we could actually rewrite this so as to offer hints on what to fix.
1075
# That's a lot of work, and a lot of convoluted code, for questionable ROI.
1077
sub _is_valid_external_link {
1078
my ($base, $section, $link) = @_;
1080
return 1 if $link =~ m!^https://github\.com/\S+/blob/(main|master)(/.*)?/\Q$base\E\.$section\.md!;
1082
return 1 if $link =~ m!^https://.*unix\.com/man-page/(linux|redhat)/$section/$base$!;
1083
return 1 if $link eq "https://man7\.org/linux/man-pages/man$section/$base\.$section\.html";
1085
if ($base =~ /systemd/) {
1086
return 1 if $link eq "https://www.freedesktop.org/software/systemd/man/$base.html";
1089
return 1 if $link eq "https://passt.top/builds/latest/web/passt.1.html";
1097
# END sanity checking of SEE ALSO links
1098
###############################################################################