U
usenet
I use a somewhat elaborate skeleton (starter, template) script to help
me code to "good practices" (I wouldn't presume to say "best
practices"), and to discourge me from thinking bad thoughts like, "I'll
add in good error reporting later." I thought I would post my skeleton
here in hopes someone may find it (or parts of it) useful. Others are
encouraged to post their skeletons here as well.
Most of my scripts run in batch mode (from crons, etc), so my skeleton
is skewed towards that type of environment (no fancy terminal controls,
etc). My "good practices" include:
- Proper POD documentation (including versioning and revision)
- Command-line parameter handling (via GetOpt::Long), inc'l --help
- External configuration file handling (via Config::IniFiles)
- Logging to screen, file, and e-mail (via Log:ispatch)
- Bail out if the script is already running.
- Pretty formatting of setup blocks and subroutines.
- Custom handlers for warn() and die().
- Define and detect killfiles.
- Get handles to a database and sftp ('cause I do that a lot).
Some of this gets a bit sophisticated. For example, the script uses
Config::IniFiles to parse a configuration file which is stipulated on
the commandline (via "--config=/path/to/my.ini"), OR it will use a
default filename of "myscript.ini" (assuming the script is named
"myscript.pl"), OR it will process configuration information embedded
within the script itself (under a __DATA__ token).
Generally, any of these sections can be safely deleted without
impacting other sections (though the little bit of skeleton code in the
main driver may throw 'strict' errors because some variable declaration
got deleted).
Anyway, here it is:
#!/usr/bin/perl
# Skeleton program. See end for POD
$main::VERSION = '0.01'; #see POD for history
###################################################################
####### ENVIRONMENT ----- ENVIRONMENT ----- ENVIRONMENT ########
###################################################################
use strict; use warnings; #always, always ##
#use diagnostics; ##
##
### Other skeleton stuff needs these... check before you delete ##
use English; #for (English) readability ##
use FindBin; ##
use Sys::Hostname; ##
##
### Optional stuff that I use often ##
use lib $FindBin::Bin; #also find modules in the program dir ##
use Fatal qwvoid open opendir chdir rename); ##
use IO::All; ##
use Data:umper; ##
use XML::Simple; ##
use Date::Manip; ##
##
select(STDERR); $| = 1; select(STDOUT); $| = 1; #unbuffered ##
##
###################################################################
####################################################################
### OPTIONS #######################################################
####################################################################
##
use Pod::Usage; ##
use Getopt::Long qwconfig auto_version); ##
my %opt; ##
GetOptions ( \%opt, # always include the first two... ##
'help|h|?', # Show help (POD) --help or --? ##
'man', # Show manpage (POD) --man ##
'test', # Run program in test mode ##
#here are some usage examples of of other parameter types ##
'config|ini=s', # Alternate configuration (.ini) file ##
'string_opt=s', # Required String --config=/tmp/x.ini ##
'integer_opt:i', # Optional Integer --border[=2] ##
'def_int_opt:5', # Optional integer but with default value ##
'option', # Boolean (true) --option ##
'opt2!', # Boolean (negatable) --opt2 or --no-opt2 ##
'more+' # Incrementable --more --more --more ##
) || pod2usage( {-verbose => 1} ); ##
#print map {"\t$_\t$opt{$_}\n"} sort keys %opt; #die "\n"; ##
##
pod2usage( {-verbose => 1} ) if $opt{'help'}; #|| ! @ARGV; ##
pod2usage( {-verbose => 2} ) if $opt{'man'}; ##
##
#Set default values if you want to ##
$opt{'string_opt'} ||= 'foo'; ##
##
####################################################################
####################################################################
### CONFIGURATION FILE #############################################
####################################################################
# Assumes script is named like 'myscript.pl' and config file ##
# is named like 'myscript.ini' (in the same directory) unless ##
# specified with a command-line config=/whatever/is.ini option ##
# OR - you can put the config info after an __END__ token ##
##
use Config::IniFiles; ##
##
my %cfg; ##
##
eval {no strict 'vars'; #in case the GetOpts block was deleted ##
$cfg{'inifile'} = $opt{'config'} # if defined $opt{'config'} ##
|| ("$FindBin::Bin/$FindBin::Script" =~ /(.*)\.pl/)[0].'.ini'; ##
}; ##
if (-e $cfg{'inifile'}) { ##
tie %cfg, 'Config::IniFiles', ( -file => $cfg{'inifile'} ) ##
or die @Config::IniFiles::errors; ##
}elsif (<DATA>) { ##
tie %cfg, 'Config::IniFiles', ( -file => *main:ATA ) ##
or die @Config::IniFiles::errors; ##
}else{ ##
warn "No Configuration info found: $cfg{'inifile'}\n"; ##
} ##
##
#Define default killfile (in addition to any that may be defined) ##
$cfg{'killfile'}{"/tmp/$FindBin::Script.kill"}++; ##
##
####################################################################
####################################################################
######### VALIDATE ###### VALIDATE ###### VALIDATE ##########
####################################################################
do{ #use 'do' to lexically isolate variables ##
my $ps = "/usr/bin/ps"; #crude, but it works for my purposes ##
die "Already Running\n" if scalar grep(/perl.*$0/, `$ps -ef`) >1; ##
##
-e && unlink for keys %{$cfg{'killfile'}}; #remove old killfiles ##
}; #do ##
####################################################################
####################################################################
### LOGGING. Levels are: ###########################################
### debug info notice warning error critical alert emergency ##
####################################################################
##
use Mail::Sendmail; ##
use Log:ispatch; ##
use Log:ispatch::Screen; ##
use Log:ispatch::File; ##
use Log:ispatch::Email::MailSendmail; ##
##
my $log; #or maybe 'our log' for some conveniences ##
##
do{ #use 'do' to lexically isolate variables ##
my $logdir = $cfg{'dir'}{'log'} || "/var/tmp"; ##
my @admin_email = @{$cfg{'email'}{'admin'}} || ##
sprintf('%s@%s', scalar getpwuid($<), hostname);##
##
unshift @{$Mail::Sendmail::mailcfg{'smtp'}}, ##
$cfg{'email'}{'smtp'} || 'localhost'; ##
##
my $add_lf = sub { my %p = @_; "$p{'message'}\n"}; ##
my $add_timestamp = sub { my %p = @_; ##
sprintf "%s - %s", scalar(localtime), ##
$p{'message'}; }; ##
##
$log = Log:ispatch->new ( callbacks => $add_lf ); ##
##
$log ->add( Log:ispatch::Screen ->new( ##
name => 'screen', ##
min_level => 'debug', ##
stderr => 0, ) ##
); ##
$log ->add( Log:ispatch::File ->new( ##
name => 'file', ##
min_level => 'debug', ##
filename => sprintf ( "%s/%s.log", ##
$logdir, ##
$FindBin::Script ), ##
mode => 'append', ##
callbacks => $add_timestamp ), ##
); ##
$log ->add( Log:ispatch::Email::MailSendmail ->new( ##
name => 'email', ##
min_level => 'error', ##
to => \@admin_email, ##
subject => "ERROR in $PROGRAM_NAME", ##
from => sprintf ("SERVER<%s\@%s>", ##
(hostname =~ /^([^\.]*)/)[0], ##
'do_not_reply.com' ) ), ##
); ##
}; #do ##
#dispatch our very first message - print all the runtime info ##
$log -> debug(sprintf ##
"[%s]\tBegin %s\n\tVersion %s on %s as %s\n" ##
."\tConfigFile: %s\n\tKillfile(s):\n%s", ##
__LINE__, __FILE__, ##
$main::VERSION, ##
hostname(), ##
"$REAL_USER_ID ($ENV{'USER'})", ##
$cfg{'inifile'}, ##
map {"\t\t$_\n"} keys %{$cfg{'killfile'}}, ##
); ##
####################################################################
####################################################################
### DATABASE #######################################################
####################################################################
##
use DBI; use DBIx::Simple; ##
##
my $dbh = DBIx::Simple -> connect( ##
sprintf ("dbi:%s:host=%s;database=%s", ##
$cfg{'dbi'}{'type'} || 'mysql', ##
$cfg{'dbi'}{'host'} || 'localhost', ##
$cfg{'dbi'}{'database'} || $FindBin::Script ), ##
$cfg{'dbi'}{'user'} || $ENV{'USER'}, ##
$cfg{'dbi'}{'password'} || '', ##
{ PrintError => $cfg{'dbi'}{'PrintError'} || 0, ##
RaiseError => $cfg{'dbi'}{'RaiseError'} || 0, ##
AutoCommit => $cfg{'dbi'}{'AutoCommit'} || 0, } ##
) or $log -> critical ( DBIx::Simple->error() ); ##
##
####################################################################
####################################################################
### SFTP ###########################################################
####################################################################
##
use Net::SFTP; ##
my $sftp = Net::SFTP->new($cfg{'sftp'}{'host'}, %{$cfg{'sftp'}}) ##
|| $log -> critical("SFTP ERROR: $!"); ##
##
####################################################################
#### # # ##### ####
# # # # # #
#### # # ##### ####
# # # # # #
# # # # # # # #
#### #### ##### ####
####################################################################
sub Dummy($) { ########################################
####################################################################
#Prints passed hash via Dummy({'foo'=>'bar'}) or Dummy(\%baz) ##
my %param = %{$_[0]}; ##
$param{'log'} -> debug( ##
map { "'$_'\t=>\t'$param{$_}'\n"} sort keys %param ); ##
} ##################################################################
##### ##### # # # ###### #####
# # # # # # # # # #
# # # # # # # ##### # #
# # ##### # # # # #####
# # # # # # # # # #
##### # # # ## ###### # #
#Define custom 'warn' and 'die' handlers
local $SIG{__WARN__} = sub {
$log->warning("OH NO! Look what happened:\n@_");
};
local $SIG{__DIE__} = sub {
$dbh->disconnect if $dbh;
$log->critical("I'm Dead, Jim!\n@_");
};
################################################
###-------- Main Program Goes Here! ---------###
################################################
#Withing the program, check and die if a killfile is detected:
-e && die "Killfile $_ Detected.\n" for keys %{$cfg{'killfile'}};
Dummy({'foo' => 'bar', 'log' => $log}); #call a sub
#### Ready for the script to end normally ####
$dbh->disconnect if $dbh; #don't forget!
$log->debug(sprintf "\n\nFinished %s - %s Seconds Elapsed",
scalar localtime,
time - $BASETIME,
);
exit(0);
__DATA__
#sample Config::IniFiles configuration file. If the external file
#doesn't exist, the information will be read directly from here.
[example]
foo = bar
#When read, $cfg{'example'}{'foo'} eq 'bar'
[sftp]
host = localhost
user = my_userid
password = my_password
[dbi]
database = test
(e-mail address removed)
me code to "good practices" (I wouldn't presume to say "best
practices"), and to discourge me from thinking bad thoughts like, "I'll
add in good error reporting later." I thought I would post my skeleton
here in hopes someone may find it (or parts of it) useful. Others are
encouraged to post their skeletons here as well.
Most of my scripts run in batch mode (from crons, etc), so my skeleton
is skewed towards that type of environment (no fancy terminal controls,
etc). My "good practices" include:
- Proper POD documentation (including versioning and revision)
- Command-line parameter handling (via GetOpt::Long), inc'l --help
- External configuration file handling (via Config::IniFiles)
- Logging to screen, file, and e-mail (via Log:ispatch)
- Bail out if the script is already running.
- Pretty formatting of setup blocks and subroutines.
- Custom handlers for warn() and die().
- Define and detect killfiles.
- Get handles to a database and sftp ('cause I do that a lot).
Some of this gets a bit sophisticated. For example, the script uses
Config::IniFiles to parse a configuration file which is stipulated on
the commandline (via "--config=/path/to/my.ini"), OR it will use a
default filename of "myscript.ini" (assuming the script is named
"myscript.pl"), OR it will process configuration information embedded
within the script itself (under a __DATA__ token).
Generally, any of these sections can be safely deleted without
impacting other sections (though the little bit of skeleton code in the
main driver may throw 'strict' errors because some variable declaration
got deleted).
Anyway, here it is:
#!/usr/bin/perl
# Skeleton program. See end for POD
$main::VERSION = '0.01'; #see POD for history
###################################################################
####### ENVIRONMENT ----- ENVIRONMENT ----- ENVIRONMENT ########
###################################################################
use strict; use warnings; #always, always ##
#use diagnostics; ##
##
### Other skeleton stuff needs these... check before you delete ##
use English; #for (English) readability ##
use FindBin; ##
use Sys::Hostname; ##
##
### Optional stuff that I use often ##
use lib $FindBin::Bin; #also find modules in the program dir ##
use Fatal qwvoid open opendir chdir rename); ##
use IO::All; ##
use Data:umper; ##
use XML::Simple; ##
use Date::Manip; ##
##
select(STDERR); $| = 1; select(STDOUT); $| = 1; #unbuffered ##
##
###################################################################
####################################################################
### OPTIONS #######################################################
####################################################################
##
use Pod::Usage; ##
use Getopt::Long qwconfig auto_version); ##
my %opt; ##
GetOptions ( \%opt, # always include the first two... ##
'help|h|?', # Show help (POD) --help or --? ##
'man', # Show manpage (POD) --man ##
'test', # Run program in test mode ##
#here are some usage examples of of other parameter types ##
'config|ini=s', # Alternate configuration (.ini) file ##
'string_opt=s', # Required String --config=/tmp/x.ini ##
'integer_opt:i', # Optional Integer --border[=2] ##
'def_int_opt:5', # Optional integer but with default value ##
'option', # Boolean (true) --option ##
'opt2!', # Boolean (negatable) --opt2 or --no-opt2 ##
'more+' # Incrementable --more --more --more ##
) || pod2usage( {-verbose => 1} ); ##
#print map {"\t$_\t$opt{$_}\n"} sort keys %opt; #die "\n"; ##
##
pod2usage( {-verbose => 1} ) if $opt{'help'}; #|| ! @ARGV; ##
pod2usage( {-verbose => 2} ) if $opt{'man'}; ##
##
#Set default values if you want to ##
$opt{'string_opt'} ||= 'foo'; ##
##
####################################################################
####################################################################
### CONFIGURATION FILE #############################################
####################################################################
# Assumes script is named like 'myscript.pl' and config file ##
# is named like 'myscript.ini' (in the same directory) unless ##
# specified with a command-line config=/whatever/is.ini option ##
# OR - you can put the config info after an __END__ token ##
##
use Config::IniFiles; ##
##
my %cfg; ##
##
eval {no strict 'vars'; #in case the GetOpts block was deleted ##
$cfg{'inifile'} = $opt{'config'} # if defined $opt{'config'} ##
|| ("$FindBin::Bin/$FindBin::Script" =~ /(.*)\.pl/)[0].'.ini'; ##
}; ##
if (-e $cfg{'inifile'}) { ##
tie %cfg, 'Config::IniFiles', ( -file => $cfg{'inifile'} ) ##
or die @Config::IniFiles::errors; ##
}elsif (<DATA>) { ##
tie %cfg, 'Config::IniFiles', ( -file => *main:ATA ) ##
or die @Config::IniFiles::errors; ##
}else{ ##
warn "No Configuration info found: $cfg{'inifile'}\n"; ##
} ##
##
#Define default killfile (in addition to any that may be defined) ##
$cfg{'killfile'}{"/tmp/$FindBin::Script.kill"}++; ##
##
####################################################################
####################################################################
######### VALIDATE ###### VALIDATE ###### VALIDATE ##########
####################################################################
do{ #use 'do' to lexically isolate variables ##
my $ps = "/usr/bin/ps"; #crude, but it works for my purposes ##
die "Already Running\n" if scalar grep(/perl.*$0/, `$ps -ef`) >1; ##
##
-e && unlink for keys %{$cfg{'killfile'}}; #remove old killfiles ##
}; #do ##
####################################################################
####################################################################
### LOGGING. Levels are: ###########################################
### debug info notice warning error critical alert emergency ##
####################################################################
##
use Mail::Sendmail; ##
use Log:ispatch; ##
use Log:ispatch::Screen; ##
use Log:ispatch::File; ##
use Log:ispatch::Email::MailSendmail; ##
##
my $log; #or maybe 'our log' for some conveniences ##
##
do{ #use 'do' to lexically isolate variables ##
my $logdir = $cfg{'dir'}{'log'} || "/var/tmp"; ##
my @admin_email = @{$cfg{'email'}{'admin'}} || ##
sprintf('%s@%s', scalar getpwuid($<), hostname);##
##
unshift @{$Mail::Sendmail::mailcfg{'smtp'}}, ##
$cfg{'email'}{'smtp'} || 'localhost'; ##
##
my $add_lf = sub { my %p = @_; "$p{'message'}\n"}; ##
my $add_timestamp = sub { my %p = @_; ##
sprintf "%s - %s", scalar(localtime), ##
$p{'message'}; }; ##
##
$log = Log:ispatch->new ( callbacks => $add_lf ); ##
##
$log ->add( Log:ispatch::Screen ->new( ##
name => 'screen', ##
min_level => 'debug', ##
stderr => 0, ) ##
); ##
$log ->add( Log:ispatch::File ->new( ##
name => 'file', ##
min_level => 'debug', ##
filename => sprintf ( "%s/%s.log", ##
$logdir, ##
$FindBin::Script ), ##
mode => 'append', ##
callbacks => $add_timestamp ), ##
); ##
$log ->add( Log:ispatch::Email::MailSendmail ->new( ##
name => 'email', ##
min_level => 'error', ##
to => \@admin_email, ##
subject => "ERROR in $PROGRAM_NAME", ##
from => sprintf ("SERVER<%s\@%s>", ##
(hostname =~ /^([^\.]*)/)[0], ##
'do_not_reply.com' ) ), ##
); ##
}; #do ##
#dispatch our very first message - print all the runtime info ##
$log -> debug(sprintf ##
"[%s]\tBegin %s\n\tVersion %s on %s as %s\n" ##
."\tConfigFile: %s\n\tKillfile(s):\n%s", ##
__LINE__, __FILE__, ##
$main::VERSION, ##
hostname(), ##
"$REAL_USER_ID ($ENV{'USER'})", ##
$cfg{'inifile'}, ##
map {"\t\t$_\n"} keys %{$cfg{'killfile'}}, ##
); ##
####################################################################
####################################################################
### DATABASE #######################################################
####################################################################
##
use DBI; use DBIx::Simple; ##
##
my $dbh = DBIx::Simple -> connect( ##
sprintf ("dbi:%s:host=%s;database=%s", ##
$cfg{'dbi'}{'type'} || 'mysql', ##
$cfg{'dbi'}{'host'} || 'localhost', ##
$cfg{'dbi'}{'database'} || $FindBin::Script ), ##
$cfg{'dbi'}{'user'} || $ENV{'USER'}, ##
$cfg{'dbi'}{'password'} || '', ##
{ PrintError => $cfg{'dbi'}{'PrintError'} || 0, ##
RaiseError => $cfg{'dbi'}{'RaiseError'} || 0, ##
AutoCommit => $cfg{'dbi'}{'AutoCommit'} || 0, } ##
) or $log -> critical ( DBIx::Simple->error() ); ##
##
####################################################################
####################################################################
### SFTP ###########################################################
####################################################################
##
use Net::SFTP; ##
my $sftp = Net::SFTP->new($cfg{'sftp'}{'host'}, %{$cfg{'sftp'}}) ##
|| $log -> critical("SFTP ERROR: $!"); ##
##
####################################################################
#### # # ##### ####
# # # # # #
#### # # ##### ####
# # # # # #
# # # # # # # #
#### #### ##### ####
####################################################################
sub Dummy($) { ########################################
####################################################################
#Prints passed hash via Dummy({'foo'=>'bar'}) or Dummy(\%baz) ##
my %param = %{$_[0]}; ##
$param{'log'} -> debug( ##
map { "'$_'\t=>\t'$param{$_}'\n"} sort keys %param ); ##
} ##################################################################
##### ##### # # # ###### #####
# # # # # # # # # #
# # # # # # # ##### # #
# # ##### # # # # #####
# # # # # # # # # #
##### # # # ## ###### # #
#Define custom 'warn' and 'die' handlers
local $SIG{__WARN__} = sub {
$log->warning("OH NO! Look what happened:\n@_");
};
local $SIG{__DIE__} = sub {
$dbh->disconnect if $dbh;
$log->critical("I'm Dead, Jim!\n@_");
};
################################################
###-------- Main Program Goes Here! ---------###
################################################
#Withing the program, check and die if a killfile is detected:
-e && die "Killfile $_ Detected.\n" for keys %{$cfg{'killfile'}};
Dummy({'foo' => 'bar', 'log' => $log}); #call a sub
#### Ready for the script to end normally ####
$dbh->disconnect if $dbh; #don't forget!
$log->debug(sprintf "\n\nFinished %s - %s Seconds Elapsed",
scalar localtime,
time - $BASETIME,
);
exit(0);
__DATA__
#sample Config::IniFiles configuration file. If the external file
#doesn't exist, the information will be read directly from here.
[example]
foo = bar
#When read, $cfg{'example'}{'foo'} eq 'bar'
[sftp]
host = localhost
user = my_userid
password = my_password
[dbi]
database = test
(e-mail address removed)