[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Re: putting config files under revision control



On 09/04/2019 21.23, Lee wrote:
> On 4/9/19, Dan Ritter wrote:
>> Lee wrote:
>>> What are people doing for putting config files in [under?] git?
>>>
>>> I'd like to have at least some system config files maintained in git
>>> so I can get a history of changes.
>>>   (and yes, I know, I really should be using a backup system for that,
>>> but I'm still at the 'rsync to usb drive' stage)
>>
>> apt install etckeeper. Choose the git backend. (I think it's the
>> default these days.)
> 
> Thank you!
> 
>> Chef or Puppet when you want to do this at scale.
> 
> Maybe someday.  They'd be nice to learn, but they seem to be massive
> overkill for home use.  ..or at least for my home use.

If you want to keep things simple, maybe this perl-script I wrote years ago
might be what you're looking for. Think of it as a visudo-style tool for config
files (really just any file you can edit with vim). Just use the --manual option
to read its man page. Basically it copies the file before editing to a location
of your choosing, keeping its attributes if you want it to.

Grx HdV

#!/usr/bin/perl

#TODO : add support for settings stored in ~/.vicfrc or an explicitly given file

our $VERSION = '0.92';

use strict;
use warnings;
use Getopt::Long qw(:config bundling);
use Pod::Usage;
use Sys::Hostname;
use Cwd qw(realpath getcwd);
use POSIX qw(strftime sysconf _PC_CHOWN_RESTRICTED);
use File::Spec;
use File::Copy;

my $hostname = hostname();

################################################################################
#User-definable defaults
################################################################################

my $root = '';
SWITCH: {
  if ($hostname eq 'mjollnir') {
    $root = '/home/hdv/backup/mjollnir/vicf_root';
    last SWITCH;
  }
  if ($hostname eq 'odin') {
    #$root = '/home/hdv/backup/odin/vicf_root';
    $root = '/home/hdv/backup/odin/vicf_root';
    last SWITCH;
  }
  if ($hostname eq 'sleipnir') {
    $root = '/home/hdv/backup/sleipnir/vicf_root';
    last SWITCH;
  }
}

my $datetime_format = '-%Y%m%d';                   #d
my $sequencenr_format = '-%02d';                   #n
my $append_sequencenr = 1;                         #a
my $keep_permissions = 1;                          #p
my $keep_owner = 1;                                #o
my $keep_group = 1;                                #g
my $keep_times = 1;                                #t
my $backup_file = 0;                               #b
my $x_editor = 0;                                  #x
my $editor_path = '/usr/bin/vim';
my $x_editor_path = '/usr/bin/gvim';
my $backup_option = '-c "set backup"';
my $no_backup_option = '-c "set nobackup"';

################################################################################
#Internal variables
################################################################################

#Defaults for commandline options
my $help = 0;
my $manual = 0;
my $show_version = 0;
my $debug = 0;
my $verbose = 0;

################################################################################
#Parse the commandline arguments
################################################################################

#Get all options
GetOptions(#Standard options
           'debug|D+', \$debug,
           'help!', \$help,
           'h|?', \$help,
           'manual!', \$manual,
           'version!', \$show_version,
           'V', \$show_version,
           'verbose|v+', \$verbose,
           #Options specific for this program
           'root|r=s', \$root,
           'datetime|d=s', \$datetime_format,
           'sequencenr|n=s', \$sequencenr_format,
           'append_sequencenr!', \$append_sequencenr,
           'a!', \$append_sequencenr,
           'permissions!', \$keep_permissions,
           'p!', \$keep_permissions,
           'owner!', \$keep_owner,
           'o!', \$keep_owner,
           'group!', \$keep_group,
           'g!', \$keep_group,
           'times!', \$keep_times,
           't!', \$keep_times,
           'backup!', \$backup_file,
           'b!', \$backup_file,
           'x_editor!', \$x_editor,
           'x!', \$x_editor
          ) or pod2usage(0);
pod2usage(verbose => 1, exitval => 0) if $help;
pod2usage(verbose => 2, exitval => 0) if $manual;
if ($show_version) {
  print "vicf version $VERSION (c) 2015 Jadev\n";
  exit 0;
}

#Assign the first non-option argument to a variable for easier use
die "No file to be edited was given.\n" unless $ARGV[0];
my $source = $ARGV[0];

#Sanitize paths for easier use
$root = expand_path($root);
die "Path pointing to the repository ($root) is invalid.\n" unless $root;
$source = expand_path($source);
die "Path pointing to the file to be edited ($source) is invalid.\n" unless $source;

#Check validity of given options
die "The given root directory does not exist.\n" unless -d $root;
die "The datetime formatstring may contain only alphanumeric or punctuation characters.\n" 
  unless $datetime_format =~ /^[[:print:]]*$/;    #May be empty
die "The sequencenumber formatstring may contain only alphanumeric or punctuation characters.\n" 
  unless $sequencenr_format =~ /^[[:print:]]*$/;    #May be empty

#Verify validity of the given arguments
#I know the file tests could be combined, but this allows for specific messages
#and performance isn't an issue here anyway.
die "The file to be edited does not exist.\n" unless -e $source;
die "The file to be edited is not a file.\n" unless -f $source;
die "The file to be edited is not readable.\n" unless -r $source;
die "The file to be edited is not writable.\n" unless -w $source;
warn "You can edit only one file at a time. Superfluous arguments will be ignored.\n"
  if @ARGV > 1;

#Print option values and arguments
if ($debug) {
  warn '-' x 80, "\n";
  warn "[Options]\n";
  warn "root                   = $root\n";
  warn "timestamp_format       = $datetime_format\n";
  warn "sequencenr_format      = $sequencenr_format\n";
  warn "append_sequencenr      = $append_sequencenr\n";
  warn "keep_permissions       = $keep_permissions\n";
  warn "keep_owner             = $keep_owner\n";
  warn "keep_group             = $keep_group\n";
  warn "keep_times             = $keep_times\n";
  warn "backup_file            = $backup_file\n";
  warn "x_editor               = $x_editor\n";
  warn "\n";
  warn "[Arguments]\n";
  if (@ARGV) {
    warn join(', ', @ARGV), "\n";
  } else {
    warn "No arguments\n";
  }
  warn "\n";
  warn "[Other variables]\n";
	warn "hostname               = $hostname\n";
  warn "source                 = $source\n";
  warn '-' x 80, "\n";
  #exit 0;
}

################################################################################
#Subroutines
################################################################################

#Expand a given path to its absolute equivalent
sub expand_path {
  my $path = shift;
  print "[DEBUG] Expanding path   $path ...\n" if $debug;
  #Get current working directory
  my $cwd = getcwd();
  #Get directory separator (getcwd() returns absolute paths!)
  my $sep = substr($cwd, 0, 1);
  #If this path is not absolute, make it so 
  if ($path !~ /^$sep/) {
    if (($sep eq '/') && ($path =~ /^~/)) { 
      #Path is relative to user's home directory (Unix specific)
      #TODO : find out if there's a better way to determine if we're running on
      #a Unix platform. Using $^O won't do, because you'd have to match all 
      #names of platforms that are (not) Unix-like
      $path =~ s{ ^ ~ ( [^/]* ) } { $1 ? ( getpwnam($1))[7] || return undef
                                       : ( $ENV{HOME} || $ENV{LOGDIR} || 
                                           (getpwuid($>))[7] ) }ex;
    }
    elsif (not File::Spec->file_name_is_absolute($path)) {
      #Path is relative to current working directory. Remember: File::Spec 
      #thinks tilde paths are relative too!
      $path = File::Spec->catdir($cwd, $path);
    }
  }
  #Resolve symlinks, embedded . and .. and superfluous file separators
  #Remember: realpath() does not need to be fed an existing path,
  #so this subroutine can be used to expand new paths too
  $path = realpath($path) or undef;
  print "[DEBUG] Expanded path is $path\n" if $debug;
  return $path;
}

#Get the current timestamp, based on the local time
sub get_timestamp {
  return strftime($datetime_format, localtime(time));
}

#Get the full path to the destination file
sub get_destination {
  my $source = shift; #Full path to the source file
  my ($src_vol, $src_dir, $src_file) = File::Spec->splitpath($source);
  my ($root_vol, $root_dir, $root_file) = File::Spec->splitpath($root, 1);
  my $dest_dir = File::Spec->catdir($root_dir, $src_dir);
  my $dest_file = $src_file . get_timestamp();
  my $destination = File::Spec->catpath($root_vol, $dest_dir, $dest_file);
  my $count = 1;
  my $tmp_dest;
  if ($append_sequencenr) {
    $tmp_dest = $destination . sprintf($sequencenr_format, $count);
  } else {
    $tmp_dest = $destination;
  }
  while (-e $tmp_dest) {
    $count++;
    $tmp_dest = $destination . sprintf($sequencenr_format, $count);
  }
  return $tmp_dest;
}

#Recursively create directories
sub make_dir {
  my $src = shift; #Full path to the source file
  my $dest = shift; #Full path to the destination file
  my ($src_vol, $src_dir, $src_file) = File::Spec->splitpath($src);
  my ($dest_vol, $dest_dir, $dest_file) = File::Spec->splitpath($dest);
  my ($root_vol, $root_dir, $root_file) = File::Spec->splitpath($root, 1);
  #Recursively step through the destination path
  my $current_src_dir = '';
  my $current_dest_dir = $root_dir;
  my $current_src_path;
  my $current_dest_path;
  print '[DEBUG] Result of splitdir ('
    . scalar(File::Spec->splitdir($src_dir)) . ') = ['
    . join(':', File::Spec->splitdir($src_dir)) . "]\n"
    if $debug;
  foreach my $sub_dir (File::Spec->splitdir($src_dir)) {
    $current_src_dir = File::Spec->catdir($current_src_dir, $sub_dir);
    $current_dest_dir = File::Spec->catdir($current_dest_dir, $sub_dir);
    $current_src_path = File::Spec->catpath($src_vol, $current_src_dir, '');
    $current_dest_path = File::Spec->catpath($dest_vol, $current_dest_dir, '');
    next if $sub_dir eq ''; #splitpath returns empty values for the first and
                            #last array entries
    next if -d $current_dest_path;
    if ($debug) {
      print "[DEBUG] Making destination directory $current_dest_path ($sub_dir) ...\n";
    } else {
      mkdir($current_dest_path) or die "Cannot create directory: $!\n";
    }
    copy_attributes($current_src_path, $current_dest_path);
  }
}

#Copy file to its destination
sub copy_file {
  my $src = shift; #Full path to the source file
  my $dest = shift; #Full path to the destination file
  #Store original access time of source file (this is ugly, I know...)
  my $atime_src_file = (stat($src))[8];
  if ($debug) {
    print "[DEBUG] Copying source file $src to destination file $dest ...\n";
    print "[DEBUG] Original atime source file = " . strftime("%Y-%m-%d %H:%M:%S", localtime($atime_src_file)) . "\n";
  } else {
    copy($src, $dest)
      or die "Could not copy file from $src to $dest: $!\n";
  }
  copy_attributes($src, $dest, $atime_src_file);
}

#Copy mode, owner and group attributes
sub copy_attributes {
  my $src = shift; #Full path to the source (can be a file or directory)
  my $dest = shift; #Full path to the destination (can be a file or directory)
  my $original_atime = shift; #Original atime of source, if source is a file
  my ($mode, $owner, $group, $atime, $mtime, $ctime) = (stat($src))[2, 4, 5, 8, 9];
  $atime = $original_atime if $original_atime;
  my $mode_str = sprintf("%04o", $mode & 07777); #Strip the file type bit
  my $owner_name = getpwuid $owner;
  my $group_name = getgrgid $group;
  my $atime_str = strftime("%Y-%m-%d %H:%M:%S", localtime($atime));
  my $mtime_str = strftime("%Y-%m-%d %H:%M:%S", localtime($mtime));
  if ($debug) {
    print "[DEBUG] Copying attributes from $src to $dest ...\n";
    print "[DEBUG] Setting mode of $dest to $mode_str ...\n" if $keep_permissions;
    print "[DEBUG] Setting owner of $dest to $owner_name ...\n" if $keep_owner;
    print "[DEBUG] Setting group of $dest to $group_name ...\n" if $keep_group;
    if ($keep_times) {
      print "[DEBUG] Setting atime of $dest to $atime_str ...\n";
      print "[DEBUG] Setting mtime of $dest to $mtime_str ...\n";
    }
  } else {
    #TODO : Make preserving attributes more portable
    chmod $mode & 07777, $dest 
      or die "Could not set mode of $dest to $mode_str: $!\n" 
      if $keep_permissions;
    if (not sysconf(_PC_CHOWN_RESTRICTED)) {
      chown $owner, -1, $dest
        or die "Could not set owner of $dest to $owner_name: $!\n" 
        if $keep_owner;
    } else {
      chown $owner, -1, $dest
        or warn "The system won't allow you (" . scalar(getpwent()) . ") to change ownership of $dest\n" 
        if $keep_owner;
    }
    chown -1, $group, $dest
      or die "Could not set group of $dest to $group_name: $!\n" 
      if $keep_group;
    #TODO : the access time of the source file is not always preserved (even
    #       though $atime is correct)!
    #TODO : print "[DEBUG] atime_str = " . strftime("%Y-%m-%d %H:%M:%S", localtime($atime)) . "\n";
    utime $atime, $mtime, $dest 
      or warn "Could not set access and modification times for $dest: $!\n"
      if $keep_times;
  }
}

#Open a file with vim
sub edit_file {
  my $file = shift; #Full path to the file to be edited
  my $editor;
  my $options;
  if ($x_editor) {
    $editor = $x_editor_path;
  } else {
    $editor = $editor_path;
  }
  if ($backup_file) {
    $options = $backup_option;
  } else {
    $options = $no_backup_option;
  }
  if ($debug) {
    print '[DEBUG] Opening source file with ' . $editor . ' ' . $options . ' ' . $file . " ... \n";
  } else {
    system $editor, $options, $file;
  }
}

################################################################################
#Main program
################################################################################

#Define the path to which the original will be copied
my $destination = get_destination($source);

#Make sure the target directory exists
make_dir($source, $destination);

#Copy the original
copy_file($source, $destination);

#Edit the original
edit_file($source);

#Exit the program gracefully
exit 0;

__END__

################################################################################
#Documentation 
################################################################################

=head1 NAME

vicf - edit a configuration file and keep a dated copy of the original.

=head1 SYNOPSIS

vicf [options] <argument>

List of options: 

[-h|--help|-?] [--manual] [-V|--version] [-r|--root <directory>] 
[-d|--datetime <format>] [-n|--sequencenr <format>] [-a|--append_sequencenr]
[-p|--permissions] [-o|--owner] [-g|--group] [-t|--times] [-b|--backup]
[-x|--x_editor]

=head1 OPTIONS

=over 4

=item B<--help>

Print a brief help message.

=item B<--manual>

Print the full manual.

=item B<--version>

Print version and copyright information.

=item B<--root>

The directory under which a dated copy of the original file should be stored.

=item B<--datetime>

A format string suitable for strftime(). Together with the local time this 
parameter will used as input for strftime(). The result will be appended to the
name of the target file. See man 3 strftime for more details.

=item B<--sequencenr>

A format string suitable for sprintf(). This will be used to generate a 
sequence number, which will be append to the filename if the target file 
already exists.

=item B<--append_sequencenr>

Append a sequence number to the filename, even if it does not exist. Used to
start sequences at 1 instead of 2.

=item B<--permissions>

Preserve the access permissions of the original file.

=item B<--owner>

Preserve the owner of the original file.

=item B<--group>

Preserve the group of the original file.

=item B<--times>

Preserve the access and modification times of the original file.

=item B<--backup>

Instruct the editor to make a backup of the original file. This is a
convenience option that has nothing to do with the dated copy of the original
file.

=item B<--x_editor>

Start the editor in graphical mode instead of console mode.

=back

At the top of the code there is a section named 'User-definable defaults' where
defaults appropriate for the current environment can be set. Doing so
alleviates the need to specify options on every invocation of the program.

=head1 ARGUMENTS

This program accepts only one argument, which is the path to the file to be
edited. 

=head1 DESCRIPTION

This program makes versioned copies of the files you edit. It is useful for
everyone who wants to have a history of changes made to their files, but
doesn't want to run a CVS, SubVersion or equivalent server just for that
purpose. A copy of the original file is stored in a repository, the root
directory, using the full path of the original file. A timestamp and optionally
a sequence number is appended to the file's name to make sure it is unique.
After that the file is opened with the preferred editor to be edited, just like
usual.

=head1 NOTES/TODO

Find out if there's a better way to determine if we're running on a Unix
platform than what is used now (first char of absolute path is a slash). Just
using $^O won't do either, because you'd have to match all names of platforms
that are or are not Unix-like

Make preserving access modes more portable (for use on non-Unix platforms).

Add support for storing settings in ~/.vicfrc or in an explicitly given
configuration file.

=head1 PREREQUISITES

=over 4

=item - Perl 5.005 or later

=back

=head1 HISTORY

=over 4

=item B<0.01 (2007-05-08)> - First public release.

=item B<0.02 (2007-05-20)> - Improved support for non-absolute paths.

=item B<0.92> (2015-11-21)> - Resolved bug in expand_path routine.

=back

=head1 BUGS

Somehow the access time of the original is not always preserved in the copy.
Need to find out what is causing this. In those cases where this happens the
atime given to utime() seems to be correct.

Let me know if you find another one (or more...).

=head1 AUTHOR

J.A. de Vries <hdv@jadev.org>

Current contact information and the master copy of this program can be found
at http://www.jadev.org

=head1 COPYRIGHT

Copyright (C) 2007 Jadev 

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.

You can find a copy of the GNU General Public License at
http://www.gnu.org/licenses/gpl.html


Reply to: