Why the latest File::Temp might surprise you

Reading time: 5 minutes

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.

•      â€¢      â€¢

If you enjoyed this or have feedback, please let me know by or