#!/usr/bin/perl -w

use Getopt::Std;
use File::Basename;

my $scdir = '/etc/sysconfig';

my $prog = basename($0);

my %hash = ();

my %opts;
getopts('b:dvw', \%opts);

my $suffix = (exists($opts{'b'}) ? $opts{'b'} : undef);
my $verbose = exists($opts{'v'});
my $diffs = exists($opts{'d'});
my $warn = exists($opts{'w'});

foreach my $arg (@ARGV) {
  unless ($arg =~ m!^([a-z][-_.a-z0-9]*):([-+#])?([A-Z][A-Za-z0-9_]*)=(.*)$!) {
    die "$prog: bad argument '$arg'\n";
  }

  my $ref = {};

  my $file = $1;
  my $name = $3;

  $ref->{OP} = $2 || '';
  $ref->{VAL} = $4;

  $hash{$file} = {} unless (exists($hash{$file}));

  $hash{$file}->{$name} = $ref;
}

foreach my $file (keys %hash) {
  my $path = $scdir . '/' . $file;

  open(FILE, "<$path") || die "$prog: couldn't open $path\n";

  open(NEWFILE, ">${path}_") || die "$prog: couldn't open ${path}_\n";

  my $modified = 0;
  while (defined(my $line = <FILE>)) {
    chomp $line;

    unless ($line =~ m!(^[A-Z][A-Za-z0-9_]*)=(.*)$!) {
copyout:
      print NEWFILE $line, "\n";
      next;
    }

    my ($name, $val) = ($1, $2);

    unless (exists($hash{$file}->{$name})) {
      goto copyout;
    }

    my $ref = $hash{$file}->{$name};

    # delete if found...
    if ($ref->{OP} eq '-') {
      delete $hash{$file}->{$name};
      # if deleting, and a value is specified, then we must match that value.
      if ($ref->{VAL} ne '' && $val ne $ref->{VAL}) {
        goto copyout;
      }
      ++$modified;
      next;
    }

    # replace or comment out if found...
    if ($ref->{OP} eq '' || $ref->{OP} eq '#') {
      delete $hash{$file}->{$name};
      # if commenting, and a value is specified, then we must match that value.
      if ($ref->{OP} eq '#') {
        if ($ref->{VAL} ne '' && $val ne $ref->{VAL}) {
          goto copyout;
        }
      }
      print NEWFILE (($ref->{OP} eq '#') ? '# ' : ''), $name, '=', $ref->{VAL}, "\n";
      ++$modified;
      next;
    }

    # otherwise, must be '+'... but we've seen duplicates! 
    if ($val ne $ref->{VAL}) {
      warn "$file:$name: already present, with conflicting value!\n";
    }
  }

  # otherwise, add in...
  foreach my $name (keys %{$hash{$file}}) {
    my $ref = $hash{$file}->{$name};

    unless ($ref->{OP} eq '+') {
      warn "$file:$name: unmodified\n" if ($warn);
      next;
    }

    # add it on at the end...
    print NEWFILE $name, '=', $ref->{VAL}, "\n";
    ++$modified;
  }
  close(NEWFILE);
  close(FILE);

  if (! $modified) {
    unlink("${path}_");
    next;
  }

  if ($diffs) {
    system("diff -U2 $path ${path}_");
  }

  my ($mod, $uid, $gid) = (stat($path))[2, 4, 5];

  chmod($mod, "${path}_") || warn "$file: couldn't change modes\n";
  chown($uid, $gid, "${path}_") || warn "$file: couldn't change owner\n";

  # move them here...
  if ($suffix) {
    rename($path, $path . $suffix) || die "$file: couldn't rename\n";
  } else {
    unlink($path);
  }

  rename("${path}_", $path) || die "$file: couldn't rename\n";
}

exit(0);

__END__

=head1 NAME

sysconfig.pl - perform simple edits of C</etc/sysconfig> files

=head1 SYNOPSIS

  sysconfig.pl [ -bI<suffix> ] [ -[dvw] ] I<file>:[-+#]I<variable>[=I<value>] ...

=head1 DESCRIPTION

This helper script for L<Kickstart> can be used in the C<%post> section
of Kickstart configuration files to perform minor changes to the
configuration files.

Examples:

  sysconfig.pl -b.orig network:+NETWORKING_IPv6=no init:GRAPHICAL=no

This example edits two files, C</etc/sysconfig/network> and
C</etc/sysconfig/init> making one change in each.  The variable
C<NETWORKING_IPv6> is replaced with the value C<no> if found,
and added to the end of the file otherwise.  The variable
C<GRAPHICAL> is replaced with C<no> only if present.

=head2 Replace (no prefix)

If no #, -, or + is expressed, then the variable's value is replaced
in the file if found.

=head2 Delete (-)

The variable is removed from the file.  If a value is specified,
then the deletion only occurs when the values match.

=head2 Add (+)

The variable is added to the file, only if it isn't already present.

=head2 Comment (#)

The variable of the file is commented out.

=head1 OPTIONS

The C<-b> flag accepts a string which is used to suffix the
saved copy of the original file.

The C<-d> flag enables generating diffs of any edits.

The C<-v> flag specifies verbose operation.

The C<-w> flag warns of any arguments not taking effect.

=head1 AUTHOR

L<philipp_subx@redfish-solutions.com>

=head1 BUGS

You can't specify subdirectories, such as C<networking/devices/eth0>.

There is no way to put a variable/value pair into a file, regardless of
whether it's present already in a invocation.  You must do:

  sysconfig.pl ... file:-VAR=
  sysconfig.pl ... file:+VAR=value

which is klunky.  It also ends up adding the new value to the
end of the file, rather than rewriting it in place.

Also, the C<=> with no value is superfluous and should be optional.

