# help-extract -- extract --help and --version output from a script.
# Copyright (C) 2020-2021 Free Software Foundation, Inc.
# This program 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 of the License, or
# (at your option) any later version.
# This program 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 this program. If not, see .
# Written by Zack Weinberg.
use strict;
use warnings;
# File::Spec itself was added in 5.005.
# File::Spec::Functions was added in 5.6.1 which is just barely too new.
use File::Spec;
# This script is not intended to be used directly. It's run by
# help2man via wrappers in man/, e.g. man/autoconf.w, as if it were
# one of autoconf's executable scripts. It extracts the --help and
# --version output of that script from its source form, without
# actually running it. The script to work from is set by the wrapper,
# and several other parameters are passed down from the Makefile as
# environment variables; see parse_args below.
# The point of this script is, the preprocessed forms of the
# executable scripts, and their wrappers for uninstalled use
# (e.g. /{bin,tests}/autoconf) do not need to exist to
# generate the corresponding manpages. This is desirable because we
# can't put those dependencies in the makefiles without breaking
# people's ability to build autoconf from a release tarball without
# help2man installed. It also ensures that we will generate manpages
# from the current source code and not from an older version of the
# script that has already been installed.
## ----------------------------- ##
## Extraction from Perl scripts. ##
## ----------------------------- ##
sub eval_qq_no_interpolation ($)
{
# The argument is expected to be a "double quoted string" including the
# leading and trailing delimiters. Returns the text of this string after
# processing backslash escapes but NOT interpolation.
# / (?\\\\)* blah /x means match blah preceded by an
# *even* number of backslashes. It would be nice if we could use \K
# to exclude the backslashes from the matched text, but that was only
# added in Perl 5.10 and we still support back to 5.006.
return eval $_[0] =~ s/ (?\\\\)* [\$\@] /\\$&/xrg;
}
sub extract_channeldefs_usage ($)
{
my ($channeldefs_pm) = @_;
my $usage = "";
my $parse_state = 0;
local $_;
open (my $fh, "<", $channeldefs_pm) or die "$channeldefs_pm: $!\n";
while (<$fh>)
{
if ($parse_state == 0)
{
$parse_state = 1 if /^sub usage\b/;
}
elsif ($parse_state == 1)
{
if (s/^ return "//)
{
$parse_state = 2;
$usage .= $_;
}
}
elsif ($parse_state == 2)
{
if (s/(?\\\\)*) "; $/$1/x)
{
$usage .= $_;
return $usage;
}
else
{
$usage .= $_;
}
}
}
die "$channeldefs_pm: unexpected EOF in state $parse_state\n";
}
sub extract_perl_assignment (*$$$)
{
my ($fh, $source, $channeldefs_pm, $what) = @_;
my $value = "";
my $parse_state = 0;
local $_;
while (<$fh>)
{
if ($parse_state == 0)
{
if (s/^\$\Q${what}\E = (?=")//o)
{
$value .= $_;
$parse_state = 1;
}
}
elsif ($parse_state == 1)
{
if (/^"\s*\.\s*Autom4te::ChannelDefs::usage\s*(?:\(\))?\s*\.\s*"$/)
{
$value .= extract_channeldefs_usage ($channeldefs_pm);
}
elsif (/^";$/)
{
$value .= '"';
return eval_qq_no_interpolation ($value);
}
else
{
$value .= $_;
}
}
}
die "$source: unexpected EOF in state $parse_state\n";
}
## ------------------------------ ##
## Extraction from shell scripts. ##
## ------------------------------ ##
sub extract_shell_assignment (*$$)
{
my ($fh, $source, $what) = @_;
my $value = "";
my $parse_state = 0;
local $_;
while (<$fh>)
{
if ($parse_state == 0)
{
if (/^\Q${what}\E=\[\"\\$/)
{
$parse_state = 1;
}
}
elsif ($parse_state == 1)
{
my $done = s/"\]$//;
$value .= $_;
if ($done)
{
# This is not strictly correct but it works acceptably
# for the escapes that actually occur in the strings
# we're extracting.
return eval_qq_no_interpolation ('"'.$value.'"');
}
}
}
die "$source: unexpected EOF in state $parse_state\n";
}
## -------------- ##
## Main program. ##
## -------------- ##
sub extract_assignment ($$$)
{
my ($source, $channeldefs_pm, $what) = @_;
open (my $fh, "<", $source) or die "$source: $!\n";
my $firstline = <$fh>;
if ($firstline =~ /\@PERL\@/ || $firstline =~ /-\*-\s*perl\s*-\*-/i)
{
return extract_perl_assignment ($fh, $source, $channeldefs_pm, $what);
}
elsif ($firstline =~ /\bAS_INIT\b/
|| $firstline =~ /bin\/[a-z0-9]*sh\b/
|| $firstline =~ /-\*-\s*shell-script\s*-\*-/i)
{
return extract_shell_assignment ($fh, $source, $what);
}
else
{
die "$source: language not recognized\n";
}
}
sub main ()
{
# Most of our arguments come from environment variables, because
# help2man doesn't allow for passing additional command line
# arguments to the wrappers, and it's easier to write the wrappers
# to not mess with the command line.
my $usage = "Usage: $0 script-source (--help | --version)
Extract help and version information from a perl or shell script.
Required environment variables:
top_srcdir relative path from cwd to the top of the source tree
channeldefs_pm relative path from top_srcdir to ChannelDefs.pm
PACKAGE_NAME the autoconf PACKAGE_NAME substitution variable
VERSION the autoconf VERSION substitution variable
RELEASE_YEAR the autoconf RELEASE_YEAR substitution variable
The script-source argument should also be relative to top_srcdir.
";
my $source = shift(@ARGV) || die $usage;
my $what = shift(@ARGV) || die $usage;
my $top_srcdir = $ENV{top_srcdir} || die $usage;
my $channeldefs_pm = $ENV{channeldefs_pm} || die $usage;
my $package_name = $ENV{PACKAGE_NAME} || die $usage;
my $version = $ENV{VERSION} || die $usage;
my $release_year = $ENV{RELEASE_YEAR} || die $usage;
if ($what eq "-h" || $what eq "--help")
{
$what = "help";
}
elsif ($what eq "-V" || $what eq "--version")
{
$what = "version";
}
else
{
die $usage;
}
my $cmd_name = $source =~ s{^.*/([^./]+)\.(?:as|in)$}{$1}r;
$source = File::Spec->catfile($top_srcdir, $source);
$channeldefs_pm = File::Spec->catfile($top_srcdir, $channeldefs_pm);
my $text = extract_assignment ($source, $channeldefs_pm, $what);
$text =~ s/\$0\b/$cmd_name/g;
$text =~ s/[@]PACKAGE_NAME@/$package_name/g;
$text =~ s/[@]VERSION@/$version/g;
$text =~ s/[@]RELEASE_YEAR@/$release_year/g;
print $text;
}
main;
### Setup "GNU" style for perl-mode and cperl-mode.
## Local Variables:
## perl-indent-level: 2
## perl-continued-statement-offset: 2
## perl-continued-brace-offset: 0
## perl-brace-offset: 0
## perl-brace-imaginary-offset: 0
## perl-label-offset: -2
## cperl-indent-level: 2
## cperl-brace-offset: 0
## cperl-continued-brace-offset: 0
## cperl-label-offset: -2
## cperl-extra-newline-before-brace: t
## cperl-merge-trailing-else: nil
## cperl-continued-statement-offset: 2
## End: