#!/usr/bin/env perl
# $Id: check-manpage,v 1.142 2025/07/05 14:18:59 tom Exp $
# -----------------------------------------------------------------------------
# Copyright 2002-2024,2025 by Thomas E. Dickey
#
#                         All Rights Reserved
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Except as contained in this notice, the name(s) of the above copyright
# holders shall not be used in advertising or otherwise to promote the
# sale, use or other dealings in this Software without prior written
# authorization.
# -----------------------------------------------------------------------------
# Scan a directory tree, looking for nroff (man/ms) files, to check their
# syntax as well as verify that their macros are consistent.

# If checknr were portable and handled manpages, it would be useful.

# TODO: utp notes that "echo .pm |nroff -man" gives list of predefinitions
# that could be filtered.  Solaris nroff gives a useful list with the
# predefined characters, groff - not so useful.  To get that, start with
# "man groff_char" for a list.
#
# For example, this is groff syntax
#       =       \[==]        equivalence     u2261       +
# but utp would say this:
#       =       \(==         identically equal
#
# Solaris recognizes these,
#       \(`` \('' \(** \(aa \(*b \(br \(bs \(bu \(da \(de \(dg \(em \(ga
#       \(hy \(lq \(mi \(or \(pd \(rg \(rn \(rq \(sl \(su \(ts \(ua \(ul
# since they are used in macros under
#       /usr/share/lib/tmac/an
#
# but the device tables (in the "charset" sections) recognize far more.
#
# Also, the traditional escapes mentioned in groff_char appear to work:
#       They include `\\', `\'', `\`', `\-', `\.', and `\e'; see groff(7).
#
# TODO: analyze .TH to warn about missing section, date, source

use warnings;
use strict;

use Getopt::Std;
use Cwd;

$| = 1;

our ( $opt_d, $opt_e, $opt_r, $opt_v, $opt_w, $opt_x );
our ( $path_groff, $path_mandoc );
our $groff_options = "-w all -z";
our %externs;
our %predef;
our @predef = (
    ".de bP",
    '.ie n  .IP \(bu 4',
    '.el    .IP \(bu 2',
    '..',
    '.de NE',
    '.fi',
    '.ft R',
    '.ie n  .in -4',
    '.el    .in -2',
    '..',
    '.de NS',
    '.ie n  .sp',
    '.el    .sp .5',
    '.ie n  .in +4',
    '.el    .in +2',
    '.nf',
    '.ft \\*(CW',
    '..',
    ".ie \\n(.g .ds AQ \\(aq",    # state 1
    ".el        .ds AQ '",        # state 2
    ".ie \\n(.g .ds '  \\(aq",    # state 1
    ".el        .ds '  '",        # state 2
    ".ie \\n(.g .ds `` \\(lq",    # state 1
    ".el        .ds `` ``",       # state 2
    ".ie \\n(.g .ds '' \\(rq",    # state 1
    ".el        .ds '' ''",       # state 2

    # alternate form from Branden Robinson which attempts 3 cases
    ".ie \n(.g \\{\\",
    ".ds `` \\(lq",
    ".ds '' \\(rq",
    ".ds '  \\(aq",
    ".\}",
    ".el \\{\\",
    ".ie t .ds `` ``",
    ".el   .ds `` \"\"",
    ".ie t .ds '' ''",
    ".el   .ds '' \"\"",
    ".ie t .ds '  \\(aq",
    ".el   .ds '  '",
    ".\}",

    # Branden Robinson, adjust margins in term.5
    ".ie n .in -2n",    # state 1
    ".el   .in +4n",    # state 2

    # Branden Robinson, glue for URLs
    ".ie \\n(.g .ds : \\:",
    ".el        .ds : \\\" empty",

    # Branden Robinson, for terminfo special characters
    ".ie \\n(.g .ds ^ \\(ha",
    ".el        .ds ^ ^",
    ".ie \\n(.g .ds ~ \\(ti",
    ".el        .ds ~ ~",

    # Branden Robinson, Access monospaced font more portably
    ".ie n .ds CW R",          # state 1
    ".el   \\{",               # state 3 (push)
    ".ie \\n(.g .ds CW CR",    # state 1
    ".el       .ds CW CW",     # state 2
    ".\}",                     # state 4 (pop)

    # Branden Robinson, fallback/redefine EX/EE
    ".if !\\n(.g \\{\\",
    ".de EX",
    ".  br",
    ".  if !\\\\n(mE \\\{\\",
    ".    nr mF \\\\n(.f",
    ".    nr mP \\\\n(PD",
    ".    nr PD 1v",
    ".    nf",
    ".    ft \\\\*(CW",
    ".    nr mE 1",
    ".  \\\}",
    "..",
    ".\}",

    # Branden Robinson, ncurses.3x
    ".de tQ",
    ".  br",
    ".  ns",
    ".  TP",
    "..",

    # Branden Robinson, curs_trace.3x
    ".de dS \\\" Start unfilled display.",
    ".nr aD \\n(.j",
    ".na",
    "..",
    ".de dE \\\" End unfilled display.",
    ".ad \\n(.j",
    ".rr aD",
    "..",

    ".if !\\\n(.g \\\{\\",
    ".de EE",
    ".  br",
    ".  if \\\\n(mE \\\{\\",
    ".    ft \\\\n(mF",
    ".    nr PD \\\\n(mP",
    ".    fi",
    ".    nr mE 0",
    ".  \\\}",
    "..",
    ".\}",

    # Branden Robinson, from an-ext.tmac
    ".de mY",
    ".  ie !\\\\n(.g \\",
    ".    nr mH 14",
    ".  el \\",
    ".    do nr mH \\\\n[.hy] \\\" groff extension register",
    "..",
    ".de mV",
    ".  ds mU \\\\\$1\\\"",
    "..",
    ".de mQ",
    ".  mY",
    ".  nh",
    "<\\\\*(mU>\\\\\$1",
    ".  hy \\\\n(mH",
    ".  rm mU",
    "..",
    ".if !\\n(.g \\{\\",
    ".de UR",
    ".  mV \\\\\$1",
    "..",
    ".\}",
    ".if !\\n(.g \{\\",
    ".de UE",
    ".  mQ \\\\\$1",
    "..",
    ".\}",

    # ded.man
    '.de Es',
    '.ne \\\\$1',
    '.nr mE \\\\n(.f',
    '.RS 5n',
    '.sp .7',
    '.nf',
    '.nh',
    '.ta 9n 17n 25n 33n 41n 49n',
    '.ft \\*(CW',
    '..',
    '.de Eh',
    '.ft \\\\n(mE',
    '.fi',
    '.hy \\\\n(HY',
    '.RE',
    '.sp .7',
    '..',

    # dialog.1, dialog.3
    ".de ES",
    ".ne 8",
    ".IP",
    "..",
    ".de Ex",
    ".RS +7",
    ".PP",
    ".nf",
    '.ft \\*(CW',
    "..",
    ".de Ee",
    ".fi",
    ".ft R",
    ".RE",
    "..",

    # xterm.man
    ".de iP",
    ".br",
    ".if n .sp",
    "..",

    # cdk
    ".de It",
    ".br",
    '.ie \\\\n(.$>=3 .ne \\\\$3',
    ".el .ne 3",
    '.IP "\\\\$1" \\\\$2',
    "..",
    ".de XX",
    "..",

    # libXaw
    ".de TQ",
    ".ns",
    ".TP",
    "..",

    # libXcursor, libXft
    ".de TA",
    ".ie n  .ta 0.8i 1.6i 2.4i 3.2i",
    ".el    .ta 0.5i 1.0i 1.5i 2.0i",
    "..",
    ".de PS",
    ".ns",
    ".TP",
    ".na",
    ".nf",
    ".ie n  .ta 0.8i 3.0i",
    ".el    .ta 0.5i 2.0i",
    "..",
    ".de PC",
    ".sp",
    ".PS",
    "..",
    ".de PE",
    ".br",
    ".ad",
    ".fi",
    ".sp",
    ".TA",
    "..",
    ".de QS",
    ".in +.2i",
    ".nf",
    ".na",
    ".ie n  .ta 1.0i 3.0i",
    ".el    .ta 0.6i 2.0i",
    "..",
    ".de QC",
    ".QS",
    ".ie n  .ta 2.0i 3.0i",
    ".el    .ta 1.6i 2.6i",
    ".ft CR",
    "..",
    ".de QE",
    ".in -.2i",
    ".ft",
    ".fi",
    ".ad",
    ".TA",
    "..",

    # mawk
    ".ie n .ds Pi pi",
    ".el   .ds Pi \\\\(*p",
    ".de SU",
    ".ie n \\\\\$1**(\\\\\$2)\\\\\$3",
    ".el   \\\\\$1\\u\\s-1\\\\\$2\\s+1\\d\\\\\$3",
    "..",

    # blame
    ".de Id",
    ".ds Rv \\\\\$3",
    ".ds Dt \\\\\$4",
    "..",
);

