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

Re: How to Keep Track of Changes to the System



On 2017-08-31 04:39, ray wrote:
> On Sunday, August 27, 2017 at 6:50:06 AM UTC-5, hdv@gmail wrote:
>> On 2017-08-26 05:14, ray wrote:
>>> I would like to find a way to keep track of changes I make to my system.  ...snip
>> Hi Ray,
>>
>> I just returned from a short holiday, so I am a bit late to the party, but... if
>> you don't want to set up a full versioning system I might have something else
>> for you. About 10 years ago I had the same need as you. What I did was write a
>> perl-script that automatically makes a timestamped backup of each file you edit
>> to a directory you define yourself (in that directory the full path of the
>> original is preserved). You use it like visudo, you just call it like this:
>>
>> vicf <name of file to edit>
>>
>> All the rest happens automagically.
>>
>> Of course this will only help for plain-text files and it doesn't provide for
>> the annotations you mentioned. But if you are interested I
>> can mail it to you.
>>
>> Grx HdV
> ...snip
> 
> Yes, I would like to work with this.  I should be able to modify the perl script to also save a tag file to hold the metadata.  So now I have a reason to learn some perl.
> 
> Thank you.
> Ray

Here it is. Make sure to set the variables in the section "User-definable
defaults". I use the SWITCH statement to configure the script for use on
multiple systems. You might not need that. In that case just make sure you set
$root.

Good luck with it!

If you make any changes, please share them with the list. Maybe others might
find them useful too.

Grx HdV

===[begin script]====


#!/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 '') {
    $root = '';
    last SWITCH;
  }
  if ($hostname eq '') {
    $root = '';
    last SWITCH;
  }
  if ($hostname eq '') {
    $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: