Intercept warnings with a __WARN__ handler

Perl defines two internal pseudo-signals that you can trap. There’s one for die, which I covered in Override die with END or CORE::GLOBAL::die and eventually told you not to use. There’s also one for warn that’s quite safe to use when you need to intercept warnings.

To catch a warning, you set a signal handler for the __WARN__ pseudo-signal. The underscores around the name distinguish it from the external signals, such as INT and USR1. The value can be the name of a subroutine or a reference to a subroutine:

$SIG{__WARN__} = 'some_sub';
$SIG{__WARN__} = \&some_sub;
$SIG{__WARN__} = sub { ... };

Replacing the default behavior is a good use for a __WARN__ handler. The cluck subroutine from Carp turns the warning message into a backtrace. If you want that for all warnings, you set it up as early as possible:

BEGIN { $SIG{__WARN__} = \&Carp::cluck; }

You don’t need to change all warnings for the entire program, though. If you need to track down the code that triggers the warning, you probably want to limit your replacement behavior to the code you’re investigating:

{
local $SIG{__WARN__} = \&Carp::cluck;
...;
}

Let’s have more fun, though.

Something more fun

You can get more fancy though, because you can do almost anything you like. Here’s a little program that issues several warnings, which at first you won’t intercept. This is a nonsense program that only exists to generate warnings, some of which you may have never seen before. Note the use of Perl 5.12 for the completely legal .... Since you never call chomp, the runtime never gets a chance to make those fatal errors even though they compile just fine:

use warnings;
use v5.12;

sub chomp { ... };
*chomp = sub { ... };
chomp( $ARGV[0] );
my $sum;
exec 'Buster';
print (STDOUT), 1, 2, 3;
print $a;
accept( SOCKET, GENERIC );
connect( SOCKET, 'Mimi' );
chmod 777, 'Mimi';
open FOO, '|Buster|';
close FOO;

say 'At the end!';

The warnings are legion:

Ambiguous call resolved as CORE::chomp(), qualify as such or use & at warnings line 8.
print (...) interpreted as function at warnings line 14.
Useless use of a constant (2) in void context at warnings line 14.
Useless use of a constant (3) in void context at warnings line 14.
Statement unlikely to be reached at warnings line 14.
        (Maybe you meant system() when you said exec()?)
Name "main::a" used only once: possible typo at warnings line 15.
Name "main::GENERIC" used only once: possible typo at warnings line 17.
Subroutine main::chomp redefined at warnings line 6.
Use of uninitialized value $ARGV[0] in scalar chomp at warnings line 8.
Can't exec "Buster": No such file or directory at warnings line 11.
Use of uninitialized value $_ in print at warnings line 14.
Use of uninitialized value $a in print at warnings line 15.
accept() on unopened socket GENERIC at warnings line 17.
connect() on unopened socket SOCKET at warnings line 18.
Can't open bidirectional pipe at warnings line 20.
Can't exec "Buster": No such file or directory at warnings line 20.
At the end!

Suppose that you wanted to count those warnings, though? You could set up a handler. Since many of those warnings are from the compile-phase, you have to set up the handler at compile time by using a BEGIN block:

use strict;
use warnings;
use v5.12;

BEGIN {
	$SIG{__WARN__} = sub {
		state $count = 0;
		printf '[%04d] %s', $count++, @_;
		};
	}

sub chomp { ... };
*chomp = sub { ... };
chomp( $ARGV[0] );
my $sum;
exec 'Buster';
print (STDOUT), 1, 2, 3;
print $a;
accept( SOCKET, GENERIC );
connect( SOCKET, 'Mimi' );
chmod 777, 'Mimi';
open FOO, '|Buster|';
close FOO;

say 'At the end!';

Now you see each warning has a number:

[0000] Ambiguous call resolved as CORE::chomp(), qualify as such or use & at warnings line 14.
[0001] print (...) interpreted as function at warnings line 17.
[0002] Useless use of a constant (2) in void context at warnings line 17.
[0003] Useless use of a constant (3) in void context at warnings line 17.
[0004] Statement unlikely to be reached at warnings line 17.
[0005]  (Maybe you meant system() when you said exec()?)
[0006] Name "main::a" used only once: possible typo at warnings line 18.
[0007] Name "main::GENERIC" used only once: possible typo at warnings line 19.
[0008] Subroutine main::chomp redefined at warnings line 13.
[0009] Use of uninitialized value $ARGV[0] in scalar chomp at warnings line 14.
[0010] Can't exec "Buster": No such file or directory at warnings line 16.
[0011] Use of uninitialized value $_ in print at warnings line 17.
[0012] Use of uninitialized value $a in print at warnings line 18.
[0013] accept() on unopened socket GENERIC at warnings line 19.
[0014] connect() on unopened socket SOCKET at warnings line 20.
[0015] Can't open bidirectional pipe at warnings line 22.
[0016] Can't exec "Buster": No such file or directory at warnings line 22.
At the end!

That’s interesting, but it can be even more interesting. Can you label the ones that are from the compile phase? You can check the phase with the ${^GLOBAL_PHASE} variable added to Perl 5.14:

use v5.14;

BEGIN {
	$SIG{__WARN__} = sub {
		state $count = 0;
		printf '[%04d] %s - %s', $count++, ${^GLOBAL_PHASE}, @_;
		};
	}
	
# ... rest of program

Now the output shows the phase too:

[0000] START - Ambiguous call resolved as CORE::chomp(), qualify as such or use & at warnings line 14.
[0001] START - print (...) interpreted as function at warnings line 17.
[0002] START - Useless use of a constant (2) in void context at warnings line 17.
[0003] START - Useless use of a constant (3) in void context at warnings line 17.
[0004] START - Statement unlikely to be reached at warnings line 17.
[0005] START -  (Maybe you meant system() when you said exec()?)
[0006] START - Name "main::a" used only once: possible typo at warnings line 18.
[0007] START - Name "main::GENERIC" used only once: possible typo at warnings line 19.
[0008] RUN - Subroutine main::chomp redefined at warnings line 13.
[0009] RUN - Use of uninitialized value $ARGV[0] in scalar chomp at warnings line 14.
[0010] RUN - Can't exec "Buster": No such file or directory at warnings line 16.
[0011] RUN - Use of uninitialized value $_ in print at warnings line 17.
[0012] RUN - Use of uninitialized value $a in print at warnings line 18.
[0013] RUN - accept() on unopened socket GENERIC at warnings line 19.
[0014] RUN - connect() on unopened socket SOCKET at warnings line 20.
[0015] RUN - Can't open bidirectional pipe at warnings line 22.
[0016] RUN - Can't exec "Buster": No such file or directory at warnings line 22.
At the end!

Now each phase has its own warning counter:

START-0000  Ambiguous call resolved as CORE::chomp(), qualify as such or use & at warnings line 15.
START-0001  print (...) interpreted as function at warnings line 18.
START-0002  Useless use of a constant (2) in void context at warnings line 18.
START-0003  Useless use of a constant (3) in void context at warnings line 18.
START-0004  Statement unlikely to be reached at warnings line 18.
START-0005      (Maybe you meant system() when you said exec()?)
START-0006  Name "main::a" used only once: possible typo at warnings line 19.
START-0007  Name "main::GENERIC" used only once: possible typo at warnings line 20.
RUN-0000  Subroutine main::chomp redefined at warnings line 14.
RUN-0001  Use of uninitialized value $ARGV[0] in scalar chomp at warnings line 15.
RUN-0002  Can't exec "Buster": No such file or directory at warnings line 17.
RUN-0003  Use of uninitialized value $_ in print at warnings line 18.
RUN-0004  Use of uninitialized value $a in print at warnings line 19.
RUN-0005  accept() on unopened socket GENERIC at warnings line 20.
RUN-0006  connect() on unopened socket SOCKET at warnings line 21.
RUN-0007  Can't open bidirectional pipe at warnings line 23.
RUN-0008  Can't exec "Buster": No such file or directory at warnings line 23.
At the end!

This leads to a deliciously evil plan: what if you can stop your program from running if it had more warnings than it did on the last run? The Test::Perl::Critic::Progressive module that already does something similar for Perl::Critic. Inside this __WARN__, you can use a die to stop the program:

use strict;
use warnings;
use v5.12;

BEGIN {
	my $file = "$0.warn";
	my $count = {};

	$SIG{__WARN__} = sub { # refactor when you figure it out
		state $previous_counts = do { 
			unless( -e $file ) { my $hash = {} }
			else {
				local @ARGV = $file;
				my $hash;
				while( <> ) {
					chomp;
					my( $phase, $count ) = split;
					$hash->{$phase} = $count;
					}
				$hash;
				}
			};
		
		$count->{${^GLOBAL_PHASE}}++;
		
		die "Too many warnings in ${^GLOBAL_PHASE}\n"
			if $count->{${^GLOBAL_PHASE}} > 
				( $previous_counts->{${^GLOBAL_PHASE}} // 0 ); #/

		printf '%s-%04d  %s', 
			${^GLOBAL_PHASE}, $count->{${^GLOBAL_PHASE}}, @_;

		};

	END { # inside a BEGIN!
		open my $f, '>', $file;
		while( my( $k, $v ) = each %$count ) {
			say $f "$k $v";
			} 
		}
	}

sub chomp { ... };
*chomp = sub { ... };

# chomp( @ARGV );  # uncomment for another warning

say 'At the end!';

When you run this, the program stops when it encounters more errors that it did before:

$ perl5.14.1 warnings
START-0001  Ambiguous call resolved as CORE::chomp(), qualify as such or use & at warnings line 46.
Too many warnings in RUN

Things to remember

  • You can intercept warnings with $SIG{__WARN__}
  • Set up $SIG{__WARN__} in a BEGIN to intercept warnings right away

One thought on “Intercept warnings with a __WARN__ handler”

Comments are closed.