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# globals
54my @files;
55my $filtered_typedefs_fh;
56
57
58sub check_indent
59{
60	system("$indent -? < $devnull > $devnull 2>&1");
61	if ($? >> 8 != 1)
62	{
63		print STDERR
64		  "You do not appear to have $indent installed on your system.\n";
65		exit 1;
66	}
67
68	if (`$indent --version` !~ m/ $INDENT_VERSION$/)
69	{
70		print STDERR
71"You do not appear to have $indent version $INDENT_VERSION installed on your system.\n";
72		exit 1;
73	}
74
75	system("$indent -gnu < $devnull > $devnull 2>&1");
76	if ($? == 0)
77	{
78		print STDERR
79		  "You appear to have GNU indent rather than BSD indent.\n";
80		exit 1;
81	}
82}
83
84
85sub load_typedefs
86{
87
88	# try fairly hard to find the typedefs file if it's not set
89
90	foreach my $try ('.', 'src/tools/pgindent', '/usr/local/etc')
91	{
92		$typedefs_file ||= "$try/typedefs.list"
93		  if (-f "$try/typedefs.list");
94	}
95
96	# try to find typedefs by moving up directory levels
97	my $tdtry = "..";
98	foreach (1 .. 5)
99	{
100		$typedefs_file ||= "$tdtry/src/tools/pgindent/typedefs.list"
101		  if (-f "$tdtry/src/tools/pgindent/typedefs.list");
102		$tdtry = "$tdtry/..";
103	}
104	die "cannot locate typedefs file \"$typedefs_file\"\n"
105	  unless $typedefs_file && -f $typedefs_file;
106
107	open(my $typedefs_fh, '<', $typedefs_file)
108	  || die "cannot open typedefs file \"$typedefs_file\": $!\n";
109	my @typedefs = <$typedefs_fh>;
110	close($typedefs_fh);
111
112	# add command-line-supplied typedefs?
113	if (defined($typedef_str))
114	{
115		foreach my $typedef (split(m/[, \t\n]+/, $typedef_str))
116		{
117			push(@typedefs, $typedef . "\n");
118		}
119	}
120
121	# remove certain entries
122	@typedefs =
123	  grep { !m/^(FD_SET|date|interval|timestamp|ANY)\n?$/ } @typedefs;
124
125	# write filtered typedefs
126	my $filter_typedefs_fh = new File::Temp(TEMPLATE => "pgtypedefXXXXX");
127	print $filter_typedefs_fh @typedefs;
128	$filter_typedefs_fh->close();
129
130	# temp file remains because we return a file handle reference
131	return $filter_typedefs_fh;
132}
133
134
135sub process_exclude
136{
137	if ($excludes && @files)
138	{
139		open(my $eh, '<', $excludes)
140		  || die "cannot open exclude file \"$excludes\"\n";
141		while (my $line = <$eh>)
142		{
143			chomp $line;
144			my $rgx = qr!$line!;
145			@files = grep { $_ !~ /$rgx/ } @files if $rgx;
146		}
147		close($eh);
148	}
149}
150
151
152sub read_source
153{
154	my $source_filename = shift;
155	my $source;
156
157	open(my $src_fd, '<', $source_filename)
158	  || die "cannot open file \"$source_filename\": $!\n";
159	local ($/) = undef;
160	$source = <$src_fd>;
161	close($src_fd);
162
163	return $source;
164}
165
166
167sub write_source
168{
169	my $source          = shift;
170	my $source_filename = shift;
171
172	open(my $src_fh, '>', $source_filename)
173	  || die "cannot open file \"$source_filename\": $!\n";
174	print $src_fh $source;
175	close($src_fh);
176}
177
178
179sub pre_indent
180{
181	my $source = shift;
182
183	## Comments
184
185	# Convert // comments to /* */
186	$source =~ s!^([ \t]*)//(.*)$!$1/* $2 */!gm;
187
188	# Adjust dash-protected block comments so indent won't change them
189	$source =~ s!/\* +---!/*---X_X!g;
190
191	## Other
192
193	# Prevent indenting of code in 'extern "C"' blocks.
194	# we replace the braces with comments which we'll reverse later
195	my $extern_c_start = '/* Open extern "C" */';
196	my $extern_c_stop  = '/* Close extern "C" */';
197	$source =~
198s!(^#ifdef[ \t]+__cplusplus.*\nextern[ \t]+"C"[ \t]*\n)\{[ \t]*$!$1$extern_c_start!gm;
199	$source =~ s!(^#ifdef[ \t]+__cplusplus.*\n)\}[ \t]*$!$1$extern_c_stop!gm;
200
201	# Protect backslashes in DATA() and wrapping in CATALOG()
202	$source =~ s!^((DATA|CATALOG)\(.*)$!/*$1*/!gm;
203
204	return $source;
205}
206
207
208sub post_indent
209{
210	my $source          = shift;
211	my $source_filename = shift;
212
213	# Restore DATA/CATALOG lines
214	$source =~ s!^/\*((DATA|CATALOG)\(.*)\*/$!$1!gm;
215
216	# Put back braces for extern "C"
217	$source =~ s!^/\* Open extern "C" \*/$!{!gm;
218	$source =~ s!^/\* Close extern "C" \*/$!}!gm;
219
220	## Comments
221
222	# Undo change of dash-protected block comments
223	$source =~ s!/\*---X_X!/* ---!g;
224
225	# Fix run-together comments to have a tab between them
226	$source =~ s!\*/(/\*.*\*/)$!*/\t$1!gm;
227
228	## Functions
229
230	# Use a single space before '*' in function return types
231	$source =~ s!^([A-Za-z_]\S*)[ \t]+\*$!$1 *!gm;
232
233	# Move prototype names to the same line as return type.  Useful
234	# for ctags.  Indent should do this, but it does not.  It formats
235	# prototypes just like real functions.
236
237	my $ident   = qr/[a-zA-Z_][a-zA-Z_0-9]*/;
238	my $comment = qr!/\*.*\*/!;
239
240	$source =~ s!
241			(\n$ident[^(\n]*)\n                  # e.g. static void
242			(
243				$ident\(\n?                      # func_name(
244				(.*,([ \t]*$comment)?\n)*        # args b4 final ln
245				.*\);([ \t]*$comment)?$          # final line
246			)
247		!$1 . (substr($1,-1,1) eq '*' ? '' : ' ') . $2!gmxe;
248
249	return $source;
250}
251
252
253sub run_indent
254{
255	my $source        = shift;
256	my $error_message = shift;
257
258	my $cmd = "$indent $indent_opts -U" . $filtered_typedefs_fh->filename;
259
260	my $tmp_fh = new File::Temp(TEMPLATE => "pgsrcXXXXX");
261	my $filename = $tmp_fh->filename;
262	print $tmp_fh $source;
263	$tmp_fh->close();
264
265	$$error_message = `$cmd $filename 2>&1`;
266
267	return "" if ($? || length($$error_message) > 0);
268
269	unlink "$filename.BAK";
270
271	open(my $src_out, '<', $filename);
272	local ($/) = undef;
273	$source = <$src_out>;
274	close($src_out);
275
276	return $source;
277
278}
279
280
281# for development diagnostics
282sub diff
283{
284	my $pre   = shift;
285	my $post  = shift;
286	my $flags = shift || "";
287
288	print STDERR "running diff\n";
289
290	my $pre_fh  = new File::Temp(TEMPLATE => "pgdiffbXXXXX");
291	my $post_fh = new File::Temp(TEMPLATE => "pgdiffaXXXXX");
292
293	print $pre_fh $pre;
294	print $post_fh $post;
295
296	$pre_fh->close();
297	$post_fh->close();
298
299	system( "diff $flags "
300		  . $pre_fh->filename . " "
301		  . $post_fh->filename
302		  . " >&2");
303}
304
305
306sub run_build
307{
308	eval "use LWP::Simple;";    ## no critic (ProhibitStringyEval);
309
310	my $code_base = shift || '.';
311	my $save_dir = getcwd();
312
313	# look for the code root
314	foreach (1 .. 5)
315	{
316		last if -d "$code_base/src/tools/pgindent";
317		$code_base = "$code_base/..";
318	}
319
320	die "cannot locate src/tools/pgindent directory in \"$code_base\"\n"
321	  unless -d "$code_base/src/tools/pgindent";
322
323	chdir "$code_base/src/tools/pgindent";
324
325	my $typedefs_list_url =
326	  "https://buildfarm.postgresql.org/cgi-bin/typedefs.pl";
327
328	my $rv = getstore($typedefs_list_url, "tmp_typedefs.list");
329
330	die "cannot fetch typedefs list from $typedefs_list_url\n"
331	  unless is_success($rv);
332
333	$ENV{PGTYPEDEFS} = abs_path('tmp_typedefs.list');
334
335	my $indentrepo = "https://git.postgresql.org/git/pg_bsd_indent.git";
336	system("git clone $indentrepo >$devnull 2>&1");
337	die "could not fetch pg_bsd_indent sources from $indentrepo\n"
338	  unless $? == 0;
339
340	chdir "pg_bsd_indent" || die;
341	system("make all check >$devnull");
342	die "could not build pg_bsd_indent from source\n"
343	  unless $? == 0;
344
345	$ENV{PGINDENT} = abs_path('pg_bsd_indent');
346
347	chdir $save_dir;
348}
349
350
351sub build_clean
352{
353	my $code_base = shift || '.';
354
355	# look for the code root
356	foreach (1 .. 5)
357	{
358		last if -d "$code_base/src/tools/pgindent";
359		$code_base = "$code_base/..";
360	}
361
362	die "cannot locate src/tools/pgindent directory in \"$code_base\"\n"
363	  unless -d "$code_base/src/tools/pgindent";
364
365	chdir "$code_base";
366
367	system("rm -rf src/tools/pgindent/pg_bsd_indent");
368	system("rm -f src/tools/pgindent/tmp_typedefs.list");
369}
370
371
372# main
373
374# get the list of files under code base, if it's set
375File::Find::find(
376	{   wanted => sub {
377			my ($dev, $ino, $mode, $nlink, $uid, $gid);
378			(($dev, $ino, $mode, $nlink, $uid, $gid) = lstat($_))
379			  && -f _
380			  && /^.*\.[ch]\z/s
381			  && push(@files, $File::Find::name);
382		  }
383	},
384	$code_base) if $code_base;
385
386process_exclude();
387
388$filtered_typedefs_fh = load_typedefs();
389
390check_indent();
391
392# any non-option arguments are files to be processed
393push(@files, @ARGV);
394
395foreach my $source_filename (@files)
396{
397
398	# Automatically ignore .c and .h files that correspond to a .y or .l
399	# file.  indent tends to get badly confused by Bison/flex output,
400	# and there's no value in indenting derived files anyway.
401	my $otherfile = $source_filename;
402	$otherfile =~ s/\.[ch]$/.y/;
403	next if $otherfile ne $source_filename && -f $otherfile;
404	$otherfile =~ s/\.y$/.l/;
405	next if $otherfile ne $source_filename && -f $otherfile;
406
407	my $source        = read_source($source_filename);
408	my $orig_source   = $source;
409	my $error_message = '';
410
411	$source = pre_indent($source);
412
413	$source = run_indent($source, \$error_message);
414	if ($source eq "")
415	{
416		print STDERR "Failure in $source_filename: " . $error_message . "\n";
417		next;
418	}
419
420	$source = post_indent($source, $source_filename);
421
422	write_source($source, $source_filename) if $source ne $orig_source;
423}
424
425build_clean($code_base) if $build;
426