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 manpage(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 CURLOPT_EGDSOCKET => 1, 79 CURLOPT_RANDOM_FILE => 1, 80 ); 81sub allsymbols { 82 open(my $f, "<", "$symbolsinversions") || 83 die "$symbolsinversions: $|"; 84 while(<$f>) { 85 if($_ =~ /^([^ ]*) +(.*)/) { 86 my ($name, $info) = ($1, $2); 87 $symbol{$name}=$name; 88 89 if($info =~ /([0-9.]+) +([0-9.]+)/) { 90 $deprecated{$name}=$info; 91 } 92 } 93 } 94 close($f); 95} 96 97 98my %ref = ( 99 'curl.1' => 1 100 ); 101sub checkref { 102 my ($f, $sec, $file, $line)=@_; 103 my $present = 0; 104 #print STDERR "check $f.$sec\n"; 105 if($ref{"$f.$sec"}) { 106 # present 107 return; 108 } 109 foreach my $d (keys %docsdirs) { 110 if( -f "$d/$f.$sec") { 111 $present = 1; 112 $ref{"$f.$sec"}=1; 113 last; 114 } 115 } 116 if(!$present) { 117 print STDERR "$file:$line broken reference to $f($sec)\n"; 118 $errors++; 119 } 120} 121 122sub scanmanpage { 123 my ($file) = @_; 124 my $reqex = 0; 125 my $inseealso = 0; 126 my $inex = 0; 127 my $insynop = 0; 128 my $exsize = 0; 129 my $synopsize = 0; 130 my $shc = 0; 131 my $optpage = 0; # option or function 132 my @sh; 133 my $SH=""; 134 my @separators; 135 my @sepline; 136 137 open(my $m, "<", "$file") || 138 die "test1173.pl could not open $file"; 139 if($file =~ /[\/\\](CURL|curl_)([^\/\\]*).3/) { 140 # This is a manpage for libcurl. It requires an example unless it's 141 # considered deprecated. 142 $reqex = 1 unless defined $deprecated{'CURL'.$2}; 143 if($1 eq "CURL") { 144 $optpage = 1; 145 } 146 } 147 my $line = 1; 148 while(<$m>) { 149 chomp; 150 if($_ =~ /^.so /) { 151 # this manpage is just a referral 152 close($m); 153 return; 154 } 155 if(($_ =~ /^\.SH SYNOPSIS/i) && ($reqex)) { 156 # this is for libcurl manpage SYNOPSIS checks 157 $insynop = 1; 158 $inex = 0; 159 } 160 elsif($_ =~ /^\.SH EXAMPLE/i) { 161 $insynop = 0; 162 $inex = 1; 163 } 164 elsif($_ =~ /^\.SH \"SEE ALSO\"/i) { 165 $inseealso = 1; 166 } 167 elsif($_ =~ /^\.SH/i) { 168 $insynop = 0; 169 $inex = 0; 170 } 171 elsif($inseealso) { 172 if($_ =~ /^\.BR (.*)/i) { 173 my $f = $1; 174 if($f =~ /^(lib|)curl/i) { 175 $f =~ s/[\n\r]//g; 176 if($f =~ s/([a-z_0-9-]*) \(([13])\)([, ]*)//i) { 177 push @separators, $3; 178 push @sepline, $line; 179 checkref($1, $2, $file, $line); 180 } 181 if($f !~ /^ *$/) { 182 print STDERR "$file:$line bad SEE ALSO format\n"; 183 $errors++; 184 } 185 } 186 else { 187 if($f =~ /.*(, *)\z/) { 188 push @separators, $1; 189 push @sepline, $line; 190 } 191 else { 192 push @separators, " "; 193 push @sepline, $line; 194 } 195 } 196 } 197 } 198 elsif($inex) { 199 $exsize++; 200 if($_ =~ /[^\\]\\n/) { 201 print STDERR "$file:$line '\\n' need to be '\\\\n'!\n"; 202 } 203 } 204 elsif($insynop) { 205 $synopsize++; 206 if(($synopsize == 1) && ($_ !~ /\.nf/)) { 207 print STDERR "$file:$line:1:ERROR: be .nf for proper formatting\n"; 208 } 209 } 210 if($_ =~ /^\.SH ([^\r\n]*)/i) { 211 my $n = $1; 212 # remove enclosing quotes 213 $n =~ s/\"(.*)\"\z/$1/; 214 push @sh, $n; 215 $shline{$n} = $line; 216 $SH = $n; 217 } 218 219 if($_ =~ /^\'/) { 220 print STDERR "$file:$line line starts with single quote!\n"; 221 $errors++; 222 } 223 if($_ =~ /\\f([BI])(.*)/) { 224 my ($format, $rest) = ($1, $2); 225 if($rest !~ /\\fP/) { 226 print STDERR "$file:$line missing \\f${format} terminator!\n"; 227 $errors++; 228 } 229 } 230 my $c = $_; 231 while($c =~ s/\\f([BI])((lib|)curl[a-z_0-9-]*)\(([13])\)//i) { 232 checkref($2, $4, $file, $line); 233 } 234 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-]+)(.)/) && 235 ($4 ne "(")) { 236 print STDERR "$file:$line curl ref to $2 without section\n"; 237 $errors++; 238 } 239 if($_ =~ /(.*)\\f([^BIP])/) { 240 my ($pre, $format) = ($1, $2); 241 if($pre !~ /\\\z/) { 242 # only if there wasn't another backslash before the \f 243 print STDERR "$file:$line suspicious \\f format!\n"; 244 $errors++; 245 } 246 } 247 if(($SH =~ /^(DESCRIPTION|RETURN VALUE|AVAILABILITY)/i) && 248 ($_ =~ /(.*)((curl_multi|curl_easy|curl_url|curl_global|curl_url|curl_share)[a-zA-Z_0-9-]+)/) && 249 ($1 !~ /\\fI$/)) { 250 print STDERR "$file:$line unrefed curl call: $2\n"; 251 $errors++; 252 } 253 254 255 if($optpage && $SH && ($SH !~ /^(SYNOPSIS|EXAMPLE|NAME|SEE ALSO)/i) && 256 ($_ =~ /(.*)(CURL(OPT_|MOPT_|INFO_|SHOPT_)[A-Z0-9_]*)/)) { 257 # an option with its own manpage, check that it is tagged 258 # for linking 259 my ($pref, $symbol) = ($1, $2); 260 if($deprecated{$symbol}) { 261 # let it be 262 } 263 elsif($pref !~ /\\fI\z/) { 264 print STDERR "$file:$line option $symbol missing \\fI tagging\n"; 265 $errors++; 266 } 267 } 268 if($_ =~ /[ \t]+$/) { 269 print STDERR "$file:$line trailing whitespace\n"; 270 $errors++; 271 } 272 $line++; 273 } 274 close($m); 275 276 if(@separators) { 277 # all except the last one need comma 278 for(0 .. $#separators - 1) { 279 my $l = $_; 280 my $sep = $separators[$l]; 281 if($sep ne ",") { 282 printf STDERR "$file:%d: bad not-last SEE ALSO separator: '%s'\n", 283 $sepline[$l], $sep; 284 $errors++; 285 } 286 } 287 # the last one should not do comma 288 my $sep = $separators[$#separators]; 289 if($sep eq ",") { 290 printf STDERR "$file:%d: superfluous comma separator\n", 291 $sepline[$#separators]; 292 $errors++; 293 } 294 } 295 296 if($reqex) { 297 # only for libcurl options man-pages 298 299 my $shcount = scalar(@sh); # before @sh gets shifted 300 if($exsize < 2) { 301 print STDERR "$file:$line missing EXAMPLE section\n"; 302 $errors++; 303 } 304 305 if($shcount < 3) { 306 print STDERR "$file:$line too few manpage sections!\n"; 307 $errors++; 308 return; 309 } 310 311 my $got = "start"; 312 my $i = 0; 313 my $shused = 1; 314 my @shorig = @sh; 315 my @order = $optpage ? @optorder : @funcorder; 316 my $blessed = $optpage ? \%optblessed : \%funcblessed; 317 318 while($got) { 319 my $finesh; 320 $got = shift(@sh); 321 if($got) { 322 if($$blessed{$got}) { 323 $i = $$blessed{$got}; 324 $finesh = $got; # a mandatory one 325 } 326 } 327 if($i && defined($finesh)) { 328 # mandatory section 329 330 if($i != $shused) { 331 printf STDERR "$file:%u Got %s, when %s was expected\n", 332 $shline{$finesh}, 333 $finesh, 334 $order[$shused-1]; 335 $errors++; 336 return; 337 } 338 $shused++; 339 if($i == scalar(@order)) { 340 # last mandatory one, exit 341 last; 342 } 343 } 344 } 345 346 if($i != scalar(@order)) { 347 printf STDERR "$file:$line missing mandatory section: %s\n", 348 $order[$i]; 349 printf STDERR "$file:$line section found at index %u: '%s'\n", 350 $i, $shorig[$i]; 351 printf STDERR " Found %u used sections\n", $shcount; 352 $errors++; 353 } 354 } 355} 356 357allsymbols(); 358 359if(!$symbol{'CURLALTSVC_H1'}) { 360 print STDERR "didn't get the symbols-in-version!\n"; 361 exit; 362} 363 364my $ind = 1; 365for my $s (@optorder) { 366 $optblessed{$s} = $ind++ 367} 368$ind = 1; 369for my $s (@funcorder) { 370 $funcblessed{$s} = $ind++ 371} 372 373for my $m (@manpages) { 374 $docsdirs{dirname($m)}++; 375} 376 377for my $m (@manpages) { 378 scanmanpage($m); 379} 380 381print STDERR "ok\n" if(!$errors); 382 383exit $errors; 384