package Config::Options; our $VERSION = 0.08; # Copyright (c) 2007 Edward Allen III. All rights reserved. # ## This program is free software; you can redistribute it and/or ## modify it under the terms of the Artistic License, distributed ## with Perl. # =pod =head1 NAME Config::Options - module to provide a configuration hash with option to read from file. =head1 SYNOPSIS use Config::Options; my $options = Config::Options->new({ verbose => 1, optionb => 2, mood => "sardonic" }); # Access option as a hash... print "My mode is ", $options->{mood}, "\n"; # Merge a hash of options... $options->options({ optionc => 5, style => "poor"}); # Merge options from file $options->options("optionfile", $ENV{HOME} . "/.myoptions.conf"); $options->fromfile_perl(); =head1 AUTHOR Edward Allen, ealleniii _at_ cpan _dot_ org =head1 DESCRIPTION The motivation for this module was to provide an option hash with a little bit of brains. It's pretty simple and used mainly by other modules I have written. =cut use strict; use Data::Dumper; use Carp; use Scalar::Util; use Config; =pod =head1 METHODS =over 4 =item new() Create new options hash. Pass it a hash ref to start with. Please note that this reference is copied, not blessed. my $options = Config::Options->new({hash_of_startup_options}); =cut sub new { my $class = shift; if ($Config{useithreads}) { require Config::Options::Threaded; return Config::Options::Threaded->new(@_); } else { return $class->_new(@_); } } sub _new { my $class = shift; my $self = {}; bless $self, $class; $self->options(@_); } =item clone() Creates a clone of options object. my $newoptions = $options->clone(); =cut sub clone { my $self = shift; my $clone = {%$self}; bless $clone, ref $self; return $clone; } =item options() This is a utility function for accessing options. If passed a hashref, merges it. If passed a scalar, returns the value. If passed two scalars, sets the option. my $optionsb = $options->options; # Duplicates option file. Not very usefull. $options->options($hashref); # Same as $options->merge($hashref); my $value = $options->options("key") # Return option value. $options->options("key", "value") # Set an option. =cut sub options { my $self = shift; my $option = shift; if ( ref $option ) { return $self->merge($option); } elsif ($option) { my $value = shift; if ( defined $value ) { $self->_setoption($option, $value); $self->{$option} = $value; } return $self->{$option}; } return $self; } =item merge() Takes a hashref as argument and merges with current options. $options->merge($hashref); =cut sub merge { my $self = shift; my $option = shift; return unless ( ref $option ); while ( my ( $k, $v ) = each %{$option} ) { $self->_setoption($k, $v); } return $self; } # Safely set an option sub _setoption { my $self = shift; my ($key, $value) = @_; my $new = $value; if (ref $value) { $new = $self->_copyref($value); } $self->{$key} = $new; return $value; } sub _newhash { return {}; } sub _newarray { return []; } # Created a shared copy of a (potentially unshared) reference sub _copyref { my $self = shift; my $in = shift; my $haveseen = shift || []; my $depth = shift || 0; if (++$depth > 20) { carp "More than 20 deep on nested reference. Is this a loop?"; return $in; } my $seen = [ @{$haveseen} ]; foreach (@{$seen}) { if(Scalar::Util::refaddr($in) == $_) { carp "Attempt to create circular reference!"; return $in } } push @{$seen}, Scalar::Util::refaddr($in); if (Scalar::Util::reftype($in) eq "HASH") { my $out = $self->_newhash(); while (my ($k, $v) = each %{$in}) { if (ref $v) { $out->{$k} = $self->_copyref($v, $seen, $depth); } else { $out->{$k} = $v; } } return $out; } elsif (Scalar::Util::reftype($in) eq "ARRAY") { my $out = $self->_newarray(); foreach my $v (@{$in}) { if (ref $v) { push @{$out}, $self->_copyref($v, $seen, $depth); } else { push @{$out}, $v; } } return $out; } elsif (ref $in) { croak "Attempt to copy unsupported reference type: " . (ref $in); } else { return $in; } } # If $from and $to are both refs of same type, merge. Otherwise $to replaces $from. # sub _mergerefs { my $self = shift; my $from = shift; my $to = shift; my $haveseen = shift || []; my $depth = shift || 0; if (++$depth > 20) { carp "More than 20 deep on nested reference. Is this a loop?"; return $to; } if (Scalar::Util::refaddr($from) == Scalar::Util::refaddr($to)) { croak "Do NOT try to merge two identical references!" } my $seen = [ @{$haveseen} ]; foreach (@{$seen}) { if(Scalar::Util::refaddr($from) == $_) { carp "Attempt to create circular reference!"; return $to } } push @{$seen}, Scalar::Util::refaddr($from), Scalar::Util::refaddr($to); return unless ((ref $from) && (ref $to)); if (Scalar::Util::reftype($from) eq Scalar::Util::reftype($to)) { if (Scalar::Util::reftype($from) eq "HASH") { while (my ($k, $v) = each %{$from} ) { if (exists $to->{$k}) { if (defined $v) { if (ref $v) { $self->_mergerefs($from->{$k}, $to->{$k}, $seen, $depth) } else { $to->{$k} = $v; } } } else { if (ref $v) { $to->{$k} = $self->_copyref($v, $seen, $depth); } else { $to->{$k} = $v; } } } } elsif (Scalar::Util::reftype($from) eq "ARRAY") { foreach my $v (@{$from}) { if (ref $v) { push @{$to}, $self->_copyref($v, $seen, $depth); } else { push @{$to}, $v; } } } } else { $to = $self->_copyref($from, $seen, $depth); } return $to; } =item deepmerge() Same as merge, except when a value is a hash or array reference. For example: my $options = Config::Options->new({ moods => [ qw(happy sad angry) ] }); $options->deepmerge({ moods => [ qw(sardonic twisted) ] }); print join(" ", @{$options->{moods}}), "\n"; The above outputs: happy sad angry sardonic twisted =cut sub deepmerge { my $self = shift; my $option = shift; $self->_mergerefs($option, $self); } =pod =item tofile_perl() This is used to store options to a file. The file is actually a perl program that returns a hash. By default uses option 'optionfile' as filename, or value passed as argument. If 'optionfile' is an array, then uses LAST option in array as default. $options->tofile_perl("/path/to/optionfile"); =cut sub tofile_perl { my $self = shift; my $filename = shift || $self->options("optionfile"); my $file; if ( ref $filename ) { $file = $filename->[-1]; } else { $file = $filename; } local *OUT; open( OUT, ">", $file ) or croak "Can't open option file: $file for write: $!"; my $data = $self->serialize(); print OUT $data; close(OUT) or croak "Error closing file: ${file}: $!"; return $self; } =pod =item fromfile_perl() This is used to retreive options from a file. The optionfile is actually a perl program that returns a hash. By default uses option 'optionfile' as filename if none is passed. If 'optionfile' is an array, reads all option files in order. Non-existant files are ignored. Please note that values for this are cached. $options->fromfile_perl("/path/to/optionfile"); =cut sub fromfile_perl { my $self = shift; my $filename = shift || $self->options("optionfile"); my @files = (); if ( ref $filename eq "ARRAY" ) { push @files, @{$filename}; } else { push @files, $filename; } my $n = 0; foreach my $f ( @files ) { if ( -e $f ) { if ( ( exists $self->{verbose} ) && ( $self->{verbose} ) ) { print STDERR "Loading options from $f\n"; } local *IN; my $sub = ""; open( IN, $f ) or croak "Couldn't open option file $f: $!"; while () { $sub .= $_; } close(IN); my $o = $self->deserialize( $sub, "Options File: $f" ); $o && $n++; } } return $n; } =pod =item deserialize($data, $source) Takes a scalar as argument and evals it, then merges option. If second option is given uses this in error message if the eval fails. my $options = $options->deserialize($scalar, $source); =cut sub deserialize { my $self = shift; my $data = shift; my $source = shift || "Scalar"; my $o = eval $data; if ($@) { croak "Can't process ${source}: $@" } else { $self->deepmerge($o); return $self; } } =pod =item serialize() Output optons hash as a scalar using Data::Dumper. my $scalar = $options->serialize(); =cut sub serialize { my $self = shift; my $d = Data::Dumper->new( [ { %{$self} } ] ); return $d->Purity(1)->Terse(1)->Deepcopy(1)->Dump; } =item del($key) Removes $key from options. =cut sub DESTROY { } =back =head1 BUGS =over 4 =item Deepmerge does a poor job at recogniaing recursive loops. For example, $options->deepmerge($options) will really screw things up. As protection, will only loop 20 deep. =item fromfile_perl provides tainted data. Since it comes from an external file, the data is considered tainted. =back =head1 SEE ALSO L =head1 LICENSE This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License, distributed with Perl. =head1 COPYRIGHT Copyright (c) 2007 Edward Allen III. Some rights reserved. =cut 1;