Perl CI Coverage Status CPAN

Name

File::pfopen - Try hard to find a file

Version

Version 0.04

Synopsis

Find a configuration file in the first of several places, and read it:

use File::pfopen 'pfopen';

my $fh = pfopen('/etc/myapp:/usr/local/etc/myapp', 'config', 'yml:json', '<');
if($fh) {
    my @lines = <$fh>;
    close($fh);
}

Find out which file was found, by asking for two return values:

use File::pfopen 'pfopen';

my ($fh, $filename) = pfopen('/etc/myapp:/usr/local/etc/myapp', 'config', 'yml:json', '<');
print "Using $filename\n" if($fh);

Build the list of directories as an array, which works the same on every operating system:

use File::Spec;
use File::pfopen 'pfopen';

my @dirs = (File::Spec->catdir($ENV{'HOME'}, '.myapp'), File::Spec->tmpdir());
my $fh = pfopen(\@dirs, 'state', 'dat', '<');

Use a full filename, with no list of suffixes:

use File::pfopen 'pfopen';

my $fh = pfopen('/var/tmp:/tmp', 'report.csv', undef, '<');

Use drive letters on Windows, where the directories are separated by ';':

use File::pfopen 'pfopen';

my $fh = pfopen('C:\myapp;D:\shared\myapp', 'config', 'ini', '<');

Open a found file to add to it, and read text in UTF-8:

use File::pfopen 'pfopen';

my $log = pfopen(['/var/log/myapp', '/tmp'], 'activity', 'log', '>>');
my $text = pfopen(['/usr/share/myapp'], 'messages', 'txt', '<:encoding(UTF-8)');

Description

This module has one function, pfopen. You give it a list of directories, a filename, and optionally a list of endings (called suffixes) for the filename. It looks for the file in each directory, in order, and opens the first one that it finds.

For example, with the directories /etc and /tmp, the filename foo and the suffixes txt:bin, it tries these files, in this order, and stops at the first one that it can open:

/etc/foo.txt
/etc/foo.bin
/tmp/foo.txt
/tmp/foo.bin

It is similar to the way that a shell uses the PATH environment variable to find a program.

The function remembers each file that it finds. If you ask for the same thing again, it opens the same file again without looking. See "COMMON PITFALLS".

Subroutines/Methods

Pfopen($Path, $Prefix, $Suffixes, $Mode)

my $fh = pfopen($path, $prefix);
my $fh = pfopen($path, $prefix, $suffixes);
my $fh = pfopen($path, $prefix, $suffixes, $mode);
my ($fh, $filename) = pfopen($path, $prefix, $suffixes, $mode);

Purpose

Look in a list of directories for a file, optionally trying a list of suffixes, and open the first file that it finds.

It only finds files. It never returns a directory. It looks in the directories in order. In each directory, it tries the suffixes in order. So a file in an earlier directory is used before a file with an earlier suffix.

It remembers each successful search until the program ends. When it is called again with the same $path, $prefix and $suffixes, it opens the same file again and does not search. If that file can no longer be opened, it forgets the file and searches again. A string and an array reference that give the same directories count as the same $path, so ['/a', '/b'] and '/a:/b' share one remembered file.

Arguments

Returns

Side Effects

It remembers the filename of each successful search, as described in "Purpose". It also forgets a remembered filename when that file can no longer be opened. In taint mode, a call with any tainted argument always searches, and doesn't remember what it finds, so a remembered filename can't hide or add taint.

It does not change $!, $@, $_, or any of its arguments.

Errors

It croaks (dies with an error message that gives your file and line number) with:

It does not croak or warn when a file can't be found or can't be opened.

When Perl runs in taint mode (-T) and $path or $prefix is tainted, a mode that writes, including the default '+<', dies with Perl's own error Insecure dependency in open while running with -T switch. Use the mode '<', or untaint the arguments first.

Usage Example

use File::pfopen 'pfopen';

my ($fh, $filename) = pfopen('/tmp:/var/tmp:/home/njh/tmp', 'foo', 'txt:bin');
my $fh2 = pfopen('/tmp:/var/tmp:/home/njh/tmp', 'foo', undef, '<');
my $fh3 = pfopen(['/tmp', '/var/tmp'], 'foo', 'txt');

Api Specification

The arguments are positional. The schemas are in the format of Params::Validate::Strict and Return::Set. The output schema describes the list that is returned in list context. In scalar context the return value is the first element of that list, or undef.

Input
[
    { name => 'path', type => ['string', 'arrayref'], position => 0 },
    { name => 'prefix', type => 'string', min => 1, position => 1 },
    { name => 'suffixes', type => 'string', optional => 1, position => 2 },
    { name => 'mode', type => 'string', optional => 1, default => '+<', position => 3, matches => qr/\A\+?(?:<|>>?)(?::.*)?\z/s },
]
Output
{
    type => 'arrayref',
    min => 0,
    max => 2,
    callback => sub { (scalar(@{$_[0]}) == 0) || ((scalar(@{$_[0]}) == 2) && defined(Scalar::Util::openhandle($_[0]->[0])) && !ref($_[0]->[1])) },
}

Common Pitfalls

Encoding

Bugs

In taint mode, writing to a file with a tainted name dies. See "Errors".

Would be better if the mode and suffixes options were the other way around, but it's too late to change that now.

Please report any bugs or feature requests to bug-file-pfopen at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=File-pfopen. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

Support

You can find documentation for this module with the perldoc command.

perldoc File::pfopen

You can also look for information at:

Author

Nigel Horne, <njh at nigelhorne.com>

Formal Specification

This section describes pfopen in the Z notation. You do not need to read it to use the module.

Pfopen

[PATH, NAME, HANDLE]

FILEMODE == { "<", ">", ">>", "+<", "+>", "+>>" }
MODE == { m : seq CHAR | (∃ f : FILEMODE; l : seq CHAR • m = f ⁀ l ∧ (l = ⟨⟩ ∨ head l = ':')) }
KEY == NAME × seq NAME × seq PATH

isFile, readable : ℙ PATH
opens : PATH ↔ MODE

┌─ PfopenState ───────────────────────────────────────────────────
│ cache : KEY ⇸ PATH
└─────────────────────────────────────────────────────────────────

┌─ InitPfopenState ───────────────────────────────────────────────
│ PfopenState'
├─────────────────────────────────────────────────────────────────
│ cache' = ∅
└─────────────────────────────────────────────────────────────────

┌─ PfopenArgs ────────────────────────────────────────────────────
│ dirs? : seq PATH
│ prefix? : NAME
│ suffixes? : seq NAME ∪ {⊥}
│ mode? : MODE
│ key : KEY
│ candidates : seq PATH
│ usable : ℙ PATH
├─────────────────────────────────────────────────────────────────
│ prefix? ≠ ⟨⟩
│ '\0' ∉ ran prefix?
│ key = (prefix?, suffixes?, dirs?)
│ suffixes? = ⊥ ⇒ candidates = ⟨ d / prefix? | d ← dirs? ⟩
│ suffixes? ≠ ⊥ ⇒ candidates = ⟨ d / (prefix? ⁀ "." ⁀ s) | d ← dirs?, s ← suffixes? ⟩
│ usable = { c : PATH | c ∈ isFile ∧ c ∈ readable ∧ (c, mode?) ∈ opens }
└─────────────────────────────────────────────────────────────────

┌─ PfopenRemembered ──────────────────────────────────────────────
│ ΞPfopenState
│ PfopenArgs
│ result! : HANDLE × PATH
├─────────────────────────────────────────────────────────────────
│ key ∈ dom cache ∧ cache(key) ∈ usable
│ second(result!) = cache(key)
└─────────────────────────────────────────────────────────────────

┌─ PfopenFound ───────────────────────────────────────────────────
│ ΔPfopenState
│ PfopenArgs
│ result! : HANDLE × PATH
├─────────────────────────────────────────────────────────────────
│ ¬(key ∈ dom cache ∧ cache(key) ∈ usable)
│ ran candidates ∩ usable ≠ ∅
│ second(result!) = head(candidates ↾ usable)
│ cache' = cache ⊕ { key ↦ second(result!) }
└─────────────────────────────────────────────────────────────────

┌─ PfopenNotFound ────────────────────────────────────────────────
│ ΔPfopenState
│ PfopenArgs
│ result! : {∅}
├─────────────────────────────────────────────────────────────────
│ ¬(key ∈ dom cache ∧ cache(key) ∈ usable)
│ ran candidates ∩ usable = ∅
│ cache' = {key} ⩤ cache
└─────────────────────────────────────────────────────────────────

┌─ PfopenError ───────────────────────────────────────────────────
│ ΞPfopenState
│ path?, prefix?, mode? : seq CHAR ∪ {⊥}
│ error! : seq CHAR
├─────────────────────────────────────────────────────────────────
│ (path? = ⊥ ∨ prefix? = ⊥ ∨ prefix? = ⟨⟩)
│     ⇒ error! = "Usage: pfopen($path, $prefix, [$suffixes, [$mode]])"
│ path? is a reference that is not an unblessed array
│     ⇒ error! = "pfopen: path must be a string or an array reference"
│ mode? ∉ MODE ∪ {⊥, "", "0"}
│     ⇒ error! = "pfopen: invalid mode '" ⁀ mode? ⁀ "'"
└─────────────────────────────────────────────────────────────────

pfopen ≙ PfopenError ∨ PfopenRemembered ∨ PfopenFound ∨ PfopenNotFound

The errors are checked in the order shown, and the first one that applies is raised. A $prefix or $suffixes containing NUL gives PfopenNotFound without changing cache. In taint mode, a call with a tainted argument behaves as if key were not in dom cache, and cache' = cache.

State Diagram

pfopen keeps one piece of state for each different set of arguments ($path, $prefix, $suffixes): either it remembers a filename for them, or it doesn't. All lookups start in the FORGOTTEN state when the program starts. The labels T1 to T7 are explained in the table below.

                program starts
                      |
                      v
               +-------------+
        +----->|  FORGOTTEN  |<-----+
        |      +-------------+      |
        |        |    |    |        |
        +--------+    |    |        |
        T1, T2        |    |        |
                      |    | T3     |
                      |    v        |
               +-------------+      |
        +----->| REMEMBERED  |------+
        |      +-------------+   T7
        |        |    |
        +--------+    |
        T4, T5, T6    |

Label  From        Trigger (a call to pfopen)       Action                       To
-----  ----------  -------------------------------  ---------------------------  ----------
T1     FORGOTTEN   bad argument                     croak with an error          FORGOTTEN
T2     FORGOTTEN   search finds no usable file      return nothing               FORGOTTEN
T3     FORGOTTEN   search finds a usable file       open it, remember the        REMEMBERED
                                                    filename, return the handle
T4     REMEMBERED  bad argument                     croak with an error          REMEMBERED
T5     REMEMBERED  remembered file opens            open it, return the handle;  REMEMBERED
                                                    don't search
T6     REMEMBERED  remembered file fails, and a     open it, remember the new    REMEMBERED
                   search finds another usable file filename, return the handle
T7     REMEMBERED  remembered file fails, and a     forget the filename,         FORGOTTEN
                   search finds no usable file      return nothing

"Return nothing" means undef in scalar context and an empty list in list context.

Notes:

Copyright 2017-2026 Nigel Horne.

Usage is subject to the GPL2 licence terms. If you use it, please let me know.