# man2html
our @predef_m2h = (

    # OPTION FLAG MACRO         .Of -x [arg]
    '.de Of',
    '.ie \\\n(.$==1      \%[\|\fB\\\$1\fR\|]',
    '.el .if \\\n(.$==2  \%[\|\fB\\\\$1\fR\0\fI\fI\\\\$2\fR\|]',
    '..',

    # SYNOPSIS START MACRO      .Ss name
    '.de Ss',
    '.na',
    '.nr aA \w\\\\$1\\\\0u',
    '.in +\\\\n(aAu',
    '\'ti -\\\\n(aAu',
    '.ta  \\\\n(aAu',
    '\&\fB\\\\$1\fR\t\c',
    '..',

    # SYNOPSIS END MACRO                .Se
    '.de Se',
    '.ad',
    '.in',
    '..',

    # bullet consistently narrow
    '.de b2',
    '.ie n  .IP \(bu 2',
    '.el    .IP \(bu 2',
    '..',
);

# libX11
our $assume_X11 = 0;
our @predef_X11 = (
    '.de Ds',
    '.nf',
    '.\\\\$1D \\\\$2 \\\\$1',
    '.ft \\*(CW',
    '.\\".ps \\\\n(PS',
    '.\\".if \\\\n(VS>=40 .vs \\\\n(VSu',
    '.\\".if \\\\n(VS<=39 .vs \\\\n(VSp',
    '..',
    '.de De',
    '.ce 0',
    '.if \\\\n(BD .DF',
    '.nr BD 0',
    '.in \\\\n(OIu',
    '.if \\\\n(TM .ls 2',
    '.sp \\\\n(DDu',
    '.fi',
    '..',
    '.de IN		\\" send an index entry to the stderr',
    '..',
    '.de Pn',
    '.ie t \\\\$1\\fB\\^\\\\$2\\^\\fR\\\\$3',
    '.el \\\\$1\\fI\\^\\\\$2\\^\\fP\\\\$3',
    '..',
    '.de ZN',
    '.ie t \\fB\\^\\\\$1\\^\\fR\\\\$2',
    '.el \\fI\\^\\\\$1\\^\\fP\\\\$2',
    '..',
    '.de hN',
    '.ie t <\\fB\\\\$1\\fR>\\\\$2',
    '.el <\\fI\\\\$1\\fP>\\\\$2',
    '..',

    # adapted from NS/NE, probably different from groff
    '.de EX',
    '.sp',
    '.nf',
    '.ft \\*(CW',
    '..',
    '.de EE',
    '.ft R',
    '.fi',
    '.sp',
    '..',
);

# libXt
our $assume_Xt = 0;
our @predef_Xt = (
    '.de De',
    '.ce 0',
    '.fi',
    '..',
    '.de Ds',
    '.nf',
    '.in +0.4i',
    '.ft \\*(CW',
    '..',
    '.de IN		\" send an index entry to the stderr',
    '..',
    '.de Pn',
    '.ie t \\\$1\fB\^\\\$2\^\fR\\\$3',
    '.el \\\$1\fI\^\\\$2\^\fP\\\$3',
    '..',
    '.de ZN',
    '.ie t \fB\^\\\$1\^\fR\\\$2',
    '.el \fI\^\\\$1\^\fP\\\$2',
    '..',
    '.de ny',
    '..',
);

our $assume_Cdk = 1;

sub read_file($) {
    my $path = shift;
    my @result;
    if ( open my $fh, $path ) {
        @result = <$fh>;
        close $fh;
        for my $n ( 0 .. $#result ) {
            chomp $result[$n];
        }
    }
    return @result;
}

sub IsComment($) {
    my $text   = shift;
    my $result = 0;
    $result = 1 if ( $text =~ /^\s*\.\s*\\"/ );
    return $result;
}

sub IsCommand($) {
    my $text   = shift;
    my $result = 0;
    $result = 1 if ( $text =~ /^\.\s*[[:alpha:]][[:alnum:]]([^[:alnum:]])?/ );
    return $result;
}

sub CommandName($) {
    my $text = shift;
    $text =~ s/^.\s*([[:alpha:]][[:alnum:]]).*/$1/;
    return $text;
}

sub CommandArgs($) {
    my $text = shift;
    $text =~ s/^.\s*([[:alpha:]][[:alnum:]])\s*//;
    return $text;
}

sub MacroName($) {
    my $text = shift;
    $text =~ s/^(\.de)\s*([^\s]{1,2})\s.*/$1 $2/;
    return $text;
}

sub StringName($) {
    my $text = shift;
    $text =~ s/^.*\s(\.ds)\s*([^\s]{1,2})\s.*/$1 $2/;
    return $text;
}

sub macro_name($) {
    my $text   = shift;
    my $result = "";
    if ( $text =~ /^\.\s*[^\s]{1,2}\b/ ) {
        $result = $text;
        $result =~ s/^\.\s*//;
        $result =~ s/\s.*$//;
    }
    return $result;
}

# Scan the file, looking the NAME section, and ensuring that it contains a "\-"
# marker for the description on the final line before the SYNOPSIS section.
# In the aliases, we should have only names, with fonts and separated by
# commas.  The final-line issue is to work with ncurses manlinks.sed, because
# collapsing newlines in a range presents unwanted development work.
sub check_aliases($$$) {
    my $path  = $_[0];
    my $type  = $_[1];
    my @lines = @{ $_[2] };
    my $state = 0;
    my $synopsis;

    for my $n ( 0 .. $#lines ) {
        my $text = $lines[$n];
        $synopsis = 1 if ( $text =~ /\bSH\s+SYNOPSIS\b/ );
        next if ( $text =~ /^\.\s*\\"/ );
        next if ( $text =~ /^\.[[:lower:]]{2}/ );
        $text =~ s/\\f.//g;
        $text =~ s/\\%//g;
        $text =~ s/^\.[BIR]+\s+//;
        if ( index( $text, ".SH" ) == 0 ) {
            $text =~ s/^[^\s]*\s+([^\s]+)/$1/;
            if ( $text eq "NAME" ) {
                $state = 1;
            }
            elsif ( $state != 0 ) {
                if ( $text ne "SYNOPSIS" and not $synopsis ) {
                    printf "%s:%d: expected SYNOPSIS after NAME section\n",
                      $path, $n + 1;
                }
                return;
            }
            else {
                last;
            }
        }
        elsif ( $state == 1 ) {
            if ( $text =~ /\\-/ ) {
                printf "%s:%d: split after \"\\-\" \n", $path, $n + 1
                  if ( $text =~ /\s\\-.+/ );
                $state = 2;
            }
            elsif ( $text =~ /-/ ) {
                printf "%s:%d: expected \"\\-\"\n", $path, $n + 1;
            }
            $text =~ s/\\-/-/;
            $text =~ s/\s*-.*/,/;
            if ($assume_Cdk) {
                next if ( $text =~ /^#(if|endif)/ );
                $text =~ s/\<(FLOAT|MIXED|MODEL)\>/$1/;
                $text =~ s/^\.XX\s+(\w+)$/$1,/;
            }
            printf "%s:%d: bad alias: %s\n", $path, $n + 1, $text
              unless ( $text =~ /^[[:alnum:]_@]+,$/ );
        }
        elsif ( $state >= 2 ) {
            $state++;
            printf
              "%s:%d: expected \"\\-\" on/before final line of NAME section\n",
              $path, $n + 1
              if ( $state > 3 );
        }
    }
}

# Scan the file, looking for mismatches between parameter counts
sub check_pcounts($$$$) {
    my $path   = $_[0];
    my $type   = $_[1];
    my @lines  = @{ $_[2] };
    my @nofill = @{ $_[3] };
    my $asis   = 0;

    for my $n ( 0 .. $#lines ) {
        $asis |= 1 if ( $lines[$n] =~ /^\.nf/ );
        $asis |= 2 if ( $lines[$n] =~ /^\.na/ );
        $asis |= 4 if ( $lines[$n] =~ /^\.TS/ );
        $asis &= ~4 if ( $lines[$n] =~ /^\.TE/ );
        $asis &= ~2 if ( $lines[$n] =~ /^\.ad/ );
        $asis &= ~1 if ( $lines[$n] =~ /^\.fi/ );
        if (    ( $opt_v or not &IsComment( $lines[$n] ) )
            and $asis == 0
            and ( $n == 0 or $lines[ $n - 1 ] !~ /^\s*\.\s*SH\s+NAME\b/ )
            and ( length $lines[$n] ) > $opt_w )
        {
            my $strip = $lines[$n];
            $strip =~ s/\\f[RIBP]//g;
            $strip =~ s/\\\*\(.././g;
            $strip =~ s/\\\*\././g;
            $strip =~ s/\\././g;
            $strip =~ s/@[[:alnum:]_]+@/X/g;
            if ( $strip ne $lines[$n] ) {
                printf "%s:%s: line longer than %d columns (%d formatted):\n"
                  . "\t$strip\n", $path, $n + 1, $opt_w, length($strip)
                  if ( length($strip) > $opt_w );
            }
            else {
                printf "%s:%s: line longer than %d columns (%d)\n", $path,
                  $n + 1, $opt_w, length($strip)
                  if ( length($strip) > $opt_w );
            }
        }
        if (    not &IsComment( $lines[$n] )
            and $nofill[$n] == 0
            and ( $asis & 5 ) == 0
            and index( $lines[$n], "." ) > 1 )
        {
            my $col = index( $lines[$n], ". " );
            printf "%s:%s:%s: embedded sentence ending\n", $path, $n + 1,
              $col + 1
              if (
                    $col > 1
                and ( substr( $lines[$n], $col - 1, 1 ) !~ /[[:upper:]]/ )
                and ( substr( $lines[$n], $col - 2, 2 ) ne ".." )
                and ( substr( $lines[$n], 0,        $col ) !~ /\b[p]{1,2}/i
                    and ( substr( $lines[$n], $col + 1 ) !~ /\s*\d+/ ) )
              );
        }
        my $minc = -1;
        my $maxc = -1;
        my $name = &macro_name( $lines[$n] );
        if ( $lines[$n] =~ /^\.\s*(BR|BI|IB|IR|RB|RI|It)\b/ ) {
            $minc = 2;
            $maxc = 8;
        }
        elsif ( $lines[$n] =~ /^\.\s*/ ) {
            if ( $predef{ ".de " . $name } ) {
                my %obj = %{ $predef{ ".de " . $name } };
                $minc = $obj{MINC};
                $maxc = $obj{MAXC};
            }
        }
        if ( $maxc > 0 ) {
            my $actual = 0;
            my $text   = $lines[$n];
            $text =~ s/^\.\s*[[:alpha:]]+\b\s*//;
            $text =~ s/\s*$//;
            $text =~ s/\s+/ /g;
            if ( ( my $off = index $text, "\\\"" ) >= 0 ) {
                $text = substr( $text, 0, $off );
            }
            while ( $text ne "" ) {
                my $l = $text;
                $actual++;
                if ( $l =~ /^"/ ) {
                    my $s = substr $l, 1;
                    my $n = index $s, '"';
                    $text = substr $text, $n + 2;
                }
                else {
                    $l =~ s/\s.*//;
                    $text = substr $text, length($l);
                }
                $text =~ s/^\s+//;
                last if ( $text eq "" );
            }
            if ( $actual > $maxc or $minc > $actual ) {
                printf "%s:%d: have %d parameter%s for $name, expected %s %d\n",
                  $path, $n + 1, $actual, ( ( $actual == 1 ) ? "" : "s" ),
                  ( $actual > $maxc ) ? "no more than" : "at least",
                  ( $actual > $maxc ) ? $maxc          : $minc;
            }
        }
    }
}

# Scan the file, looking for places to optimize font-switching.  Warn about
# cases where a bold/italic font is left dangling at the end of a line.
sub check_fonting($$$) {
    my $path    = $_[0];
    my $type    = $_[1];
    my @lines   = @{ $_[2] };
    my $changes = 0;
    my $section = "";

    for my $n ( 0 .. $#lines ) {
        my $font = "R";
        my $safe = 0;
        if ( $lines[$n] =~ /^\.SH\b/ ) {
            $section = $lines[$n];
            $section =~ s/^[^\s]+\s+//;
            $section =~ s/\s.*//;
            next;
        }
        if ( $lines[$n] =~ /^\.[RIB]([RI])?\b.*\w[.,;]$/ ) {
            printf "%s:%d: expected separate parameter: %s\n", $path, $n + 1,
              $lines[$n];
        }
        if ( $lines[$n] =~ /^\.B\b/ ) {
            $font = "B";
            $safe = 1;
        }
        elsif ( $lines[$n] =~ /^\.I\b/ ) {
            $font = "I";
            $safe = 1;
        }
        elsif ( $lines[$n] =~ /^\.ft\b/ ) {
            my $check = $lines[$n];
            $check =~ s/^\.ft\s+//;
            if ( $check =~ /^C[RW]\b/ ) {

                # ignore
            }
            elsif ( $check =~ /\\\*\(C[IW]/ ) {

                # ignore
            }
            elsif ( $check eq ".ft" ) {

                # ignore
            }
            elsif ( $check !~ /^[RIB]/ ) {
                printf "%s:%d: unexpected font %s\n", $path, $n + 1, $check;
            }
            next;
        }
        elsif ( $lines[$n] =~ /^\./ ) {
            next;    # ignore other cases
        }

        # Skip the line unless it contains font escapes.
        next unless ( $lines[$n] =~ /\\f[RIBP]/ );
        my $update = $lines[$n];
        $update =~ s/\\f[RP](\\f[BI])/$1/g;
        if ( $update ne $lines[$n] ) {
            $update =~ s/\\fP/\\fR/g;
            printf "%s:%d: shorten %d to %d\n", $path, $n + 1,
              length( $lines[$n] ), length($update);
            printf "<\t%s\n", $lines[$n];
            printf ">\t%s\n", $update;
            ++$changes;
        }

        # Macros that set a font for a line reset it at the end.
        # If ordinary text, look for dangling font.
        if ( $safe == 0 ) {
            my $done = 1;
            for my $n2 ( $n .. $#lines ) {
                my $check = $lines[$n2];
                last if ( $done == 1 && $check !~ /\\f[RIBP]/ );
                $check =~ s/^.*(\\f[RIBP])/$1/;
                $done = ( $check =~ /\\f[IB]/ ) ? 0 : 1;
                next if ( $check =~ /\\$/ );

                # If the next line sets a font, pretend it is not dangling.
                for my $n3 ( $n2 + 1 .. $#lines ) {
                    my $check2 = $lines[$n3];
                    $done = 1 if ( $check2 =~ /^\s*\\f[RIB]/ );
                    $done = 1
                      if ( $check2 =~ /^.(B|I|BI|BR|IB|IR|RB|RI|SS|SH)\b/ );
                    last unless ( $check2 =~ /^\.\w/ );
                }
                last if ( $done == 0 );
            }
            if ( $done == 0 ) {
                printf "%s:%d: dangling font: %s\n", $path, $n + 1, $lines[$n];
            }
        }

        # Check consistency in the section with function prototypes.
        # Bold: "();," and possibly whitespace
        # Normal: "[]" and whitespace
        # Other: italics (parameters) or bold (literal) but no whitespace
        if ( $section eq "SYNOPSIS" ) {
            my $parse = $lines[$n];
            my $shift = 0;
            if ( $parse !~ /^\s*\\f/ ) {
                if ( $parse =~ /^\.$font\s+"[^"]+"$/ ) {
                    $parse =~ s/^\.$font\s+"([^"]+)"/\\f$font$1/;
                }
                elsif ( $parse =~ /^\.$font\s+.+$/ ) {
                    $parse =~ s/^\.$font\s+(.+)/\\f$font$1/;
                }
            }

            # now $parse is in a form that lets us check for font at each step
            if ( $parse =~ /^\\f[RB]/ ) {
                my $n3 = 0;
                my $f1 = "R";
                my $f2 = "R";
                my $f3 = "";

                #printf "TEST:%s\n", $parse;
                for my $n2 ( 0 .. length($parse) - 1 ) {
                    next unless ( $n2 == $n3 );
                    my $c3  = substr( $parse, $n2, 3 );
                    my $row = $n + 1;
                    my $col = $n2 + 1 - $shift;
                    if ( $c3 =~ /^\\f[RIBP]$/ ) {
                        $n3 = $n2 + 3;
                        $f3 = substr( $c3, 2, 1 );
                        $f3 = $f1 if ( $f3 eq "P" );
                        $f1 = $f2;
                        $f2 = $f3;
                    }
                    elsif ( $c3 =~ /^\/\*/ ) {
                        $c3 = substr( $parse, $n2 );
                        my $c4 = index( $c3, '*/' );
                        if ( $c4 <= 0 ) {
                            printf "%s:%d:%d: dangling comment\n",
                              $path, $row, $col, $n2 + 1;
                            last;
                        }
                        $c4 += 2;
                        $c3 = substr( $c3, 0, $c4 );
                        if ( $f3 ne "I" ) {
                            printf "%s:%d:%d: expected italic, have $f3\n",
                              $path, $row, $col;
                        }
                        elsif ( index( $c3, '\fB' ) > 0 ) {

                            # see curs_variables.3x
                            printf "%s:%d:%d: unexpected font in comment\n",
                              $path, $row, $col;
                        }
                        $n3 = $n2 + $c4;
                    }
                    else {
                        $n3 = $n2 + 1;
                        my $c1 = substr( $parse, $n2, 1 );
                        if ( $f3 eq "I" ) {
                            printf "%s:%d:%d: unexpected italic \"%s\"\n",
                              $path, $row, $col, $c1
                              if ( $c1 =~ /^[();,*]$/ );
                        }
                        elsif ( $f3 eq "B" ) {
                            printf "%s:%d:%d: unexpected bold \"%s\"\n", $path,
                              $row, $col, $c1
                              if ( $c1 =~ /^[[]]$/ );
                        }
                        elsif ( $f3 eq "R" ) {
                            printf "%s:%d:%d: unexpected regular \"%s\"\n",
                              $path, $row, $col, $c1
                              if ( $c1 =~ /^[();,*]$/ );
                        }
                    }
                }
            }
        }
        $lines[$n] = $update if ( $opt_x and $changes > 0 );
    }
    if ( $opt_x and $changes > 0 ) {
        my $newfile = $path . ".new";
        open( my $fh, ">", $newfile ) or die "cannot open $newfile $!";
        for my $n ( 0 .. $#lines ) {
            printf $fh "%s\n", $lines[$n];
        }
        close $fh;
        rename $newfile, $path;
    }
}

# Scan the file, looking for fake quotes:
#       ``quote''
#       `quote'
sub check_dquotes($$$) {
    my $path  = $_[0];
    my $type  = $_[1];
    my @lines = @{ $_[2] };
    for my $n ( 0 .. $#lines ) {
        next if ( $lines[$n] =~ /^\./ );
        my $ref = $lines[$n];
        $ref =~ s/\\\*\(``//g;
        $ref =~ s/\\\*\(''//g;
        $ref =~ s/\\\*\`//g;
        $ref =~ s/\\\*\'//g;
        if ( $ref =~ /``[^']*''/ ) {
            printf "%s:%d: ``fake quotes''\n", $path, $n + 1;
        }
        elsif ( $ref =~ /`[^']*'/ ) {
            printf "%s:%d: `fake quotes'\n", $path, $n + 1;
        }
    }
}

# Check section-order.  Some of this comes from Linux man-pages(7), but I use
# additional sections because DESCRIPTION is too limiting.
sub check_section($$$) {
    my $path  = $_[0];
    my $type  = $_[1];
    my @lines = @{ $_[2] };
    my $state = 0;

    return unless ( $type eq "man" );

    my $part = $path;
    $part =~ s/^.*\.(\d+).*$/$1/;
    $part = 1 unless ( $part =~ /^\d/ );

    my @want;
    $want[ $#want + 1 ] = "NAME";
    $want[ $#want + 1 ] = "SYNOPSIS";
    $want[ $#want + 1 ] = "CONFIGURATION" if ( $part == 4 );
    $want[ $#want + 1 ] = "DESCRIPTION";
    $want[ $#want + 1 ] = "OPTIONS" if ( $part =~ /^[168]$/ );
    if ( $part == 1 ) {    #mine
        $want[ $#want + 1 ] = "RUN-TIME CONFIGURATION";
        $want[ $#want + 1 ] = "KEY BINDINGS";
    }
    elsif ( $part == 2 or $part == 3 ) {    #mine
        $want[ $#want + 1 ] = "CONSTANTS";
        $want[ $#want + 1 ] = "PREDEFINED TYPES";
        $want[ $#want + 1 ] = "VARIABLES";
        $want[ $#want + 1 ] = "FUNCTIONS";
    }
    if ($assume_Cdk) {
        $want[ $#want + 1 ] = "STANDARD WIDGET BEHAVIOR";
        $want[ $#want + 1 ] = "AVAILABLE FUNCTIONS";
        $want[ $#want + 1 ] = "KEY BINDINGS";
    }
    $want[ $#want + 1 ] = "EXIT STATUS"  if ( $part == 1 or $part == 8 );
    $want[ $#want + 1 ] = "DIAGNOSTICS"  if ( $part == 1 or $part == 8 ); # mine
    $want[ $#want + 1 ] = "RETURN VALUE" if ( $part == 2 or $part == 3 );
    $want[ $#want + 1 ] = "ERRORS"       if ( $part == 2 or $part == 3 );
    $want[ $#want + 1 ] = "ENVIRONMENT";
    $want[ $#want + 1 ] = "ALTERNATE CONFIGURATIONS"
      if ( $part == 2 or $part == 3 );
    $want[ $#want + 1 ] = "FILES";
    $want[ $#want + 1 ] = "VERSIONS"   if ( $part == 3 or $part == 3 );
    $want[ $#want + 1 ] = "ATTRIBUTES" if ( $part == 3 or $part == 3 );
    $want[ $#want + 1 ] = "CONFORMING TO";
    $want[ $#want + 1 ] = "NOTES";
    $want[ $#want + 1 ] = "EXTENSIONS";                                   # mine
    $want[ $#want + 1 ] = "PORTABILITY";                                  # mine
    $want[ $#want + 1 ] = "COMPATIBILITY";                                # mine
    $want[ $#want + 1 ] = "HISTORY";                                      # mine
    $want[ $#want + 1 ] = "CAVEATS";
    $want[ $#want + 1 ] = "BUGS";
    $want[ $#want + 1 ] = "EXAMPLES";
    $want[ $#want + 1 ] = "AUTHORS";
    $want[ $#want + 1 ] = "SEE ALSO";

    my @have;
    my $last = -1;

    for my $n ( 0 .. $#lines ) {
        my $line = $lines[$n];
        if ( $line =~ /^\.SS/ ) {
            $line =~ s/^\.SS\s+//;
            next if ( $line =~ /^".*"$/ );
            printf "%s:%d: expected quoted subsection string\n", $path, $n + 1
              if ( $line =~ /[^[:alnum:]_.-]/ );
            next;
        }
        elsif ( $line !~ /^\.SH\b/ ) {
            next;
        }
        $line =~ s/^\.SH\s+//;
        my $save = $line;
        $line =~ s/"//g;
        if ( $save !~ /^".*"$/ ) {
            printf "%s:%d: expect quoted section string\n", $path, $n + 1
              if ( $line =~ /[^[:upper:][:digit:] _.-]/ );
        }
        $have[ $#have + 1 ] = $line;
        my $have = -1;
        for my $p ( 0 .. $#want ) {
            if ( $want[$p] eq $line ) {
                $have = $p;
                last;
            }
        }
        if ( $have < 0 ) {
            printf "%s:%d: unexpected section $line\n", $path, $n + 1;
        }
        else {
            if ( $last >= 0 ) {
                printf "%s:%d: expected section %s before %s\n", $path, $n + 1,
                  $line, $have[ $#have - 1 ]
                  if ( $have < $last );
            }
            $last = $have;
        }
    }
}

# Scan the file, looking for strings defined using ".ds", and for references
# to strings.  groff will warn about undefined strings if the reference is
# correctly formatted, but ignore some misformatted cases:
sub check_strings($$$) {
    my $path  = $_[0];
    my $type  = $_[1];
    my @lines = @{ $_[2] };
    my %where;
    my %usage;
    my %builtin;
    my $state = 0;

    # The macros are preferred to the builtin, since the latter interfere
    # with editing (because the word boundaries are merged).
    if ( not $opt_v ) {
        $builtin{lq} = 0;
        $builtin{rq} = 0;
        $builtin{R}  = 0;
    }

    for my $n ( 0 .. $#lines ) {
        next if ( $lines[$n] =~ /^\.\s*\\"/ );
        if ( $lines[$n] =~ /^\.de\s/ ) {
            $state = 1;
        }
        elsif ( $state != 0 ) {
            $state = 0 if ( $lines[$n] =~ /^\.\./ );
        }
        elsif ( $lines[$n] =~ /\.\s*ds\s+../ ) {
            my $name = $lines[$n];
            $name =~ s/^.*\.\s*ds\s+//;
            $name =~ s/\s.*//;
            printf "string %d{%s}:%s\n", $n + 1, $name, $lines[$n] if ($opt_d);
            $where{$name} = $n + 1;
            $usage{$name} = 0;
            my $value = $lines[$n];
            $value =~ s/^\.ds\s+//;

            if ( $value =~ /^tk\s+X Toolkit$/ ) {
                if ( $assume_Xt++ == 0 ) {
                    %predef =
                      &find_macros( "<predefined>", \@predef_Xt, \%predef );
                }
            }
            elsif ( $assume_Xt > 0 ) {

                # ignore the X11 case
            }
            elsif ( $value =~ /^xT\s+X Toolkit Intrinsics.*/ ) {
                if ( $assume_X11++ == 0 ) {
                    %predef =
                      &find_macros( "<predefined>", \@predef_X11, \%predef );
                }
            }
            $usage{$name} = 1 if ( $assume_Xt or $assume_X11 );
        }
        elsif ( $type eq "ms" and $lines[$n] =~ /^\.\[\]\s/ ) {
            my $name = $lines[$n];
            $name =~ s/^.*\.\[\]\s+//;
            $name =~ s/\s.*//;
            printf "string %d{%s}:%s\n", $n + 1, $name, $lines[$n] if ($opt_d);
            $where{$name} = $n + 1;
            $usage{$name} = 0;
        }
        else {
            my $text = $lines[$n];
            my $name;
            my $find;
            my $s;
            if ( $text =~ /^\.[[:alpha:]].*/ ) {
                $find = '\\*';
            }
            elsif ( $text =~ /^\..*/ ) {
                next;
            }
            else {
                $find = '\*';
            }
            printf "XXX %s\n", $text if ($opt_d);
            while ( ( $s = ( index $text, $find ) ) >= 0 ) {
                $text = substr $text, $s + length($find);
                printf "GOT %s\n", $text if ($opt_d);
                if ( $text =~ /^\(..*/ ) {
                    $name = substr $text, 1, 2;
                }
                else {
                    $name = substr $text, 0, 1;
                }
                if ( defined $usage{$name} ) {
                    $usage{$name}++;
                }
                elsif ( defined $builtin{$name} ) {
                    $builtin{$name}++;
                }
                else {
                    printf "%s:%d: undefined string %s\n", $path, $n + 1, $name;
                }
            }
        }
    }
    for my $key ( sort keys %usage ) {
        printf "%s:%d: unused string definition '%s'\n", $path, $where{$key},
          $key
          if ( $usage{$key} == 0 );
    }
}

# Scan the file, looking for repeated words (delimited by blanks or any
# punctuation other than "_").
sub check_stutter($$$) {
    my $path  = $_[0];
    my $type  = $_[1];
    my @lines = @{ $_[2] };
    my $last  = "";

    for my $n ( 0 .. $#lines ) {
        my $text = $lines[$n];
        $text =~ s/([.,;?!:\(\)])/$1 /g;
        my @words = split /[\s\t`~@#$%^&+={}|\\\[\]'"<>\/-]+/, $text;
        next if ( $#words < 0 );
        for my $w ( 0 .. $#words ) {
            if ( length( $words[$w] ) <= 2
                and $words[$w] !~ /^(be|in|to|of|a|an|do|so|as|is|it|no|or)$/i )
            {
                # printf "SKIP %s\n", $words[$w];
            }
            elsif ( $words[$w] !~ /^[[:alpha:]]+$/ ) {

                # ignore numbers
            }
            elsif ( $words[$w] eq $last ) {
                printf "%s: %d: repeated \"%s\"\n", $path, $n + 1, $last;
            }
            $last = $words[$w];
        }
    }
}

# Scan the file, looking for patterns like
#       \fBfoo(1)\fP
# which should be
#       \fBfoo\fP(1)
sub check_externs($$$) {
    my $path  = $_[0];
    my $type  = $_[1];
    my @lines = @{ $_[2] };
    for my $n ( 0 .. $#lines ) {
        if (
            $lines[$n] =~ /\\f[BI]
                           (\\%)?[[:alnum:]_-]+
                           \([1-9][[:alnum:]]*\)
                           \\f[PR]/x
            or $lines[$n] =~ /\\f[RP]
                           (\\%)[[:alnum:]_-]+
                           \\f[BIPR]
                           \([1-9][[:alnum:]]*\)/x
          )
        {
            printf "%s:%d: BAD link: %s\n", $path, $n + 1, $lines[$n];
        }
        if (
            $lines[$n] =~ /\\fI
                           (\\%)?[[:alnum:]_-]+
                           \\f[PR]
                           \([1-9][[:alnum:]]*\)/x
          )
        {
            my $line = $lines[$n];
            while ( $line ne "" ) {
                my $find = index( $line, "\\fI" );
                last if ( $find < 0 );
                $line = substr( $line, $find );
                my $last = index( $line, ")" );
                last if ( $last < 0 );
                my $part = substr( $line, 0, $last + 1 );
                if (
                    $part =~ /^\\fI
                              (\\%)?[[:alnum:]_-]+
                              \\f[PR]
                              \([1-9][[:alnum:]]*\)/x
                  )
                {
                    printf "%s:%d: prefer bold link: %s\n", $path, $n + 1, $part
                      unless ( defined $externs{$part} );
                    $line = substr( $line, $last + 1 );
                }
                else {
                    $line =~ s/^\\fI//;
                }
            }
        }
    }
}

# Return a hash on "de XX" or "ds XX", which in turn points to hashes with
# these keys:
#   LINE - the beginning line-number of the macro/string definition
#   DATA - an array of the contents of the macro/string definition.
#   MINC - minimum expected number of parameters
#   MAXC - maximum expected number of parameters
sub find_macros($$$) {
    my $path   = $_[0];
    my @lines  = @{ $_[1] };
    my %result = %{ $_[2] };
    my $state  = 0;
    my $first  = -1;
    my $named  = "";
    my $prior  = "";
    my $cname  = "";
    my $lists  = 0;
    my @stack;
    my $stack = 0;

    for my $n ( 0 .. $#lines ) {
        my $saved = $state;
        if ( $lines[$n] =~ /^\.de\s/ ) {
            $named = &MacroName( $lines[$n] );
            $state = 1;
            $first = $n;
        }
        elsif ( $lines[$n] =~ /^\.\.(\s.*$)?/ ) {
            if ( $state == 1 ) {
                printf "\tMACRO($named): %d..%d\n", $first + 1, $n + 1
                  if ($opt_v);
                my @result;
                my $minc = -1;
                my $maxc = -1;
                for my $r ( $first .. $n ) {
                    my $text = $lines[$r];
                    my $eqls = ( $text =~ /\.\$[!=]=/ );
                    $result[ $r - $first ] = $text;
                    while ( $text =~ /\$\d/ ) {
                        $text =~ s/^[^\$]\$*//;
                        if ( $text =~ /^\d+/ ) {
                            my $value = $text;
                            $text  =~ s/^\d+//;
                            $value =~ s/[^\d].*//;
                            $minc = $value
                              if (  $value > $minc
                                and $text ne ""
                                and not $eqls );
                            $maxc = $value if ( $value > $maxc );
                        }
                    }
                }
                my %obj;
                $obj{LINE}      = $first;
                $obj{DATA}      = \@result;
                $obj{MINC}      = $minc;
                $obj{MAXC}      = $maxc;
                $result{$named} = \%obj;
            }
            $state = 0;
            $named = "";
        }
        elsif ( $lines[$n] =~ /\s\.nr\s+LL\b/ ) {

            # ignore ctlseqs.ms changing line-length
            $state = 0;
            $named = "";
        }
        elsif ( $lines[$n] =~ /^\.ie\s.*\.ds\s/ ) {
            if ( $state == 0 ) {
                $named = &StringName( $lines[$n] );
                $state = 2;
                $first = $n;
            }
        }
        elsif ( $lines[$n] =~ /^\.el\s+.*\\\{/ ) {
            $stack[ $stack++ ] = $state;
            $state = 0;
        }
        elsif ( $lines[$n] =~ /^\.\\\}/ ) {
            if ( $stack > 0 ) {
                $state = $stack[ --$stack ];
                $state = 0 if ( $state == 2 );
            }
            else {
                # OOPS
            }
        }
        elsif ( $lines[$n] =~ /^\.el\s.*\.ds\s/ ) {
            if ( $state == 2 ) {
                printf "\tSTRING($named): %d..%d\n", $first + 1, $n + 1
                  if ($opt_v);
                my @result;
                for my $r ( $first .. $n ) {
                    $result[ $r - $first ] = $lines[$r];
                }
                my %obj;
                $obj{LINE}      = $first;
                $obj{DATA}      = \@result;
                $result{$named} = \%obj;
                $state          = 0;
                $named          = "";
            }
            elsif ( $state != 1 ) {
                printf "%s:%d: unexpected state\n", $path, $n + 1;
                printf "\t%s\n", $lines[$n];
            }
        }
        elsif ( $state == 2 and $lines[$n] =~ /^\.(ie|el)\b/ ) {
            printf "%s:%d: unexpected state\n", $path, $n + 1;
            printf "\t%s\n", $lines[$n];
        }
        elsif ( $lines[$n] =~ /^\.St/ ) {
            $lists++;
        }
        elsif ( $lines[$n] =~ /^\.Ed/ ) {
            $lists--;
        }
        if ( $state == 2 and $first != $n ) {
            printf "%s:%d: expected .el after this\n", $path, $n + 1;
        }
        if ( $state == 0 ) {
            if ( &IsCommand( $lines[$n] ) ) {
                $cname = &CommandName( $lines[$n] );
                if ( $cname eq "IP" ) {
                    printf "%s:%d: expected .iP first\n", $path,
                      $n + 1
                      if (
                            $result{".de iP"}
                        and ( $lists > 0 )
                        and ( $n <= 0 or ( $prior ne "iP" and $prior ne "St" ) )
                      );
                }
                elsif ( $cname eq "iP" ) {
                    printf "%s:%d:redundant .iP\n", $path, $n + 1
                      if ( $prior eq "St" or $prior eq "sP" );
                }
                $prior = $cname;
            }
            elsif ( $lines[$n] !~ /^\./ ) {
                $prior = "";
            }
        }
        printf "%s:%d:%s\n", $path, $n + 1, $lines[$n]
          if ( $opt_v and ( $state != 0 and $saved == 0 ) );
    }
    return %result;
}

sub find_nofill_macros( $$$ ) {
    my $result = $_[0];
    my $macro  = $_[1];
    my %macros = %{ $_[2] };
    my %object = %{ $macros{$macro} };
    if ( not defined $object{FILL} ) {
        my $nofill = 0;
        my @lines  = @{ $object{DATA} };
        for my $n ( 0 .. $#lines ) {
            if ( $lines[$n] =~ /^\.fi\b/ ) {
                $nofill--;
            }
            elsif ( $lines[$n] =~ /^\.nf\b/ ) {
                $nofill++;
            }
        }
        $object{FILL} = $nofill;
        $macros{DATA} = \%object;
    }
    $result += $object{FILL};
    return $result;
}

sub find_nofill_lines( $$$ ) {
    my $path   = $_[0];
    my @lines  = @{ $_[1] };
    my %macros = %{ $_[2] };
    my @result;
    my $nofill = 0;
    my $macdef = 0;
    for my $n ( 0 .. $#lines ) {
        $result[$n] = $nofill;
        next unless ( $lines[$n] =~ /^\./ );
        next if ( $lines[$n] =~ /^\.\s*\\"/ );
        if ( $macdef > 0 ) {
            printf "%s:%d: nested macro definition %s\n", $path, $n + 1,
              $lines[$n]
              if ( $lines[$n] =~ /^\.de\b/ );
            $macdef = 0 if ( $lines[$n] eq ".." );
        }
        elsif ( $lines[$n] =~ /^\.(EX|nf)\b/ ) {
            $nofill++;
        }
        elsif ( $lines[$n] =~ /^\.(EE|fi)\b/ ) {
            $nofill--;
        }
        else {
            my $name = $lines[$n];
            $name =~ s/\s+/ /g;
            $name =~ s/^\.\s+/./;
            $name =~ s/^(\.\w+)\s+(\w+).*/$1 $2/;
            if ( $name =~ /^\.de\s+\w+/ ) {
                $macdef = 1;
            }
            else {
                $name =~ s/^\.(\w+).*/.de $1/;
                $name =~ s/^(\.de\s)/.de /;
                $nofill = &find_nofill_macros( $nofill, $name, \%macros )
                  if ( defined $macros{$name} );
            }
        }
        if ( $nofill > 1 ) {
            printf "%s:%d: nested no-fill: %s\n", $path, $n + 1, $lines[$n]
              if ($opt_v);
            $result[$n] = 1;
        }
        elsif ( $nofill < 0 ) {
            printf "%s:%d: unnested fill: %s\n", $path, $n + 1, $lines[$n]
              if ($opt_v);
            $result[$n] = 0;
        }
        else {
            $result[$n] = $nofill;
        }
    }
    return @result;
}

sub find_program($) {
    my $program = shift;
    my $result  = "";
    my @list    = split( /:/, $ENV{PATH} );
    for my $n ( 0 .. $#list ) {
        next if ( $list[$n] eq "" );
        my $check = sprintf( "%s/%s", $list[$n], $program );
        if ( -x $check ) {
            $result = $check;
            last;
        }
    }
    return $result;
}

sub trimmed($) {
    my $text = shift;
    $text =~ s/\s*$//;
    $text =~ s/\s+/ /g;
    return $text;
}

sub Mismatched($$) {
    my $cmp    = shift;
    my $ref    = shift;
    my $result = 0;
    $result = 1 if ( &trimmed($cmp) ne &trimmed($ref) );
    return $result;
}

sub only_so($) {
    my @data   = @{ $_[0] };
    my $result = 0;
    $result = 1 if ( $#data == 0 and $data[0] =~ /^\.so\b/ );
    return $result;
}

sub do_file($) {
    my $path = shift;
    my $type = "";
    $type   = "man" if ( $path =~ /\.man$|\.[1-9]$|\.[1-9][[:alpha:]]*$/ );
    $type   = "ms"  if ( $path =~ /\.ms$/ );
    %predef = &find_macros( "<predefined>", \@predef_m2h, \%predef )
      if ( $path =~ /man2html/ );
    if ( $type ne "" ) {
        my @lines = &read_file($path);
        if ( not &only_so( \@lines ) ) {
            if ( $path_groff ne "" ) {
                system( "tbl $path | " . "$path_groff -$type $groff_options" );
            }
            if ( $path_mandoc ne "" and $type eq "man" ) {
                my @errs =
                  &read_file("$path_mandoc -T lint -Wall $path 2>&1 | ");
                for my $e ( 0 .. $#errs ) {
                    my $ee = $errs[$e];
                    $ee =~ s/^mandoc:\s+//;
                    next if index( $ee, "printing literally: \\\\" ) >= 0;
                    next if index( $ee, "skipping request: ft CR" ) >= 0;
                    next
                      if index( $ee,
                        "STYLE: fill mode already enabled, skipping:" ) >= 0;
                    next
                      if index( $ee,
                        "STYLE: fill mode already disabled, skipping:" ) >= 0;
                    next
                      if index( $ee, "lower case character in document title" )
                      >= 0;
                    printf "%s\n", $ee;
                }
            }
        }
        my %empty;
        my %macro  = &find_macros( $path, \@lines, \%empty );
        my @nofill = &find_nofill_lines( $path, \@lines, \%macro );
        &check_dquotes( $path, $type, \@lines );
        &check_strings( $path, $type, \@lines );
        &check_stutter( $path, $type, \@lines );
        &check_externs( $path, $type, \@lines );
        &check_pcounts( $path, $type, \@lines, \@nofill );
        &check_fonting( $path, $type, \@lines );
        &check_aliases( $path, $type, \@lines );
        &check_section( $path, $type, \@lines );

        for my $name ( sort keys %macro ) {

            my %cmp = %{ $macro{$name} };
            if ( $predef{$name} ) {
                my @cmp = @{ $cmp{DATA} };
                my %ref = %{ $predef{$name} };
                my @ref = @{ $ref{DATA} };
                if ( $#cmp != $#ref ) {
                    printf "%s:%d: have %d lines, expected %d\n",
                      $path, $cmp{LINE} + 1, $#cmp + 1, $#ref + 1;
                    for my $n ( 0 .. $#ref ) {
                        printf "\t%s\n", $ref[$n];
                    }
                }
                else {
                    for my $n ( 0 .. $#cmp ) {
                        if ( &Mismatched( $cmp[$n], $ref[$n] ) ) {
                            my $tag = sprintf( "%s:%d: expected ",
                                $path, $cmp{LINE} + 1 + $n );
                            printf "%s\"%s\"\n", $tag, $ref[$n];
                            $tag =~ s/./ /g;
                            $tag =~ s/^.........../... have ->/;
                            printf "%s\"%s\"\n", $tag, $cmp[$n];
                        }
                    }
                }
            }
            elsif ( $type eq "man" ) {
                printf "%s:%d: missing $name\n", $path, $cmp{LINE} + 1;
            }
        }
    }
}

sub read_externs() {
    my @data = &read_file("manhtml.externs");
    for my $n ( 0 .. $#data ) {
        my $value = $data[$n];
        next if ( $value =~ /^\s*#/ );
        next unless ( $value =~ /^[[:alnum:]_]+\([1-9][[:alnum:]_]*\)$/ );
        $value =~ s/^([[:alnum:]_]+)(\([1-9][[:alnum:]_]*\))/\\fI$1\\fP$2/;
        $externs{$value} = 1;
        $value =~ s/I/I\\%/;
        $externs{$value} = 1;
        $value =~ s/\\fP/\\fR/;
        $externs{$value} = 1;
    }
}

sub ignore_dir($) {
    my $path   = shift;
    my $result = 0;
    if ($opt_r) {
        $result = 1 if ( $path =~ /\b\.git$/ );
        $result = 1 if ( $path =~ /\b\.svn$/ );
        $result = 1 if ( $path =~ /\bCVS$/ );
        $result = 1 if ( $path =~ /\bRCS$/ );
        $result = 1 if ( $path =~ /\bSCCS$/ );
    }
    return $result;
}

sub do_tree($) {
    my $path = shift;
    if ( -l $path ) {

        # ignore
    }
    elsif ( -d $path ) {
        if ( &ignore_dir($path) ) {

            # ignore
        }
        elsif ( opendir( my $dh, $path ) ) {
            my @list = sort readdir($dh);
            closedir $dh;
            for my $n ( 0 .. $#list ) {
                chomp $list[$n];
                &do_tree( sprintf "%s/%s", $path, $list[$n] )
                  unless ( $list[$n] =~ /^\.(\.)?$/ );
            }
        }
    }
    elsif ( -f $path ) {
        &do_file($path);
    }
}

sub main::HELP_MESSAGE() {
    printf STDERR <<EOF
Usage: $0 [options]

Options:
 -d       debug
 -e       accept "manhtml.externs" file for italic link-style of externs
 -r       recur on directories
 -v       verbose
 -w COLS  line-length (default: 80)
 -x       update files where fonting can be shortened
EOF
      ;
    exit 1;
}

$Getopt::Std::STANDARD_HELP_VERSION = 1;
&getopts('dervw:x') || &main::HELP_MESSAGE;
&read_externs if ($opt_e);
$opt_w = 80 unless ($opt_w);

$assume_Cdk = 1 if ( index( getcwd, "/cdk/" ) > 0 );

%predef = &find_macros( "<predefined>", \@predef, \%predef );

$path_groff  = &find_program("groff");
$path_mandoc = &find_program("mandoc");

if ( $path_groff ne "" ) {
    my @groff_version = &read_file("groff --version 2>&1 |");
    $groff_options .= " -Wdelim"
      if ( index( $groff_version[0], "1.23.0" ) > 0 );
}

if ( $#ARGV >= 0 ) {
    while ( $#ARGV >= 0 ) {
        &do_tree( shift @ARGV );
    }
}
else {
    &do_tree(".");
}

1;
