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