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: