• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env perl
2#***************************************************************************
3#                                  _   _ ____  _
4#  Project                     ___| | | |  _ \| |
5#                             / __| | | | |_) | |
6#                            | (__| |_| |  _ <| |___
7#                             \___|\___/|_| \_\_____|
8#
9# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
10#
11# This software is licensed as described in the file COPYING, which
12# you should have received as part of this distribution. The terms
13# are also available at https://curl.se/docs/copyright.html.
14#
15# You may opt to use, copy, modify, merge, publish, distribute and/or sell
16# copies of the Software, and permit persons to whom the Software is
17# furnished to do so, under the terms of the COPYING file.
18#
19# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
20# KIND, either express or implied.
21#
22# SPDX-License-Identifier: curl
23#
24###########################################################################
25#
26# Scan man page(s) and detect some simple and yet common formatting mistakes.
27#
28# Output all deviances to stderr.
29
30use strict;
31use warnings;
32use File::Basename;
33
34# get the file name first
35my $symbolsinversions=shift @ARGV;
36
37# we may get the dir roots pointed out
38my @manpages=@ARGV;
39my $errors = 0;
40
41my %docsdirs;
42my %optblessed;
43my %funcblessed;
44my @optorder = (
45    'NAME',
46    'SYNOPSIS',
47    'DESCRIPTION',
48     #'DEFAULT', # CURLINFO_ has no default
49    'PROTOCOLS',
50    'EXAMPLE',
51    'AVAILABILITY',
52    'RETURN VALUE',
53    'SEE ALSO'
54    );
55my @funcorder = (
56    'NAME',
57    'SYNOPSIS',
58    'DESCRIPTION',
59    'EXAMPLE',
60    'AVAILABILITY',
61    'RETURN VALUE',
62    'SEE ALSO'
63    );
64my %shline; # section => line number
65
66my %symbol;
67
68# some CURLINFO_ symbols are not actual options for curl_easy_getinfo,
69# mark them as "deprecated" to hide them from link-warnings
70my %deprecated = (
71    CURLINFO_TEXT => 1,
72    CURLINFO_HEADER_IN => 1,
73    CURLINFO_HEADER_OUT => 1,
74    CURLINFO_DATA_IN => 1,
75    CURLINFO_DATA_OUT => 1,
76    CURLINFO_SSL_DATA_IN => 1,
77    CURLINFO_SSL_DATA_OUT => 1,
78    );
79sub allsymbols {
80    open(my $f, "<", "$symbolsinversions") ||
81        die "$symbolsinversions: $|";
82    while(<$f>) {
83        if($_ =~ /^([^ ]*) +(.*)/) {
84            my ($name, $info) = ($1, $2);
85            $symbol{$name}=$name;
86
87            if($info =~ /([0-9.]+) +([0-9.]+)/) {
88                $deprecated{$name}=$info;
89            }
90        }
91    }
92    close($f);
93}
94
95
96my %ref = (
97    'curl.1' => 1
98    );
99sub checkref {
100    my ($f, $sec, $file, $line)=@_;
101    my $present = 0;
102    #print STDERR "check $f.$sec\n";
103    if($ref{"$f.$sec"}) {
104        # present
105        return;
106    }
107    foreach my $d (keys %docsdirs) {
108        if( -f "$d/$f.$sec") {
109            $present = 1;
110            $ref{"$f.$sec"}=1;
111            last;
112        }
113    }
114    if(!$present) {
115        print STDERR "$file:$line broken reference to $f($sec)\n";
116        $errors++;
117    }
118}
119
120sub scanmanpage {
121    my ($file) = @_;
122    my $reqex = 0;
123    my $inseealso = 0;
124    my $inex = 0;
125    my $insynop = 0;
126    my $exsize = 0;
127    my $synopsize = 0;
128    my $shc = 0;
129    my $optpage = 0; # option or function
130    my @sh;
131    my $SH="";
132    my @separators;
133    my @sepline;
134
135    open(my $m, "<", "$file") || die "no such file: $file";
136    if($file =~ /[\/\\](CURL|curl_)[^\/\\]*.3/) {
137        # This is a man page for libcurl. It requires an example!
138        $reqex = 1;
139        if($1 eq "CURL") {
140            $optpage = 1;
141        }
142    }
143    my $line = 1;
144    while(<$m>) {
145        chomp;
146        if($_ =~ /^.so /) {
147            # this man page is just a referral
148            close($m);
149            return;
150        }
151        if(($_ =~ /^\.SH SYNOPSIS/i) && ($reqex)) {
152            # this is for libcurl man page SYNOPSIS checks
153            $insynop = 1;
154            $inex = 0;
155        }
156        elsif($_ =~ /^\.SH EXAMPLE/i) {
157            $insynop = 0;
158            $inex = 1;
159        }
160        elsif($_ =~ /^\.SH \"SEE ALSO\"/i) {
161            $inseealso = 1;
162        }
163        elsif($_ =~ /^\.SH/i) {
164            $insynop = 0;
165            $inex = 0;
166        }
167        elsif($inseealso) {
168            if($_ =~ /^\.BR (.*)/i) {
169                my $f = $1;
170                if($f =~ /^(lib|)curl/i) {
171                    $f =~ s/[\n\r]//g;
172                    if($f =~ s/([a-z_0-9-]*) \(([13])\)([, ]*)//i) {
173                        push @separators, $3;
174                        push @sepline, $line;
175                        checkref($1, $2, $file, $line);
176                    }
177                    if($f !~ /^ *$/) {
178                        print STDERR "$file:$line bad SEE ALSO format\n";
179                        $errors++;
180                    }
181                }
182                else {
183                    if($f =~ /.*(, *)\z/) {
184                        push @separators, $1;
185                        push @sepline, $line;
186                    }
187                    else {
188                        push @separators, " ";
189                        push @sepline, $line;
190                    }
191                }
192            }
193        }
194        elsif($inex)  {
195            $exsize++;
196            if($_ =~ /[^\\]\\n/) {
197                print STDERR "$file:$line '\\n' need to be '\\\\n'!\n";
198            }
199        }
200        elsif($insynop)  {
201            $synopsize++;
202            if(($synopsize == 1) && ($_ !~ /\.nf/)) {
203                print STDERR "$file:$line:1:ERROR: be .nf for proper formatting\n";
204            }
205        }
206        if($_ =~ /^\.SH ([^\r\n]*)/i) {
207            my $n = $1;
208            # remove enclosing quotes
209            $n =~ s/\"(.*)\"\z/$1/;
210            push @sh, $n;
211            $shline{$n} = $line;
212            $SH = $n;
213        }
214
215        if($_ =~ /^\'/) {
216            print STDERR "$file:$line line starts with single quote!\n";
217            $errors++;
218        }
219        if($_ =~ /\\f([BI])(.*)/) {
220            my ($format, $rest) = ($1, $2);
221            if($rest !~ /\\fP/) {
222                print STDERR "$file:$line missing \\f${format} terminator!\n";
223                $errors++;
224            }
225        }
226        my $c = $_;
227        while($c =~ s/\\f([BI])((lib|)curl[a-z_0-9-]*)\(([13])\)//i) {
228            checkref($2, $4, $file, $line);
229        }
230        if(($_ =~ /\\f([BI])((libcurl|CURLOPT_|CURLSHOPT_|CURLINFO_|CURLMOPT_|curl_easy_|curl_multi_|curl_url|curl_mime|curl_global|curl_share)[a-zA-Z_0-9-]+)(.)/) &&
231           ($4 ne "(")) {
232            print STDERR "$file:$line curl ref to $2 without section\n";
233            $errors++;
234        }
235        if($_ =~ /(.*)\\f([^BIP])/) {
236            my ($pre, $format) = ($1, $2);
237            if($pre !~ /\\\z/) {
238                # only if there wasn't another backslash before the \f
239                print STDERR "$file:$line suspicious \\f format!\n";
240                $errors++;
241            }
242        }
243        if(($SH =~ /^(DESCRIPTION|RETURN VALUE|AVAILABILITY)/i) &&
244           ($_ =~ /(.*)((curl_multi|curl_easy|curl_url|curl_global|curl_url|curl_share)[a-zA-Z_0-9-]+)/) &&
245           ($1 !~ /\\fI$/)) {
246            print STDERR "$file:$line unrefed curl call: $2\n";
247            $errors++;
248        }
249
250
251        if($optpage && $SH && ($SH !~ /^(SYNOPSIS|EXAMPLE|NAME|SEE ALSO)/i) &&
252           ($_ =~ /(.*)(CURL(OPT_|MOPT_|INFO_|SHOPT_)[A-Z0-9_]*)/)) {
253            # an option with its own man page, check that it is tagged
254            # for linking
255            my ($pref, $symbol) = ($1, $2);
256            if($deprecated{$symbol}) {
257                # let it be
258            }
259            elsif($pref !~ /\\fI\z/) {
260                print STDERR "$file:$line option $symbol missing \\fI tagging\n";
261                $errors++;
262            }
263        }
264        if($_ =~ /[ \t]+$/) {
265            print STDERR "$file:$line trailing whitespace\n";
266            $errors++;
267        }
268        $line++;
269    }
270    close($m);
271
272    if(@separators) {
273        # all except the last one need comma
274        for(0 .. $#separators - 1) {
275            my $l = $_;
276            my $sep = $separators[$l];
277            if($sep ne ",") {
278                printf STDERR "$file:%d: bad not-last SEE ALSO separator: '%s'\n",
279                    $sepline[$l], $sep;
280                $errors++;
281            }
282        }
283        # the last one should not do comma
284        my $sep = $separators[$#separators];
285        if($sep eq ",") {
286            printf STDERR "$file:%d: superfluous comma separator\n",
287                $sepline[$#separators];
288            $errors++;
289        }
290    }
291
292    if($reqex) {
293        # only for libcurl options man-pages
294
295        my $shcount = scalar(@sh); # before @sh gets shifted
296        if($exsize < 2) {
297            print STDERR "$file:$line missing EXAMPLE section\n";
298            $errors++;
299        }
300
301        if($shcount < 3) {
302            print STDERR "$file:$line too few man page sections!\n";
303            $errors++;
304            return;
305        }
306
307        my $got = "start";
308        my $i = 0;
309        my $shused = 1;
310        my @shorig = @sh;
311        my @order = $optpage ? @optorder : @funcorder;
312        my $blessed = $optpage ? \%optblessed : \%funcblessed;
313
314        while($got) {
315            my $finesh;
316            $got = shift(@sh);
317            if($got) {
318                if($$blessed{$got}) {
319                    $i = $$blessed{$got};
320                    $finesh = $got; # a mandatory one
321                }
322            }
323            if($i && defined($finesh)) {
324                # mandatory section
325
326                if($i != $shused) {
327                    printf STDERR "$file:%u Got %s, when %s was expected\n",
328                        $shline{$finesh},
329                        $finesh,
330                        $order[$shused-1];
331                    $errors++;
332                    return;
333                }
334                $shused++;
335                if($i == scalar(@order)) {
336                    # last mandatory one, exit
337                    last;
338                }
339            }
340        }
341
342        if($i != scalar(@order)) {
343            printf STDERR "$file:$line missing mandatory section: %s\n",
344                $order[$i];
345            printf STDERR "$file:$line section found at index %u: '%s'\n",
346                $i, $shorig[$i];
347            printf STDERR " Found %u used sections\n", $shcount;
348            $errors++;
349        }
350    }
351}
352
353allsymbols();
354
355if(!$symbol{'CURLALTSVC_H1'}) {
356    print STDERR "didn't get the symbols-in-version!\n";
357    exit;
358}
359
360my $ind = 1;
361for my $s (@optorder) {
362    $optblessed{$s} = $ind++
363}
364$ind = 1;
365for my $s (@funcorder) {
366    $funcblessed{$s} = $ind++
367}
368
369for my $m (@manpages) {
370    $docsdirs{dirname($m)}++;
371}
372
373for my $m (@manpages) {
374    scanmanpage($m);
375}
376
377print STDERR "ok\n" if(!$errors);
378
379exit $errors;
380