1#!/usr/bin/perl -w
2
3# manlint - report "errors" in man page(s).
4
5# USAGE:
6#  manlint [list of files to check]
7#
8# EXAMPLE:
9#  manlint /usr/man/man*/*.* | less
10
11# An error is anything not known to be a safe construct in a man page;
12# see man(7) for more information.
13# Currently it's excessively paranoid, but that's the point -- this
14# program assumes there's a problem, and if it isn't we can add that to the
15# ruleset so that what's safe is explicitly spelled out.
16# Currently this program only examines tmac.an based pages, the normal
17# kind encountered in Linux.  This is different than the BSD manddoc format,
18# which is used by a number of man pages.
19
20# (C) 1999 David A. Wheeler (dwheeler@ida.org)
21
22# This program is free software; you can redistribute it and/or modify
23# it under the terms of the GNU General Public License as published by
24# the Free Software Foundation; either version 2 of the License, or
25# (at your option) any later version.
26#
27# This program is distributed in the hope that it will be useful,
28# but WITHOUT ANY WARRANTY; without even the implied warranty of
29# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
30# GNU General Public License for more details.
31#
32# You should have received a copy of the GNU General Public License
33# along with this program; if not, write to the Free Software
34# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
35
36
37require 5.002;  # Requires Perl 5.002 because functions are prototyped.
38
39# First, set up configuration.
40
41$debug = 0;
42$errs = $totalerrs = 0;
43$goodfiles = $badfiles = $skipfiles = 0;
44$filename = '';
45
46# Allow options for small or large safe set; just printing if a file fails
47# instead of detail; auto-skip BSD files.
48
49# This is a list of "safe" macros, with their value being the
50# maximum number of allowed parameters (-1 = any, 0=no parameters allowed)
51%safemacros = (
52  'TH' => 5,
53  # Font Control:
54  'B' => -1, 'BI' => -1, 'BR' => -1,
55  'I' => -1, 'IB' => -1, 'IR' => -1,
56  'RB' => -1, 'RI' => -1, 'SB' => -1, 'SM' => -1,
57  # tmac.an other macros:
58  'SH' => 1,
59  'LP' => 0, 'P' => 0,
60  'PP' => 0,
61  'RS' => 1, 'RE' => 0,
62  'HP' => 1, 'IP' => 2, 'TP' => 1,
63  'DT' => 0, 'PD' => 1, 'SS' => 1,
64  # We'll allow IX (indexing).
65  'IX' => -1,
66  # I'm adding the UR, UN, and UE macros that will permit embedded URIs.
67  'UR' => 1,
68  'UN' => 1,
69  'UE' => 0,
70  # allowed troff macros
71  '\\"' => -1,  # troff comments
72  'ps' => 1,    # Point size
73  'ft' => 1,    # Font commands (not recommended, may be ignored in some cases)
74  'hy' => 1,    # Hyphenation (probably ignored in translation)
75  'bp' => 0,    # Force page break; optional parameter forbidden.
76  'ne' => 1,    # Need lines (likely to be ignored in translation)
77  'br' => 0,
78  'nf' => 0,    # No-fill; insert breaks at end of each line.
79  'fi' => 0,
80  'ig' => 1,
81  '.'  => 0,     # standard end-of-ignore/end-of-definition.
82  'ce' => 1,     # Center next N lines
83  'ad' => 1,
84  'na' => 0,
85  # Will probably need to handle some if.
86  'if' => -1,    # LIMITED VERSION.
87  'ie' => -1,    # LIMITED VERSION.
88  'el' => -1,
89  'so' => 1,     # Handle 'so' for shared man pages
90  'sp' => 1,     # Vertical Space - only permit positive values.
91  'de'  => 1,    # Handling 'macro define' is a pain, but many pages require it.
92  'ds' => -1,    # Allow string defines.
93  'in' => 1,     # Require that every indent be paired with a negative indent.
94  'ti' => 1,     # Temporary indent may be ignored
95  'hy' => 1,     # Hypenation almost certainly ignored by anyone else.
96  'nh' => 1,     # Again, hyphenation likely ignored.
97  'tr' => 1,     # Translations limited, see below.
98);
99
100# Allowed parameters for the ft (font) troff command.
101%allowed_ft_parameter = (
102  '1' => 1,
103  '2' => 1,
104  '3' => 1,
105  '4' => 1,
106  'R' => 1,
107  'I' => 1,
108  'B' => 1,
109  'P' => 1,
110  'CW' => 1,
111  '' => 1,
112);
113
114%allowed_tr = (
115 '\\(ts"' => 1,
116 '\\(is\'' => 1,
117 '\\(if`' => 1,
118 '\\(pd"' => 1,
119 '\\(*W-|\(bv\*(Tr' => 1,
120 '\\*(Tr' => 1,
121);
122
123sub problem($) {
124 # Report a problem, if you should.
125 my $message = shift;
126 print "${ARGV}: $message\n";
127 $errs++;
128}
129
130sub clean_state {
131  %defined_macros = ();
132  $is_skipped = 0;
133}
134
135sub process_line {
136 # Process line already read in $_ (default input line).
137 my $macro;
138 my $parameters;
139 if (m/^[.']\s*([^\s]+)\s*(.*)?/) {
140   $macro=$1;
141   $parameters=$2;
142   $macro =~ s/\s//g;
143   print "Found macro: #${macro}#\n" if $debug;
144   if ($macro =~ m/Dd/) { # Is this the BSD macro set and not a tmac.an set?
145      problem("Uses BSD mandoc conventions instead of tmac.an");
146      $errs--; # Patch up error count.
147      # print "${ARGV}: Uses BSD mandoc conventions instead of tmac.an.\n";
148      close(ARGV); # Skip the rest of this file.
149      $is_skipped = 1;
150      return;
151   }
152   if ($macro =~ m/\\"/) {return;} # Skip troff comments.
153   if (exists($defined_macros{$macro})) {
154     return;  # ??? Should examine the macro parameters.
155   }
156   if (exists($safemacros{$macro}) ) {
157     # ??? Check parameter count.
158     # ??? Check that .TH is the first macro (note: bash.1, etc., break this)
159     if ( ($macro eq 'if') || ($macro eq 'ie' )) {
160       # Only permit checking 't' or 'n' for now.
161       if ($parameters =~ m/^[tn]\s/) {
162          $_ = $parameters;
163          s/^[tn]\s+//;
164          process_line();  # Re-examine line without the if statement.
165       } else {
166         problem("unsafe use of if/ie");
167       }
168       # ??? sp: only no-parameter or positive values.
169     } elsif ($macro eq 'de') {
170       $parameters =~ m/^([^\s]+)/;
171       $is_defining = $1;
172       $defined_macros{$is_defining} = 1;
173     } elsif ($macro eq 'so') {
174       $parameters =~ m/^([^\s]+)/;
175       $new_file = $1;
176       while (<$new_file>) { process_line(); }
177      } elsif (($macro eq 'ft') && (defined($parameters))
178            && (! exists($allowed_ft_parameter{$parameters}))) {
179        problem("forbidden ft parameter $parameters");
180      } elsif (($macro eq 'tr') && (defined($parameters))
181            && (! exists($allowed_tr{$parameters}))) {
182        problem("forbidden tr parameter $parameters");
183     }
184    # ??? 'in': Require that every indent be paired with a negative indent.
185    # ??? For macros with text after them, check their text's escapes.
186   } else {
187     problem("unsafe macro $macro");
188   }
189 } else {
190 # ??? Regular text; check escape clauses.
191 }
192}
193
194
195# Main loop: Process files, looking for errors.
196
197clean_state();
198
199while (<>) {
200 if ($ARGV ne $filename) {
201   print "Processing $ARGV; up to now good=$goodfiles bad=$badfiles skip=$skipfiles\n";
202   $filename=$ARGV;
203 }
204 process_line();
205} continue {
206 if (eof) {    # End of processing this file.
207  close ARGV;  # Perl magic to get line #s to be accurate.
208  $totalerrs += $errs;
209  if ($errs) { $badfiles++ } else {
210      if ($is_skipped) {$skipfiles++} else {$goodfiles++};
211  }
212  $errs = 0;
213  clean_state();
214 }
215}
216
217print "Number of good files = $goodfiles\n";
218print "Number of bad files = $badfiles\n";
219print "Number of skipped files = $skipfiles\n";
220exit $errs;
221
222# ??? Handle .so better (esp. the error messages)
223# currently error messages don't report the traceback & they should.
224
225
226