1#!/usr/bin/perl 2 3use strict; 4use warnings; 5use 5.008001; 6 7use Cwd qw(abs_path getcwd); 8use File::Find; 9use File::Spec qw(devnull); 10use File::Temp; 11use IO::Handle; 12use Getopt::Long; 13 14# Update for pg_bsd_indent version 15my $INDENT_VERSION = "2.0"; 16 17# Our standard indent settings 18my $indent_opts = 19 "-bad -bap -bbb -bc -bl -cli1 -cp33 -cdb -nce -d0 -di12 -nfc1 -i4 -l79 -lp -lpl -nip -npro -sac -tpg -ts4"; 20 21my $devnull = File::Spec->devnull; 22 23my ($typedefs_file, $typedef_str, $code_base, $excludes, $indent, $build); 24 25my %options = ( 26 "typedefs=s" => \$typedefs_file, 27 "list-of-typedefs=s" => \$typedef_str, 28 "code-base=s" => \$code_base, 29 "excludes=s" => \$excludes, 30 "indent=s" => \$indent, 31 "build" => \$build,); 32GetOptions(%options) || die "bad command line argument\n"; 33 34run_build($code_base) if ($build); 35 36# command line option wins, then first non-option arg, 37# then environment (which is how --build sets it) , 38# then locations. based on current dir, then default location 39$typedefs_file ||= shift if @ARGV && $ARGV[0] !~ /\.[ch]$/; 40$typedefs_file ||= $ENV{PGTYPEDEFS}; 41 42# build mode sets PGINDENT 43$indent ||= $ENV{PGINDENT} || $ENV{INDENT} || "pg_bsd_indent"; 44 45# no non-option arguments given. so do everything in the current directory 46$code_base ||= '.' unless @ARGV; 47 48# if it's the base of a postgres tree, we will exclude the files 49# postgres wants excluded 50$excludes ||= "$code_base/src/tools/pgindent/exclude_file_patterns" 51 if $code_base && -f "$code_base/src/tools/pgindent/exclude_file_patterns"; 52 53# The typedef list that's mechanically extracted by the buildfarm may omit 54# some names we want to treat like typedefs, e.g. "bool" (which is a macro 55# according to <stdbool.h>), and may include some names we don't want 56# treated as typedefs, although various headers that some builds include 57# might make them so. For the moment we just hardwire a whitelist of names 58# to add and a blacklist of names to remove; eventually this may need to be 59# easier to configure. Note that the typedefs need trailing newlines. 60my @whitelist = ("bool\n"); 61 62my %blacklist = map { +"$_\n" => 1 } qw( 63 ANY FD_SET U abs allocfunc boolean date digit ilist interval iterator other 64 pointer printfunc reference string timestamp type wrap 65); 66 67# globals 68my @files; 69my $filtered_typedefs_fh; 70 71 72sub check_indent 73{ 74 system("$indent -? < $devnull > $devnull 2>&1"); 75 if ($? >> 8 != 1) 76 { 77 print STDERR 78 "You do not appear to have $indent installed on your system.\n"; 79 exit 1; 80 } 81 82 if (`$indent --version` !~ m/ $INDENT_VERSION$/) 83 { 84 print STDERR 85 "You do not appear to have $indent version $INDENT_VERSION installed on your system.\n"; 86 exit 1; 87 } 88 89 system("$indent -gnu < $devnull > $devnull 2>&1"); 90 if ($? == 0) 91 { 92 print STDERR 93 "You appear to have GNU indent rather than BSD indent.\n"; 94 exit 1; 95 } 96 97 return; 98} 99 100 101sub load_typedefs 102{ 103 104 # try fairly hard to find the typedefs file if it's not set 105 106 foreach my $try ('.', 'src/tools/pgindent', '/usr/local/etc') 107 { 108 $typedefs_file ||= "$try/typedefs.list" 109 if (-f "$try/typedefs.list"); 110 } 111 112 # try to find typedefs by moving up directory levels 113 my $tdtry = ".."; 114 foreach (1 .. 5) 115 { 116 $typedefs_file ||= "$tdtry/src/tools/pgindent/typedefs.list" 117 if (-f "$tdtry/src/tools/pgindent/typedefs.list"); 118 $tdtry = "$tdtry/.."; 119 } 120 die "cannot locate typedefs file \"$typedefs_file\"\n" 121 unless $typedefs_file && -f $typedefs_file; 122 123 open(my $typedefs_fh, '<', $typedefs_file) 124 || die "cannot open typedefs file \"$typedefs_file\": $!\n"; 125 my @typedefs = <$typedefs_fh>; 126 close($typedefs_fh); 127 128 # add command-line-supplied typedefs? 129 if (defined($typedef_str)) 130 { 131 foreach my $typedef (split(m/[, \t\n]+/, $typedef_str)) 132 { 133 push(@typedefs, $typedef . "\n"); 134 } 135 } 136 137 # add whitelisted entries 138 push(@typedefs, @whitelist); 139 140 # remove blacklisted entries 141 @typedefs = grep { !$blacklist{$_} } @typedefs; 142 143 # write filtered typedefs 144 my $filter_typedefs_fh = new File::Temp(TEMPLATE => "pgtypedefXXXXX"); 145 print $filter_typedefs_fh @typedefs; 146 $filter_typedefs_fh->close(); 147 148 # temp file remains because we return a file handle reference 149 return $filter_typedefs_fh; 150} 151 152 153sub process_exclude 154{ 155 if ($excludes && @files) 156 { 157 open(my $eh, '<', $excludes) 158 || die "cannot open exclude file \"$excludes\"\n"; 159 while (my $line = <$eh>) 160 { 161 chomp $line; 162 my $rgx = qr!$line!; 163 @files = grep { $_ !~ /$rgx/ } @files if $rgx; 164 } 165 close($eh); 166 } 167 return; 168} 169 170 171sub read_source 172{ 173 my $source_filename = shift; 174 my $source; 175 176 open(my $src_fd, '<', $source_filename) 177 || die "cannot open file \"$source_filename\": $!\n"; 178 local ($/) = undef; 179 $source = <$src_fd>; 180 close($src_fd); 181 182 return $source; 183} 184 185 186sub write_source 187{ 188 my $source = shift; 189 my $source_filename = shift; 190 191 open(my $src_fh, '>', $source_filename) 192 || die "cannot open file \"$source_filename\": $!\n"; 193 print $src_fh $source; 194 close($src_fh); 195 return; 196} 197 198 199sub pre_indent 200{ 201 my $source = shift; 202 203 ## Comments 204 205 # Convert // comments to /* */ 206 $source =~ s!^([ \t]*)//(.*)$!$1/* $2 */!gm; 207 208 # Adjust dash-protected block comments so indent won't change them 209 $source =~ s!/\* +---!/*---X_X!g; 210 211 ## Other 212 213 # Prevent indenting of code in 'extern "C"' blocks. 214 # we replace the braces with comments which we'll reverse later 215 my $extern_c_start = '/* Open extern "C" */'; 216 my $extern_c_stop = '/* Close extern "C" */'; 217 $source =~ 218 s!(^#ifdef[ \t]+__cplusplus.*\nextern[ \t]+"C"[ \t]*\n)\{[ \t]*$!$1$extern_c_start!gm; 219 $source =~ s!(^#ifdef[ \t]+__cplusplus.*\n)\}[ \t]*$!$1$extern_c_stop!gm; 220 221 # Protect wrapping in CATALOG() 222 $source =~ s!^(CATALOG\(.*)$!/*$1*/!gm; 223 224 return $source; 225} 226 227 228sub post_indent 229{ 230 my $source = shift; 231 my $source_filename = shift; 232 233 # Restore CATALOG lines 234 $source =~ s!^/\*(CATALOG\(.*)\*/$!$1!gm; 235 236 # Put back braces for extern "C" 237 $source =~ s!^/\* Open extern "C" \*/$!{!gm; 238 $source =~ s!^/\* Close extern "C" \*/$!}!gm; 239 240 ## Comments 241 242 # Undo change of dash-protected block comments 243 $source =~ s!/\*---X_X!/* ---!g; 244 245 # Fix run-together comments to have a tab between them 246 $source =~ s!\*/(/\*.*\*/)$!*/\t$1!gm; 247 248 ## Functions 249 250 # Use a single space before '*' in function return types 251 $source =~ s!^([A-Za-z_]\S*)[ \t]+\*$!$1 *!gm; 252 253 # Move prototype names to the same line as return type. Useful 254 # for ctags. Indent should do this, but it does not. It formats 255 # prototypes just like real functions. 256 257 my $ident = qr/[a-zA-Z_][a-zA-Z_0-9]*/; 258 my $comment = qr!/\*.*\*/!; 259 260 $source =~ s! 261 (\n$ident[^(\n]*)\n # e.g. static void 262 ( 263 $ident\(\n? # func_name( 264 (.*,([ \t]*$comment)?\n)* # args b4 final ln 265 .*\);([ \t]*$comment)?$ # final line 266 ) 267 !$1 . (substr($1,-1,1) eq '*' ? '' : ' ') . $2!gmxe; 268 269 return $source; 270} 271 272 273sub run_indent 274{ 275 my $source = shift; 276 my $error_message = shift; 277 278 my $cmd = "$indent $indent_opts -U" . $filtered_typedefs_fh->filename; 279 280 my $tmp_fh = new File::Temp(TEMPLATE => "pgsrcXXXXX"); 281 my $filename = $tmp_fh->filename; 282 print $tmp_fh $source; 283 $tmp_fh->close(); 284 285 $$error_message = `$cmd $filename 2>&1`; 286 287 return "" if ($? || length($$error_message) > 0); 288 289 unlink "$filename.BAK"; 290 291 open(my $src_out, '<', $filename); 292 local ($/) = undef; 293 $source = <$src_out>; 294 close($src_out); 295 296 return $source; 297 298} 299 300 301# for development diagnostics 302sub diff 303{ 304 my $pre = shift; 305 my $post = shift; 306 my $flags = shift || ""; 307 308 print STDERR "running diff\n"; 309 310 my $pre_fh = new File::Temp(TEMPLATE => "pgdiffbXXXXX"); 311 my $post_fh = new File::Temp(TEMPLATE => "pgdiffaXXXXX"); 312 313 print $pre_fh $pre; 314 print $post_fh $post; 315 316 $pre_fh->close(); 317 $post_fh->close(); 318 319 system( "diff $flags " 320 . $pre_fh->filename . " " 321 . $post_fh->filename 322 . " >&2"); 323 return; 324} 325 326 327sub run_build 328{ 329 eval "use LWP::Simple;"; ## no critic (ProhibitStringyEval); 330 331 my $code_base = shift || '.'; 332 my $save_dir = getcwd(); 333 334 # look for the code root 335 foreach (1 .. 5) 336 { 337 last if -d "$code_base/src/tools/pgindent"; 338 $code_base = "$code_base/.."; 339 } 340 341 die "cannot locate src/tools/pgindent directory in \"$code_base\"\n" 342 unless -d "$code_base/src/tools/pgindent"; 343 344 chdir "$code_base/src/tools/pgindent"; 345 346 my $typedefs_list_url = 347 "https://buildfarm.postgresql.org/cgi-bin/typedefs.pl"; 348 349 my $rv = getstore($typedefs_list_url, "tmp_typedefs.list"); 350 351 die "cannot fetch typedefs list from $typedefs_list_url\n" 352 unless is_success($rv); 353 354 $ENV{PGTYPEDEFS} = abs_path('tmp_typedefs.list'); 355 356 my $indentrepo = "https://git.postgresql.org/git/pg_bsd_indent.git"; 357 system("git clone $indentrepo >$devnull 2>&1"); 358 die "could not fetch pg_bsd_indent sources from $indentrepo\n" 359 unless $? == 0; 360 361 chdir "pg_bsd_indent" || die; 362 system("make all check >$devnull"); 363 die "could not build pg_bsd_indent from source\n" 364 unless $? == 0; 365 366 $ENV{PGINDENT} = abs_path('pg_bsd_indent'); 367 368 chdir $save_dir; 369 return; 370} 371 372 373sub build_clean 374{ 375 my $code_base = shift || '.'; 376 377 # look for the code root 378 foreach (1 .. 5) 379 { 380 last if -d "$code_base/src/tools/pgindent"; 381 $code_base = "$code_base/.."; 382 } 383 384 die "cannot locate src/tools/pgindent directory in \"$code_base\"\n" 385 unless -d "$code_base/src/tools/pgindent"; 386 387 chdir "$code_base"; 388 389 system("rm -rf src/tools/pgindent/pg_bsd_indent"); 390 system("rm -f src/tools/pgindent/tmp_typedefs.list"); 391 return; 392} 393 394 395# main 396 397# get the list of files under code base, if it's set 398File::Find::find( 399 { 400 wanted => sub { 401 my ($dev, $ino, $mode, $nlink, $uid, $gid); 402 (($dev, $ino, $mode, $nlink, $uid, $gid) = lstat($_)) 403 && -f _ 404 && /^.*\.[ch]\z/s 405 && push(@files, $File::Find::name); 406 } 407 }, 408 $code_base) if $code_base; 409 410process_exclude(); 411 412$filtered_typedefs_fh = load_typedefs(); 413 414check_indent(); 415 416# any non-option arguments are files to be processed 417push(@files, @ARGV); 418 419foreach my $source_filename (@files) 420{ 421 422 # Automatically ignore .c and .h files that correspond to a .y or .l 423 # file. indent tends to get badly confused by Bison/flex output, 424 # and there's no value in indenting derived files anyway. 425 my $otherfile = $source_filename; 426 $otherfile =~ s/\.[ch]$/.y/; 427 next if $otherfile ne $source_filename && -f $otherfile; 428 $otherfile =~ s/\.y$/.l/; 429 next if $otherfile ne $source_filename && -f $otherfile; 430 431 my $source = read_source($source_filename); 432 my $orig_source = $source; 433 my $error_message = ''; 434 435 $source = pre_indent($source); 436 437 $source = run_indent($source, \$error_message); 438 if ($source eq "") 439 { 440 print STDERR "Failure in $source_filename: " . $error_message . "\n"; 441 next; 442 } 443 444 $source = post_indent($source, $source_filename); 445 446 write_source($source, $source_filename) if $source ne $orig_source; 447} 448 449build_clean($code_base) if $build; 450