Why the latest File::Temp might surprise you

There was a subtle API change in File::Temp 0.23 that improves consistency, but might break old, buggy code.

Prior to 0.23, here was the calling signature for the functional and object oriented interfaces for File::Temp (with some creative spacing to show the problem):

# functional
my ( $fh, $filename ) = tempfile( $template, %options );
my $tempdir           = tempdir ( $template, %options );

# object oriented
my $tmp = File::Temp->new       (            %options );
my $dir = File::Temp->newdir    ( $template, %options );

Notice how new() doesn't take a template argument. Instead, you're supposed to pass it as an option in the %options hash: TEMPLATE => 'tempXXXXX'.

Frankly, this interface sucks. There are too many ways to get confused or do it wrong:

  • What happens if you pass a leading template to new()?
  • What happens if you leave off the leading template for newdir()?
  • What happens if you pass a TEMPLATE option to newdir(), tempfile() or tempdir()?
  • What happens if you call tempfile() or tempdir() as methods?

A test program

Here's a little test program to try out some variations. Notice that a leading template argument is 'arg_XXXX' and a TEMPLATE option is 'opt_XXXX', so we can see which takes precedence if we try with both:

#!/usr/bin/env perl
use v5.10;
use strict;
use warnings;
use File::Temp qw/tempfile tempdir/;

my @cases = (
    # documented API
    q{tempfile            ('arg_XXXX'                        )},
    q{tempdir             ('arg_XXXX'                        )},
    q{File::Temp->new     (            TEMPLATE => 'opt_XXXX')},
    q{File::Temp->newdir  ('arg_XXXX'                        )},

    # variations with both arg and TEMPLATE
    q{tempfile            ('arg_XXXX', TEMPLATE => 'opt_XXXX')},
    q{tempdir             ('arg_XXXX', TEMPLATE => 'opt_XXXX')},
    q{File::Temp->new     ('arg_XXXX', TEMPLATE => 'opt_XXXX')},
    q{File::Temp->newdir  ('arg_XXXX', TEMPLATE => 'opt_XXXX')},

    # newdir called like new
    q{File::Temp->newdir  (            TEMPLATE => 'opt_XXXX')},

    # functions called as methods
    q{File::Temp->tempfile('arg_XXXX'                        )},
    q{File::Temp->tempdir ('arg_XXXX'                        )},
    q{File::Temp->tempfile('arg_XXXX', TEMPLATE => 'opt_XXXX')},
    q{File::Temp->tempdir ('arg_XXXX', TEMPLATE => 'opt_XXXX')},
    q{File::Temp->tempfile(            TEMPLATE => 'opt_XXXX')},
    q{File::Temp->tempdir (            TEMPLATE => 'opt_XXXX')},
);

for my $c ( @cases ) {
    my @result = eval $c;
    my $err = $@;
    $err =~ s/\n.*//ms;
    say $c;
    say "    " . ( $result[-1] ? "Got $result[-1]" : $err ) . "\n";
}

Results with File::Temp 0.22

Here are the result running under File::Temp 0.22 for the documented API:

tempfile            ('arg_XXXX'                        )
    Got arg_Y9B5

tempdir             ('arg_XXXX'                        )
    Got arg_Joq0

File::Temp->new     (            TEMPLATE => 'opt_XXXX')
    Got opt_p9I5

File::Temp->newdir  ('arg_XXXX'                        )
    Got arg_PmNf

That's just as we expect.

Now, let's try those odd cases. First, calling everything with both a leading template and a TEMPLATE option:

tempfile            ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got arg_gIL3

tempdir             ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got arg_xPXg

File::Temp->new     ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got /var/folders/5t/sy1gxkwj2l1gfd20s2g470200000gn/T/AYeB74PT0K

File::Temp->newdir  ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got arg_GfwP

For everything except new(), the TEMPLATE argument is ignored and the leading argument works just like in the documented API. But how about new()? You see what's happening don't you? Here's what it thinks you did:

File::Temp->new( arg_XXXX => 'TEMPLATE', opt_XXXX => undef );

Since none of those keys are known, it uses the default directory and template.

What about more wrong variations:

File::Temp->newdir  (            TEMPLATE => 'opt_XXXX')
    Got /var/folders/5t/sy1gxkwj2l1gfd20s2g470200000gn/T/AkI6pFjyq_

File::Temp->tempfile('arg_XXXX'                        )
    Got /var/folders/5t/sy1gxkwj2l1gfd20s2g470200000gn/T/3F2V8UPIbx

File::Temp->tempdir ('arg_XXXX'                        )
    Got /var/folders/5t/sy1gxkwj2l1gfd20s2g470200000gn/T/aSljGO6feU

File::Temp->tempfile('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got /var/folders/5t/sy1gxkwj2l1gfd20s2g470200000gn/T/MGCo_TSXX5

File::Temp->tempdir ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got /var/folders/5t/sy1gxkwj2l1gfd20s2g470200000gn/T/TEAXNoECbB

We get more weird behavior. The newdir method doesn't know about TEMPLATE. And calling functions as methods is like doing this:

tempfile( 'File::Temp' => 'arg_XXXX', TEMPLATE => 'opt_XXXX' );

Again, it can't find the template and the default is used.

And finally, there's this:

File::Temp->tempfile(            TEMPLATE => 'opt_XXXX')
    Error in tempfile() using File::Temp: The template must end with at least 4 'X' characters

File::Temp->tempdir (            TEMPLATE => 'opt_XXXX')
    Error in tempdir() using File::Temp: The template must end with at least 4 'X' characters

Why is that an error when the previous method calls weren't? Because it looks like this:

tempfile( 'File::Temp', TEMPLATE => 'opt_XXXX' );

Since there are an odd number of arguments, it thinks it was given a (bad) leading template and some arguments.

If you're ready to facepalm, go right ahead.

What about File::Temp 0.23

In 0.23, sanity (of a sort) returns. All the functions and methods now respect both ways of specifying a template.

tempfile            ('arg_XXXX', TEMPLATE => 'opt_XXXX'); # fine
File::Temp->newdir  (            TEMPLATE => 'opt_XXXX'); # fine

If you specify both, the last one wins, just as if you gave multiple TEMPLATE arguments.

But there is a catch.

Calling the functions as methods is now an error. In 0.22, you could call functions as methods and File::Temp would (usually) just quietly give you a tempfile where you didn't expect it. That was a bug and now it's a fatal error.

Here's the same test program under 0.2301:

tempfile            ('arg_XXXX'                        )
    Got arg_l2TB

tempdir             ('arg_XXXX'                        )
    Got arg_5y15

File::Temp->new     (            TEMPLATE => 'opt_XXXX')
    Got opt_sziU

File::Temp->newdir  ('arg_XXXX'                        )
    Got arg_3imY

tempfile            ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got opt_NTAn

tempdir             ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got opt_TZzT

File::Temp->new     ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got opt_CFPu

File::Temp->newdir  ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    Got opt_ueeQ

File::Temp->newdir  (            TEMPLATE => 'opt_XXXX')
    Got opt_vkNh

File::Temp->tempfile('arg_XXXX'                        )
    'tempfile' can't be called as a method at (eval 19) line 1.

File::Temp->tempdir ('arg_XXXX'                        )
    'tempdir' can't be called as a method at (eval 20) line 1.

File::Temp->tempfile('arg_XXXX', TEMPLATE => 'opt_XXXX')
    'tempfile' can't be called as a method at (eval 21) line 1.

File::Temp->tempdir ('arg_XXXX', TEMPLATE => 'opt_XXXX')
    'tempdir' can't be called as a method at (eval 22) line 1.

File::Temp->tempfile(            TEMPLATE => 'opt_XXXX')
    'tempfile' can't be called as a method at (eval 23) line 1.

File::Temp->tempdir (            TEMPLATE => 'opt_XXXX')
    'tempdir' can't be called as a method at (eval 24) line 1.

If you are calling functions as methods, your code will break. This is sensible because functions and method have very different scope implications.

  • Functions are "global": files and directories get cleaned up at the end of the program
  • Methods are "lexical": they return objects that clean up when the object is destroyed

If you are calling a function as a method, File::Temp has no way to know which way you want it and so it can't DWIM. So BOOM! It dies.

Now go fix your code.

This entry was posted in perl programming and tagged , , . Bookmark the permalink. Both comments and trackbacks are currently closed.

4 Comments

  1. Posted May 9, 2013 at 1:33 pm | Permalink

    I ran into the "'tempfile' can't be called as a method" failure when running 'git svn dcommit'.

    Speaking as an end-user, it would have been nice to maybe have a deprecation "grace period" for a couple months wherein the module printed a warning, but then did the best it could to do "a" right thing, rather than simply break:


    sub tempfile {
    if ( @_ && $_[0] eq 'File::Temp' ) {
    # This will be deprecated in v0.24; see http://.../ for details
    carp "'tempfile' shouldn't be called as a method";
    shift(@_);
    }
    # ...
    }

    That way, downstream developers would hear about the problem (and still have time to fix it) but existing applications wouldn't up and die... yet. :-}

    • Posted May 9, 2013 at 2:58 pm | Permalink

      If it were a supported feature, sure, deprecation would make sense.

      But making erroneous usage fatal doesn't rise to that standard, as far as I'm concerned.

      I bet things get fixed faster when they break than when they warn, too.

  2. Posted May 9, 2013 at 4:43 pm | Permalink

    IMHO (and, since I'm not the one maintaining the code, I realize it's a *VERY* humble opinion), the hard-line approach is a little bit like the old "cutting off your nose to spite your face" adage:

    * "My system worked, then I updated CPAN; now my system doesn't work. Ergo, CPAN must be to blame."

    * The end user -- who isn't the one who made the mistake in the first place, and isn't responsible for fixing it (assuming he/she even knows HOW) -- is the one who suffers.

    I guess I just consider this an example of when the "Right" thing to do isn't necessarily the "Best" thing to do... «shrug»

    Note: I'm certainly not arguing that the entire history of misfeatures has to be preserved for compatibility -- e.g. Windows -- but keeping an ecosystem robust requires at least a moderately conservative approach to mitigate unforeseen failures, methinks. (Sure, making an omelet means breaking a few eggs, but you could at least do it over the sink, rather than spilling food all over the floor...)

    At any rate, keep up the good work! Personal frustrations aside[*], I heartily respect the effort and commitment you and the rest of the Perl community have invested to make the world a better place. :-D

    [* - FYI, the broken method invocation came after a good day and a half of being unable to check in changes at $work; I had to reinstall SVN::Base / Alien::SVN under Mountain Lion, which wasn't working due to a number of non-universal -- i386 vs. x86_64 -- shared libraries, both in the Subversion build itself and its "libneon.dylib" dependency. Finally, after getting all the right "CFLAGS=" options figured out over there, this otherwise trivial bug popped up. Sorry if it was the proverbial 'last straw'...]

    • Justin
      Posted December 2, 2013 at 3:10 pm | Permalink

      I totally agree with Dabe, although the fix was in the "right", it makes CPAN look bad to people who have come to rely on it. What if it wasn't my code that was taking advantage of this "bug"? Just saying "now go fix your code" is a bit snarky to say the least. No wonder Perl is taking a steep dive into a shallow pool.

© 2009-2014 David Golden All Rights Reserved