"There's seldom an excuse to have an undocumented Perl script."
"WWJD? JWRTFM."
So say Jeffreys Copeland and Haemer, and I humbly concur.
In one of their very last columns from the now-defunct Server/Workstation Expert magazine (formerly SunExpert), the Jeffreys extolled the virtues of providing user-documentation in your camel-code with a particular pair of Perl modules: Getopt::Long and Pod::Usage:
- (necessary modules are) included in the standard Perl distribution
- they keep the docs in the same file as the program
- program messages are extracted from the documentsion itself
- argument parsing is relatively short
To put that in my own words:
- you don't have to convince a clue-free-PHB or snarling-SysAdmin* to let you install Yet Another Module
- you don't have to maintain separate (easy-to-fall-out-of-sync) doc files
- you don't have to maintain internal (easy-to-fall-out-of-sync) &Usage() subroutines
- Perl makes it easy and fun
The Jeffreys went so far as to helpify and manatize a helloworld.pl script. However, in the interest of avoiding plaguarism lawsuits, I took this oportunity to make a still-small yet ever-so-slightly-more-useful program to fetch current condition reports from Weather Underground**. By doculating it with these swell modules, I hope to show One Way To Do It.
Before we look at some code, here's a summary of what we'll do with These Fine Modules.
- use Getopt::Long to set up --help and --man arguments.
- use Getopt::Long to setup other program arguments (completely separate from Pod::Usage):
- wunderg.pl --debug=1
- wunderg.pl --versions
- call the pod2usage() function to generate the associated messages.
- The --help message is always extracted from pod sections titled USAGE, ARGUMENTS, and OPTIONS (case-sensitive).
- The --man message is always the pod in it's entirety.
Code, discussion, runs, and sundry comments follow.
cheers,
ybiC
*No offense intended to monks of this ilk; I'm a recovering server-guy myself
**thanks to merlyn for mentioning the Weather::Underground module
***thanks to someFineMonk, I forget who, who pointed me to this keen pair o'doc-jones modules a year or so ago
****I'm sure gonna miss Mr Protocol 8^(
*****kudos to grinder for Parsing your script's command line, which I didn't spot until *after* posting this 8^P
Update:
Thanks to brothers gellyfish, belg4mit, and danger for code fixes
and to brothers impossiblerobot, Corion, wmono, and podmaster for posting+editing improvements 8^)
RELEVANT CODE, and DISCUSSION:
The Getopt::Long and Pod::Usage modules have *great* pod. (hint, hint) So rather than regurgitate that, let's walk through relevant chunks of our example program. Starting from the top:
Of course, you have to tell Perl to load the needed modules (you do use warnings and strict, don't you??):
#!/usr/bin/perl -w
use strict; # avoid d'oh! bugs
use Getopt::Long; # support options+arguments
use Pod::Usage; # avoid redundant &Usage()
Since these scalar names start with "$opt_", we know that they're Getopt::Long-facilitated commandline option . I happen to want a default value of 0 for debug, as it's used to set the verbosity level of Weather::Underground output. The other three are declared as lexicals here, for use in just a bit.
my $opt_debug = 0;
my ($opt_help, $opt_man, $opt_versions);
As the comment says, this Getopt::Long function allocates options and arguments for the program. The first (alpha) part of the hash keys are the long form of the command-line switches. The second (=1|!) part of the keys are argument specifiers. Our example program uses two of the seven available argument specifiers. "=i" requires an integer argument to assign as the variable's value, and "!" takes no argument, but does 'def' the variable. You can perldoc Getopt::Long to see what the other 5 arg specs are.
GetOptions(
'debug=i' => \$opt_debug,
'help!' => \$opt_help,
'man!' => \$opt_man,
'versions!' => \$opt_versions,
) or...
The hash values indicate the name of the variable, like our friend $opt_debug above.
So far, we've been dealing with Getopt::Long, but pod2usage is obviously a Pod::Usage function. If the Getoptions() call above fails, we have Pod::Usage print the program's pod USAGE, OPTIONS, and ARGUMENTS (as spec'd by verbose level one), then end the program run. Much friendlier than some cryptic or die $!; message, eh?
...or pod2usage(-verbose => 1) && exit;
pod2usage() also supports specifying error message, exit value, and output filehandle. This program might benefit in places from a pod2usage() error message, but I'll leave that as an exercise for the reader. Hint: perldoc Pod::Usage 8^)
In the same vein, we print the 'help' messages if an invalid value was entered for --debug, or if you purposely called --help:
pod2usage(-verbose => 1) && exit if ($opt_debug !~ /^[01]$/);
pod2usage(-verbose => 1) && exit if defined $opt_help;
Likewise, print the 'man' messages if the program was called with --man:
pod2usage(-verbose => 2) && exit if defined $opt_man;
The body of the program falls here, but since it does the Weather::Underground fetch stuff, we'll ignore it for now.
And finally, if the program is called with --versions, this block runs and prints a mess of version number info:
END{
if(defined $opt_versions){
print
"\nModules, Perl, OS, Program info:\n",
" Weather::Underground $Weather::Underground::VERSION\n",
" Pod::Usage $Pod::Usage::VERSION\n",
" Getopt::Long $Getopt::Long::VERSION\n",
" strict $strict::VERSION\n",
" Perl $]\n",
" OS $^O\n",
" wunderg.pl $wunderg_VER\n",
" $0\n",
" $site\n",
"\n\n";
}
}
RUN the PROGRAM, WILLYA?!
So, when we call our program like so wunderg.pl --help we get this message on screen:
Usage:
wunderg.pl Paris,France Omaha,NE 'London, United Kingdom'
Arguments:
Place
--help print Options and Arguments instead of fetching weather data
--man print complete man page instead of fetching weather data
Place can be individual name:
City
State
Country
Place can be combinations like:
City,State
City,Country
Note that if Place contains any spaces it must be surrounded with single
or double quotes:
'London,United Kingdom'
'San Jose,CA'
'Omaha, Nebraska'
Options:
--versions print Modules, Perl, OS, Program info
--debug 0 don't print debugging information (default)
--debug 1 print debugging information
And wunderg.pl --man results in:
Nah, let's skip that, it's too long. If you want to see it, go to The Full Monty below and find the pod.
wunderg.pl paris,france 'london, united kingdom' actually *does* something, thanks to the fine folks at wunderground.com and Weather::Underground:
Sat Mar 30 12:34:53 2002
http://www.wunderground.com/members/signup.php
paris,france
conditions = Unknown
humidity = 34
fahrenheit = 59
london,united kingdom
conditions = Partly Cloudy
humidity = 48
fahrenheit = 58
And wunderg.pl --debug 'new york, new york' has W::U to tell us more details of the fetch:
Sat Mar 30 12:37:04 2002
http://www.wunderground.com/members/signup.php
Weather::Underground DEBUG NOTE: Creating a new Weather::Underground object
Weather::Underground DEBUG NOTE: Getting weather info for new york, new york
Weather::Underground DEBUG NOTE: Retrieving url http://www.wunderground.com/cgi-bin/findweather/getForecast?query=new york, new york
Weather::Underground DEBUG NOTE: SINGLE-LOCATION PARSED 1: new york, new york: Scattered Clouds: 70 * F . 46% humity
Weather::Underground DEBUG NOTE: Temperature in Fahrenheit. Converting accordingly
new york, new york
conditions = Scattered Clouds
humidity = 46
fahrenheit = 70
SUNDRY COMMENTS:
* on Getopt::Long *
Options can be called in either longform:
wunderg.pl --versions Minot,ND
wunderg.pl --debug=1 Minot,ND
wunderg.pl --help
wunderg.pl --man
Or shortform:
wunderg.pl -v Minot,ND
wunderg.pl -d 1 Minot,ND
wunderg.pl -h
wunderg.pl -m
* on Pod::Usage *
podusage() called with only a 0 or 1 as argument is shortform verbose level:
pod2usage(1);
podusage() called with only a quoted text string as argument is shortform for error message:
pod2usage("Hrm...");
Arguments can be combined, but only in longform:
pod2usage({-verbose=>1, -message=>"Hrm...",});
THE FULL MONTY:
This is the only place where I've used <code> tags, so that you can use the d/l button below to fetch this program.
#!/usr/bin/perl -w
# wunderg.pl
# pod at tail
use strict; # avoid d'oh! bugs
use Getopt::Long; # support options+arguments
use Pod::Usage; # avoid redundant &Usage()
use Weather::Underground; # fetch weather from www.wunderground.com
my $wunderg_VER = '0.02.05';
my $site = 'http://www.wunderground.com/members/signup.php';
my $opt_debug = 0;
my ($opt_help, $opt_man, $opt_versions);
GetOptions(
'debug=i' => \$opt_debug,
'help!' => \$opt_help,
'man!' => \$opt_man,
'versions!' => \$opt_versions,
) or pod2usage(-verbose => 1) && exit;
pod2usage(-verbose => 1) && exit if ($opt_debug !~ /^[01]$/);
pod2usage(-verbose => 1) && exit if defined $opt_help;
pod2usage(-verbose => 2) && exit if defined $opt_man;
# Check this last to avoid parsing options as Places,
# and so don't override $opt_man verbose level
my @places = @ARGV;
pod2usage(-verbose => 1) && exit unless @places;
print "\n", my $time = localtime, "\n$site\n\n";
for (@places){
my $weather = Weather::Underground ->new(
place => $_,
debug => $opt_debug,
) or die "Error creating object:\n$@\n";
my $arrayref = $weather->getweather()
or die "Error fetching:\n$@\n";
for(@$arrayref){
print "$_->{place}\n" if exists($_->{place});
while (my ($key, $value) = each %{$_}) {
print " $key = $value\n"
unless ($key eq 'celsius' or $key eq 'place');
# celcius matches misspelling in W::U
# fixed in W::U v2.02
}
}
print "\n";
}
BEGIN{
# allow to run from cron:
# but doesn't work 8^(
$ENV{HTTP_PROXY} = 'http://proxy:port/';
}
END{
if(defined $opt_versions){
print
"\nModules, Perl, OS, Program info:\n",
" Weather::Underground $Weather::Underground::VERSION\n",
" Pod::Usage $Pod::Usage::VERSION\n",
" Getopt::Long $Getopt::Long::VERSION\n",
" strict $strict::VERSION\n",
" Perl $]\n",
" OS $^O\n",
" wunderg.pl $wunderg_VER\n",
" $0\n",
" $site\n",
"\n\n";
}
}
=head1 NAME
wunderg.pl
=head1 SYNOPSIS
wunderg.pl Paris,France Omaha,NE 'London, United Kingdom'
=head1 DESCRIPTION
Fetch and print weather conditions for one or more cities.
Weather::Underground appears to read http_proxy environment variable,
so wunderg.pl works behind a proxy (non-auth proxy, at least).
Switches that don't define a value can be done in long or short form.
eg:
wunderg.pl --man
wunderg.pl -m
=head1 ARGUMENTS
Place
--help print Options and Arguments instead of fetching weather d
+ata
--man print complete man page instead of fetching weather data
Place can be individual name:
City
State
Country
Place can be combinations like:
City,State
City,Country
Note that if Place contains any spaces it must be surrounded with sin
+gle
or double quotes:
'London,United Kingdom'
'San Jose,CA'
'Omaha, Nebraska'
=head1 OPTIONS
--versions print Modules, Perl, OS, Program info
--debug 0 don't print debugging information (default)
--debug 1 print debugging information
=head1 AUTHOR
ybiC
=head1 CREDITS
Core loop derived directly from Weather::Underground pod.
Thanks to merlyn for pointing out this cool weather module,
gellyfish for tip to use regex match for valid $opt_debug values,
belg4mit for cleaner syntax for printing "Place" key,
danger for tip+fix for 5.6.1 warning on 'unless defined(places)'
Oh yeah, and to some guy named vroom.
You don't have to subscribe to www.wunderground.com to fetch their da
+ta.
But it's only $5USD/year, so why not?
=head1 TESTED
Weather::Underground 2.01
Pod::Usage 1.14
Getopt::Long 2.2602
Perl 5.00503
Debian 2.2r5
=head1 BUGS
None that I know of.
=head1 TODO
Test from cron
Test on Cygwin
Test on ActivePerl
Make it run from cron when behind proxy
Use printf() to line up weather output in columns
Print modules... info on error
=head1 UPDATES
2002-03-29 17:30 CST
Replace 'unless defined(@places)' with 'unless(@places)'
to avoid warning on 5.6.1
Perlish idiom instead of looping through hash twice
Post to PerlMonks
2002-03-29 12:05 CST
Initial working code
=cut