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