#!/usr/bin/perl # Copyright (c) 2021-2025, PostgreSQL Global Development Group # Program to maintain uniform layout style in our C code. # Exit codes: # 0 -- all OK # 1 -- error invoking pgindent, nothing done # 2 -- --check mode and at least one file requires changes # 3 -- pg_bsd_indent failed on at least one file use strict; use warnings FATAL => 'all'; use Cwd qw(abs_path getcwd); use File::Find; use File::Spec; use File::Temp; use IO::Handle; use Getopt::Long; # Update for pg_bsd_indent version my $INDENT_VERSION = "2.1.2"; # Our standard indent settings my $indent_opts = "-bad -bap -bbb -bc -bl -cli1 -cp33 -cdb -nce -d0 -di12 -nfc1 -i4 -l79 -lp -lpl -nip -npro -sac -tpg -ts4"; my $devnull = File::Spec->devnull; my ($typedefs_file, $typedef_str, @excludes, $indent, $diff, $check, $help, @commits,); $help = 0; my %options = ( "help" => \$help, "commit=s" => \@commits, "typedefs=s" => \$typedefs_file, "list-of-typedefs=s" => \$typedef_str, "excludes=s" => \@excludes, "indent=s" => \$indent, "diff" => \$diff, "check" => \$check,); GetOptions(%options) || usage("bad command line argument"); usage() if $help; usage("Cannot use --commit with command line file list") if (@commits && @ARGV); # command line option wins, then environment, then locations based on current # dir, then default location $typedefs_file ||= $ENV{PGTYPEDEFS}; # get indent location for environment or default $indent ||= $ENV{PGINDENT} || $ENV{INDENT} || "pg_bsd_indent"; my $sourcedir = locate_sourcedir(); # if it's the base of a postgres tree, we will exclude the files # postgres wants excluded if ($sourcedir) { my $exclude_candidate = "$sourcedir/exclude_file_patterns"; push(@excludes, $exclude_candidate) if -f $exclude_candidate; } # The typedef list that's mechanically extracted by the buildfarm may omit # some names we want to treat like typedefs, e.g. "bool" (which is a macro # according to ), and may include some names we don't want # treated as typedefs, although various headers that some builds include # might make them so. For the moment we just hardwire a list of names # to add and a list of names to exclude; eventually this may need to be # easier to configure. Note that the typedefs need trailing newlines. my @additional = ("bool\n"); my %excluded = map { +"$_\n" => 1 } qw( ANY FD_SET U abs allocfunc boolean date digit ilist interval iterator other pointer printfunc reference string timestamp type wrap ); # globals my @files; my $filtered_typedefs_fh; sub check_indent { if (system("$indent -? < $devnull > $devnull 2>&1") != 0) { if ($? >> 8 != 1) { print STDERR "You do not appear to have $indent installed on your system.\n"; exit 1; } } if (`$indent --version` !~ m/ $INDENT_VERSION /) { print STDERR "You do not appear to have $indent version $INDENT_VERSION installed on your system.\n"; exit 1; } if (system("$indent -gnu < $devnull > $devnull 2>&1") == 0) { print STDERR "You appear to have GNU indent rather than BSD indent.\n"; exit 1; } return; } sub locate_sourcedir { # try fairly hard to locate the sourcedir my $sub = "./src/tools/pgindent"; return $sub if -d $sub; # try to find it from an ancestor directory $sub = "../src/tools/pgindent"; foreach (1 .. 4) { return $sub if -d $sub; $sub = "../$sub"; } return; # undef if nothing found } sub load_typedefs { # try fairly hard to find the typedefs file if it's not set foreach my $try ('.', $sourcedir, '/usr/local/etc') { last if $typedefs_file; next unless defined $try; $typedefs_file = "$try/typedefs.list" if (-f "$try/typedefs.list"); } die "cannot locate typedefs file \"$typedefs_file\"\n" unless $typedefs_file && -f $typedefs_file; open(my $typedefs_fh, '<', $typedefs_file) || die "cannot open typedefs file \"$typedefs_file\": $!\n"; my @typedefs = <$typedefs_fh>; close($typedefs_fh); # add command-line-supplied typedefs? if (defined($typedef_str)) { foreach my $typedef (split(m/[, \t\n]+/, $typedef_str)) { push(@typedefs, $typedef . "\n"); } } # add additional entries push(@typedefs, @additional); # remove excluded entries @typedefs = grep { !$excluded{$_} } @typedefs; # write filtered typedefs my $filter_typedefs_fh = new File::Temp(TEMPLATE => "pgtypedefXXXXX"); print $filter_typedefs_fh @typedefs; $filter_typedefs_fh->close(); # temp file remains because we return a file handle reference return $filter_typedefs_fh; } sub process_exclude { foreach my $excl (@excludes) { last unless @files; open(my $eh, '<', $excl) || die "cannot open exclude file \"$excl\"\n"; while (my $line = <$eh>) { chomp $line; next if $line =~ m/^#/; my $rgx = qr!$line!; @files = grep { $_ !~ /$rgx/ } @files if $rgx; } close($eh); } return; } sub read_source { my $source_filename = shift; my $source; open(my $src_fd, '<', $source_filename) || die "cannot open file \"$source_filename\": $!\n"; local ($/) = undef; $source = <$src_fd>; close($src_fd); return $source; } sub write_source { my $source = shift; my $source_filename = shift; open(my $src_fh, '>', $source_filename) || die "cannot open file \"$source_filename\": $!\n"; print $src_fh $source; close($src_fh); return; } sub pre_indent { my $source = shift; ## Comments # Convert // comments to /* */ $source =~ s!^([ \t]*)//(.*)$!$1/* $2 */!gm; # Adjust dash-protected block comments so indent won't change them $source =~ s!/\* +---!/*---X_X!g; ## Other # Prevent indenting of code in 'extern "C"' blocks. # we replace the braces with comments which we'll reverse later my $extern_c_start = '/* Open extern "C" */'; my $extern_c_stop = '/* Close extern "C" */'; $source =~ s!(^#ifdef[ \t]+__cplusplus.*\nextern[ \t]+"C"[ \t]*\n)\{[ \t]*$!$1$extern_c_start!gm; $source =~ s!(^#ifdef[ \t]+__cplusplus.*\n)\}[ \t]*$!$1$extern_c_stop!gm; # Protect wrapping in CATALOG() $source =~ s!^(CATALOG\(.*)$!/*$1*/!gm; return $source; } sub post_indent { my $source = shift; # Restore CATALOG lines $source =~ s!^/\*(CATALOG\(.*)\*/$!$1!gm; # Put back braces for extern "C" $source =~ s!^/\* Open extern "C" \*/$!{!gm; $source =~ s!^/\* Close extern "C" \*/$!}!gm; ## Comments # Undo change of dash-protected block comments $source =~ s!/\*---X_X!/* ---!g; # Fix run-together comments to have a tab between them $source =~ s!\*/(/\*.*\*/)$!*/\t$1!gm; ## Functions # Use a single space before '*' in function return types $source =~ s!^([A-Za-z_]\S*)[ \t]+\*$!$1 *!gm; return $source; } sub run_indent { my $source = shift; my $error_message = shift; my $cmd = "$indent $indent_opts -U" . $filtered_typedefs_fh->filename; my $tmp_fh = new File::Temp(TEMPLATE => "pgsrcXXXXX"); my $filename = $tmp_fh->filename; print $tmp_fh $source; $tmp_fh->close(); $$error_message = `$cmd $filename 2>&1`; return "" if ($? || length($$error_message) > 0); unlink "$filename.BAK"; open(my $src_out, '<', $filename) || die $!; local ($/) = undef; $source = <$src_out>; close($src_out); return $source; } sub diff { my $indented = shift; my $source_filename = shift; my $post_fh = new File::Temp(TEMPLATE => "pgdiffXXXXX"); my $post_fh_filename = $post_fh->filename; print $post_fh $indented; $post_fh->close(); my $diff = `diff -upd "$source_filename" "$post_fh_filename" 2>&1`; return $diff; } sub usage { my $message = shift; my $helptext = <<'EOF'; Usage: pgindent [OPTION]... [FILE|DIR]... Options: --help show this message and quit --commit=gitref use files modified by the named commit --typedefs=FILE file containing a list of typedefs --list-of-typedefs=STR string containing typedefs, space separated --excludes=PATH file containing list of filename patterns to ignore --indent=PATH path to pg_bsd_indent program --diff show the changes that would be made --check exit with status 2 if any changes would be made The --excludes and --commit options can be given more than once. EOF if ($help) { print $helptext; exit 0; } else { print STDERR "$message\n", $helptext; exit 1; } } # main $filtered_typedefs_fh = load_typedefs(); check_indent(); my $wanted = sub { my ($dev, $ino, $mode, $nlink, $uid, $gid); (($dev, $ino, $mode, $nlink, $uid, $gid) = lstat($_)) && -f _ && /^.*\.[ch]\z/s && push(@files, $File::Find::name); }; # any non-option arguments are files or directories to be processed File::Find::find({ wanted => $wanted }, @ARGV) if @ARGV; # commit file locations are relative to the source root chdir "$sourcedir/../../.." if @commits && $sourcedir; # process named commits by comparing each with their immediate ancestor foreach my $commit (@commits) { my $prev = "$commit~"; my @affected = `git diff --diff-filter=ACMR --name-only $prev $commit`; die "git error" if $?; chomp(@affected); push(@files, @affected); } warn "No files to process" unless @files; # remove excluded files from the file list process_exclude(); my %processed; my $status = 0; foreach my $source_filename (@files) { # skip duplicates next if $processed{$source_filename}; $processed{$source_filename} = 1; # ignore anything that's not a .c or .h file next unless $source_filename =~ /\.[ch]$/; # don't try to indent a file that doesn't exist unless (-f $source_filename) { warn "Could not find $source_filename"; next; } # Automatically ignore .c and .h files that correspond to a .y or .l # file. indent tends to get badly confused by Bison/flex output, # and there's no value in indenting derived files anyway. my $otherfile = $source_filename; $otherfile =~ s/\.[ch]$/.y/; next if $otherfile ne $source_filename && -f $otherfile; $otherfile =~ s/\.y$/.l/; next if $otherfile ne $source_filename && -f $otherfile; my $source = read_source($source_filename); my $orig_source = $source; my $error_message = ''; $source = pre_indent($source); $source = run_indent($source, \$error_message); if ($source eq "") { print STDERR "Failure in $source_filename: " . $error_message . "\n"; $status = 3; next; } $source = post_indent($source); if ($source ne $orig_source) { if (!$diff && !$check) { write_source($source, $source_filename); } else { if ($diff) { print diff($source, $source_filename); } if ($check) { $status ||= 2; last unless $diff; } } } } exit $status;