#!/usr/bin/perl -c package Exception::Base; =head1 NAME Exception::Base - Lightweight exceptions =head1 SYNOPSIS # Use module and create needed exceptions use Exception::Base 'Exception::Runtime', # create new module 'Exception::System', # load existing module 'Exception::IO', => { isa => 'Exception::System' }, # create new based on existing 'Exception::FileNotFound' => { isa => 'Exception::IO', # create new based on previous message => 'File not found', # override default message has => [ 'filename' ], # define new rw attribute string_attributes => [ 'message', 'filename' ], }; # output message and filename # eval is used as "try" block eval { open my $file, '/etc/passwd' or Exception::FileNotFound->throw( message=>'Something wrong', filename=>'/etc/passwd'); }; # syntax for Perl >= 5.10 use feature 'switch'; if ($@) { given (my $e = Exception::Base->catch) { when ($e->isa('Exception::IO')) { warn "IO problem"; } when ($e->isa('Exception::Eval')) { warn "eval died"; } when ($e->isa('Exception::Runtime')) { warn "some runtime was caught"; } when ($e->matches({value=>9})) { warn "something happened"; } when ($e->matches(qr/^Error/)) { warn "some error based on regex"; } default { $e->throw; } # rethrow the exception } } # standard syntax for older Perl if ($@) { my $e = Exception::Base->catch; # convert $@ into exception if ($e->isa('Exception::IO')) { warn "IO problem"; } elsif ($e->isa('Exception::Eval')) { warn "eval died"; } elsif ($e->isa('Exception::Runtime')) { warn "some runtime was caught"; } elsif ($e->matches({value=>9})) { warn "something happened"; } elsif ($e->matches(qr/^Error/)) { warn "some error based on regex"; } else { $e->throw; } # rethrow the exception } # $@ has to be recovered ASAP! eval { die "this die will be caught" }; my $e = Exception::Base->catch; eval { die "this die will be ignored" }; if ($e) { (...) } # the exception can be thrown later my $e = Exception::Base->new; # (...) $e->throw; # ignore our package in stack trace package My::Package; use Exception::Base '+ignore_package' => __PACKAGE__; # define new exception in separate module package Exception::My; use Exception::Base (__PACKAGE__) => { has => ['myattr'], }; # run Perl with changed verbosity for debugging purposes $ perl -MException::Base=verbosity,4 script.pl =head1 DESCRIPTION This class implements a fully OO exception mechanism similar to L or L. It provides a simple interface allowing programmers to declare exception classes. These classes can be thrown and caught. Each uncaught exception prints full stack trace if the default verbosity is uppered for debugging purposes. The features of C: =over 2 =item * fast implementation of the exception class =item * fully OO without closures and source code filtering =item * does not mess with C<$SIG{__DIE__}> and C<$SIG{__WARN__}> =item * no external run-time modules dependencies, requires core Perl modules only =item * the default behavior of exception class can be changed globally or just for the thrown exception =item * matching the exception by class, message or other attributes =item * matching with string, regex or closure function =item * creating automatically the derived exception classes (L interface) =item * easly expendable, see L class for example =item * prints just an error message or dumps full stack trace =item * can propagate (rethrow) an exception =item * can ignore some packages for stack trace output =item * some defaults (i.e. verbosity) can be different for different exceptions =back =for readme stop =cut use 5.006; use strict; use warnings; our $VERSION = '0.2401'; use utf8; ## no critic qw(ProhibitConstantPragma RequireArgUnpacking RequireCarping RequireCheckingReturnValueOfEval RequireInitializationForLocalVars) # Safe operations on symbol stash BEGIN { eval { require Symbol; Symbol::qualify_to_ref('Symbol::qualify_to_ref'); }; if (not $@) { *_qualify_to_ref = \*Symbol::qualify_to_ref; } else { *_qualify_to_ref = sub ($;) { no strict 'refs'; \*{ $_[0] } }; }; }; # Use weaken ref on stack if available BEGIN { eval { require Scalar::Util; my $ref = \1; Scalar::Util::weaken($ref); }; if (not $@) { *_HAVE_SCALAR_UTIL_WEAKEN = sub () { !! 1 }; } else { *_HAVE_SCALAR_UTIL_WEAKEN = sub () { !! 0 }; }; }; BEGIN { my %OVERLOADS = (fallback => 1); =head1 OVERLOADS =over =item Boolean context True value. See C method. eval { Exception::Base->throw( message=>"Message", value=>123 ) }; if ($@) { # the exception object is always true } =cut $OVERLOADS{'bool'} = 'to_bool'; =item Numeric context Content of attribute pointed by C attribute. See C method. eval { Exception::Base->throw( message=>"Message", value=>123 ) }; print 0+$@; # 123 =cut $OVERLOADS{'0+'} = 'to_number'; =item String context Content of attribute which is combined from C attributes with additional informations, depended on C setting. See C method. eval { Exception::Base->throw( message=>"Message", value=>123 ) }; print "$@"; # "Message at -e line 1.\n" =cut $OVERLOADS{'""'} = 'to_string'; =item "~~" Smart matching operator. See C method. eval { Exception::Base->throw( message=>"Message", value=>123 ) }; print "Message" ~~ $@; # 1 print qr/message/i ~~ $@; # 1 print ['Exception::Base'] ~~ $@; # 1 print 123 ~~ $@; # 1 print {message=>"Message", value=>123} ~~ $@; # 1 Warning: The smart operator requires that the exception object is a second argument. =back =cut $OVERLOADS{'~~'} = 'matches' if ($] >= 5.010); use overload; overload->import(%OVERLOADS); }; # Constant regexp for numerify value check use constant _RE_NUM_INT => qr/^[+-]?\d+$/; =head1 CONSTANTS =over =item ATTRS Declaration of class attributes as reference to hash. The attributes are listed as I => {I}, where I is a list of attribute properties: =over =item is Can be 'rw' for read-write attributes or 'ro' for read-only attributes. The attribute is read-only and does not have an accessor created if 'is' property is missed. =item default Optional property with the default value if the attribute value is not defined. =back The read-write attributes can be set with C constructor. Read-only attributes and unknown attributes are ignored. The constant have to be defined in derived class if it brings additional attributes. package Exception::My; use base 'Exception::Base'; # Define new class attributes use constant ATTRS => { %{Exception::Base->ATTRS}, # base's attributes have to be first readonly => { is=>'ro' }, # new ro attribute readwrite => { is=>'rw', default=>'blah' }, # new rw attribute }; package main; use Exception::Base ':all'; eval { Exception::My->throw( readwrite => 2 ); }; if ($@) { my $e = Exception::Base->catch; print $e->readwrite; # = 2 print $e->defaults->{readwrite}; # = "blah" } =back =cut BEGIN { my %ATTRS = (); =head1 ATTRIBUTES Class attributes are implemented as values of blessed hash. The attributes are also available as accessors methods. =over =cut =item message (rw, default: 'Unknown exception') Contains the message of the exception. It is the part of the string representing the exception object. eval { Exception::Base->throw( message=>"Message" ); }; print $@->message if $@; It can also be an array reference of strings and then the L is used to get a message. Exception::Base->throw( message => ["%s failed", __PACKAGE__] ); =cut $ATTRS{message} = { is => 'rw', default => 'Unknown exception' }; =item value (rw, default: 0) Contains the value which represents numeric value of the exception object in numeric context. eval { Exception::Base->throw( value=>2 ); }; print "Error 2" if $@ == 2; =cut $ATTRS{value} = { is => 'rw', default => 0 }; =item verbosity (rw, default: 2) Contains the verbosity level of the exception object. It allows to change the string representing the exception object. There are following levels of verbosity: =over 2 =item 0 Empty string =item 1 Message =item 2 Message at %s line %d. The same as the standard output of die() function. It doesn't include "at %s line %d." string if message ends with C<"\n"> character. This is the default option. =item 3 Class: Message at %s line %d %c_ = %s::%s() called in package %s at %s line %d ...propagated in package %s at %s line %d. ... The output contains full trace of error stack without first C lines and those packages which are listed in C and C settings. =item 4 The output contains full trace of error stack. In this case the C, C and C settings are meaning only for first line of exception's message. =back If the verbosity is undef, then the default verbosity for exception objects is used. If the verbosity set with constructor (C or C) is lower than 3, the full stack trace won't be collected. If the verbosity is lower than 2, the full system data (time, pid, tid, uid, euid, gid, egid) won't be collected. This setting can be changed with import interface. use Exception::Base verbosity => 4; It can be also changed for Perl interpreter instance, i.e. for debugging purposes. sh$ perl -MException::Base=verbosity,4 script.pl =cut $ATTRS{verbosity} = { is => 'rw', default => 2 }; =item ignore_package (rw) Contains the name (scalar or regexp) or names (as references array) of packages which are ignored in error stack trace. It is useful if some package throws an exception but this module shouldn't be listed in stack trace. package My::Package; use Exception::Base; sub my_function { do_something() or throw Exception::Base ignore_package=>__PACKAGE__; throw Exception::Base ignore_package => [ "My", qr/^My::Modules::/ ]; } This setting can be changed with import interface. use Exception::Base ignore_package => __PACKAGE__; =cut $ATTRS{ignore_package} = { is => 'rw', default => [ ] }; =item ignore_class (rw) Contains the name (scalar) or names (as references array) of packages which are base classes for ignored packages in error stack trace. It means that some packages will be ignored even the derived class was called. package My::Package; use Exception::Base; Exception::Base->throw( ignore_class => "My::Base" ); This setting can be changed with import interface. use Exception::Base ignore_class => "My::Base"; =cut $ATTRS{ignore_class} = { is => 'rw', default => [ ] }; =item ignore_level (rw) Contains the number of level on stack trace to ignore. It is useful if some package throws an exception but this module shouldn't be listed in stack trace. It can be used with or without I attribute. # Convert warning into exception. The signal handler ignores itself. use Exception::Base 'Exception::My::Warning'; $SIG{__WARN__} = sub { Exception::My::Warning->throw( message => $_[0], ignore_level => 1 ); }; =cut $ATTRS{ignore_level} = { is => 'rw', default => 0 }; =item time (ro) Contains the timestamp of the thrown exception. Collected if the verbosity on throwing exception was greater than 1. eval { Exception::Base->throw( message=>"Message" ); }; print scalar localtime $@->time; =cut $ATTRS{time} = { is => 'ro' }; =item pid (ro) Contains the PID of the Perl process at time of thrown exception. Collected if the verbosity on throwing exception was greater than 1. eval { Exception::Base->throw( message=>"Message" ); }; kill 10, $@->pid; =cut $ATTRS{pid} = { is => 'ro' }; =item tid (ro) Contains the tid of the thread or undef if threads are not used. Collected if the verbosity on throwing exception was greater than 1. =cut $ATTRS{tid} = { is => 'ro' }; =item uid (ro) =cut $ATTRS{uid} = { is => 'ro' }; =item euid (ro) =cut $ATTRS{euid} = { is => 'ro' }; =item gid (ro) =cut $ATTRS{gid} = { is => 'ro' }; =item egid (ro) Contains the real and effective uid and gid of the Perl process at time of thrown exception. Collected if the verbosity on throwing exception was greater than 1. =cut $ATTRS{egid} = { is => 'ro' }; =item caller_stack (ro) Contains the error stack as array of array with informations about caller functions. The first 8 elements of the array's row are the same as first 8 elements of the output of C function. Further elements are optional and are the arguments of called function. Collected if the verbosity on throwing exception was greater than 1. Contains only the first element of caller stack if the verbosity was lower than 3. If the arguments of called function are references and C::weaken> function is available then reference is weakened. eval { Exception::Base->throw( message=>"Message" ); }; ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, @args) = $@->caller_stack->[0]; =cut $ATTRS{caller_stack} = { is => 'ro' }; =item propagated_stack (ro) Contains the array of array which is used for generating "...propagated at" message. The elements of the array's row are the same as first 3 elements of the output of C function. =cut $ATTRS{propagated_stack} = { is => 'ro' }; =item max_arg_len (rw, default: 64) Contains the maximal length of argument for functions in backtrace output. Zero means no limit for length. sub a { Exception::Base->throw( max_arg_len=>5 ) } a("123456789"); =cut $ATTRS{max_arg_len} = { is => 'rw', default => 64 }; =item max_arg_nums (rw, default: 8) Contains the maximal number of arguments for functions in backtrace output. Zero means no limit for arguments. sub a { Exception::Base->throw( max_arg_nums=>1 ) } a(1,2,3); =cut $ATTRS{max_arg_nums} = { is => 'rw', default => 8 }; =item max_eval_len (rw, default: 0) Contains the maximal length of eval strings in backtrace output. Zero means no limit for length. eval "Exception->throw( max_eval_len=>10 )"; print "$@"; =cut $ATTRS{max_eval_len} = { is => 'rw', default => 0 }; =item defaults Meta-attribute contains the list of default values. my $e = Exception::Base->new; print defined $e->{verbosity} ? $e->{verbosity} : $e->{defaults}->{verbosity}; =cut $ATTRS{defaults} = { }; =item default_attribute (default: 'message') Meta-attribute contains the name of the default attribute. This attribute will be set for one argument throw method. This attribute has meaning for derived classes. use Exception::Base 'Exception::My' => { has => 'myattr', default_attribute => 'myattr', }; eval { Exception::My->throw("string") }; print $@->myattr; # "string" =cut $ATTRS{default_attribute} = { default => 'message' }; =item numeric_attribute (default: 'value') Meta-attribute contains the name of the attribute which contains numeric value of exception object. This attribute will be used for representing exception in numeric context. use Exception::Base 'Exception::My' => { has => 'myattr', numeric_attribute => 'myattr', }; eval { Exception::My->throw(myattr=>123) }; print 0 + $@; # 123 =cut $ATTRS{numeric_attribute} = { default => 'value' }; =item eval_attribute (default: 'message') Meta-attribute contains the name of the attribute which is filled if error stack is empty. This attribute will contain value of C<$@> variable. This attribute has meaning for derived classes. use Exception::Base 'Exception::My' => { has => 'myattr', eval_attribute => 'myattr' }; eval { die "string" }; print $@->myattr; # "string" =cut $ATTRS{eval_attribute} = { default => 'message' }; =item string_attributes (default: ['message']) Meta-attribute contains the array of names of attributes with defined value which are joined to the string returned by C method. If none of attributes are defined, the string is created from the first default value of attributes listed in the opposite order. use Exception::Base 'Exception::My' => { has => 'myattr', myattr => 'default', string_attributes => ['message', 'myattr'], }; eval { Exception::My->throw( message=>"string", myattr=>"foo" ) }; print $@->myattr; # "string: foo" eval { Exception::My->throw() }; print $@->myattr; # "default" =back =cut $ATTRS{string_attributes} = { default => [ 'message' ] }; *ATTRS = sub () { \%ATTRS }; }; # Cache for class' ATTRS my %Class_Attributes; # Cache for class' defaults my %Class_Defaults; # Cache for $obj->isa(__PACKAGE__) my %Isa_Package; =head1 IMPORTS =over =item C' => I;> Changes the default value for I. If the I name has no special prefix, its default value is replaced with a new I. use Exception::Base verbosity => 4; If the I name starts with "C<+>" or "C<->" then the new I is based on previous value: =over =item * If the original I was a reference to array, the new I can be included or removed from original array. Use array reference if you need to add or remove more than one element. use Exception::Base "+ignore_packages" => [ __PACKAGE__, qr/^Moose::/ ], "-ignore_class" => "My::Good::Class"; =item * If the original I was a number, it will be incremented or decremented by the new I. use Exception::Base "+ignore_level" => 1; =item * If the original I was a string, the new I will be included. use Exception::Base "+message" => ": The incuded message"; =back =item C', ...;> Loads additional exception class module. If the module is not available, creates the exception class automatically at compile time. The newly created class will be based on C class. use Exception::Base qw{ Exception::Custom Exception::SomethingWrong }; Exception::Custom->throw; =item C' => { isa => I, version => I, ... };> Loads additional exception class module. If the module's version is lower than given parameter or the module can't be loaded, creates the exception class automatically at compile time. The newly created class will be based on given class and has the given $VERSION variable. =over =item isa The newly created class will be based on given class. use Exception::Base 'Exception::My', 'Exception::Nested' => { isa => 'Exception::My }; =item version The class will be created only if the module's version is lower than given parameter and will have the version given in the argument. use Exception::Base 'Exception::My' => { version => 1.23 }; =item has The class will contain new rw attibute (if parameter is a string) or new rw attributes (if parameter is a reference to array of strings) or new rw or ro attributes (if parameter is a reference to hash of array of strings with rw and ro as hash key). use Exception::Base 'Exception::Simple' => { has => 'field' }, 'Exception::More' => { has => [ 'field1', 'field2' ] }, 'Exception::Advanced' => { has => { ro => [ 'field1', 'field2' ], rw => [ 'field3' ] } }; =item message =item verbosity =item max_arg_len =item max_arg_nums =item max_eval_len =item I The class will have the default property for the given attribute. =back use Exception::Base 'Exception::WithDefault' => { message => 'Default message' }, 'Exception::Reason' => { has => [ 'reason' ], string_attributes => [ 'message', 'reason' ] }; =back =cut # Create additional exception packages sub import { my $class = shift; while (defined $_[0]) { my $name = shift @_; if ($name eq ':all') { # do nothing for backward compatibility } elsif ($name =~ /^([+-]?)([a-z0-9_]+)$/) { # Lower case: change default my ($modifier, $key) = ($1, $2); my $value = shift; $class->_modify_default($key, $value, $modifier); } else { # Try to use external module my $param = {}; $param = shift @_ if defined $_[0] and ref $_[0] eq 'HASH'; my $version = defined $param->{version} ? $param->{version} : 0; if (caller ne $name) { next if eval { $name->VERSION($version) }; # Package is needed { local $SIG{__DIE__}; eval { $class->_load_package($name, $version); }; }; if ($@) { # Die unless can't load module if ($@ !~ /Can\'t locate/) { Exception::Base->throw( message => ["Can not load available %s class: %s", $name, $@], verbosity => 1 ); }; } else { # Module is loaded: go to next next; }; }; next if $name eq __PACKAGE__; # Package not found so it have to be created if ($class ne __PACKAGE__) { Exception::Base->throw( message => ["Exceptions can only be created with %s class", __PACKAGE__], verbosity => 1 ); }; $class->_make_exception($name, $version, $param); } } return $class; }; =head1 CONSTRUCTORS =over =item new([%I]) Creates the exception object, which can be thrown later. The system data attributes like C