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 <h...@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