#!/usr/bin/perl

# systemflightrecordereditor
# multi-file (non-) interactive command or editor execution with 
# - logging to a journal,
# - file locking, and
# - backup and basic revision control of changed files

# 20090729 PJ   0.1  jakobi@acm.org -- initial version
# 20090825 PJ        added getrcsdiffcmd + improved getrcs guesswork
#                    added pr in messages to protect the terminal against
#                    control codes in filenames
# copyright:  GPL v3 or later
# archive:    http://jakobi.github.com/script-archive-doc/
our $version="0.1.1";
our $debug=0;


# Required dependencies:
# - locking (both available at http://jakobi.github.com)
#   - lock.pl
#   - Compact_pm/Flock.pm (used by lock.pl)
# - changetrack (e.g. from the debian package or directly
#   from changetrack.sf.net)


# locking: use o_nolock to skip locking the command,
#          change lockcmd to '' to avoid locking for
#          journal and backup access.
our $o_nolock=0;            # set to one to skip flock 
# quiet (stripped if verbose); timeout for command less than 1h
# retry interval 5s, report fact of being delayed ONCE, quiet otherwise
our $lockcmd='lock.pl -1 -r 5 -q -t 3300'; 
our $getrcsdiffcmd='diff'; # 'diff -E -b -B -t' # ignore all space change; gnu
our $cocmd='co -M -zLT';   # 'co -M -zLT'       # default to local tz for -d, original mtime
our $changetrackcmd='changetrack';


# Bugs:
#
# - Q: what shall be the default when confronted with sudo? currently we 
#      changetrack those files. but maybe it's saner to do have the user
#      do sudo 'sfre...' instead of doing sfre -e 'sudo...' and tracking
#      files from/for another user's backupdir? 
#      at least the backup permissions are saner that way.
#      ==? turn this into an example note?==
#
# To be implemented/changed:
#
# - do I already log the RC of the executed command??? - ADD IT
# - do keep @files in order, but strip dupes (currently lock.pl
#   does this for us)
# - allow explicit filelist file to include in @files if existing
#     - DO add contents to filesN, this list could be e.g. generated by
#          a wrapper or say known modified files when e.g. invoking ypmake
#          or similar stuff. options --add --addlist
# - rejection lock/desc/journal command list
#          (esp. for things like blank vi or emv or from within GIT,
#           vi -R (assuming the user doesn't override vim's readonly
#                  flag manually, as we'd have no backup then; thus 
#                  vi -R is somewhat questionable; while e.g. vimming non
#                  own files in non-owned dirs w/o write perms for dir
#                  and file are reasonably safe, a suspend editor to
#                  sudo chmod is a sufficiently high-profile conscious
#                  act that ignoring those files can be deemed safe)
#       add blacklist/whitelist,
#       still I want paranoia flag for lock/journal/flock
#       to turn off all things that reduce the tracked filesets... 
#       (incl. lock-/journal-/blacklisted commands)
#       HAVE FILE<N> BE THE FULL LIST, WITH BLACKLISTS APPLIED 
#       JUST LOCALLY: FILES := FILES - (BLACKLIST UNLESS WHITELIST)
# - rejection filename lists for validation of changetrackable files
#     - DO implement it as a skip hash (&action style), but check it
#          early during filesN selection.
#     - DO add an option to load a \n pattern list into %skip
#          --skip, --skiplist, --skiplist-regex
#     e.g. when using the wrapper to e.g. vi -R an file outside of the scope
#          of the project and files to be tracked.
#     add blacklist/whitelist both
# - reduce backups and questions for non-changable files
#     avoid changetracking  any non-changable files
#     - DO: options --mode/--modeappend
#           (no)tty          # previously  o_notty
#           (no)ask          #             o_ask
#           (no)journal      #             o_nojournal
#           ---
#           (no)lock,        #             o_nolock
#           (no)version      #             o_noversion
#           (no)(version|lock|)unchange?able
#           allfilesifsudo' in editor and args 
#                           if encountered and true wrt editor/@ARGV: 
#                           set lock, version and reloop.
#           --> for word in mode: turn into hash, and for each mode unset
#               opposites then finally turn back into string
#     - DO: if command does not contain \bsu(do)?\b [$sudopat] AND
#       not (-w or -W dirof(file)) and not (-w or -W file), 
#       [DONT: AND file has NOT CHANGED timestamps; this would require
#        a hidden copy to stash away... - DO NOT DO THIS], then
#       avoid including file into file1 / file3 / file4.
#       have a flag being part of mode to turn this on.
#       == above all files if sudo mode flag
#       --> check this wrt the command white/blacklist above
#     - DO: have the deselected stuff as %rfilesN/@rfilesN,
#       to still be included in the journal with a RFILE: prefix.
#     - == these three points should do the trick.
#     avoid questions when only non-changable files
#        can we move-down the description query (but we use domain/user
#        from that for loading the config [not possible, adding a skip
#        on detecting vi -R however might work, or not, see configuration]!)?
#        [DO skip query before loading config, and place a o_message only
#        after loading the configs, see next point:]
#     - DO: adding an option to assert o_user / o_domain to be unchanged
#       might be the ticket. If set, skip generating o_user/o_domain
#       question, and thus also skip asking about o_message on o_writableonly 
#       DO make this option -U 
#     - == both points should do the trick.
# - special handling of vi -R and empty vi invocations
#     - DO ask no message, maybe also avoid journaling if no o_message
#      
# Ultimate wishlist:
#
# - actually modified files...
#   - now wouldn't it be nice to check all written files ptrace-style and
#     consider that the apps working file set... . More realistic: if there's
#     a wrapper logging the accessed files (but that's too late). We'd need
#     a dtrace-style trace that suspends the process, changetracks the 
#     about-to-be-written/-deleted to file, then resumes. There's a published
#     example for solaris dtrace doing some kind of 
#     safe-delete-allowing-for-undelete.
#     I don't know of a suitable ptrace example [beyond early basics like 
#     fakeroot-ng; a FUSE style trick might also be a ticket to do something
#     similar; wasn't there a syscall tracer like that?]
#   - and contrary to strace, it should never hang and be utterly transparent
#     to the file (unless maybe setuid, in which case a switch should be 
#     available to turn off things, and a second more restrictive sudopat
#     to do this automatically if we 'smell' a sudo 'hint' as the string su
#     or \bsudo\b being part of an argument or filename).


# Notes:
#
# - ' #' in the command can suppress arguments and logging command output
#   (lack of command screening is intentional, see -examples).
# - use of symlink as the script name also changes the implicit
#   filenames for the configuration files. Intended for customization.
# - does not parse changetrack configurations (Q: which/how to guess) 
#   to extract emails for notification; that's still the task for the 
#   nightly cronjob. 
#   If you want email notification also for manually changed and 
#   changetracked files, use -d to use a different changetrack
#   repository; however by default, we use the standard location for
#   debian/ubuntu and skip email notification instead.

# changetrack's use of RCS imports a few RCS peculiarities:
# - changetrack and RCS is a bit painful due to the renaming to :-pathes
#   and RCS inistence to never cat a version directly from the repo
#   even rcsdiff cannot and insists on an existing base file. Worse,
#   rcsdiff goes out of its way to disallow gnudiffs -N to work...
#   looks like I need to do a sfrecat that tries to cat older versions...
#   incl. locking and stuff. See the --diff/--cat workaround 
#   to feel the pain.
# - symlinks   --> treated as if it were a plain file
# - dirs       --> ignored by changetrack 
#   (use --addversion "dir/*" for a single level if necessary)


# -------------------------------------------------------------

use strict;
use vars;
use warnings;

$|=1;
our $Me="sfre";
our $start=time;

our($o_verbose, $o_changetrack, $o_journaledit, $o_addoutput)=(0,0,0,0);
our($o_nobackup, $o_notty, $o_nojournal)=(0,0,0);
our($o_configfile, $o_message, $o_user, $o_domain, $o_ask)=("","","","",0);
our($o_journalgrep, $o_journalecho)=("",0);
our($o_backupdiff, $o_backupcat, $o_backupecho)=(undef,undef,0);
our($o_echohistory,$o_echodir,$o_echoyesterday)=(0,0,0);

# $lockcmd and $changetrackcmd are just the commands, 
# while $editorcmd may also be a command with arguments
our($shellcmd, $editorcmd,$log)=("","","");

our($o_hostname, $o_fqdn, $o_whoami);
chomp($o_hostname=qx!hostname!);
      $o_hostname=$ENV{HOSTNAME} if not $o_hostname;
chomp($o_fqdn=qx!hostname -f!);
      $o_fqdn=$o_hostname        if not $o_fqdn;
chomp($o_whoami=qx!whoami!);
      $o_whoami=$ENV{LOGNAME}    if not $o_whoami;

$o_user=$ENV{SUDO_USER}          if not $o_user;
$o_user=$ENV{LOGNAME}            if not $o_user;
chomp($o_user=qx!whoami!)        if not $o_user;
$o_user=$<                       if not $o_user;
$o_user=$ENV{SFREUSER}           if $ENV{SFREUSER};
$o_configfile=$ENV{SFRERC}       if $ENV{SFRERC};
$o_domain=$ENV{SFREDOMAIN}       if $ENV{SFREDOMAIN};
$o_domain=$ENV{HOSTNAME}         if not $o_domain;
$o_domain=$o_hostname            if not $o_domain;
$o_message=$ENV{SFREMESSAGE}     if $ENV{SFREMESSAGE};
$editorcmd=$ENV{SFREEDITOR}      if $ENV{SFREEDITOR};


# emv contains some more configurability for a similar case,
# but that just adds noise to the script without any sane effect
# NOTE that csh, tcsh, zsh and dash don't support pipefail
# NOTE that even a mere pd-ksh should suffice
for my $i (qw!/bin /usr/bin /opt/pkg/bin!) {
   for my $j (qw!bash ksh93 ksh!) {
      $shellcmd="$i/$j" if not $shellcmd or not -x $shellcmd;
   }
}
$shellcmd=$ENV{SFRESHELL}        if $ENV{SFRESHELL};
die "# $Me: aborting - no shell - set \$SFRESHELL (must support set -o pipefail)\n" 
   if not $shellcmd or not -x $shellcmd;


our($backupdir, $journal)=("","");   # backupdir and system journal
our(@journal)=();                    # extra files to consider with journaledit (ignored otherwise)
our(@o_addlock,@o_addversion)=();    # extra files ... for locking and versioning (ignored otherwise)
our(@ARGVOPT)=();

# hook variables for evaluation
our(%action1,%action2,%action3,%action4)=();   # file specific actions: pattern => perlscrap
our($preaction,$postaction)=("","");           # configuration defined global actions
our(@files1,%files1,@files3,%files3,@files4,%files4)=(); # currently existing plainfiles + c/mtimes

our($rc,$rc0,$rc1,$rc2,$rc3,$rc4)=(0,0,0,0,0,0); # collect to allow exit with the sum of rc


# lock options and break delays
our $lockoptjournal="-b 60  -r 1";
our $lockoptbackup= "-b 300 -r 1";
our $lockoptcommand="";
our $changetrackopt="-q -u";


sub examples {

&checkrequirements;

   warn <<EOF;

On required programs to run sfre:

This  script  requires  changetrack and lock.pm (if you  don't  see  a
warning above, you're probably fine, unless lock.pl -f cannot find its
own Compact_pm/Flock.pm implementation).


Usage scenario / comparing sfre to revision control:

It  isn't.  Or  rather  it is.  A  basic  single-branch,  non-release,
no-checkout,  no-revision-locking  versioning setup not  allowing  for
off-site/offline  remote use. Offering far less features than the  RCS
sfre uses.

sfre's  main idea is keeping a journal of e.g. administrators' actions
in  a  multi-administrator group to allow quickly to  pinpoint  recent
changes  to  a system involving specific files or services, with  more
interesting  revision control (or versioning packaging of  components)
being  done  outside  of sfre, with sfre just noticing  and  archiving
newly installed file versions.

That said, if you stash away a backup with a special suffix instead of
naming  release  revisions,  in a pinch, it should substitute  a  real
version  control  system  well  enough for e.g. SOHO  or  single  user
development with mostly independent files.


Usage scenario 2 / on security 

sfre  is  NOT  a security tool. It is merely a tool to  augment  group
memory. As such, having a command contain a '#' in the wrong place can
disallow  the  command to see part of its arguments, but it will  also
suppress  appending  the command output to the journal (by  commenting
out the tee to the log). On the other hand, this very placement may be
extactly the correct position while debugging some problem. Therefore,
sfre does not make any effort to screen commands.

Consider  a sudo-cum-script whitelist approach if you require a secure
and restricted environment. Given sfre's use of multiple configuration
files,  you  probably  do not wish to run sfre  in  this  environment;
also given the restriction on possible actions and the available logs,
there's little need for sfre.


On using sfre to wrap all interactive editor invocations

The  following  example  executes sfre instead of vi, but  passes  all
options and arguments on to vi (Bourne-shell and descendants, assuming
/usr/bin/vi  as location). To add invocation-specific sfre options  to
'vi', invoke the alias as SFREOPT="--nolock" vi.

   if [ -n "\$PS1" ]; then
      alias vim=vi
      alias vi='sfre -e /usr/bin/vi --'
      EDITOR=sfre
      VISUAL=sfre
   fi


On cron, changetrack, sfre (also skipped changetrack email):

If  you  want  changetrack  to send email notifications  from  e.g.  a
cronjob  NOT  using  sfre, then keep sfre and  changetrack  backupdirs
separate (e.g. SFREOPT="-d /var/lib/sfre").

This  currently is only an issue for root, as both changetrack  hourly
cronjobs  and sfre both use /var/lib/changetrack as backupdir, leading
to  sfre updating the backups before the cronjob sees the change, thus
resulting in skipped email notifications.

Note  that  for this shared use, the changetrack cronjobs need  to  be
modified to honor locking with procmail's lockfile:
   f=/var/lib/changetrack/.locked; lockfile -s10 -l600 \$f
   <other statements>
   rc=\$?; rm \$f
or with lock.pl
   f=/var/lib/changetrack/.locked; <PATH>/lock.pl -t 3300 -r 10 -b 600
   <other statements>
   rc=\$?; rmdir \$f


On locking (-> defaults can be changed by configurations):
   lockcmd         $lockcmd
   lockoptbackup   $lockoptbackup
   lockoptcommand  $lockoptcommand
   lockoptjournal  $lockoptjournal

Locking aborts the attempted command by default after waiting for 3300
sec  (\$lockcmd),  by default without attempting to break  locks  (type
flock).  For  journal access (type flock), logging INTENT  and  COMMIT
each  breaks earlier locks after 120 seconds. For backup (type mkdir),
the  lock is broken after 300 seconds. Lock.pl defaults to  non-greedy
locking,  add -g to retain all claimed locks when waiting for the  yet
unclaimed ones.


Accessing the journal records with -jedit/-jgrep:

The  journal consists of paragraphs delimited by newlines. Each  entry
consists  of an email-style header, followed by "File:" entries giving
any  currently existing file from the arguments, and optionally by the
quoted command output.

   Type:         COMMIT
   From:         jakobi
   Date:         2009-08-16 18:13:55
   Domain:       anuurn
   Command:      ls
   Command-Args: -ld /etc/hosts
   Start-Date:   2009-08-16 18:12:54
   Message-ID:   <COMMIT:jakobi.1000.:20090816181355\@anuurn.compact>
   Message:      /etc/hosts
   File: /etc/hosts
   Output:       captured -e command output follows.
      > ls:
      > -rw-r--r-- 1 root   root     754 2009-08-13 09:52 /etc/hosts

To  search  the journal get a report by date, use the  --grep  option;
however that is also just
   perl -00ne 'print if /REGEX/' JOURNALFILE

Do  lengthy  journal edits outside of the locking: -jedit and  save  a
copy, clear the file, save and quit), edit at leisure, then -jedit and
insert  the edited version back at the beginning of the journal,  save
and quit.


On configuration and hook variables for file-specific actions

At  the end of the script, you find a commented example  configuration
file,  that  also  includes some actions. Look at the  hash  keys  and
invoke sfre with files matching those keys and experiment.

Note that invoking the script using a symlink, you also change some of
the names of the implicit configuration files we try to load:
  - user request with -c, otherwise
  - one or more of 
    /etc/.sfrerc
    \$0.sfrerc
    \$0.sfrerc.{\$o_domain,\$o_user}
    \$0.sfrerc.{\$o_domain.\$o_user,\$o_user.\$o_domain}
    ~/.sfrerc

This  should  allow easy configuration of task specific  (symlink)  or
host/user  specific  modifications,  without requiring too  much  perl
within the configuration files.

The  script  also honors exported shell variables as  initial  default
values (unless null):
  - SFREDOMAIN    -- --domain (default hostname)
  - SFREMESSAGE   -- -m (no default session description)
  - SFREUSER      -- -u (default \$LOGNAME)

  - SFREEDITOR    -- EDITOR to use, e.g. vi (also -e)
  - SFREOPT       -- prepended to args after splitting on (?<!\\\\)\\s
                     (avoid quotes except to denote the empty string)
  - SFRERC        -- configuration file to use (also -c)
  - SFRESHELL     -- /bin/bash or /bin/ksh (should support pipefail)


On RCS, --diff and --cat

The  RCS  revisions  can be accessed with --cat/--diff, with  take  an
optional  date  or version string, which sfre will provide to co as  a
-dDATE  or -rREV option. The default is '0', which is the most  recent
version in the changetrack archive.

To  specify the version option yourself, use a '=' as first character:
=-d'Thu,  11  Jan  1990  20:00:00  -0800'.  Non-option  arguments  are
provided  to the diff/cat command used. The diff command can be set in
\$getrcsdiffcmd  and  takes e.g. on Linux the usual GNU  Diff  options,
e.g.  -E -b -B -t to skip whitespace changes. Some valid date  strings
are  '8:00  pm', 'Thu', '1990-01-12 04:00:00+01'. Note that sfre  adds
-zLT to co, making the local time zone the default.


Alternatives to sfre:

  - a real version control system, if you can guarantee all group
    members to always checkin/checkout the running version on each 
    and every change even if changing only a single line for a
    quick test. Which includes indirect calls like crontab -e, or
    scripted changes of e.g. configuration files which ideally should
    also be tracked or wrapped with sfre.

  - sudo with script and maybe strace -efile or some other syscall
    tracer. In contrast to sfre, this approach also allows for
    shared restricted or even jailed root setups. Depending on the
    repository locations, sfre might be usable as part of such a
    concept. Otherwise a different way must be devised to obtain 
    command and  change file set for backup and journaling. With
    a strongly nailed down whitelisted sudo and security concept,
    the number of involved files per accepted task however should
    small enough to not require a kitchensink-approach like sfre.
    

EOF
}

sub usage {

   &checkrequirements;
   warn <<EOF;

systemflightrecordereditor - a tool to support group memory and 
                             to avoid stepping on each other's 
                             and everybody's cronjobs' toes

                             - by forcing a meaningful description
                             - by logging into a journal
                             - by f-locking mentioned files
                             - by archiving changed files

SFREOPT="[OPTIONS]" $Me [OPTIONS] -- <EDITOR-ARGUMENTS AND FILES>

version: $version.

This  script  wraps  a command (\$EDITOR by default) with  locking  and
versioning  for  plain files (changetrack/RCS is the only  option  for
now).  In addition it updates journal with a short session description
by  the user (including the set of files involved and optionally  also
the command output).

If  part  of  the description is missing and a tty is  available,  the
script  gives  the user a change to provide a complete description  of
his  action.  By providing a configuration file, sfre can be asked  to
automatically  perform  pre  and  post action depending  on  files  or
session description.

The  implementation  is safe for concurrent use by multiple users  and
cron-jobs  incl.  a multiple root scenario. sfre can be used  to  wrap
your default editor in interactive shells, see --examples. It can also
wrap  arbitrary  commands like cronjobs, protect them by  locking  and
journal/backup  their  changes.  

By  default,  sfre will abort the command after waiting for nearly  an
hour, without trying to break locks (see \$lockcmd).

For  more information, examples incl. configuration examples:
  - say --examples
  - take a peek at the comments in the source or 
    the configuration file example at the end of the script


Options:
  -h       --help        -- help; further information with --examples
  -v       --verbose     -- verbosity
  --                     -- last option

The type for the second argument for some of the following options 
is one of F(ile), D(irector), R(egex), or (S)strings.

Options for configuration and commands
  -c   F   --cfg         -- load config file F 
  -d   D   --dir         -- directory to place backups in
  -e   S   --command     -- run command S instead of \$EDITOR
  -j   F   --journal     -- journal to log to (:-separated)

Options for the journal
  -J       --addoutput   -- include command output in journal
  -m   S   --message     -- session description for the journal
  -u   S   --user        -- username for the journal
  --ask                  -- always ask the user to confirm the description
  --domain S             -- set 'logical domain' (default is hostname)
  --notty                -- do not ask for missing description

Ooptions for locking and versioning
  -C       --changetrack -- use changetrack 
  --addlock    F         -- also lock file F before running command
  --addversion F         -- also track this file (also allows "dir/*")
  --nolock               -- no flocking of file arguments
  --noversion            -- no backups
  
Accessing journal and versions (if pathes are set within config
files, consider providing -u and --domain as well)
  --cat       [S]        -- both commands use RCS co with S being either
  --diff      [S]           -d<DATE> or -r<revision number or name>
  --echo                 -- print the RCS file name (not checked out)
  --echod(ir)            -- print changetrack's backupdir
  --echoh(istory)        -- print changetrack's .history filename
  --echoy(esterday)      -- print changetrack's .yesterday filename
  --jecho                -- print journal path
  --jedit                -- edit the journal (you've 60s before 
                            other instances may break the lock)
  --jgrep      R         -- grep the journal (perl expression for
                            a simple perl -00ne 'print if /R/')
  --nojournal            -- no journal (also set by --jedit/--jgrep 
                            and --cat/--diff)

EOF
}


if ($ENV{SFREOPT}) {
   my @tmp=split /(?<!\\)\s+/, $ENV{SFREOPT};
   for(reverse @tmp) {
      s/^(["'])\1$//;
      unshift @ARGV, $_;
   }
}
while($_=$ARGV[0],defined $_){ 
   my($t);
   /^-(c|-?cfg)$/o          and do{shift; $o_configfile=shift;        push @ARGVOPT, $_, $o_configfile; next};
   /^-(C|-?changetrack)$/o  and do{shift; $o_changetrack=1;           push @ARGVOPT, $_;                next};
#   /^-(-?diff)$/o          and do{shift; $o_diff=0; $o_diff=shift if $ARGV[0]!~/^-/; next};
   /^-(e|-?command)$/o      and do{shift; $editorcmd=shift;           push @ARGVOPT, $_, $editorcmd;    next};
   /^-(d|-?dir)$/o          and do{shift; $backupdir=shift;           push @ARGVOPT, $_, $backupdir;    next};
   /^-(j|-?journal)$/o      and do{shift; $journal=shift;             push @ARGVOPT, $_, $journal;      next};
   /^-(-?jedit)$/o          and do{shift; $o_journaledit=1;                                             next};
   /^-(-?jgrep)$/o          and do{shift; $o_journalgrep=shift;                                         next};
   /^-(-?jecho)$/o          and do{shift; $o_journalecho=1;                                             next};
   /^--?echod(ir)?$/o       and do{shift; $o_echodir=1;                                                 next};
   /^--?echoh(istory)?$/o   and do{shift; $o_echohistory=1;                                             next};
   /^--?echoy(esterday)?$/o and do{shift; $o_echoyesterday=1;                                           next};
   /^-(-?echo)$/o           and do{shift; $o_backupecho=1;                                              next};
   /^-(-?cat)$/o            and do{shift; $o_backupcat=0;  $o_backupcat=shift  if $ARGV[0]!~/^-/;       next};
   /^-(-?diff)$/o           and do{shift; $o_backupdiff=0; $o_backupdiff=shift if $ARGV[0]!~/^-/;       next};
   /^-(-?ask)$/o            and do{shift; $o_ask=1;                   push @ARGVOPT, $_;                next};
   /^-(m|-?message)$/o      and do{shift; $o_message=shift;           push @ARGVOPT, $_, $o_message;    next};
   /^-(u|-?user)$/o         and do{shift; $o_user=shift;              push @ARGVOPT, $_, $o_user;       next};
   /^-?-addoutput$/o        and do{shift; $o_addoutput=1;             push @ARGVOPT, $_;                next};
   /^-?-notty$/o            and do{shift; $o_notty=1;                 push @ARGVOPT, $_;                next};
   /^-?-nolock$/o           and do{shift; $o_nolock=1;                push @ARGVOPT, $_;                next};
   /^-?-nojournal$/o        and do{shift; $o_nojournal=1;             push @ARGVOPT, $_;                next};
   /^-?-nobackup$/o         and do{shift; $o_nobackup=1;              push @ARGVOPT, $_;                next};
   /^-?-domain$/o           and do{shift; $o_domain=shift;            push @ARGVOPT, $_, $o_domain;     next};
   /^-?-addlock$/o          and do{shift; push @o_addlock,$t=shift;   push @ARGVOPT, $_, $t;            next};
   /^-?-addversion$/o       and do{shift; push @o_addversion,$t=shift;push @ARGVOPT, $_, $t;            next};
   /^-(h|-?help|\?)$/o      and do{&usage; exit 1};
   /^--?examples?$/o        and do{&examples; exit 1};
   /^-(v|-?verbose)$/o      and do{shift; $o_verbose++;               push @ARGVOPT, $_;                next};
   /^--?$/o                 and do{shift;                                                               last};
   last;
}
for(@ARGVOPT) { $_="" if not defined $_; }
@o_addlock=grep {defined $_ and /./} @o_addlock;
@o_addversion=grep {defined $_ and /./} @o_addversion;



# stage 0-get info+cfg ---------------------------------------

# where to log and diff to?
$journal  ="$ENV{HOME}/.JOURNAL"     if not $journal and $> == $< and $>;
$journal  ="/etc/JOURNAL"            if not $journal;

$backupdir="$ENV{HOME}/.sfre_backup" if not $backupdir and $> == $< and $>;
$backupdir="/var/lib/changetrack"    if not $backupdir and $o_changetrack;
$backupdir="/var/lib/sfre_backup"    if not $backupdir;


# force user to complete journal message (unless -notty)
if ($o_nojournal or 
    $o_journaledit or $o_journalgrep or $o_journalecho or 
    defined $o_backupcat or defined $o_backupdiff or $o_backupecho or
    $o_echohistory or $o_echoyesterday or $o_echodir) {
   # we skip journaling in this invocation, so don't ask the user
   # if we need user/domain to have some config file setting the
   # correct backup location, then better hope for the user providing
   # -u / --domain on his own. Or -j / -d instead, which are the only
   # values we're really interested with above options, besides maybe 
   # @journal.
} else {
   if (  not $o_user or 
         not $o_domain or 
         not $o_message) {
      if (not $o_notty) {
         ($o_user,$o_domain,$o_message)=getanswer(
             "Please enter/confirm: ", sprintf("Username: %-20s",$o_user),    $o_user, 
                                       sprintf("Domain:   %-20s",$o_domain),  $o_domain,
                                       sprintf("Message:  %-20s",$o_message), $o_message);
      }
   } 
}

chomp($o_user=qx!whoami!) if not $o_user;
$o_user=$<                if not $o_user;

$o_domain="nowhere"       if not $o_domain;
$o_message="noplan"       if not $o_message;
$o_user="noone"           if not $o_user;


# read configuration, overlaying defaults if no user request
# to avoid loading a config, just say -c /dev/null
if (not $o_configfile or not -f $o_configfile) {
   $o_configfile="";
   for("/etc/.sfrerc", "$0.sfrerc", "$0.sfrerc.$o_domain",
      "$0.sfrerc.$o_user", "$0.sfrerc.$o_domain.$o_user", "$0.sfrerc.$o_user.$o_domain",
      "$ENV{HOME}/.sfrerc"){
      do $_ or die "# $Me: problem with config file $_ $@" if -f $_;
   }
} elsif ($o_configfile and $o_configfile ne "/dev/null") {
   do $o_configfile or die "# $Me: problem with config file $_ $@";
} else {
   warn "# $Me: skipping configuration as requested\n" if $o_verbose;
}

# try to avoid accidental recursion
$editorcmd=$ENV{EDITOR}     if not $editorcmd and $ENV{EDITOR}!~/sfre|systemflightrecorder/;
$editorcmd=$ENV{VISUAL}     if not $editorcmd and $ENV{VISUAL}!~/sfre|systemflightrecorder/;
$editorcmd="/bin/vi"        if not $editorcmd and -x "/bin/vi";
$editorcmd="/usr/bin/vi"    if not $editorcmd and -x "/usr/bin/vi";
$editorcmd="/usr/bin/nano"  if not $editorcmd and -x "/usr/bin/nano";
if (not $editorcmd) {
   my($tmp);
   chomp($tmp=`which vi`);
   $ENV{file}=$tmp;
   $ENV{file1}=$Me;
   if ($tmp and not -B $tmp) { 
      system "grep -e '$Me' -e sfre -e systemflightrecorder \$file >/dev/null 2>&1 ||".
          "fgrep -e \$file1 \$file  >/dev/null 2>&1";
      $editorcmd="$tmp" if $?; # otherwise probably recursion
   } else {
      $editorcmd="$tmp" if $tmp;
   }
}
if (not $editorcmd) {
   die "# !!! $Me: cannot determine sane editor (that isn't $Me)\n";
}


if ($backupdir eq "/var/lib/changetrack" and -r "/etc/cron.hourly/changetrack") {
   system "fgrep .locked /etc/cron.hourly/changetrack 2>&1 >dev/null";
   if ($?) {
      $backupdir="/var/lib/sfre";
      warn "\n** $Me: forced change of backupdir as the hourly changetrack cron\n".
             "** seems to lack locking. Using /var/lib/sfre instead of\n".
             "** /var/lib/changetrack. See note in --examples for more info!\n".
             "** Consider separate repositories if you require changetrack cron\n".
             "** to send notification emails for _ALL_ changes, including\n".
             "** those done with sfre (tradeoff disk-space vs. email-notification).\n\n";
   }
}


&checkrequirements; # give user a hint in case of missing lock.pl/changetrack


# stage 0-commands-and-exit -------------------------------
# commands that short-circuit processing
# (or redefine too many options at once like -jedit :))

if      ($o_journaledit) { 
   # reinvoke happens w/o -jedit option (I want to lock the journal...)
   push @ARGV, $journal, @journal;
   @ARGVOPT=grep {not /^-addoutput$/} @ARGVOPT;
   push @ARGVOPT,"--nojournal";
} elsif ($o_journalgrep) {
   $ENV{match}=$o_journalgrep;
   exec "perl","-00ne",'print if /$ENV{match}/',$journal,@journal;
} elsif ($o_journalecho) {
   print "$journal\n"; exit 0;
} elsif (defined $o_backupcat or defined $o_backupdiff or
         $o_backupecho or $o_echohistory or $o_echoyesterday or $o_echodir) {
   my $file=pop @ARGV;
   my $ans=&getrcsfile($file, @ARGV);
   exit $ans if $ans=~/^\d+$/;
   print $ans;
   exit 0;
}



# stage 1-lock-and-reinvoke -----------------------------------
&actions(\%action1,@ARGV); # allow for preparing non-existing files
                           # !!! can be invoked TWICE, once by sfre,
                           # !!! and once by the sfre child of lock.pl 

# get the list of already existing files for locking
for(@ARGV)  { 
   my @stat=stat $_;
   if (-f _) {
      push @files1, $_;
      $files1{$_}=$stat[9].":".$stat[10];
   }
}

# and lock by reinvoking ourselves AT MOST ONCE,
# that way unlocking is automatic 
if (not $ENV{SFRELOCKED} and $lockcmd and not $o_nolock and @files1) {
   my @cmd;
   $lockoptcommand.="-v -v " if $o_verbose>1;
   warn "# $Me: reinvoking $0 with $lockcmd\n" if $o_verbose>1;
   $ENV{SFRELOCKED}=1;
   push @ARGVOPT, "-u", $o_user, "--domain", $o_domain, "-m", $o_message, "--";
   @cmd=($shellcmd, "-c", $lockcmd." -f ".$lockoptcommand.' ${1:+"$@"}', "${Me}lock", 
                         @files1, @o_addlock, "--", $0, @ARGVOPT, @ARGV);
   warn "# $Me: reinvoking as ", join(" ",map({"'".pr(sq($_))."'"} @cmd)), "'\n" if $o_verbose>1;
   exec @cmd; 
}

# moved down, as the -e command is allowed to fail,
# which is already checked for.
$lockcmd=~s/ -q / / if ($o_verbose or $debug) and $lockcmd=~/lock\.pl/;

if($o_verbose) {
   print "# -j journal:   $journal\n";
   print "# -d backupdir: $backupdir\n";
}



# stage 2-preprocess ---------------------------------------
# from now on, logging is permitted, but we're possibly 
# a-child-process-of-lock instead of being the original running
# sfre instance.

$preaction and do{eval $preaction; warn "# !! $Me: \$preaction failed: $@" if $@};
&actions(\%action2,@files1,@ARGV); # allow for preparing non-existing files
&journal("INTENT", $start, 0, @files1); # this may block for upto 5 minutes, so go early
&versioning(@files1,@o_addversion);
warn "# sfre preprocessing problems: $rc1\n" if $rc1;



# stage 2-command -----------------------------------------
my($tmpfile,$cmd);
# the script command either sticks around interactively incl. ignoring exit 
# or makes a mess of quoting CMD options by only allowing -c CMD 
# -> NO GO.
$cmd=$editorcmd . ' ${1:+"$@"} ';
if ($o_addoutput) {
   chomp($tmpfile=`tempfile -d /tmp -p sfre.`);
   $cmd="set -o pipefail; { ".$editorcmd . ' ${1:+"$@"} ;} 2>&1 | tee -a ' . $tmpfile if $tmpfile;
}

#&debug_status;

warn "# $Me: command ", pr(join(" ", $shellcmd, "-c", $cmd, "${Me}cmd", @ARGV)), "\n" if $o_verbose>1;
system $shellcmd, "-c", $cmd, "${Me}cmd", @ARGV;
$rc2=$?>>8;
$log=""; $log=`cat $tmpfile;rm $tmpfile` if $o_addoutput;
warn "# $Me: command returns $rc2\n" if $rc2 and $o_verbose;

&debug_status; 
#die  "# enough for now\n";



# stage 3-postprocess modified files -----------------------
for(@ARGV)  { # get the lists for stages 3 and 4
   my @stat=stat $_;
   if (-f _) {
      push @files4, $_;
      $files4{$_}=$stat[9].":".$stat[10];
      if (not $files1{$_} or $files4{$_} ne $files1{$_}) {
         push @files3, $_;
         $files3{$_}=$files4{$_};
      }
   }
}
&actions(\%action3,@files3);

# allow changetrack to do its own job and check ALL files in stage 4



# stage 4-postprocess --------------------------------------
&actions(\%action4,@files4);
&journal("COMMIT", $start, time, @files4);
&versioning(@files4,@o_addversion);
warn "# sfre postprocessing problems: $rc3\n" if $rc3;

$rc=$rc1+$rc2+$rc3+$rc4;
$postaction and do{eval $postaction; warn "# !! $Me: \$postaction failed: $rc): $@" if $@};

&debug_status(0);
#die  "# enough for now\n";

# return error or status of command
exit $rc;



# #####################################################################

# ---------------------------------------------------------------------
# sub routines
# ---------------------------------------------------------------------


sub actions { # arrayref, files...
   my($perlscrap,%files)=();
   my($actref, @files)=@_;
   @files=grep {not $files{$_} and do{$files{$_}=1}} @files; # dedupe
   
   # for each re, check all fails against the re, with $_ being the file
   for my $re (sort keys %$actref){
      $perlscrap=$actref->{$re};  
      for my $file (@files){
         $_=$file;
         if (/$re/) {
            warn "# $Me: action match ($re : ".pr($file).")\n" if $o_verbose>1;
            eval($perlscrap);
            warn "$ ! Me: action configuration error ($re : ".pr($file).") $@" if $@;
            $@="";
         }
      }
   }
}


sub versioning {
   # currently the great versioning system switch ...
   # ... does know only changetrack, which may be for the
   # best given that we'd also need switching 
   # --diff / --cat / --*echo* implementations as well
   &versioning_changetrack;
}
sub versioning_changetrack { # changetrack

   my(@files)=@_;
   return if $o_nobackup or not @files;

   # this however is sufficient reason to be finally allowed to panic...
   die "!!! $Me: backupdir invalid\n" if not $backupdir or not -d $backupdir and not mkdir $backupdir;

   my($lock,$rc,$t,$conf,$log);
   $lock="$backupdir/.locked";
   $log="$backupdir/.sfre.log";
   $ENV{lock}=$lock;
   $conf="$backupdir/.sfre.conf_ct.$$.$o_hostname";
   $rc=0;

   if ($lockcmd) {
      system("$lockcmd $lockoptbackup \$lock");
      $rc=$?>>8;
      warn "# $Me: cannot lock backup - continue\n" if $rc;
      #$lock=undef;
   } else {
      $lock=undef;
   }

   # QQ: parse some yet to detect changetrack config 
   #     to determine email addresses to add to config?
   #     Skip - if we use 2 repos, the emails will happen
   #            at the cost of some disk space

   # create configuration filelist
   open(FH, ">", $conf) and
   print FH join("\n",map({absname($_)} @files),"") and
   close FH or 
   do{$rc=1; warn "# $Me: cannot write changetrack config\n"};

   # run changetrack
   $t="$o_user\@$o_domain: $o_message"; $t=~s/\n/ /g;
   not $rc and do { 
      $ENV{log}=$log;
      my @cmd=($shellcmd, "-c", $changetrackcmd." >\$log 2>&1 ".$changetrackopt." ".
      "-c '". sq($conf)."' ".
      "-d '". sq($backupdir)."' ".
      "-m '". sq($t)."' ".
      '${1:+"$@"}', "${Me}version_ct");
      open (FH, ">>", $log);
      print FH "# Me: changetracking on ", gettime(time), ": ", join(" ", @cmd,"\n");
      print FH join("\n",map({absname($_)} @files),"\n");
      close FH;
      system(@cmd);
      $rc=$?>>8;
   };
   warn "# $Me: changetrack error $rc - continue\n" if $rc;
   unlink $conf;

   rmdir $lock if $lock;
   return $rc;
}


sub journal {
   # log a message of "type" suitably dated to the journal, incl. the 
   # affected files. Multi-line $o_message descriptings are ok, subsequent
   # lines will have prepended whitespace in the log. Each log entry is a paragraph.
   return 0 if ($o_nojournal);
   my($type, $start, $now, @files)=@_; 
   my($log0)=("");
   $type="" if not defined $type;
   $start=time if not $start;
   $now=$start if not $now;

   my $msgid=uc "$type"; $msgid=~s/\s.*//g;
   $msgid.=":$o_whoami.$<.". ( $ENV{SUDO_USER} ? $ENV{SUDO_USER} : "" ) . ":".gettimeshort($now)."\@$o_fqdn";

   my($ok)=(0);
   $ENV{journal}=$journal;
   ( $lockcmd ? 
         open(FH,"|  $lockcmd --flock $lockoptjournal \$journal -- cat >> \$journal") :
         open(FH,">>", $journal) 
      )  and 
      ++$ok or 
      warn "# $Me: cannot append to journal $journal\n";

   # add an excess \n in case somebody adds notes to the journal by hand
   if ($log and $o_addoutput) {
      $log0=$log;
      $log0=~s/(?<!\n)\z/\n/;
      $log0=~s/^/   > /mg;
      $log0="Output:       captured -e command output follows.\n".$log0;
   }
   print FH "\n" .
            headerize("Type:         $type\n") .
            headerize("From:         $o_user\n") .
            headerize("Date:         ". gettime($now) ."\n") .
            headerize("Domain:       $o_domain\n") .
            headerize("Command:      $editorcmd\n") .
            headerize("Command-Args: " . join(" ", map({"'".sq($_)."'"} @ARGV)) . "\n") .
            headerize("Start-Date:   ". gettime($start) ."\n") .
            headerize("Message-ID:   <$msgid>\n") .
            headerize("Message:      $o_message\n") .
            join(     "File: ", "", map({my $s=$_; $s=~s/\n/?/g; "$s\n"} @files)) .
            $log0 .
            "\n";
   close FH and ++$ok or warn "# $Me: cannot append to journal $journal\n";

   return 2==$ok ? 1 : 0;
}


# @ = getanswer($firstprompt, $prompt1, $default1, ...)
#
# guess usable tty and ask user for input in case of missing description items
# if the user replies <CR>, just return the caller-provided 'current value'
# Tries to short circuit in case of only one missing (null) value. The prompt
# is assumed to already contain the current value if required as information
# for the user.
sub getanswer {
   my($firstprompt)=shift;
   $firstprompt="" if not defined $firstprompt;
   $firstprompt.="\n" if $firstprompt and $firstprompt!~/\n\z/;

   my($IN,$OUT,$INclose,$OUTclose);
   if (-t main::STDOUT){
      $OUT=\*main::STDOUT;
   } elsif (-t main::STDERR) {
      $OUT=\*main::STDERR;
   } else {
      open($OUT,">/dev/tty"); $OUTclose=1;
   }
   if (-t main::STDIN){
      $IN=\*main::STDIN;
   } else {
      open($IN,"</dev/tty"); $INclose=1;
   }

   my($missing,$i,@prompt,@current);
   ($missing,$i)=(-1,-1);
   print $OUT $firstprompt if (@_);
   while(@_){
      $i++;
      $_=shift; $_="" if not defined $_;
      push @prompt,$_;
      $_=shift; $_="" if not defined $_; 
      push @current,$_;
      if ($_!~/\S/) {
         $missing = -1==$missing ? $i : 99;
      }
   }

   my($ans,@ans);
   if(99>$missing and -1!=$missing and not $o_ask) {
      # we have only one missing item, so
      # attempt to just ask for this item,
      # but also allow the user to check all
      # items.
      $ans=$prompt[$missing]; $ans=~s/\s*$//;
      print $OUT "$ans [<STRING>/";
      print $OUT "<E>dit all/" if $#prompt>0;
      print $OUT "<CR>] ? ";
      $ans=<$IN>; chomp($ans); 
      if ($ans=~/^E$/i) {
         @ans=();
         $missing=99;
      } elsif ($ans!~/\S/) {
         @ans=@current;
         $missing=-1;
      } else {
         @ans=@current;
         $ans[$missing]=$ans;
         $missing=-1;
      }
   } else {
      $missing=99;
   }

   if (99==$missing) {
      my($prompt,$current);
      for $i (0..$#prompt){
         ($prompt,$current)=($prompt[$i],$current[$i]);
         $prompt=$prompt."? " if $prompt!~/[\n:\.\?]\s{0,1}\z/;
         print $OUT $prompt;
         $ans=<$IN>; chomp($ans); 
         $ans=$current if $ans!~/[^\s\r]/;
         push @ans, $ans;
      }
   }
   close $IN  if $INclose;
   close $OUT if $OUTclose;
   return @ans;
}



# small helpers -----------------------------------------------------

sub sq{ # escape hack for single-quoting
   my($tmp)=@_;
   $tmp=~s/'/'"'"'/g;
   return($tmp);
}

sub gettimeshort {
   my @time=localtime($_[0]);
   my $time=sprintf "%4d%02d%02d%02d%02d%02d", 1900+$time[5],1+$time[4],$time[3],@time[2,1,0];
   return $time;
}

sub gettime {
   my @time=localtime($_[0]);
   my $time=sprintf "%4d-%02d-%02d %02d:%02d:%02d", 1900+$time[5],1+$time[4],$time[3],@time[2,1,0];
   return $time;
}

sub headerize { # add blanks to additional lines, like rfc822 mail headers
   local($_);
   ($_)=(@_);
   s/\n+\z//g;
   s/(\n)(?=\S)/$1   /g;
   return $_ . "\n";
}

sub absname {
   local($_)=@_;
   $_=$ENV{PWD}."/".$_ if not m!^/!;
   s!(/.|/+)(?=/)!!g;
   return $_;
}



# insanity checks and debug help --------------------------

sub pr { # return printable chars -- protect the tty against control codes below \x80
         # Q: 9b vs. utf8 - leave alone for now
   local($_)=@_;
   # s/[\0-\x1f\x7f]/"%".unpack("H2",$&)/ge;
   s/[\0-\x1f\x7f]/?/go;
   return $_;
}


sub checkrequirements {
   my $tmp;
   $tmp=$changetrackcmd; $tmp=~s/ .*//g; 
   -x $tmp or chomp($tmp=`which $tmp`) and -x $tmp or $rc=1;
   $tmp=$lockcmd; $tmp=~s/ .*//g; 
   -x $tmp or chomp($tmp=`which $tmp`) and -x $tmp or $rc=1;
   warn "\n** PLEASE CHECK FOR POSSIBLY MISSING DEPENDENCIES:\n".
          "** - changetrack (debian package or changetrack.sf.net)\n".
          "** - lock.pl and Compact_pm/Flock.pm (jakobi.github.com)\n".
          "** (if this is just 'which' acting up, please edit\n".
          "** \$lockcmd and \$changetrackcmd to use absolute paths):\n".
          "** \n".
          "** lockcmd:        $lockcmd\n".
          "** changetrackcmd: $changetrackcmd\n".
          "\n" if $rc;
}


sub debug_status {
   return if not $debug;
   my($mode);
   $mode=shift; $mode="" if not defined $mode;
   warn pr(join " ",@_),"\n" if @_;
   if ($mode) {
      my $tmp="";
      for(qw/editorcmd shellcmd lockcmd o_configfile o_user o_domain o_message/) {
         $tmp.=qq!printf main::STDERR "# D %-20s: %s\\n", "$_", \$$_; !;
      }
      eval($tmp);
      warn "# D ARGV:    ". pr(join(" ",@ARGV)) .  "\n";
      warn "# D ARGVOPT: ". pr(join(" ",@ARGVOPT))."\n";
   }
   warn "# D files1:  ". pr(join(" ",@files1)) ."\n" if @files1;
   warn "# D files3:  ". pr(join(" ",@files3)) ."\n" if @files3;
}



# RCS -----------------------------------------------------------------

sub changetrackpath {
   local($_)=@_;
   $_=absname($_);
   s!/!:!g;
   s!^:!!;
   return $_;
}


sub getrcsfile { # return string or shell exit code from diff
   my($file, @ARGV)=@_;
   my($rc)=0;
   # RCS annoyances, thus changetrack annoyances:
   # - rcsdiff refuses to return diffs against missing sources               
   # - rcsdiff refuses to pass -N to gnudiff (which would allows this)
   # - there is no rcscat... so to get a file, I've to do an
   #   explicit checkout or create a mess of dummies for rcsdiff
   # - while co has a -dDATE option, rdiff doesn't
   # - co supports a -p[rev] option to output to pipe; but the
   #   external diff should probably see the correct original time
   #   of the revision

   # untested / not yet bitten: do we need to suppress keyword expansion?

   # this work around requires write access to the repo 
   #   (if this is a problem, we can omit the backdir/RCS
   #   prefix and place the RCS/*,v file in question in a
   #   tmpdir, hopefully in the same fs with ln or cp otherwise;
   #   cd to tmpdir and invoke co locally)
  
   if (not $file) {
      # looks like the non-option arg was
      # NOT a version string, but the file
      $file=$o_backupdiff if $o_backupdiff;
      $file=$o_backupcat  if $o_backupcat;
      $o_backupdiff=0 if defined $o_backupdiff;
      $o_backupcat =0 if defined $o_backupcat;
      return "cannot find file" if not $file;
   }

   my $absfile=absname($file);
   my $ctname      =changetrackpath($file);
   my $ctnametmp   ="$o_hostname.$$.::".$ctname;
   my $ctfile      ="$backupdir/".$ctname;
   my $ctfiletmp   ="$backupdir/$ctnametmp";
   my $ctfilercstmp="$backupdir/RCS/$ctnametmp,v";
   my $ctfilercs   ="$backupdir/RCS/$ctname,v";

   do{return "$ctfile\n"}           if $o_backupecho;
   do{return "$ctfile.history\n"}   if $o_echohistory;
   do{return "$ctfile.yesterday\n"} if $o_echoyesterday;
   do{return "$backupdir\n"}        if $o_echodir;

   my $cmd;
   link $ctfilercs,$ctfilercstmp and do{
      my $opt=$o_backupdiff; $opt=$o_backupcat if $o_backupcat;
      if ($opt) {
         if($opt=~s/^=//) {
           ;
         } elsif($opt=~/[ :]|^\d+[:\-\/]|^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)( |$)/i) { # assume -ddate
           $opt="-d$opt";
         } else {                    # assume -rrev number or symbolic name
           $opt="-r$opt";
         }
         $opt="'".sq($opt)."'";
      } else {
         $opt="";
      }

      $cmd="cd '".sq($backupdir)."'; $cocmd $opt -T '".sq($ctnametmp)."'";
      system $cmd;
   };
   $cmd="";
   $cmd=$getrcsdiffcmd; $cmd="cat" if defined $o_backupcat;
   $cmd="cd '".sq($backupdir)."'; $cmd";
   for(@ARGV) { $cmd.=" '".sq($_)."'"; }
   $cmd.=" '".sq($ctnametmp)."'";
   $cmd.=" '".sq($absfile)."'" if defined $o_backupdiff and not defined $o_backupcat;


   system $shellcmd, "-c", "set -o pipefail; ".$cmd;
   $rc=$?>>8;
   unlink $ctfilercstmp, $ctfiletmp;
   return $rc;
}


__END__


warn "# loading config sfre.sfrerc\n";

# in a configuration file, you can set/unset option variables,
# define additional files to lock, file-specific action, ...

## global variables of interest:
# $journal   the journal
# $backupdir the changetrack repository
# the description variables:
# - $o_user
# - $o_domain
# - $o_message

## customization
# $getrcsdiffcmd='diff -E -b -B -t'; # ignore all space change; gnu

## hooks

# hook variables are available for evaluation by sfre.
# these can be e.g. checking out a not-yet existing file
# in action1 to e.g. an action3 to send an email on change
# or invoke a make/ypmake/newaliases on change.

# variables of interest:
# - $_ is the argument string or filename

# available hooks in temporal order:

# 1) before locking
# against argument array (possibly not filenames, but command args).
# check before locking, can be checked a second time in a different
# process instance after locking. can create files to be included
# in the locking.
$action1{"/etc/passwd"}='{warn "A1 triggered /etc/passwd\n"}';

# 2g) global hook
#$preaction

# 2) before execution
# checked before journal and versioning, existing plain files only
$action2{"/etc/passwd"}='{warn "A2 triggered /etc/passwd\n"}';

# 3) after execution, changed files
# checked after command execution, just existing plain files
# with changed # mtime/ctime timestamps
$action3{'/\.bashrc'}=  '{warn "A3 triggered on a bashrc: $_\n"}';

# 4) after execution
# checked after command execution, all existing plain files only
$action4{"/etc/passwd"}='{warn "A4 triggered /etc/passwd\n"}';
$action4{"/etc/group"}= '{warn "A4 triggered /etc/group\n"}';

# 4g) global hook
# $postaction


# don't forget to signal the success of reading your config with true :)
1;

