--- /dev/null
+#!/usr/bin/perl
+#
+# Copyright (c) 2013 Sine Nomine Associates
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
+# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# checkman - run everything in the given directory of binaries, and try to
+# find mismatches between the -help output, and the man page for that command
+
+use strict;
+use warnings;
+
+use Getopt::Long;
+use File::Find;
+
+my $bindir;
+my $mandir;
+
+sub usage {
+ print STDERR "WARNING: Running checkman can be dangerous, as it tries to \n";
+ print STDERR "blindly run almost everything in the given binaries dir.\n\n";
+ print STDERR "Usage: $0 --bindir <binaries_dir> --mandir <manpages_dir>";
+}
+
+GetOptions(
+ "b|bindir=s" => \$bindir,
+ "M|mandir=s" => \$mandir,
+) or die("Error while parsing options\n");
+
+if (not defined($bindir)) {
+ usage();
+}
+if (not defined($mandir)) {
+ usage();
+}
+
+if (not -d $bindir) {
+ die("--bindir $bindir is not a directory\n");
+}
+if (not -d $mandir) {
+ die("--mandir $mandir is not a directory\n");
+}
+if (not -d "$mandir/man1") {
+ die("--mandir must point to a dir containing man1, man8, etc\n");
+}
+
+my %cmd_blacklist = (
+ rmtsysd => '',
+ pagsh => '',
+ 'pagsh.krb' => '',
+ kpwvalid => '',
+ 'afs.rc' => '',
+);
+
+my %cmd_map;
+my $mismatch = 0;
+
+# find a list of all possible commands we can run, and map them to their full
+# path
+find(sub {
+ if (-f and -x and -s) {
+ $cmd_map{$_} = $File::Find::name;
+ }
+}, $bindir);
+
+my %opt_map;
+my @error_cmds;
+
+sub parsehelp($$;$);
+
+sub
+check_opts($$)
+{
+ my ($manstr, $helpout) = @_;
+
+ my %help_opts;
+
+ my %syn_opts;
+ my %man_opts;
+ my %man_just_opts;
+
+ $helpout =~ tr/\n/ /;
+
+ # match everything that looks like an option
+ # basically, find stuff that begins with a hyphen, and is surrounded by
+ # brackets or spaces, or precedes a '='
+ for ($helpout =~ m/(?:\[| )-([a-zA-Z0-9_-]+)(?=\s|[][]|=)/g) {
+ #print " help str $manstr opt -$_\n" if ($manstr =~ /ptserver/);
+ if ($_ eq 'c') {
+ # Almost everything lists '-c' as an alias for '-cell'.
+ # We don't put that in the first synopsis for each man
+ # page, so just pretend it's not there.
+ next;
+ }
+ $help_opts{$_} = 1;
+ }
+
+ my $manout = `man -s 8,1 -M '$mandir' $manstr 2>/dev/null`;
+
+ my $insyn = 0;
+ my $inopts = 0;
+
+ my $syn_sections = 0;
+ my $lastline;
+ my $curline;
+
+ for (split /^/, $manout) {
+ $lastline = $curline if (defined($curline));
+ $curline = $_;
+
+ if (m/^SYNOPSIS$/) {
+ $insyn = 1;
+ $inopts = 0;
+ next;
+ }
+ if (m/^OPTIONS$/) {
+ $insyn = 0;
+ $inopts = 1;
+ next;
+ }
+ if (m/^[A-Z]+$/) {
+ if ($inopts) {
+ # don't need anything after OPTIONS
+ $inopts = 0;
+ last;
+ }
+ $insyn = 0;
+ next;
+ }
+ if (m/^\s+[a-z]/ and $insyn) {
+ $syn_sections++;
+ if ($syn_sections > 1) {
+ # don't need anything in the synopsis after the first area
+ $insyn = 0;
+ next;
+ }
+ }
+
+ if ($insyn) {
+ # check for options in the synopsis...
+ for (m/(?:\[|\s)-([a-zA-Z0-9_-]+)(?=\s|\]|\[)/g) {
+ #print " man page $manstr syn opt -$_\n" if ($manstr =~ /ptserver/);
+ $syn_opts{$_} = 1;
+ }
+ }
+ if ($inopts) {
+ # check for options in the OPTIONS section
+ #print "last: $lastline, cur: $_\n";
+ if ($lastline =~ m/^(\s*|OPTIONS)$/ && m/^\s+-[a-zA-Z0-9_-]+/) {
+ # Options only appear after a blank line (or right after the
+ # OPTIONS line), so only go here if the last
+ # line was blank, and we see what looks like an
+ # option as the first thing on the current
+ # line.
+
+ # Find all options on the current line. Option
+ # aliases can appear on the same =items line,
+ # so get all of the aliases.
+ for (m/\s-([a-zA-Z0-9_-]+)/g) {
+ $man_just_opts{$_} = 1;
+ if (exists $syn_opts{$_}) {
+ # only count them if they also appeared in the synopsis earlier
+ $man_opts{$_} = 1;
+ }
+ }
+ }
+ }
+ }
+
+ if (not %man_opts and not %syn_opts) {
+ # we found no options in the man page output; so probably, we didn't
+ # actually get a man page back. just print a single message, so we don't
+ # print out something for every single option
+ print "man page $manstr missing\n";
+ return;
+ }
+
+ for (keys %help_opts) {
+ if (not exists $man_opts{$_}) {
+ my $extra = '';
+ if (exists $syn_opts{$_}) {
+ $extra = " from OPTIONS";
+ } elsif (exists $man_just_opts{$_}) {
+ $extra = " from synopsis";
+ }
+
+ print "man page $manstr missing option -$_$extra\n";
+ $mismatch = 1;
+ }
+ }
+ my %tmphash = (%syn_opts, %man_just_opts);
+ for (keys %tmphash) {
+ if (not exists $help_opts{$_}) {
+ my $extra = '';
+ if (not exists $syn_opts{$_}) {
+ $extra = " in OPTIONS";
+ } elsif (not exists $man_just_opts{$_}) {
+ $extra = " in synopsis";
+ }
+
+ print "man page $manstr extra option -$_$extra\n";
+ $mismatch = 1;
+ }
+ }
+}
+
+sub
+parsehelp($$;$) {
+ my ($cmd, $path, $subcmd) = @_;
+
+ my $runstr;
+ my $manstr;
+
+ $runstr = $path;
+ $manstr = $cmd;
+
+ if (defined($subcmd)) {
+ $runstr = "$path $subcmd";
+ if ($subcmd ne "initcmd") {
+ $manstr = "$cmd"."_"."$subcmd";
+ }
+ }
+
+ if (defined($cmd_blacklist{$cmd})) {
+ return;
+ }
+
+ my $out = `$runstr -help 2>&1`;
+ if (defined($out)) {
+ if ($out =~ m/^Usage: /) {
+ # actual help output, listing options etc
+ check_opts($manstr, $out);
+ return;
+ }
+
+ if ($out =~ m/Commands are:$/m) {
+ # multi-command program
+ if (defined($subcmd)) {
+ die("Subcommand $cmd $subcmd gave more subcommands?");
+ }
+
+ if ($out =~ m/^initcmd.*initialize the program$/m) {
+ # not actually multi-command; we just need to give the initcmd
+ # pseudo-subcommand
+ parsehelp($cmd, $path, "initcmd");
+ return;
+ }
+
+ # find all of the subcommands, and call parsehelp() on them
+ for (split /^/, $out) {
+ chomp;
+ next if m/Commands are:$/;
+ next if m/^apropos\s/ or m/^help\s/;
+ if (m/^(\S+)\s+[\S ]+$/) {
+ parsehelp($cmd, $path, $1);
+ } else {
+ print "for cmd $cmd got unmatched line $_\n";
+ }
+ }
+
+ return;
+ }
+ }
+
+ if (not defined($subcmd)) {
+ $subcmd = "";
+ }
+
+ print "Skipped command $path $subcmd\n";
+
+ # not sure what to do about this one
+ push @error_cmds, "$path $subcmd";
+}
+
+for my $cmd (keys %cmd_map) {
+ my $path = $cmd_map{$cmd};
+
+ parsehelp($cmd, $path);
+}
+
+exit($mismatch);