#!/usr/bin/perl -w
###############################################################################
#    Copyright (C) 2002-2204 by Eric Gerbier
#    Bug reports to: gerbier@users.sourceforge.net
#    $Id: afickonfig.pl,v 1.11 2004/12/08 15:08:59 gerbier Exp $
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
###############################################################################
# afickonfig is designed to modify afick's config file in a batch way
# it just add, replace, remove any components (macro, alias, directives, rules)
# of this file
###############################################################################

use strict;

# debuggging
use diagnostics;

# use Data::Dumper;
#use Carp qw(cluck);	# debugging

use Getopt::Long;    # option analysis

use File::Basename;  # for path
my $dirname = dirname($0);
require $dirname . '/afick-common.pl';

###############################################################################
#                     global variables
###############################################################################

my $Version = '0.3-0';
my $Verbose;

#######################################################
# debug(message, level)
sub debug($;$) {
	my $msg = shift(@_);
	print "DEBUG: $msg" if ($Verbose);
}
#######################################################
sub warning($) {
	my $msg = shift(@_);
	warn "WARNING: $msg";
}
#######################################################
# usage
# print some help
sub usage($) {
	my $version = shift(@_);
	print <<EOHELP;

Usage: $0 [afickonfig option] [afick options] [alias] [macros] [rules] [directives]

afickonfig options (control afickonfig)  : 
 -c|--config_file file        configname of config file to use
 -C|--check_config            only check config file and exit
 -G|--clean_config            check and clean configuration, then exit
 -h|--help                    show this help page
 --print_config               display internals variables after arguments and config file parsing
 				(for debugging purposes)
 -V|--version                 show afickonfig version
 -v|verbose		     for debugging

afick options (change afick's options) :

 -a|--ignore_case             helpful on windows plateforms, dangerous on unix ones
 				reverse : --noignore_case
 -d|--debug level	      set a level of debugging messages, from 0 (none) to 3 (full)
 				default : 0
 -D| --database file          force the database name 
 -f|--full_newdel             report full information for new or deleted directories
                               default: no
			       reverse : --nofull_newdel 
 -m|--missing_files           warn about files declared in config files 
                               which do not exists, 
			       default: no; 
			       reverse : --nomissing_files
 -r|--running_files           warn about "running" files : modified since program begin
                               default: no 
                               reverse: --norunning_files
 -s|--dead_symlinks           warn about dead symlinks 
                               default: no 
                               reverse: --nodead_symlinks
 -t|--timing		      Print timing statistics
				default: no
				reverse : --notiming
 -x|--exclude_suffix ext1 ext2        list of file/dir suffixes to ignore
 -X|--exclude_prefix pre1 pre2        list of file/dir prefix to ignore
 -R|--exclude_re pat1 pat2        list of file/dir patterns (regular expressions) to ignore
 -y|--history file	      history file of all runs with summary
 -A|--archive directory	      directory where archive files are stored
 -S|--max_checksum_size	size  maximum cheksum size (bytes) : for bigger file, just compute checksum on begin of file
 				default : 0 (no limit)

the following options are to be set in configuration file format :
[macros] : '\@\@define macro value'
[alias]  : 'newrule = attributes'
[rules]  : 'file alias'
[directives] : 'directive := value'

Disclaimer:
This script is intended to provide a means for
detecting changes made to files, via a regular
comparison of MD5 hashes to an established baseline. 

Copyright (c) 2002 Eric Gerbier <gerbier\@users.sourceforge.net>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
EOHELP

}
#############################################################
# wrapper for afick commands
sub wrapper ($$) {
	my $configfile = shift(@_);
	my $option     = shift(@_);
	system("afick -c $configfile $option");
}
#############################################################
# build config line according type
sub buildligne($$$$) {
	my $title   = shift(@_);    # type of ligne
	my $changes = shift(@_);    # ref to hash, containing changes
	my $onlydir = shift(@_);    # ref to hash, for selection
	my $key     = shift(@_);    # parameter value

	my %h_format = (
		macro     => '@@define %s %s',
		alias     => '%s = %s',
		directive => '%s := %s'
	);

	if ( $title eq 'rule' ) {

		# return of the quotes
		my $newkey = ( $key =~ m/\s/ ) ? "\"$key\"" : $key;
		my $newligne;
		if ( !defined $changes->{$key} ) {

			#negative rule
			$newligne = "! $newkey";
		}
		elsif ( exists $onlydir->{$key} ) {
			$newligne = "= $newkey $changes->{$key}";
		}
		else {
			$newligne = "$newkey $changes->{$key}";
		}
		return $newligne;
	}
	else {
		return sprintf( $h_format{$title}, $key, $changes->{$key} );
	}
}
#############################################################
# generic sub to apply changes on a parameter type
sub change($$$$$) {
	my $title        = shift(@_);    # parameter name
	my $changes      = shift(@_);    # ref to hash, containing changes
	my $onlydir      = shift(@_);    # ref to hash, for selection
	my $test_pattern = shift(@_);    # ref to sub to detect adequate pattern
	my $config       = shift(@_);    # array of config file lines

	my $nb_changes = 0;
	foreach my $key ( keys %$changes ) {

		debug("$title : $key");
		my $found          = 0;
		my $nb_changes_key = 0;
		my $i              = 0;
		my $newligne       = buildligne( $title, $changes, $onlydir, $key );
		foreach my $ligne (@$config) {

			my @ret;
			if ( @ret = &$test_pattern($ligne) ) {

				# found pattern
				#print Dumper(@ret);
				my $key_lu = shift(@ret);
				if ( $key_lu eq $key ) {

					# found same key
					if ( $ligne eq $newligne ) {
						print "no changes for $title $key\n";
					}
					elsif ( ( defined $changes->{$key} )
						and ( $changes->{$key} eq '' ) )
					{
						print "delete $title $key\n";
						$ligne = '# ' . $ligne;
						@{$config}[$i] = $ligne;
						$nb_changes_key++;
					}
					else {
						print "replace $title $ligne by $newligne\n";
						@{$config}[$i] = $newligne;
						$nb_changes_key++;
					}
					$found = 1;
				}    # found key
			}    # test pattern
			$i++;
		}    # foreach config
		if ( !$found ) {
			if ( ( defined $changes->{$key} ) and ( $changes->{$key} eq '' ) ) {
				warning("can not delete $title $key : not found");
			}
			else {
				print "add $title line $newligne\n";
				push( @$config, $newligne );
				$nb_changes_key++;
			}
		}
		if ( $nb_changes_key > 1 ) {
			warning("too many changes for $title $key : $nb_changes_key");
		}
		$nb_changes += $nb_changes_key;
	}    #foreach change

	return $nb_changes;
}
#############################################################
# apply changes
sub change_config($$$$$$) {
	my $configfile = shift(@_);    # config file name
	my $directives = shift(@_);    # ref to hash containing directives changes
	my $macros     = shift(@_);    # ref to hash containing macro changes
	my $alias      = shift(@_);    # ref to hash containing alias changes
	my $rule       = shift(@_);    # ref to hash containing rules changes
	my $onlydir    = shift(@_);    # ref to hash for equal selections

	my @config;
	read_config( $configfile, \@config ) or die get_error();

	# begin directives
	my $nb_changes_dir =
	  change( 'directive', $directives, $onlydir, \&is_directive, \@config );

	# begin macros
	my $nb_changes_macros =
	  change( 'macro', $macros, $onlydir, \&is_macro, \@config );

	# begin alias
	my $nb_changes_alias =
	  change( 'alias', $alias, $onlydir, \&is_alias, \@config );

	# begin rules
	my $nb_changes_rules =
	  change( 'rule', $rule, $onlydir, \&is_anysel, \@config );

	my $nb_changes = $nb_changes_dir + $nb_changes_macros + $nb_changes_alias +
	  $nb_changes_rules;

	if ($nb_changes) {

		# save modified config file
		print "rewrite changed $configfile ($nb_changes)\n";
		print
"directives ($nb_changes_dir) macros($nb_changes_macros) alias ($nb_changes_alias) rules ($nb_changes_rules)\n";
		write_config( $configfile, \@config );
	}
	return $nb_changes;
}
#############################################################
sub version($) {
	my $version = shift(@_);
	print "\n";
	print
"afickonfig : another file integrity checker configurator\nversion $version\n";
}
#############################################################
#                          main
#############################################################

my $default_config_file = get_default_config();

$| = 1;

# variables for parameter analysis
my $configfile;    # config file name
my $help;
my $version;
my $print_config;
my $check_config;
my $clean_config;

# parameters for afick
my (
	$Archive,   $Debug_level,       $Ignore_case, $Report_full_newdel,
	$History,   $Warn_missing_file, $Running,     $Warn_dead_symlinks,
	$sufx_list, $prefx_list,        $re_list,     $Timing,
	$Database,  $Max_checksum_size
);

Getopt::Long::Configure('no_ignore_case');
unless (
	GetOptions(

		# afickonfig options
		'config_file|c=s' => \$configfile,
		'check_config|C'  => \$check_config,
		'clean_config|G'  => \$clean_config,
		'help|h'          => \$help,
		'print_config'    => \$print_config,
		'version|V'       => \$version,
		'verbose|v'       => \$Verbose,

		# afick options
		'archive=s'             => \$Archive,
		'database|D=s'          => \$Database,
		'ignore_case|a!'        => \$Ignore_case,
		'full_newdel|f!'        => \$Report_full_newdel,
		'history|y=s'           => \$History,
		'missing_files|m!'      => \$Warn_missing_file,
		'running_files|r!'      => \$Running,
		'dead_symlinks|s!'      => \$Warn_dead_symlinks,
		'exclude_suffix|x=s'    => \$sufx_list,
		'exclude_prefix|X=s'    => \$prefx_list,
		'exclude_re|R=s'        => \$re_list,
		'timing|t!'             => \$Timing,
		'debug|d=i'             => \$Debug_level,
		'max_checksum_size|S=i' => \$Max_checksum_size

	)
  )
{
	usage($Version);
	die "abort : incorrect option\n";
}

if ($help) {

	# -h : help
	usage($Version);
	exit;
}
elsif ($version) {

	# -V : version
	version($Version);
	exit;
}

if ($configfile) {
}
elsif ( -e $default_config_file ) {
	$configfile = $default_config_file;
}
else {
	usage($Version);
	die
"abort : missing configfile name (-c flag) and default config file $default_config_file\n";
}

# some more controls
if ( !-e $configfile ) {
	die "abort : missing configfile name $configfile\n";
}
elsif ( !-w $configfile ) {
	die "abort : configfile name $configfile is not writable\n";
}

if ($print_config) {
	wrapper( $configfile, '--print_config' );
	exit;
}
elsif ($check_config) {
	wrapper( $configfile, '--check_config' );
	exit;
}
elsif ($clean_config) {
	wrapper( $configfile, '--clean_config' );
	exit;
}

my %directives;

# convert afick like options to %directives
$directives{'archive'}     = $Archive     if ( defined $Archive );
$directives{'database'}    = $Database    if ( defined $Database );
$directives{'debug'}       = $Debug_level if ( defined $Debug_level );
$directives{'history'}     = $History     if ( defined $History );
$directives{'ignore_case'} = $Ignore_case if ( defined $Ignore_case );
$directives{'report_full_newdel'} = $Report_full_newdel
  if ( defined $Report_full_newdel );
$directives{'running_files'} = $Running if ( defined $Running );
$directives{'warn_dead_symlinks'} = $Warn_dead_symlinks
  if ( defined $Warn_dead_symlinks );
$directives{'warn_missing_file'} = $Warn_missing_file
  if ( defined $Warn_missing_file );
$directives{'exclude_suffix'} = $sufx_list  if ( defined $sufx_list );
$directives{'exclude_prefix'} = $prefx_list if ( defined $prefx_list );
$directives{'exclude_re'}     = $re_list    if ( defined $re_list );
$directives{'timing'}         = $Timing     if ( defined $Timing );
$directives{'max_checksum_size'} = $Max_checksum_size
  if ( defined $Max_checksum_size );

# get old config from, to be able to check new aliases/rules
my %macros;
my %alias = get_default_alias();
my %directive;
my %rules;
my %onlydir;

get_configuration( $configfile, \%macros, \%alias, \%directive, \%rules,
	\%onlydir );

# look at others parameters
my %newmacros;
my %newalias;
my %newdirectives;
my %newrules;
my %newonlydir;

foreach my $elem (@ARGV) {
	remove_trailing_spaces( \$elem );

	my @ret = ();
	if ( @ret = is_macro($elem) ) {

		# macros
		my $key = shift(@ret);
		my $val = shift(@ret);

		if ( !defined check_macro( $key, $val, 1 ) ) {
			warning( "skip macro $elem : " . get_error() );
		}
		else {
			$newmacros{$key} = $val;
		}
	}
	elsif ( @ret = is_directive($elem) ) {

		# directives
		# another way to set directives
		my $key = shift(@ret);
		my $val = shift(@ret);

		if ( !defined check_directive( $key, $val, 1 ) ) {
			warning( "skip directive $elem : " . get_error() );
		}
		else {
			debug("find directive $key : $val\n");
			$newdirectives{$key} = $val;
		}
	}
	elsif ( @ret = is_alias($elem) ) {

		# alias
		my $key = shift(@ret);
		my $val = shift(@ret);

		# we do not try to resolv aliases, because it can depends
		# on config file definitions
		if ( !defined check_alias( $val, \%alias, 1 ) ) {
			warning( "skip alias $elem : " . get_error() );
		}
		else {
			debug("find alias $key : $val\n");
			$newalias{$key} = $val;

			# add in alias list to allow a rule to use it
			$alias{$key} = $val;
		}
	}
	elsif ( @ret = is_negsel($elem) ) {

		# negative option
		my $key = shift(@ret);
		if ( !is_anyfile($key) ) {
			warning( "skip rule $elem : " . get_error() );
		}
		else {
			$newrules{$key} = undef;
		}
	}
	elsif ( @ret = is_equalsel($elem) ) {

		# only dir option
		my $name      = shift(@ret);
		my $attribute = shift(@ret) || '';

		# do not check resolv globbing
		# just check attribute syntaxe
		if ( !defined check_alias( $attribute, \%alias, 1 ) ) {
			warning( "skip rule $elem : " . get_error() );
		}
		else {
			debug("find rule $name : $attribute\n");
			$newrules{$name}   = $attribute;
			$newonlydir{$name} = 1;
		}
	}
	elsif ( @ret = is_sel($elem) ) {

		# classic selection
		my $name      = shift(@ret);
		my $attribute = shift(@ret) || '';

		# do not check resolv globbing
		# just check attribute syntaxe
		if ( !defined check_alias( $attribute, \%alias, 1 ) ) {
			warning( "skip rule $elem : " . get_error() );
		}
		else {
			debug("find rule $name : $attribute\n");
			$newrules{$name} = $attribute;
		}
	}
	else {
		warning("unknown element $elem (ignored)");
	}
}

my $return_value = change_config(
	$configfile, \%newdirectives, \%newmacros,
	\%newalias,  \%newrules,      \%newonlydir
);

wrapper( $configfile, '--check_config --debug 0' );

exit $return_value;

__END__


=head1 NAME

afickonfig - a tool to manage Afick's config files

=head1 DESCRIPTION

C<afickonfig> is to change parameters in afick's config file, in a batch way. 
It can add, replace, remove any components (macro, alias, directives, rules)
It was designed to work with same options names as afick (directives).

Note : in the current version, it can checks some arguments syntaxe before applying,

The idea came from the "postconf" utility from postfix.

=head1 SYNOPSIS

afickonfig.pl  [L<options|options>] [L<action|actions>] [L<macros|macros>] [L<alias|alias>] [L<directives|directives>] [L<rules|rules>]

afick use posix syntaxe, which allow many possibilities : 

=over 4

=item *

long (--) options

=item *

short (-) options

=item *

negative (--no) options

=back

=head1 OPTIONS

options are used to control afickconfig

=over 4

=item *
--config_file|-c configfile

read the configuration in config file named "configfile".

=item *
--check_config|-C

only check config file syntaxe and exit with the number of errors

=item *
--clean_config|-G

check config file syntaxe, clean bad line, and exit with the number of errors

=item *
--help|-h

Output help information and exit.

=item *
--print_config

display internals variables after arguments and config file parsing (for debugging purposes)

=item *
--version|-V

Output version information and exit.

=item *
--verbose|-v

add debugging messages

=back

=head1 ACTIONS

actions are used to change afick's configuration

=over 4

=item *
--archive|-A directory

write reports to "directory".

=item *
--database|-D name

use the database named "name".

=item *
--debug|-d level

set a level of debugging messages, from 0 (none) to 3 (full)

=item *
--full_newdel|-f,(--nofull_newdel)

(do not) report full information on new and deleted directories. Default : no

=item *
--history|-y historyfile

write session status to history file

=item *
--ignore_case|-a

ignore case for file names. Can be helpfull on windows plateforms, but is dangerous on unix ones.

=item *
--missing_files|-m,(--nomissing_files)

(do not) warn about files declared in config files which does not exists. Default : no

=item *
--max_checksum_size|-S size

fix a maximum size (bytes) for checksum. on bigger files, compute checksum only on first 'size' bytes.
(default is 0 : no limit)

=item *
--dead_symlinks|-s,(--nodead_symlinks)

(do not) warn about dead symlinks. Default : no

=item *
--running_files|-r,(--norunning_files)

(do not) warn about "running" files : modified since program begin. Default : no

=item *
--timing|-t,(--notiming)

(do not) Print timing statistics. Default : no

=item *
--exclude_suffix|-x "ext1 ext2 ... extn"

list of suffixes (files/dir ending in .ext1 or .ext2 ...) to ignore

=item *
--exclude_prefix|-X "pre1 pre2 ... pren"

list of prefix (files/dir beginning with pre1 or pre2 ...) to ignore

=item *
--exclude_re|-R "pat1 pat2 ... patn"

list of patterns (regular expressions) to ignore files or directories

=back

=head1 MACROS

macros are to be set in afick configruration format (see afick.conf(5)) : C< '@@define macro value'>

=head1 ALIAS

aliases are to be set in afick configruration format (see afick.conf(5)) : C<'newrule = attributes'>

=head1 DIRECTIVES

directives are to be set in afick configruration format (see afick.conf(5)) : C<'directive := value'>

=head1 RULES

rules are to be set in afick configruration format (see afick.conf(5)) : C<'file alias'>

=head1 FILES

if no config file on command line, afick try to open F</etc/afick.conf> (unix) or F<windows.conf> (windows) as
default config

for config file syntax see afick.conf(5)

=head1 USE

afickonfig may 

=over 4

=item *
change a config

if it can find an old config

=item *
add a config

if it does not find a previous value

=item *
remove a config

you just have to specify a parameter without any value. the old line is commented

=back

=head1 EXAMPLES

To use this program, you must just run it with same afick command line options :

C<afickonfig.pl -c linux.conf --timing --norunnig_files --debug=1 --archive=afick_archive>

or the same in configuration file syntaxe :

C<afickonfig.pl -c linux.conf 'timing := 1' ' running_files := no' 'debug:=1 'archive:=afick_archive'>

and a mix of all types

C<afickonfig.pl -c linux.conf --timing 'debug:=1' '@@define BATCH 0' 'newrule = p+u+g' '/tmp newrule'>

remove lines

C<afickonfig -c linux.conf 'debug:=' '@@define BATCH' 'newrule=' '/tmp'>

=head1 RETURN VALUES

The exit status is the number of real changes

=head1 NOTES

this program only use perl and its standard modules.

=head1 SEE ALSO

=for html
<a href="afick.conf.5.html">afick.conf(5)</a> for configuration file
<br>
<a href="afick-tk.1.html">afick-tk(1)</a> for graphical interface
<br>
<a href="afick.1.html">afick(1)</a> for the command-line interface

=for man
\fIafick.conf\fR\|(5) for configuration file
.PP
\fIafick\-tk\fR\|(1) for graphical interface
.PP
\fIafick\fR\|(1) for the command-line interface

=head1 COPYRIGHT

Copyright (c) 2002,2003,2004 Eric Gerbier
All rights reserved.

This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.

=head1 AUTHORS

Eric Gerbier

you can report any bug or suggest to gerbier@users.sourceforge.net
