perlglob

Why is glob failing after the first iteration of foreach


I'm new to Perl and seem to have come across some behavior that I don't understand and was unexpected.

I'm trying to write a function that attempts to load a config file from a list of possible locations stored in an array. I'm using a foreach loop to iterate over the array and check for the existence of the file, but after the first iteration, glob is returning undef, even when the value passed to it is a static, single-quoted string. Why?

Here is the code:

package MyPackage;
use warnings; use strict;

our @ConfigSearchPaths = (".", "~");
our $DefaultConfigName = ".my_config_file";

sub load_user_config
{
    my ( $obj, $filename ) = @_;

    $filename ||= $DefaultConfigName;

    CONFIG_SEARCH: foreach my $search_path (@ConfigSearchPaths)
    {
        my $file_path = glob( "$search_path/$filename" );

        # I added this line to test if the issue was related to interpolation
        # but to my surprise, this also returns 'undef' after the first iteration.
        my $file_path_2 = glob( '~/.my_config_file' );

        if( -r $file_path )
        {
            # Parse the file...
        }
    }
}

Solution

  • The answer is in the documentation: see glob.

    In scalar context, glob iterates through such filename expansions, returning undef when the list is exhausted.

    In other words: in the first iteration, glob returns the file itself; on the second iteration, it returns undef, as there aren't any more matches.

    By assigning to something that isn't an aggregate, you are using scalar context. ( See also: Scalar vs List Assignment Operator. )

    Keep in mind that in scalar context, glob does not observe the changes to $search_path and instead iterates over the list results of the first call, using the non-interpolated string to "remember" the list being iterated.

    If you don't want to iterate, use list context:

        my @file_paths  = glob "$search_path/$filename";  # Captures the whole array
        # or
        my ($file_path) = glob "$search_path/$filename";  # Discards all but the first item
    

    I'd recommend reaching for Path::Tiny or a similar module to handle paths for you.