Skip to main content

Cultured Perl: Application configuration with Perl, Part 2

Advanced usage of the CPAN AppConfig module

Teodor Zlatanov (tzz@iglou.com), Programmer, Gold Software Systems
Teodor Zlatanov graduated with an M.S. in computer engineering from Boston University in 1999. He has worked as a programmer since 1992, using Perl, Java, C, and C++. His interests are in open source work on text parsing, 3-tier client-server database architectures, UNIX system administration, CORBA, and project management.

Summary:  File-based configurations break down quickly if a hand-built method is used. In this article, Ted shows how the AppConfig module can handle local configuration storage. He examines advanced features of the AppConfig module: validation, autoaction, modifying hash and array variables, and "radio button"-style mutually exclusive options.

Date:  01 Jul 2002
Level:  Advanced
Activity:  1148 views

In this article, I'm going to discuss advanced file-based configuration for Perl programs using the CPAN AppConfig module. For an introduction to AppConfig and the rationale behind using a configuration management module, see my previous "Cultured Perl" installment on this topic.

Some of the code in this second article draws upon code in the first article without explaining the details. In order to understand the material I'm going to cover, you'll need to read the first article.

Before you begin, you should have Perl 5.005 or later on your system, and the CPAN AppConfig module installed on your system (see Resources later in this article for links to these packages if needed).

Validation

Often forgotten in the rush of development, data validation is the vaccine your program must take now so it won't keel over and die later. Users expect your program to handle their input, and you should expect the unexpected.

Fortunately, you can use the AppConfig data validation routines. You can either reject bad data, or abort the entire program when you find unsavory data, depending on the PEDANTIC setting given at the time you initialize AppConfig.


Listing 1. Validate configuration values

# SSN: must be a string with exactly 9 digits
# LIMITED: must be a string key existing in the hash %limited_hash

$config->define(
# more complex subroutine validation
                'SSN'      => { ARGCOUNT => ARGCOUNT_ONE,
				VALIDATE => sub	
				{
				 my $varname = shift @_;
				 my $value = shift @_;
				 # only succeed with 9 digits in $value
				 return (9 == ($value =~ tr/0-9//));
				}
			      },
# limited values, must be 'alpha' or 'beta'
                'LIMITED'  => { ARGCOUNT => ARGCOUNT_ONE,
				VALIDATE => sub 
				{
				 my $varname = shift @_;
				 my $value = shift @_;
				 my %limited_hash = ( alpha => 1, beta => 2 );
				 return exists $limited_hash{$value};
				}
			      },
               );

Note that the VALIDATE subroutine is much more limited than the ACTION subroutines (described in the next section). The AppConfig::State instance is not passed to VALIDATE subroutines, so we can't access other variables in our configuration easily. This was probably done intentionally, so an adventurous programmer doesn't place AppConfig::State into an indeterminate state.


Autoaction

The AppConfig module makes it easy to trigger functions when a variable's value is set. The ACTION subroutine is called after the value is set, whereas the VALIDATE subroutine is invoked before the value is set. You should not use ACTION to set the value of the current variable. You will start an infinite loop if you are not careful. Try the following, for instance:


Listing 2. An infinite autoaction loop -- DO NOT USE!

$config->define(
                'TRIGGER' => { ARGCOUNT => ARGCOUNT_ONE,
                                ACTION => sub # autoaction
       				          {
					   my $config = shift @_;
					   my $varname = shift @_;
					   my $value = shift @_;
					   print "$varname = $value\n";
					   $value++;
					   $config->TRIGGER($value);
					  }
	                      }
               );

The code in Listing 2 will run forever. You can put safeguards in place with external variables, but that would get messy very quickly. Instead, do the postprocessing of your data after all the data has been read in.

Autoactions will be explored further in a future article, when we discuss how a variable's value can set off triggers for other variables. There you will see how to set a variable's value safely from within its own trigger for mutually exclusive variables.


Modifying AppConfig hash and array variables

It's pretty obvious how to modify scalar values in an AppConfig object: you just call $config->VARIABLE(value).

Arrays and hashes, however, are more complex. The AppConfig object will give you an array or hash reference when you ask for a variable's value. You have to modify the data in the object, not the data that's passed to you. You can't remove a single element; you have to remove all of them and then reinsert the wanted ones.

You can modify arrays like this:


Listing 3. Modifying arrays

$config->define('HOSTS' => { ARGCOUNT => ARGCOUNT_LIST });

# add a value to the array named HOSTS
$config->HOSTS("jove");

# list the values in the HOSTS array
print "$_\n" foreach @{$config->HOSTS};

# reset the array to 0 entries
$config->_default("HOSTS");

There's no API for directly accessing the values of arrays. You can do it by accessing internal AppConfig::State data or the array reference returned to you as the value of the array variable, but it is not recommended that you do so.

Hash entries are, similarly to arrays, easy to set but difficult to remove. Also, they are limited to scalar strings or undef for the values.


Listing 4. Modifying Hashes

$config->define('PHONE' => { ARGCOUNT => ARGCOUNT_HASH });

# add values to the hash named PHONE
$config->PHONE("jimmy=1-800-453-2211");

# unusual cases
$config->PHONE("equals=a=b"); # the value will be "a=b"
$config->PHONE("harry=");     # the value will be a string of length 0 ('')
$config->PHONE("harry");      # the value will be undef

# list the sorted keys and their values in the PHONE hash
print "$_ => ", $config->PHONE()->{$_}, "\n" foreach sort keys %{$config->PHONE};

# you can also do it with Data::Dumper
print Dumper $config->PHONE;

# reset the hash to 0 entries
$config->_default("PHONE");

Just like arrays, there is no API for directly accessing hash values. You can do it by accessing internal AppConfig::State data or the hash reference returned to you as the value of the hash variable, but it is not recommended that you do so.


Radio buttons

Radio buttons are familiar to all GUI users: a series of mutually exclusive options are defined. For instance, the type of a file could be "executable," "directory," or "special" (anything else). You can handle this with numeric types; for instance, executable is 1, directory is 2, and special is 3. With radio buttons, though, when any of the three options is set, the other two are automatically set to 0.

The implementation relies on a binary state of the option (on or off). When the option is off, nothing extra needs to be done. When the option is on, the other two options will be set to 0.

Setting all three options to 0 is acceptable; it can be treated as an unknown state (so there are four possible states: file, directory, special, and unknown).


Listing 5. Radio buttons

my $config = AppConfig->new();

$config->define(
     # mutually exclusive options
  'FILE'   => { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ACTION => \&toggle_type },
  'DIRECTORY'  => { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ACTION => \&toggle_type },
  'SPECIAL'    => { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 1, ACTION => \&toggle_type },
	      );

# {{{ toggle_type: do right when file type is specified (3 mutually exclusive options)
sub toggle_type
{
 my $self    = shift @_;
 my $varname = shift @_;
 my $value   = shift @_;

 return unless $value;			# do nothing if we're being turned off

 # turn off all options except the one that invoked this action
 $self->set($_, 0) foreach grep !/^$varname$/i, qw/FILE DIRECTORY SPECIAL/;
}

The key to the code above is that is works along with all other AppConfig usage, and does not require a new module. It can go right into your code. It could have been implemented differently if the internals of AppConfig::State were accessed (so, for instance, we wouldn't have to invoke set() ), but for our example of three mutually exclusive settings, that was not necessary.


Interdependent options

Here is the code that would modify the internals of AppConfig::State if the options are more complex. The important thing is how we modify the values of variables directly in the internals of AppConfig::State -- something we're not supposed to do normally.

This example will automatically set the WINNER and LOSER variables when the TIMES hash has more than one entry. It is a complete example you can run.

Note also how complex the interplay of variables can be. When is it safe to set WINNER and LOSER? Should WINNER and LOSER, when set directly, use the TIMES hash, or should they modify values directly in the AppConfig::State internals? This is for only three variables. Imagine how complex it can get for a small program that wishes to use interdependencies! You should look into finite state machines if you have lots of options that trigger other options or actions. FSMs are best suited to that sort of complex environment.


Listing 6. Interdependent options

#!/usr/bin/perl -w

use strict;
use AppConfig qw/:argcount/;

my $config = AppConfig->new();

$config->define(
	       'TIMES'	  => { ARGCOUNT => ARGCOUNT_HASH, ACTION => \&toggle_option },
	       'WINNER'   => { ARGCOUNT => ARGCOUNT_ONE,  ACTION => \&cheat_action  },
	       'LOSER'    => { ARGCOUNT => ARGCOUNT_ONE,  ACTION => \&cheat_action  },
	       'CHEATERS' => { ARGCOUNT => ARGCOUNT_NONE },
	      );

$config->TIMES("Johnny Boy = .444");
$config->TIMES("Johnny Appleseed = 2.45");
$config->TIMES("Johnny Be Good = 8.002");

print "Winner: ", $config->WINNER, "\n";
print "Loser: ", $config->LOSER, "\n";

$config->WINNER("Johnny Be Good");

print "Winner: ", $config->WINNER, "\n";
print "Loser: ", $config->LOSER, "\n";

$config->LOSER("Johnny Be Good");

print "Winner: ", $config->WINNER, "\n";
print "Loser: ", $config->LOSER, "\n";

# {{{ toggle_option: toggle interdependent options 
sub toggle_option
{
 my $self    = shift @_;
 my $varname = shift @_;
 my $value   = shift @_; 

 my @sorted = sort { $self->TIMES()->{$a} <=> $self->TIMES()->{$b} } keys %{$self->TIMES};

 if (scalar @sorted > 1)	# we need more than 1 member to have a winner and a loser
 {
  # we have to set winner and loser directly to avoid the cheat_action
  $self->{VARIABLE}->{winner} = $sorted[0];
  $self->{VARIABLE}->{loser} = $sorted[-1];
 }
}
# }}}

# {{{ cheat_action: set TIMES hash value so it's always the winner or the loser
sub cheat_action
{
 my $self    = shift @_;
 my $varname = shift @_;
 my $value   = shift @_; 

 my $time;
 $varname = uc $varname;		# make sure we match the variable name
 if ($varname eq 'WINNER')
 {
  # set the time to 0 unconditionally
  $time = 0;
 }
 elsif ($varname eq 'LOSER')
 {
  # set the time to the sum of all the times (it will always be the worst time, then)
  $time += $_ foreach values %{$config->TIMES()};
 }

 # trigger a resorting by inserting the newly wanted time
 $self->TIMES("$value=$time");
}
# }}}


Taking it further

The advanced techniques shown here should help you improve the configuration of your own programs. Don't be afraid to experiment with your own AppConfig extensions!


Resources

About the author

Teodor Zlatanov graduated with an M.S. in computer engineering from Boston University in 1999. He has worked as a programmer since 1992, using Perl, Java, C, and C++. His interests are in open source work on text parsing, 3-tier client-server database architectures, UNIX system administration, CORBA, and project management.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Linux
ArticleID=11300
ArticleTitle=Cultured Perl: Application configuration with Perl, Part 2
publish-date=07012002
author1-email=tzz@iglou.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers