Skip to content

Instantly share code, notes, and snippets.

@manchicken
Created August 20, 2021 04:51
Show Gist options
  • Save manchicken/4262ab6fe063bc10f9b0097aaf14d1fb to your computer and use it in GitHub Desktop.
Save manchicken/4262ab6fe063bc10f9b0097aaf14d1fb to your computer and use it in GitHub Desktop.
This is a piece of code that my dad sent me to review, roughly a year before he died.
package CfgFile;
use strict;
use JSON;
################################################################################
=head1 NAME
CfgFile - Extended JSON formatted flat configuration file utility
=head1 SYNOPSIS
use JSON;
use CfgFile;
# Creating a CfgFile object to use
my $cfg = CfgFile->new();
# Registering an specializes error handling routine (optional)
$cfg->errHandler(\&myErrHandler);
# Open (and create if new) the configuration file
my $cfgFile = "mySettings.cfg";
$cfg->open($cfgFile);
# Open (and create if new) the configuration file and indicating
# desired chmod value to be used each time the "store" is called
$cfg->open($cfgFile, $chmod);
# Storing configuration information
my %cfg = ( COLOR => 'red', FLOWER => 'daisy' );
$cfg->store(%cfg);
# Loading configuration information
%cfg = $cfg->load();
# Close configuration file
$cfg->close();
# Storing JSON string instead of a hash. This is internally
# called by the "store" method.
my $json = encode_json(\%cfg);
$cfg->store_json($json);
=head1 DESCRIPTION
This package provides a simple text file to store configuration and other small
to medimum data sets. The format is a JSON object that has been reformatted by
indenting the JSON components allowing for easier hand editting of the file
if required. The "load" method slurps the entire file into a single hash and
the "store" method overwrites the file with the contents of that single hash.
The file can only contain a single hash.
=back
=cut
################################################################################
=pod
=head1 new( )
Creates a new CfgFile object
=cut
sub new
{
my ($class) = @_;
my $self = {};
$self->{_FILENAME} = "";
$self->{_ERRHANDLER} = \&defErrHandler;
$self->{_CHMOD} = -1;
bless($self, $class);
return $self;
}
################################################################################
=pod
=head1 open($filename)
=head1 open($filename, $chmod)
Opens and creates (if required) the configuration file
Arguments:
=over
=item * B<$filename> - Name of the configuration file
=item * B<$chmod> - String to use with the 'chmod' command each time "store"
is called. This must be numeric value, generally octal
in a format like 0755 for exacutables (see "perldoc chmod"
for more details)
=back
=cut
sub open
{
my ($self, $filename, $chmod) = @_;
# Remember the chmod setting if supplied
if ($chmod != undef)
{
$self->{_CHMOD} = $chmod;
}
# If the file exists, just make sure we can open it by opening it
if (-f $filename)
{
CORE::open (FIL,"<",$filename)
|| return $self->{_ERRHANDLER}("?CfgFile::open failed to open file '$filename' - $!");
close FIL;
$self->{_FILENAME} = $filename;
}
# If the file does not exists, create it by storing an empty hash
else
{
$self->{_FILENAME} = $filename;
my %nothing = ();
store($self, %nothing);
}
}
################################################################################
=pod
=head1 store(%hash)
Store the contents of the hash variable in the configuration file
Arguments:
=over
=item * B<%hash> - Perl hash to be stored
=back
=cut
sub store
{
my ($self, %data) = @_;
# JSON encode the hash and call the json store routine
store_json($self, encode_json(\%data));
}
################################################################################
=pod
=head1 store_json($json)
Store the a JSON encoded string into the configuration file.
Arguments:
=over
=item * B<$json> - JSON encoded string to store in the file
=back
=cut
sub store_json
{
my ($self, $json) = @_;
# No filename means the open() method has not been called yet
if ($self->{_FILENAME} eq "")
{
return $self->{_ERRHANDLER}("?CfgFile::store called before CfgFile::open");
}
my $filename = $self->{_FILENAME};
# Open the file
CORE::open (FIL,">",$filename)
|| return $self->{_ERRHANDLER}("?CfgFile::store failed to open file '$filename' - $!");
# Split the JSON encoded string into peices at natural indention points
my @parts = ();
for my $part (split(/([\{\}\[\]\"\,\:\\])/,$json))
{
if ($part ne "") { push(@parts,$part); }
}
# Build a blank padding string for the indenting
# (TODO: is 100 enough?)
my @pad = ("");
for (1..100) { push(@pad,$pad[$#pad]." "); }
my $depth = 0;
my $nl = "";
my $quote = 0;
my $last = "";
# Write the parts split apart in an indented way
while ($#parts >= 0)
{
my $part = shift @parts;
if ($part eq "\\")
{
print FIL $part;
$part = shift @parts;
print FIL $part;
}
elsif ($part eq '"')
{
if ($quote)
{
print FIL $part;
$quote = 0;
if ($parts[0] eq ",")
{
$nl = "";
}
else
{
$nl = "\n";
}
}
else
{
print FIL $nl.$pad[$depth].$part;
$quote = 1;
}
}
elsif ($quote)
{
print FIL $part;
}
elsif ($part eq "{")
{
print FIL $nl.$pad[$depth++]."$part\n";
$nl = "";
}
elsif ($part eq "}")
{
print FIL $nl.$pad[--$depth]."$part";
if ($parts[0] ne ",") { print FIL "\n"; }
$nl = "";
}
elsif ($part eq "[")
{
print FIL $nl.$pad[$depth++]."$part\n";
$nl = "";
}
elsif ($part eq "]")
{
print FIL $nl.$pad[--$depth]."$part\n";
if ($parts[0] ne ",") { print FIL "\n"; }
$nl = "";
}
elsif ($part eq ":")
{
print FIL $part;
if (($parts[0] eq "{") || ($parts[0] eq "["))
{
$nl = "\n";
}
else
{
$nl = "";
}
}
elsif ($part eq ",")
{
print FIL $part;
$nl = "\n";
}
else
{
print FIL $part;
}
$last = $part;
}
print FIL "\n";
close FIL;
# If a chmod value was supplied, use it
if ($self->{_CHMOD} != -1)
{
my $chmod = $self->{_CHMOD};
chmod $chmod, $filename
|| return $self->{_ERRHANDLER}("?CfgFile::store cannot 'chmod' file '$filename' - $!");
}
}
################################################################################
=pod
=head1 load( )
Loads the reformatted JSON string from the configuration file and returns a
perl hash
Returns:
=over
=item * B<%hash> - Perl hash variable of the contents from the file
=back
=cut
sub load
{
my ($self) = @_;
# No filename means the open() method has not been called yet
if ($self->{_FILENAME} eq "")
{
return $self->{_ERRHANDLER}("?CfgFile::load called before CfgFile::open");
}
my $filename = $self->{_FILENAME};
# Open the configuration file
CORE::open (FIL,"<",$filename)
|| return $self->{_ERRHANDLER}("?CfgFile::load failed to open file '$filename' - $!");
# Read the entire file, removing all indents and blank lines
my @lines = ();
while (my $line = <FIL>)
{
chomp $line;
$line =~ s/^\s*//;
$line =~ s/\s*$//;
if ($line ne "")
{
push(@lines, $line);
}
}
close FIL;
# If no lines were read from the return nothing
if (scalar @lines < 1)
{
return;
}
# Join the lines into a single JSON string, decode it and return the hash
return %{decode_json(join("",@lines))};
}
################################################################################
=pod
=head1 close( )
Close the configuration file and clears the CfgFile object.
=cut
sub close
{
my ($self) = @_;
$self->{_FILENAME} = "";
$self->{_CHMOD} = -1;
}
################################################################################
=pod
=head1 errHandler(\&routine)
This method resgisters an error handling routine to be called whenever CfgFile
encounters an error.
=over
=item * B<\&routine> - Error handler routine name
=back
The default error handler is 'die'. The error handler routine will be passed
a single message string. Any returned value is ignored since it is expected
the error handler supplied is just a more graceful 'die'.
=cut
sub errHandler
{
my ($self, $routine) = @_;
$self->{_ERRHANDLER} = $routine;
}
################################################################################
sub defErrHandler
################################################################################
{
my ($self, $msg) = @_;
die "$msg\n";
}
1
__END__
=head1 DIAGNOSTICS
-- to do --
=head1 DEPENDENCIES
use JSON;
=head1 AUTHOR
Michael Stemle, Sr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment