#!/usr/pkg/bin/perl

package main;

########################################################################
# debugging
########################################################################

# See 'Mastering Perl', chapter 4.

# use strict;
# use warnings;
# use diagnostics;

use Carp;
$SIG{__DIE__} = sub { &Carp::croak; };

use Data::Dumper;

########################################################################
# Legalese
########################################################################

our $Legalese;

{
  use constant VERSION => '1.3.2'; # version of glilypond

### This constant 'LICENSE' is the license for this file 'GPL' >= 3
  use constant LICENSE => q*
glilypond - integrate 'lilypond' into 'groff' files

Copyright (C) 2013-2020 Free Software Foundation, Inc.
  Written by Bernd Warken <groff-bernd.warken-72@web.de>

This file is part of 'GNU groff'.

  'GNU groff' 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.

  'GNU groff' 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 'groff', see the files 'COPYING' and 'LICENSE' in the top
directory of the 'groff' source package.  If not, see
<http://www.gnu.org/licenses/>.
*;


  $Legalese =
    {
     'version' => VERSION,
     'license' => LICENSE,
    }

}

##### end legalese


########################################################################
# global variables and BEGIN
########################################################################

use integer;
use utf8;

use Cwd qw[];
use File::Basename qw[];
use File::Copy qw[];
use File::HomeDir qw[];
use File::Spec qw[];
use File::Path qw[];
use File::Temp qw[];
use FindBin qw[];
use POSIX qw[];


BEGIN {

  use constant FALSE => 0;
  use constant TRUE => 1;
  use constant EMPTYSTRING => '';
  use constant EMPTYARRAY => ();
  use constant EMPTYHASH => ();

  our $Globals =
    {
     'before_make' => FALSE,
     'groff_version' => EMPTYSTRING,
     'prog' => EMPTYSTRING,
    };

  {
    ( my $volume, my $directory, $Globals->{'prog'} ) =
      File::Spec->splitpath($0);
    # $Globals->{'prog'} is 'glilypond' when installed,
    # 'glilypond.pl' when not
  }


  $\ = "\n";	# adds newline at each print
  $/ = "\n";	# newline separates input
  $| = 1;       # flush after each print or write command


  {
    {
      # script before run of 'make'
      my $at = '@';
      $Globals->{'before_make'} = TRUE if '1.23.0' eq "${at}VERSION${at}";
    }

    my $file_test_pl;
    my $glilypond_libdir;

    if ( $Globals->{'before_make'} ) { # in source, not yet installed
      my $glilypond_dir = $FindBin::Bin;
      $glilypond_dir = Cwd::realpath($glilypond_dir);
      $glilypond_libdir = $glilypond_dir;

    } else {			# already installed
      $Globals->{'groff_version'} = '1.23.0';
      $glilypond_libdir = '/usr/pkg/lib/groff/glilypond';
    }

    unshift(@INC, $glilypond_libdir);

    umask 0077; # octal output: 'printf "%03o", umask;'
  }

  use integer;
  use utf8;
  use feature 'state';

  my $P_PIC;
  # $P_PIC = '.PDFPIC';
  $P_PIC = '.PSPIC';

  ######################################################################
  # subs for using several times
  ######################################################################

  sub create_ly2eps {		       # '--ly2eps' default
    our ( $out, $Read, $Temp );

    my $prefix = $Read->{'file_numbered'};   # w/ dir change to temp dir

    # '$ lilypond --ps -dbackend=eps -dgs-load-fonts \
    #      output=file_without_extension file.ly'
    # extensions are added automatically
    my $opts = '--ps -dbackend=eps -dinclude-eps-fonts -dgs-load-fonts'
      . " --output=$prefix $prefix";
    &run_lilypond("$opts");

    Cwd::chdir $Temp->{'cwd'} or
	die "Could not change to former directory '" .
	  $Temp->{'cwd'} . "': $!";

    my $eps_dir = $Temp->{'eps_dir'};
    my $dir = $Temp->{'temp_dir'};
    opendir( my $dh, $dir ) or
      die "could not open temporary directory '$dir': $!";

    my $re = qr<
		 ^
		 $prefix
		 -
		 .*
		 \.eps
		 $
	       >x;
    my $file;
    while ( readdir( $dh ) ) {
      chomp;
      $file = $_;
      if ( /$re/ ) {
	my $file_path = File::Spec->catfile($dir, $file);
	if ( $eps_dir ) {
	  my $could_copy = FALSE;
	  File::Copy::copy($file_path, $eps_dir)
	      and $could_copy = TRUE;
	  if ( $could_copy ) {
	    unlink $file_path;
	    $file_path = File::Spec->catfile($eps_dir, $_);
	  }
	}
	$out->print( $P_PIC . ' ' . $file_path );
      }
    }				# end while readdir
    closedir( $dh );
  }				# end sub create_ly2eps()


  sub create_pdf2eps {		       # '--pdf2eps'
    our ( $v, $stdout, $stderr, $out, $Read, $Temp );

    my $prefix = $Read->{'file_numbered'};   # w/ dir change to temp dir

    &run_lilypond("--pdf --output=$prefix $prefix");

    my $file_pdf = $prefix . '.pdf';
    my $file_ps = $prefix . '.ps';

    # pdf2ps in temp dir
    my $temp_file = &next_temp_file;
    $v->print( "\n##### run of 'pdf2ps'" );
    # '$ pdf2ps file.pdf file.ps'
    my $output = `pdf2ps $file_pdf $file_ps 2> $temp_file`;
    die 'Program pdf2ps does not work.' if ( $? );
    &shell_handling($output, $temp_file);
    $v->print( "##### end run of 'pdf2ps'\n" );

    # ps2eps in temp dir
    $temp_file = &next_temp_file;
    $v->print( "\n##### run of 'ps2eps'" );
    # '$ ps2eps file.ps'
    $output = `ps2eps $file_ps 2> $temp_file`;
    die 'Program ps2eps does not work.' if ( $? );
    &shell_handling($output, $temp_file);
    $v->print( "##### end run of 'ps2eps'\n" );

    # change back to former dir
    Cwd::chdir $Temp->{'cwd'} or
	die "Could not change to former directory '" .
	  $Temp->{'cwd'} . "': $!";

    # handling of .eps file
    my $file_eps = $prefix . '.eps';
    my $eps_path = File::Spec->catfile($Temp->{'temp_dir'}, $file_eps);
    if ( $Temp->{'eps_dir'} ) {
      my $has_copied = FALSE;
      File::Copy::copy( $eps_path, $Temp->{'eps_dir'} )
	  and $has_copied = TRUE;
      if ( $has_copied ) {
	unlink $eps_path;
	$eps_path = File::Spec->catfile( $Temp->{'eps_dir'}, $file_eps );
      } else {
	$stderr->print( "Could not use EPS-directory." );
      } # end Temp->{'eps_dir'}
    }
    # print into groff output
    $out->print( $P_PIC . ' ' . $eps_path );
  }				# end sub create_pdf2eps()


  sub is_subdir {		# arg1 is subdir of arg2 (is longer)
    my ( $dir1, $dir2 ) = @_;
    $dir1 = &path2abs( $dir1 );;
    $dir2 = &path2abs( $dir2 );;
    my @split1 = File::Spec->splitdir($dir1);
    my @split2 = File::Spec->splitdir($dir2);
    for ( @split2 ) {
      next if ( $_ eq shift @split1 );
      return FALSE;
    }
    return TRUE;
  }


  sub license {
    our ( $Legalese, $stdout );
    &version;
    $stdout->print( $Legalese->{'license'} );
  } # end sub license()


  sub make_dir {		# make directory or check if it exists
    our ( $v, $Args );

    my $dir_arg = shift;
    chomp $dir_arg;
    $dir_arg =~ s/^\s*(.*)\s*$/$1/;

    unless ( $dir_arg ) {
      $v->print( "make_dir(): empty argument" );
      return FALSE;
    }

    unless ( File::Spec->file_name_is_absolute($dir_arg) ) {
      my $res = Cwd::realpath($dir_arg);
      $res = File::Spec->canonpath($dir_arg) unless ( $res );
      $dir_arg = $res if ( $res );
    }

    return $dir_arg if ( -d $dir_arg && -w $dir_arg );


    # search thru the dir parts
    my @dir_parts = File::Spec->splitdir($dir_arg);
    my @dir_grow;
    my $dir_grow;
    my $can_create = FALSE;	# dir could be created if TRUE

   DIRPARTS: for ( @dir_parts ) {
      push @dir_grow, $_;
      next DIRPARTS unless ( $_ ); # empty string for root directory

      # from array to path dir string
      $dir_grow = File::Spec->catdir(@dir_grow);

      next DIRPARTS if ( -d $dir_grow );

      if ( -e $dir_grow ) {  # exists, but not a dir, so must be removed
	die "Couldn't create dir '$dir_arg', it is blocked by "
	  . "'$dir_grow'." unless ( -w $dir_grow );

	# now it's writable, but not a dir, so it can be removed
	unlink ( $dir_grow ) or
	  die "Couldn't remove '$dir_grow', " .
	    "so I cannot create dir '$dir_arg': $!";
      }

      # $dir_grow no longer exists, so the former dir must be writable
      # in order to create the directory
      pop @dir_grow;
      $dir_grow = File::Spec->catdir(@dir_grow);

      die "'$dir_grow' is not writable, " .
	"so directory '$dir_arg' can't be created."
	  unless ( -w $dir_grow );

      # former directory is writable, so '$dir_arg' can be created

      File::Path::make_path( $dir_arg,
			     {
			      mask => oct('0700'),
			      verbose => $Args->{'verbose'},
			     }
			   )	#  'mkdir -P'
	  or die "Could not create directory '$dir_arg': $!";

      last DIRPARTS;
    }

    die "'$dir_arg' is not a writable directory"
      unless ( -d $dir_arg && -w $dir_arg );

    return $dir_arg;

  } # end sub make_dir()


  my $number = 0;
  sub next_temp_file {
    our ( $Temp, $v, $Args );
    ++$number;
    my $temp_basename = $Args->{'prefix'} . '_temp_' . $number;
    my $temp_file = File::Spec->catfile( $Temp->{'temp_dir'} ,
					 $temp_basename );
    $v->print( "next temporary file: '$temp_file'" );
    return $temp_file;
  }				# end sub next_temp_file()


  sub path2abs {
    our ( $Temp, $Args );

    my $path = shift;
    $path =~ s/
		^
		\s*
		(
		  .*
		)
		\s*
		$
	      /$1/x;

    die "path2abs(): argument is empty." unless ( $path );

    # Perl does not support shell '~' for home dir
    if ( $path =~ /
		    ^
		    ~
		  /x ) {
      if ( $path eq '~' ) {	# only own home
	$path = File::HomeDir->my_home;
      } elsif ( $path =~ m<
			    ^
			    ~ /
			    (
			      .*
			    )
			    $
			  >x ) {	# subdir of own home
	$path = File::Spec->catdir( $Temp->{'cwd'}, $1 );
      } elsif ( $path =~ m<
			    ^
			    ~
			    (
			      [^/]+
			    )
			    $
			  >x ) {	# home of other user
	$path = File::HomeDir->users_home($1);
      } elsif ( $path =~ m<
			    ^
			    ~
			    (
			      [^/]+
			    )
			    /+
			    (
			      .*
			    )
			    $
			  >x ) {	# subdir of other home
	$path = File::Spec->
	  catdir( File::HomeDir->users_home($1), $2 );
      }
    }

    $path = File::Spec->rel2abs($path);

    # now $path is absolute
    return $path;
  } # end sub path2abs()


  sub run_lilypond {
    # arg is the options collection for 'lilypond' to run
    # either from ly or pdf

    our ( $Temp, $v );

    my $opts = shift;
    chomp $opts;

    my $temp_file = &next_temp_file;
    my $output = EMPTYSTRING;

    # change to temp dir
    Cwd::chdir $Temp->{'temp_dir'} or
	die "Could not change to temporary directory '" .
	  $Temp->{'temp_dir'} . "': $!";

    $v->print( "\n##### run of 'lilypond " . $opts . "'" );
    $output = `lilypond $opts 2>$temp_file`;
    die "Program lilypond does not work, see '$temp_file': $?"
      if ( $? );
    chomp $output;
    &shell_handling($output, $temp_file);
    $v->print( "##### end run of 'lilypond'\n" );

    # stay in temp dir
  } # end sub run_lilypond()


  sub shell_handling {
    # Handle ``-shell-command output in a string (arg1).
    # stderr goes to temporary file $TempFile.

    our ( $out, $v, $Args );

    my $out_string = shift;
    my $temp_file = shift;

    my $a = &string2array($out_string); # array ref
    for ( @$a ) {
      $out->print( $_ );
    }

    $temp_file && -f $temp_file && -r $temp_file ||
      die "shell_handling(): $temp_file is not a readable file.";
    my $temp = new FH_READ_FILE($temp_file);
    my $res = $temp->read_all();
    for ( @$res ) {
      chomp;
      $v->print($_);
    }

    unlink $temp_file unless ( $Args->{'keep_all'} );
  } # end sub shell_handling()


  sub string2array {
    my $s = shift;
    my @a = ();
    for ( split "\n", $s ) {
      chomp;
      push @a, $_;
    }
    return \@a;
  } # end string2array()


  sub usage {			# for '--help'
    our ( $Globals, $Args );

    my $p = $Globals->{'prog'};
    my $usage = EMPTYSTRING;
    $usage = '###### usage:' . "\n" if ( $Args->{'verbose'} );
    $usage .= qq*Options for $p:
Read a 'roff' file or standard input and transform 'lilypond' parts
(everything between '.lilypond start' and '.lilypond end') into
'EPS'-files that can be read by groff using '.PSPIC'.

There is also a command '.lilypond include <file_name>' that can
include a complete 'lilypond' file into the 'groff' document.


# Breaking options:
$p -?|-h|--help|--usage    # usage
$p --version               # version information
$p --license               # the license is GPL >= 3


# Normal options:
$p [options] [--] [filename ...]

There are 2 options for influencing the way how the 'EPS' files for the
'roff' display are generated:
--ly2eps           'lilypond' generates 'EPS' files directly (default)
--pdf2eps          'lilypond' generates a 'PDF' file that is transformed

-k|--keep_all      do not delete any temporary files
-v|--verbose       print much information to STDERR

Options with an argument:
-e|--eps_dir=...   use a directory for the EPS files
-o|--output=...    sent output in the groff language into file ...
-p|--prefix=...    start for the names of temporary files
-t|--temp_dir=...  provide the directory for temporary files.

The directories set are created when they do not exist.
*;

    # old options:
    # --keep_files       -k: do not delete any temporary files
    # --file_prefix=...  -p: start for the names of temporary files

    $main::stdout->print( $usage );
  } # end sub usage()


  sub version { # for '--version'
    our ( $Globals, $Legalese, $stdout, $Args );
    my $groff_version = '';
    if ( $Globals->{'groff_version'} ) {
      $groff_version = "(groff $Globals->{'groff_version'}) ";
    }

    my $output = EMPTYSTRING;
    $output = "$Globals->{'prog'} ${groff_version}version "
      .  $Legalese->{'version'};

    $stdout->print($output);
  } # end sub version()
}

#die "test: ";
########################################################################
# OOP declarations for some file handles
########################################################################

use integer;

########################################################################
# OOP for writing file handles that are open by default, like STD*
########################################################################

# -------------------------- _FH_WRITE_OPENED --------------------------

{	# FH_OPENED: base class for all opened file handles, like $TD*

  package _FH_WRITE_OPENED;
  use strict;

  sub new {
    my ( $pkg, $std ) = @_;
    bless {
	   'fh' => $std,
	  }
  }

  sub open {
  }

  sub close {
  }

  sub print {
    my $self = shift;
    for ( @_ ) {
      print { $self->{'fh'} } $_;
    }
  }

}


# ------------------------------ FH_STDOUT ----------------------------

{			     # FH_STDOUT: print to normal output STDOUT

  package FH_STDOUT;
  use strict;
  @FH_STDOUT::ISA = qw( _FH_WRITE_OPENED );

  sub new {
    &_FH_WRITE_OPENED::new( '_FH_WRITE_OPENED', *STDOUT );
  }

}				# end FH_STDOUT


# ------------------------------ FH_STDERR -----------------------------

{				# FH_STDERR: print to STDERR

  package FH_STDERR;
  use strict;
  @FH_STDERR::ISA = qw( _FH_WRITE_OPENED );

  sub new {
    &_FH_WRITE_OPENED::new( 'FH_OPENED', *STDERR );
  }

}				# end FH_STDERR


########################################################################
# OOP for file handles that write into a file or string
########################################################################

# ------------------------------- FH_FILE ------------------------------

{	       # FH_FILE: base class for writing into a file or string

  package FH_FILE;
  use strict;

  sub new {
    my ( $pkg, $file ) = @_;
    bless {
	   'fh' => undef,
	   'file' => $file,
	   'opened' => main::FALSE,
	  }
  }

  sub DESTROY {
    my $self = shift;
    $self->close();
  }

  sub open {
    my $self = shift;
    my $file = $self->{'file'};
    if ( $file && -e $file ) {
      die "file $file is not writable" unless ( -w $file );
      die "$file is a directory" if ( -d $file );
    }
    open $self->{'fh'}, ">", $self->{'file'}
      or die "could not open file '$file' for writing: $!";
    $self->{'opened'} = main::TRUE;
  }

  sub close {
    my $self = shift;
    close $self->{'fh'} if ( $self->{'opened'} );
    $self->{'opened'} = main::FALSE;
  }

  sub print {
    my $self = shift;
    $self->open() unless ( $self->{'opened'} );
    for ( @_ ) {
      print { $self->{'fh'} } $_;
    }
  }

}				# end FH_FILE


# ------------------------------ FH_STRING -----------------------------

{				# FH_STRING: write into a string

  package FH_STRING;		# write to \string
  use strict;
  @FH_STRING::ISA = qw( FH_FILE );

  sub new {
    my $pkg = shift;		# string is a reference to scalar
    bless
      {
       'fh' => undef,
       'string' => '',
       'opened' => main::FALSE,
      }
    }

  sub open {
    my $self = shift;
    open $self->{'fh'}, ">", \ $self->{'string'}
      or die "could not open string for writing: $!";
    $self->{'opened'} = main::TRUE;
  }

  sub get { # get string, move to array ref, close, and return array ref
    my $self = shift;
    return '' unless ( $self->{'opened'} );
    my $a = &string2array( $self->{'string'} );
    $self->close();
    return $a;
  }

}				# end FH_STRING


# -------------------------------- FH_NULL -----------------------------

{				# FH_NULL: write to null device

  package FH_NULL;
  use strict;
  @FH_NULL::ISA = qw( FH_FILE FH_STRING );

  use File::Spec;

  my $devnull = File::Spec->devnull();
  $devnull = '' unless ( -e $devnull && -w $devnull );

  sub new {
    my $pkg = shift;
    if ( $devnull ) {
      &FH_FILE::new( $pkg, $devnull );
    } else {
      &FH_STRING::new( $pkg );
    }
  } # end new()

}				# end FH_NULL


########################################################################
# OOP for reading file handles
########################################################################

# ---------------------------- FH_READ_FILE ----------------------------

{ # FH_READ_FILE: read a file

  package FH_READ_FILE;
  use strict;

  sub new {
    my ( $pkg, $file ) = @_;
    die "File '$file' cannot be read." unless ( -f $file && -r $file );
    bless {
	   'fh' => undef,
	   'file' => $file,
	   'opened' => main::FALSE,
	  }
  }

  sub DESTROY {
    my $self = shift;
    $self->close();
  }

  sub open {
    my $self = shift;
    my $file = $self->{'file'};
    if ( $file && -e $file ) {
      die "file $file is not writable" unless ( -r $file );
      die "$file is a directory" if ( -d $file );
    }
    open $self->{'fh'}, "<", $self->{'file'}
      or die "could not read file '$file': $!";
    $self->{'opened'} = main::TRUE;
  }

  sub close {
    my $self = shift;
    close $self->{'fh'} if ( $self->{'opened'} );
    $self->{'opened'} = main::FALSE;
  }

  sub read_line {
    # Read 1 line of the file into a chomped string.
    # Do not close the read handle at the end.
    my $self = shift;
    $self->open() unless ( $self->{'opened'} );

    my $res;
    if ( defined($res = CORE::readline($self->{'fh'}) ) ) {
      chomp $res;
      return $res;
    } else {
      $self->close();
      return undef;
    }
  }

  sub read_all {
    # Read the complete file into an array reference.
    # Close the read handle at the end.
    # Return array reference.
    my $self = shift;
    $self->open() unless ( $self->{'opened'} );

    my $res = [];
    my $line;
    while ( defined ( $line = CORE::readline $self->{'fh'} ) ) {
      chomp $line;
      push @$res, $line;
    }
    $self->close();
    $self->{'opened'} = main::FALSE;
    return $res;
  }

}

# end of OOP definitions


our $stdout = new FH_STDOUT();
our $stderr = new FH_STDERR();

# verbose printing, not clear whether this will be set by '--verbose',
# so store this now into a string, which can be gotten later on, when
# it will become either STDERR or /dev/null
our $v = new FH_STRING();

# for standard output, either STDOUT or output file
our $out;

# end of FH


########################################################################
# Args: command-line arguments
########################################################################

# command-line arguments are handled in 2 runs:
# 1) split short option collections, '=' optargs, and transfer abbrevs
# 2) handle the transferred options with subs

our $Args =
  {
   'eps_dir' => EMPTYSTRING, # can be overwritten by '--eps_dir'

   # 'eps-func' has 2 possible values:
   # 1) 'pdf' '--pdf2eps' (default)
   # 2) 'ly' from '--ly2eps'
   'eps_func' => 'pdf',

   # files names of temporary files start with this string,
   # can be overwritten by '--prefix'
   'prefix' => 'ly',

   # delete or do not delete temporary files
   'keep_all' => FALSE,

   # the roff output goes normally to STDOUT, can be a file with '--output'
   'output' => EMPTYSTRING,

   # temporary directory, can be overwritten by '--temp_dir',
   # empty for default of the program
   'temp_dir' => EMPTYSTRING,

   # regulates verbose output (on STDERR), overwritten by '--verbose'
   'verbose' => FALSE,
  };

{ # 'Args'
  use integer;

  our ( $Globals, $Args, $stderr, $v, $out );

  # ----------
  # subs for second run, for remaining long options after splitting and
  # transfer
  # ----------

  my %opts_with_arg =
    (

     '--eps_dir' => sub {
       $Args->{'eps_dir'} = shift;
     },

     '--output' => sub {
       $Args->{'output'} = shift;
     },

     '--prefix' => sub {
       $Args->{'prefix'} = shift;
     },

     '--temp_dir' => sub {
       $Args->{'temp_dir'} = shift;
     },

    );				# end of %opts_with_arg


  my %opts_noarg =
    (

     '--help' => sub {
       &usage;
       exit;
     },

     '--keep_all' => sub {
       $Args->{'keep_all'} = TRUE;
     },

     '--license' => sub {
       &license;
       exit;
     },

     '--ly2eps' => sub {
       $Args->{'eps_func'} = 'ly';
     },

     '--pdf2eps' => sub {
       $Args->{'eps_func'} = 'pdf';
     },

     '--verbose' => sub {
       $Args->{'verbose'} = TRUE;
     },

     '--version' => sub {
       &version;
       exit;
     },

    );				# end of %opts_noarg


  # used variables in both runs

  my @files = EMPTYARRAY;


  #----------
  # first run for command-line arguments
  #----------

  # global variables for first run

  my @splitted_args;
  my $double_minus = FALSE;
  my $arg = EMPTYSTRING;
  my $has_arg = FALSE;


  # Split short option collections and transfer these to suitable long
  # options from above.  Note that '-v' now means '--verbose' in version
  # 'v1.1', earlier versions had '--version' for '-v'.

  my %short_opts =
    (
     '?' => '--help',
     'e' => '--eps_dir',
     'h' => '--help',
     'l' => '--license',
     'k' => '--keep_all',
     'o' => '--output',
     'p' => '--prefix',
     't' => '--temp_dir',
     'v' => '--verbose',
     'V' => '--verbose',
    );


  # transfer long option abbreviations to the long options from above

  my @long_opts;

  $long_opts[3] =
    {				# option abbreviations of 3 characters
     '--e' => '--eps_dir',
     '--f' => '--prefix',		# --f for --file_prefix
     '--h' => '--help',
     '--k' => '--keep_all',	# and --keep_files
     '--o' => '--output',
     '--p' => '--prefix',		# and --file_prefix
     '--t' => '--temp_dir',
     '--u' => '--help',		# '--usage' is mapped to '--help'
    };

  $long_opts[4] =
    {				# option abbreviations of 4 characters
     '--li' => '--license',
     '--ly' => '--ly2eps',
     '--pd' => '--pdf2eps',
     '--pr' => '--prefix',
    };

  $long_opts[6] =
    {				# option abbreviations of 6 characters
     '--verb' => '--verbose',
     '--vers' => '--version',
    };


  # subs for short splitting and replacing long abbreviations

  my $split_short = sub {

    my @chars = split //, $1;	# omit leading dash

       # if result is TRUE: run 'next SPLIT' afterwards

     CHARS: while ( @chars ) {
	 my $c = shift @chars;

	 unless ( exists $short_opts{$c} ) {
	   $stderr->print( "Unknown short option '-$c'." );
	   next CHARS;
	 }

	 # short option exists

	 # map or transfer to special long option from above
	 my $transopt = $short_opts{$c};

	 if ( exists $opts_noarg{$transopt} ) {
	   push @splitted_args, $transopt;
	   $Args->{'verbose'}  = TRUE if ( $transopt eq '--verbose' );
	   next CHARS;
	 }

	 if ( exists $opts_with_arg{$transopt} ) {
	   push @splitted_args, $transopt;

	   if ( @chars ) {
	     # if @chars is not empty, option $transopt has argument
	     # in this arg, the rest of characters in @chars
	     push @splitted_args, join "", @chars;
	     @chars = EMPTYARRAY;
	     return TRUE;		# use 'next SPLIT' afterwards
	   }

	   # optarg is the next argument
	   $has_arg = $transopt;
	   return TRUE;		# use 'next SPLIT' afterwards
	 }			# end of if %opts_with_arg
       }				# end of while CHARS
       return FALSE;		# do not do anything
  };				# end of sub for short_opt_collection


  my $split_long = sub {
    my $from_arg = shift;
    $from_arg =~ /^([^=]+)/;
    my $opt_part = lc($1);
    my $optarg = undef;
    if ( $from_arg =~ /=(.*)$/ ) {
      $optarg = $1;
    }

   N: for my $n ( qw/6 4 3/ ) {
      $opt_part =~ / # match $n characters
		     ^
		     (
		       .{$n}
		     )
		   /x;
      my $argn = $1;		# get the first $n characters

      # no match, so luck for fewer number of chars
      next N unless ( $argn );

      next N unless ( exists $long_opts[$n]->{$argn} );
      # not in $n hash, so go on to next loop for $n

      # now $n-hash has arg

      # map or transfer to special long opt from above
      my $transopt = $long_opts[$n]->{$argn};

      # test on option without arg
      if ( exists $opts_noarg{$transopt} ) { # opt has no arg
	$stderr->print( 'Option ' . $transopt . 'has no argument: ' .
			$from_arg . '.' ) if ( defined($optarg) );
	push @splitted_args, $transopt;
	$Args->{'verbose'} = TRUE if ( $transopt eq '--verbose' );
	return TRUE;		# use 'next SPLIT' afterwards
      }				# end of if %opts_noarg

      # test on option with arg
      if ( exists $opts_with_arg{$transopt} ) { # opt has arg
	push @splitted_args, $transopt;

	# test on optarg in arg
	if ( defined($optarg) ) {
	  push @splitted_args, $1;
	  return TRUE; # use 'next SPLIT' afterwards
	} # end of if optarg in arg

	# has optarg in next arg
	$has_arg = $transopt;
	return TRUE; # use 'next SPLIT' afterwards
      } # end of if %opts_with_arg

      # not with and without option, so is not permitted
      $stderr->print( "'" . $transopt .
		      "' is unknown long option from '" . $from_arg . "'" );
      return TRUE; # use 'next SPLIT' afterwards
    } # end of for N
    return FALSE; # do nothing
  }; # end of split_long()


  #----------
  # do split and transfer arguments
  #----------
  sub run_first {

   SPLIT: foreach (@ARGV) {
      # Transform long and short options into some given long options.
      # Split long opts with arg into 2 args (no '=').
      # Transform short option collections into given long options.
      chomp;

      if ( $has_arg ) {
	push @splitted_args, $_;
	$has_arg = EMPTYSTRING;
	next SPLIT;
      }

      if ( $double_minus ) {
	push @files, $_;
	next SPLIT;
      }

      if ( $_ eq '-' ) {		# file arg '-'
	push @files, $_;
	next SPLIT;
      }

      if ( $_ eq '--' ) {		# POSIX arg '--'
	push @splitted_args, $_;
	$double_minus = TRUE;
	next SPLIT;
      }

      if ( / # short option or collection of short options
	     ^
	     -
	     (
	       [^-]
	       .*
	     )
	     $
	   /x ) {
	$split_short->($1);
	next SPLIT;
      }				# end of short option

      if ( /^--/ ) {		# starts with 2 dashes, a long option
	$split_long->($_);
	next SPLIT;
      }				# end of long option

      # unknown option without leading dash is a file name
      push @files, $_;
      next SPLIT;
    }				# end of foreach SPLIT

				  # all args are considered
    $stderr->print( "Option '$has_arg' needs an argument." )
      if ( $has_arg );


    push @files, '-' unless ( @files );
    @ARGV = @splitted_args;

  };		    # end of first run, splitting with map or transfer


  #----------
  # open or ignore verbose output
  #----------
  sub install_verbose {
    if ( $Args->{'verbose'} ) { # '--verbose' was used
      # make verbose output into $v
      # get content of string so far as array ref, close
      my $s = $v->get();

      $v = new FH_STDERR(); # make verbose output into STDERR
      if ( $s ) {
	for ( @$s ) {
	  # print the file content into new verbose output
	  $v->print($_);
	}
      }
      # verbose output is now active (into STDERR)
      $v->print( "Option '-v' means '--verbose'." );
      $v->print( "Version information is printed by option"
	         . " '--version'."
      );
      $v->print( "#" x 72 );

    } else { # '--verbose' was not used
      # do not be verbose, make verbose invisible

      $v->close(); # close and ignore the string content

      $v = new FH_NULL();
      # this is either into /dev/null or in an ignored string

    } # end if-else about verbose
    # '$v->print' works now in any case

    $v->print( "Verbose output was chosen." );

    my $s = $Globals->{'prog_is_installed'} ? '' : ' not';
    $v->print( $Globals->{'prog'} . " is" . $s .
	       " installed." );

    $v->print( 'The command-line options are:' );

    $s = "  options:";
    $s .= " '" . $_ . "'" for ( @ARGV );
    $v->print( $s );

    $s = "  file names:";
    $s .= " '" . $_ . "'\n" for ( @files );
    $v->print( $s );
  } # end install_verbose()


  #----------
  # second run of command-line arguments
  #----------
  sub run_second {
      # Second run of args with new @ARGV from the former splitting.
      # Arguments are now split and transformed into special long
      # options.

      my $double_minus = FALSE;
      my $has_arg = FALSE;

    ARGS: for my $arg ( @ARGV ) {

	# ignore '--', file names are handled later on
	last ARGS if ( $arg eq '--' );

	if ( $has_arg ) {
	  unless ( exists $opts_with_arg{$has_arg} ) {
	    $stderr->print( "'\%opts_with_args' does not have key '" .
			      $has_arg . "'." );
	    next ARGS;
	  }

	  $opts_with_arg{$has_arg}->($arg);
	  $has_arg = FALSE;
	  next ARGS;
	} # end of $has_arg

	if ( exists $opts_with_arg{$arg} ) {
	  $has_arg = $arg;
	  next ARGS;
	}

	if ( exists $opts_noarg{$arg} ) {
	  $opts_noarg{$arg}->();
	  next ARGS;
	}

	# not a suitable option
	$stderr->print( "Wrong option '" . $arg . "'." );
	next ARGS;

      } # end of for ARGS:


      if ( $has_arg ) { # after last argument
	die "Option '$has_arg' needs an argument.";
      }

    }; # end of second run


  sub handle_args {
    # handling the output of args

    if ( $Args->{'output'} ) { # '--output' was set in the arguments
      my $out_path = &path2abs($Args->{'output'});
      die "Output file name $Args->{'output'} cannot be used."
	unless ( $out_path );

      my ( $file, $dir );
      ( $file, $dir ) = File::Basename::fileparse($out_path)
	or die "Could not handle output file path '" . $out_path
	  . "': directory name '" . $dir . "' and file name '" . $file
	  . "'.";

      die "Could not find output directory for '" . $Args->{'output'}
        . "'" unless ( $dir );
      die "Could not find output file: '" . $Args->{'output'} .
	"'" unless ( $file );

      if ( -d $dir ) {
	die "Could not write to output directory '" . $dir . "'."
	  unless ( -w $dir );
      } else {
	$dir = &make_dir($dir);
	die "Could not create output directory in: '" . $out_path . "'."
	  unless ( $dir );
      }

      # now $dir is a writable directory

      if ( -e $out_path ) {
	die "Could not write to output file '" . $out_path . "'."
	  unless ( -w $out_path );
      }

      $out = new FH_FILE( $out_path );
      $v->print( "Output goes to file '" . $out_path . "'." );
    } else { # '--output' was not set
      $out = new FH_STDOUT();
    }
    # no $out is the right behavior for standard output

  #  $Args->{'prefix'} .= '_' . $Args->{'eps_func'} . '2eps';

    @ARGV = @files;
  }

  &run_first();
  &install_verbose();
  &run_second();
  &handle_args();
}

# end 'Args'


########################################################################
# temporary directory .../tmp/groff/USER/lilypond/TIME
########################################################################

our $Temp =
  {
   # store the current directory
   'cwd' => Cwd::getcwd(),

   # directory for EPS files
   'eps_dir' => EMPTYSTRING,

   # temporary directory
   'temp_dir' => EMPTYSTRING,
  };

{ # 'Temp'

  if ( $Args->{'temp_dir'} ) {

    #----------
    # temporary directory was set by '--temp_dir'
    #----------

    my $dir = $Args->{'temp_dir'};

    $dir = &path2abs($dir);
    $dir = &make_dir($dir) or
      die "The directory '$dir' cannot be used temporarily: $!";


    # now '$dir' is a writable directory

    opendir( my $dh, $dir ) or
      die "Could not open temporary directory '$dir': $!";
    my $file_name;
    my $found = FALSE;
    my $prefix = $Args->{'prefix'};
    my $re = qr<
		 ^
		 $prefix
		 _
	       >x;

  READDIR: while ( defined($file_name = readdir($dh)) ) {
      chomp $file_name;
      if ( $file_name =~ /$re/ ) { # file name starts with $prefix_
	$found = TRUE;
	last READDIR;
      }
      next;
    }

    $Temp->{'temp_dir'} = $dir;
    my $n = 0;
    while ( $found ) {
      $dir = File::Spec->catdir( $Temp->{'temp_dir'}, ++$n );
      next if ( -e $dir );

      $dir = &make_dir($dir) or next;

      $found = FALSE;
      last;
    }

    $Temp->{'temp_dir'} = $dir;


  } else { # $Args->{'temp_dir'} not given by '--temp_dir'

    #----------
    # temporary directory was not set
    #----------

    { # search for or create a temporary directory

      my @tempdirs = EMPTYARRAY;
      {
	my $tmpdir = File::Spec->tmpdir();
	push @tempdirs, $tmpdir
	  if ( $tmpdir && -d $tmpdir && -w $tmpdir );

	my $root_dir = File::Spec->rootdir(); # '/' in Unix
	my $root_tmp = File::Spec->catdir($root_dir, 'tmp');
	push @tempdirs, $root_tmp
	  if ( $root_tmp ne $tmpdir && -d $root_tmp && -w $root_tmp );

	# home directory of the actual user
	my $home = File::HomeDir->my_home;
	my $home_tmp = File::Spec->catdir($home, 'tmp');
	push @tempdirs, $home_tmp if ( -d $home_tmp && -w $home_tmp );

	# '/var/tmp' in Unix
	my $var_tmp = File::Spec->catdir('', 'var', 'tmp');
	push @tempdirs, $var_tmp if ( -d $var_tmp && -w $var_tmp );
      }


      my @path_extension = qw( groff ); # TEMPDIR/groff/USER/lilypond/N
      {
	# '$<' is UID of actual user,
	# 'getpwuid' gets user name in scalar context
	my $user = getpwuid($<);
	push @path_extension, $user if ( $user );

	push @path_extension, qw( lilypond );
      }


    TEMPS: foreach ( @tempdirs ) {

	my $dir; # final directory name in 'while' loop
	$dir = &path2abs($_);
	next TEMPS unless ( $dir );

	# beginning of directory name
	my @dir_begin =
	  ( File::Spec->splitdir($dir), @path_extension );


	my $n = 0;
	my $dir_blocked = TRUE;
      BLOCK: while ( $dir_blocked ) {
	  # should become the final dir name
	  $dir = File::Spec->catdir(@dir_begin, ++$n);
	  next BLOCK if ( -d $dir );

	  # dir name is now free, create it, and end the blocking
	  my $res = &make_dir( $dir );
	  die "Could not create directory: $dir" unless ( $res );

	  $dir = $res;
	  $dir_blocked = FALSE;
	}

	next TEMPS unless ( -d $dir && -w $dir  );

	# $dir is now a writable directory
	$Temp->{'temp_dir'} = $dir; # tmp/groff/USER/lilypond/TIME
	last TEMPS;
      } # end foreach tmp directories
    } # end to create a temporary directory

    die "Could not find a temporary directory" unless
      ( $Temp->{'temp_dir'} && -d $Temp->{'temp_dir'} &&
	-w $Temp->{'temp_dir'} );

  } # end temporary directory

  $v->print( "Temporary directory: '" . $Temp->{'temp_dir'} . "'\n" );
  $v->print( "file_prefix: '" . $Args->{'prefix'} . "'" );


  #----------
  # EPS directory
  #----------

  my $make_dir = FALSE;
  if ( $Args->{'eps_dir'} ) { # set by '--eps_dir'
    my $dir = $Args->{'eps_dir'};

    $dir = &path2abs($dir);

    if ( -e $dir ) {
      goto EMPTY unless ( -w $dir );

      # '$dir' is writable
      if ( -d $dir ) {
	my $upper_dir = $dir;

	my $found = FALSE;
	opendir( my $dh, $upper_dir ) or $found = TRUE;
	my $prefix = $Args->{'prefix'};
	my $re = qr<
		     ^
		     $prefix
		     _
		   >x;
	while ( not $found ) {
	  my $file_name = readdir($dh);
	  if ( $file_name =~ /$re/ ) { # file name starts with $prefix_
	    $found = TRUE;
	    last;
	  }
	  next;
	}

	my $n = 0;
	while ( $found ) {
	  $dir = File::Spec->catdir($upper_dir, ++$n);
	  next if ( -d $dir );
	  $found = FALSE;
	}
	$make_dir = TRUE;
	$Temp->{'eps_dir'} = $dir;
      } else { # '$dir' is not a dir, so unlink it to create it as dir
	if ( unlink $dir ) { # could remove '$dir'
	  $Temp->{'eps_dir'} = $dir;
	  $make_dir = TRUE;
	} else { # could not remove
	  $stderr->print( "Could not use EPS dir '" . $dir .
			  "', use temp dir." );
	} # end of unlink
      } # end test of -d $dir
    } else {
      $make_dir = TRUE;
    } # end of if -e $dir


    if ( $make_dir ) { # make directory '$dir'
      my $made = FALSE;
      $dir = &make_dir($dir) and $made = TRUE;

      if ( $made ) {
	$Temp->{'eps_dir'} = $dir;
	$v->print( "Directory for useful EPS files is '" . $dir . "'." );
      } else {
	$v->print( "The EPS directory '" . $dir . "' cannot be used: $!" );
      }
    } else { # '--eps_dir' was not set, so take the temporary directory
      $Temp->{'eps_dir'} = $Args->{'temp_dir'};
    } # end of make dir
  }

 EMPTY: unless ( $Temp->{'eps_dir'} ) {
    # EPS-dir not set or available, use temp dir,
    # but leave $Temp->{'}eps_dir'} empty
    $v->print( "Directory for useful EPS files is the " .
      "temporary directory '" . $Temp->{'temp_dir'} . "'." );
  }

} # end 'Temp'


########################################################################
# Read: read files or stdin
########################################################################

our $Read =
  {
   'file_numbered' => EMPTYSTRING,
   'file_ly' => EMPTYSTRING, # '$file_numbered.ly'
  };

{ # read files or stdin

  my $ly_number = 0; # number of lilypond file

  # '$Args->{'prefix'}_[0-9]'

  my $lilypond_mode = FALSE;

  my $arg1; # first argument for '.lilypond'
  my $arg2; # argument for '.lilypond include'

  my $path_ly; # path of ly-file


  my $check_file = sub { # for argument of '.lilypond include'
    my $file = shift; # argument is a file name
    $file = &path2abs($file);
    unless ( $file ) {
      die "Line '.lilypond include' without argument";
      return '';
    }
    unless ( -f $file && -r $file ) {
      die "Argument '$file' in '.lilypond include' is not a readable file";
    }

    return $file;
  }; # end sub &$check_file()


  my $increase_ly_number = sub {
    ++$ly_number;
    $Read->{'file_numbered'} = $Args->{'prefix'} . '_' . $ly_number;
    $Read->{'file_ly'} =  $Read->{'file_numbered'} . '.ly';
    $path_ly = File::Spec->catdir($Temp->{'temp_dir'}, $Read->{'file_ly'} );
  };


  my %eps_subs =
    (
     'ly' => \&create_ly2eps,   # lilypond creates EPS files
     'pdf' => \&create_pdf2eps, # lilypond creates PDF file
    );

  # about lines starting with '.lilypond'

  my $ly;
  my $fh_include_file;
  my %lilypond_args =
    (

     'start' => sub {
       $v->print( "\nline: '.lilypond start'" );
       die "Line '.lilypond stop' expected." if ( $lilypond_mode );

       $lilypond_mode = TRUE;
       &$increase_ly_number;

       $v->print( "ly-file: '" . $path_ly . "'" );

       $ly = new FH_FILE($path_ly);
     },


     'end' => sub {
       $v->print( "line: '.lilypond end'\n" );
       die "Expected line '.lilypond start'." unless ( $lilypond_mode );

       $lilypond_mode = FALSE;
       $ly->close();

       if ( exists $eps_subs{ $Args->{'eps_func'} } ) {
	 $eps_subs{ $Args->{'eps_func'} }->();
       } else {
	 die "Wrong argument for \%eps_subs: " . $Args->{'eps_func'} . "'";
       }
     },


     'include' => sub { # '.lilypond include file...'

       # this may not be used within lilypond mode
       next LILYPOND if ( $lilypond_mode );

       my $file_arg = shift;

       my $file = &$check_file($file_arg);
       next LILYPOND unless ( $file );
       # file can be read now


       # '$fh_write_ly' must be opened
       &$increase_ly_number;

       $ly = new FH_FILE($path_ly);

       my $include = new FH_READ_FILE($file);
       my $res = $include->read_all(); # is a reference to an array
       foreach ( @$res ) {
	 chomp;
	 $ly->print($_);
       }
       $ly->close();

       if ( exists $eps_subs{ $Args->{'eps_func'} } ) {
	 $eps_subs{ $Args->{'eps_func'} }->();
       } else {
	 die "Wrong argument for \$eps_subs: '" . $Args->{'eps_func'} . "'";
       }
     }, # end '.lilypond include'

    ); # end definition %lilypond_args


 LILYPOND: foreach my $filename (@ARGV) {
    my $input;
    if ($filename eq '-') {
      $input = \*STDIN;
    } elsif (not open $input, '<', $filename) {
      warn $!;
      next;
    }
    while (<$input>) {
      chomp;
      my $line = $_;


      # now the lines with '.lilypond ...'

      if ( /
	     ^
	     [.']
	     \s*
	     lilypond
	     (
	       .*
	     )
	     $
	   /x ) { # .lilypond ...
	my $args = $1;
	$args =~ s/
		    ^
		    \s*
		  //x;
	$args =~ s/
		    \s*
		    $
		  //x;
	$args =~ s/
		    ^
		    (
		      \S*
		    )
		    \s*
		  //x;
	my $arg1 = $1; # 'start', 'end' or 'include'
	$args =~ s/["'`]//g;
	my $arg2 = $args; # file argument for '.lilypond include'

	if ( exists $lilypond_args{$arg1} ) {
	  $lilypond_args{$arg1}->($arg2);
	  next;
	} else {
	  # not a suitable argument of '.lilypond'
	  $stderr->print( "Unknown command: '$arg1' '$arg2':  '$line'" );
	}

	next LILYPOND;
      } # end if for .lilypond


      if ( $lilypond_mode ) { # do lilypond-mode
	# see '.lilypond start'
	$ly->print( $line );
	next LILYPOND;
      } # do lilypond-mode

      # unknown line without lilypond
      unless ( /
		 ^
		 [.']
		 \s*
		 lilypond
	       /x ) { # not a '.lilypond' line
	$out->print($line);
	next LILYPOND;
      }
    } # end while <$input>
  } # end foreach $filename
} # end Read


########################################################################
# clean up
########################################################################

END {

  exit unless ( defined($Temp->{'temp_dir'}) );

  if ( $Args->{'keep_all'} ) {
    # With --keep_all, no temporary files are removed.
    $v->print( "keep_all: 'TRUE'" );
    $v->print( "No temporary files will be deleted:" );

    opendir my $dh_temp, $Temp->{'temp_dir'} or
      die "Cannot open " . $Temp->{'temp_dir'} . ": $!";
    for ( sort readdir $dh_temp ) {
      next if ( /         # omit files starting with a dot
		  ^
		  \.
		/x );
      if ( /
	     ^
	     $Args->{'prefix'}
	     _
	   /x ) {
	my $file = File::Spec->catfile( $Temp->{'temp_dir'}, $_ );
	$v->print( "- " . $file );
	next;
      }
      next;
    } # end for sort readdir
    closedir $dh_temp;

  } else { # keep_all is not set
    # Remove all temporary files except the eps files.

    $v->print( "keep_all: 'FALSE'" );
    $v->print( "All temporary files except *.eps will be deleted" );


    if ( $Temp->{'eps_dir'} ) {
      # EPS files are in another dir, remove temp dir

      if ( &is_subdir( $Temp->{'eps_dir'}, $Temp->{'temp_dir'} ) ) {
	$v->print( "EPS dir is subdir of temp dir, so keep both." );
      } else { # remove temp dir
	$v->print( "Try to remove temporary directory '" .
	  $Temp->{'temp_dir'} ."':" );
	if ( File::Path::remove_tree($Temp->{'temp_dir'}) ) {
	  # remove succeeds
	  $v->print( "...done." );
	} else { # did not remove
	  $v->print( "Failure to remove temporary directory." );
	} # end test on remove
      } # end is subdir

    } else { # no EPS dir, so keep EPS files

      opendir my $dh_temp, $Temp->{'temp_dir'} or
	die "Cannot open " . $Temp->{'temp_dir'} . ": $!";
      for ( sort readdir $dh_temp ) {
	next if ( /          # omit files starting with a dot
		    ^
		    \.
		  /x );
	next if ( /          # omit EPS-files
		    \.eps
		    $
		  /x );
	if ( /
	       ^
	       $Args->{'prefix'}
	       _
	     /x ) { # this includes 'PREFIX_temp*'
	  my $file = File::Spec->catfile( $Temp->{'temp_dir'},  $_ );
	  $v->print( "Remove '" . $file . "'" );
	  unlink $file or $stderr->print( "Could not remove '$file': $!" );
	  next;
	} # end if prefix
	next;
      } # end for readdir temp dir
      closedir $dh_temp;
    } # end if-else EPS files
  } # end if-else keep files


  if ( $Temp->{'eps_dir'} ) {
    # EPS files in $Temp->{'eps_dir'} are always kept
    $v->print( "As EPS directory is set as '" .
      $Temp->{'eps_dir'} . "', no EPS files there will be deleted." );

    opendir my $dh_temp, $Temp->{'eps_dir'} or
      die "Cannot open '" . $Temp->{'eps_dir'} . ": $!";
    for ( sort readdir $dh_temp ) {
      next if ( /         # omit files starting with a dot
		  ^
		  \.
		/x );
      if ( /
	     ^
	     $Args->{'prefix'}
	     _
	     .*
	     \.eps
	     $
	   /x ) {
	my $file = File::Spec->catfile( $Temp->{'eps_dir'}, $_ );
	$v->print( "- " . $file );
	next;
      } # end if *.eps
      next;
    } # end for sort readdir
    closedir $dh_temp;

  }

  1;
} # end package Clean


1;
# Local Variables:
# fill-column: 72
# mode: CPerl
# End:
# vim: set autoindent textwidth=72:
