eval '(exit $?0)' && eval 'exec perl -wS "$0" "$@"'
& eval 'exec perl -wS "$0" $argv:q'
if 0;
# This file is part of GNU Mailutils.
# Copyright (C) 2017-2021 Free Software Foundation, Inc.
#
# GNU Mailutils is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3, or (at
# your option) any later version.
#
# GNU Mailutils is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with GNU Mailutils. If not, see .
use strict;
use warnings;
use Getopt::Long qw(:config gnu_getopt no_ignore_case require_order auto_version);
use File::Basename;
use File::Temp qw(tempdir tempfile);
use Pod::Man;
use Pod::Usage;
use Cwd 'abs_path';
=head1 NAME
gylwrap - wrapper for yacc, lex and similar programs
=head1 SYNOPSIS
B
[B<-?>]
[B<--reset>]
[B<--yyrepl=>I]
[B<--yysym=>I]
[B<--help>]
[B<--version>]
I
[I I]...
B<--> I [I]
B
B<--dump>
[B<--reset>]
[B<--yyrepl=>I]
[I]
=head1 DESCRIPTION
Wraps B and B invocations to rename their output files.
It also ensures that multiple I instances can be invoked
in a single directory in parallel and allows for renaming global
symbols to avoid clashes when multiple parsers and/or lexers are
linked in a single executable.
To achieve this, B creates a temporary directory, changes
to it, and runs I, with I and I as its arguments.
Upon successful exit from I, B processes the
I-I pairs. Each I file is then renamed
to the file I, taking care to fix up any eventual B<#line>
directives.
If B<--yyrepl=I> is given, the global symbols that can cause
name clashes are renamed by replacing the initial B with I.
For a list of symbols that are subject for replacement, inspect the
B<@sym> variable at the start of the script. Additional names can be
added to this list using the B<--yysym> option.
Prior to running the I, B program checks whether the
file named B exists in directory of the I file.
If found, it is parsed as follows. Empty lines and comments (introduced
by the hash sign) are ignored. Rest of lines are either option
assignements, or section headings.
Option assignements have the form B = I>, and generally,
have the same meaning as the corresponding command line option without
the leading two dashes:
=over 4
=item B I
Replace the B prefix with I in the identifiers.
=item B I
Add I to the list of symbols suitable for prefix replacement.
This keyword can appear multiple times.
=item B I
Add I to the invocation of I. This is useful, if you
have several parsers in the same directory, and some of them require
the B<-d> option, while others don't.
=back
Section headers have the form B<[I]>. The settings under a
section header have effect only if I is the same as the I
command line argument.
=head1 OPTIONS
=over 4
=item B<--dump>
Dumps the entire program (after applying any eventual B<--yysym> options)
to I. If I is not given, rewrite the program file with
the output. Use this option to hardcode more replaceable symbols into
this program. See the BOOTSTRAP section for an example.
=item B<--reset>
Clears the yysym array.
=item B<--yyrepl=>I
Replace the B prefix in global symbols with I.
=item B<--yysym=>I
Add I to the list of symbols subject for replacement.
=item B<-?>, B<--help>
Displays help text and exit
=item B<--version>
Displays program version and exits.
=back
=head1 NOTE
This script is an improved version of the B script, included
in the GNU Automake distribution.
=head1 BOOTSTRAP
This version of gylwrap was bootstrapped as follows:
gylwrap --dump --reset --yysym=yymaxdepth --yysym=yyparse\
--yysym=yylex --yysym=yyerror --yysym=yylval\
--yysym=yychar --yysym=yydebug --yysym=yypact\
--yysym=yyr1 --yysym=yyr2 --yysym=yydef --yysym=yychk\
--yysym=yypgo --yysym=yyact --yysym=yyexca\
--yysym=yyerrflag --yysym=yynerrs --yysym=yyps\
--yysym=yypv --yysym=yys --yysym=yy_yys\
--yysym=yystate --yysym=yytmp --yysym=yyv\
--yysym=yy_yyv --yysym=yyval --yysym=yylloc\
--yysym=yyreds --yysym=yytoks --yysym=yylhs\
--yysym=yylen --yysym=yydefred --yysym=yydgoto\
--yysym=yysindex --yysym=yyrindex --yysym=yygindex\
--yysym=yytable --yysym=yycheck --yysym=yyname\
--yysym=yyrule --yysym=yy_create_buffer\
--yysym=yy_delete_buffer --yysym=yy_flex_debug\
--yysym=yy_init_buffer --yysym=yy_flush_buffer\
--yysym=yy_load_buffer_state\
--yysym=yy_switch_to_buffer --yysym=yyin\
--yysym=yyleng --yysym=yylex --yysym=yylineno\
--yysym=yyout --yysym=yyrestart --yysym=yytext\
--yysym=yywrap --yysym=yyalloc --yysym=yyrealloc\
--yysym=yyfree --yysym=yy_scan_buffer\
--yysym=yy_scan_bytes --yysym=yy_scan_string\
--yysym=yyget_debug --yysym=yyget_in\
--yysym=yyget_leng --yysym=yyget_lineno\
--yysym=yyget_out --yysym=yyget_text\
--yysym=yylex_destroy --yysym=yypop_buffer_state\
--yysym=yypush_buffer_state --yysym=yyset_debug\
--yysym=yyset_in --yysym=yyset_lineno\
--yysym=yyset_out
=cut
# List of symbols suitable for prefix replacements. See the
# options --yyrepl and --yysym, and similar statements in the configuration
# file.
my @yysym = qw(
yymaxdepth
yyparse
yylex
yyerror
yylval
yychar
yydebug
yypact
yyr1
yyr2
yydef
yychk
yypgo
yyact
yyexca
yyerrflag
yynerrs
yyps
yypv
yys
yy_yys
yystate
yytmp
yyv
yy_yyv
yyval
yylloc
yyreds
yytoks
yylhs
yylen
yydefred
yydgoto
yysindex
yyrindex
yygindex
yytable
yycheck
yyname
yyrule
yy_create_buffer
yy_delete_buffer
yy_flex_debug
yy_init_buffer
yy_flush_buffer
yy_load_buffer_state
yy_switch_to_buffer
yyin
yyleng
yylex
yylineno
yyout
yyrestart
yytext
yywrap
yyalloc
yyrealloc
yyfree
yy_scan_buffer
yy_scan_bytes
yy_scan_string
yyget_debug
yyget_in
yyget_leng
yyget_lineno
yyget_out
yyget_text
yylex_destroy
yypop_buffer_state
yypush_buffer_state
yyset_debug
yyset_in
yyset_lineno
yyset_out
);
my @addsym;
our $VERSION = '1.01';
# If prefix replacement is requested, the list above is assembled into
# a single regular expression, stored here.
my $yyrx = q{(?:_(?:(?:crea|dele)te_buffer|fl(?:ex_debug|ush_buffer)|init_buffer|load_buffer_state|s(?:can_(?:b(?:uffer|ytes)|string)|witch_to_buffer)|yy[sv])|a(?:ct|lloc)|ch(?:ar|(?:ec)?k)|d(?:e(?:bug|f(?:red)?)|goto)|e(?:rr(?:flag|or)|xca)|free|g(?:et_(?:debug|in|l(?:eng|ineno)|(?:ou|tex)t)|index)|(?:le|i)n|l(?:e(?:ng|x(?:_destroy)?)|hs|ineno|loc|val)|maxdepth|n(?:ame|errs)|(?:ou|pac)t|p(?:arse|go|op_buffer_state|ush_buffer_state|[sv])|r(?:e(?:alloc|ds|start)|index|ule|[12])|s(?:et_(?:debug|in|lineno|out)|index|tate)?|t(?:able|ext|mp|oks)|v(?:al)?|wrap)};
# String to replace the "yy" prefix with.
my $yyrepl;
# Input directory with special characters escaped, for "#line" directive
# fixup.
my $input_rx;
# Configuration settings from the "gylwrap.conf" file. Indexed by
# input file name. Default entry is ''.
my %config;
# Name of the first output file. This is used to avoid bailing out if
# one of the output files (except the principal one) does not exist.
my $parser;
# Name this program was invoked as.
my $progname = basename($0);
# List of files created during the run, for cleanup purposes.
my @created;
sub filter {
my ($from, $to) = @_;
my $target = basename($to);
my $ifd;
unless (open($ifd, '<', $from)) {
return if $from ne $parser;
die "can't open input file $from: $!";
}
open(my $ofd, '>', $to)
or die "can't open output file $to: $!";
push @created, $to;
while (<$ifd>) {
if (/^#/) {
s{$input_rx/}{};
s{"$from"}{"$target"};
}
if ($yyrepl) {
s{\byy($yyrx)\b}{${yyrepl}$1}g;
}
print $ofd $_
}
close $ifd;
close $ofd;
}
sub readconf {
my $file = shift;
open(my $fd, '<', $file)
or die "can't open $file: $!";
my $key = '';
while (<$fd>) {
chomp;
s/^\s+//;
if (/^#/ || /^$/) {
next;
} elsif (/^\[(.+)\]/) {
$key = $1;
} elsif (m/(.+?)\s*=\s*(.+)$/) {
if ($1 eq 'yysym' || $1 eq 'flags') {
push @{$config{$key}{$1}}, (split /\s+/, $2);
} else {
$config{$key}{$1} = $2;
}
} else {
print STDERR "$file:$.: unrecognized line\n";
}
}
close($fd);
}
sub mkrx {
my $ret = eval {
require List::Regexp;
List::Regexp::regexp_opt({ type => 'pcre' }, map { s/^yy//; $_ } @yysym);
};
if ($@) {
if ($@ =~ /^Can't locate.*Regexp\.pm/) {
die "Perl module List::Regexp is not installed. Please install it and try again";
} else {
die $@;
}
}
return $ret;
}
sub backup {
my $file = shift;
my $level = shift || 0;
my $bak = "$file~";
if (-e $bak) {
if ($level == 3) {
unlink $bak
or die "can't unlink outdated backup file $bak: $!";
} else {
backup($bak, $level + 1);
}
}
rename $file, $bak
or die "can't rename $file to $bak";
}
sub dumpme {
my $outname = shift || $0;
die "too many arguments for --dump option" if @_;
open(my $in, '<', $0)
or die "can't open $0 for reading: $!";
my ($out, $tempname) = tempfile(basename($outname) . ".XXXXXX",
DIR => dirname($outname));
push @yysym, @addsym;
my $skip;
while (<$in>) {
chomp;
if ($skip) {
next unless /^=/;
$skip = 0;
}
if (/^(my\s+\$yyrx)\s*(?=.*)?;\s*?/) {
my $rx = mkrx;
print $out "$1 = q{$rx};\n";
} elsif (s/^(my \@yysym\s+=).*/$1/) {
my $start = $.;
print $out "$_ qw(\n"
. join("\n", map { " $_" } @yysym) . "\n";
while (<$in>) {
if (/^\);/) {
$start = undef;
last;
}
}
die "can't find closing parenthesis in definition at $0:$start"
if defined $start;
redo;
} elsif (/^=head1\s+BOOTSTRAP/) {
print $out "$_\n\n";
print $out "This version of gylwrap was bootstrapped as follows:\n\n";
my $s = " gylwrap --dump --reset ";
print $out $s;
my $len = length($s);
foreach my $sym (@yysym) {
my $opt = "--yysym=$sym";
my $l = length($opt);
if ($len + $l + 1 > 64) {
print $out "\\\n ";
$len = 10;
} else {
$opt = " $opt";
}
print $out $opt;
$len += length($opt);
}
print $out "\n\n";
$skip = 1;
} else {
print $out "$_\n";
}
}
close($in);
close($out);
if (-e $outname) {
backup($outname);
}
rename $tempname, $outname
or die "can't rename $tempname to $outname: $!";
chmod 0755, $outname;
exit(0);
}
my $input;
my @output;
my $dump;
GetOptions("yyrepl=s" => \$yyrepl,
"yysym=s@" => \@addsym,
"reset" => sub { $yyrx = undef; @yysym = () },
"dump" => \$dump,
"help|?" => sub {
pod2usage(-exitstatus => 0, -verbose => 2);
}
) or exit(1);
die "some --yysym arguments don't start with yy"
if @addsym && grep(!/^yy/, @addsym);
dumpme(@ARGV) if $dump;
$input = shift @ARGV;
while (my $arg = shift @ARGV) {
last if ($arg eq '--');
push @output, $arg;
}
pod2usage(-exitstatus => 1, -verbose => 0, -output => \*STDERR)
unless (@output && (@output % 2) == 0);
# Make sure input file name is absolute
$input = abs_path($input);
my $input_dir = dirname($input);
$input_rx = qr($input_dir);
my $confile = "$input_dir/gylwrap.conf";
readconf($confile) if -r $confile;
my $input_base = basename($input);
unless ($yyrepl) {
$yyrepl = $config{$input_base}{yyrepl} || $config{''}{yyrepl};
}
if ($yyrepl) {
push @addsym, @{$config{$input_base}{yysym}}
if exists $config{$input_base}{yysym};
push @addsym, @{$config{''}{yysym}}
if exists $config{''}{yysym};
if (@addsym) {
push @yysym, @addsym;
$yyrx = undef;
}
}
$yyrx = mkrx unless defined($yyrx);
if (my $flags = $config{$input_base}{flags} || $config{''}{flags}) {
push @ARGV, @$flags;
}
push @ARGV, $input;
$parser = $output[0];
# Create working directory
my $wd = tempdir("ylXXXXXX", DIR => '.', CLEANUP => 1)
or die "cannot create temporary directory";
chdir $wd
or die "cannot change to the temporary directory";
END {
if ($?) {
unlink @created;
}
chdir "..";
}
system(@ARGV);
if ($? == -1) {
print STDERR "$ARGV[0]: $!\n";
exit(127);
} elsif ($? & 127) {
print STDERR "$ARGV[0] died with signal ".($? & 127)."\n";
exit(127);
} else {
my $code = $? >> 8;
exit($code) if $code;
}
while (my $from = shift @output) {
my $to = shift @output;
$to = '../' . $to unless $to =~ m{^/};
filter($from, $to);
}
exit 0;
# Local Variables:
# mode: perl
# End: