jamgill has asked for the wisdom of the Perl Monks concerning the following question:

My little script has a datafile which may not exist when the program is first run. I need to open the file if it exists. If it doesn't I need to make sure that the script can create it. If it does but the script can't read or write it, handle that situation ... and on and on.

What I'm coming up with is a long if elsif elsif elsif elsif .. else statement that looks and feels kludgey. I haven't found much other than the list of file test operators to help me do this Properly.

Can someone point me to an example of how to open or create a file for reading/writing that handles the possible situations gracefully .. or a module that helps tame this beast?

...or maybe i'm just being lazy, but i hope the outcome is reusable at least.

tia,
jamgill

Replies are listed 'Best First'.
Re: file testing is hard
by PodMaster (Abbot) on Nov 30, 2003 at 19:04 UTC
    I know the feeling, yes, you are being lazy :) IIRC, the perl function sysopen can do this (create if it doesn't exist and open for read/write in one fell swoop or fail => sysopen FILEHANDLE, "filename", O_CREAT | O_RDWR or die "wah $!").

    MJD says "you can't just make shit up and expect the computer to know what you mean, retardo!"
    I run a Win32 PPM repository for perl 5.6.x and 5.8.x -- I take requests (README).
    ** The third rule of perl club is a statement of fact: pod is sexy.

      cool, thanks. I didn't know about sysopen and after reading about it in the camel book, it seems to be limited enough to reduce the possibilities dramatically, simplifying my code.

Re: file testing is hard
by dpmott (Scribe) on Dec 01, 2003 at 05:03 UTC
    I've had to deal with this from time to time. I also cover this in a PERL class that I teach at my company.

    What I've learned, through various approaches, is that you want to depend on the result of open() (or sysopen()). A file might exist, for instance, because -e "file" returns true, but you still might not be able to open it (because, perhaps, you don't have read permissions for it, only list permissions on a UNIX system).

    Also, if it's important to read the file, even when you can't write to it, then you should handle reading and writing separately (as shown below). If you must be able to do both, then don't bother testing for the read-only case -- open for read+update, and fail if that doesn't work.

    So, when I read your problem statement, I'm left with the impression that you might be working with an INI file, which you'll create if missing, update if writable, use if read-only, and ignore if none of the above. (If that's not right, feel free to take or leave the following as you see fit).

    Here's how I would go about it:
    # load_ini # returns undef on error, hashref on success sub load_ini($) { my ($ini) = @_; # first, check for all of those special conditions # that you wanted to avoid, like directories, etc: return undef if ( -d $ini or ! -r $ini or ! -f $ini ); my $fh = new FileHandle($ini); return undef unless defined $fh; # read INI here, represent it in a data struct my $ini_data = parse_ini($fh); # return handy structure (could be an object) # that keeps the INI filename in it for later use return { ini => $file, data => $ini_data }; } # load_ini_safe # same as load_ini, but never "fails" sub load_ini_safe($) { my ($ini) = @_; # provide a default if load_ini() fails # This would prevent you from saving to the INI file # if we load defaults. But see below... # return load_ini($ini) # or { ini => undef, ini_data => default_ini() }; # This will allow you to SAVE the ini settings if you # ONLY have write permissions... (yeah, odd case, # that). return load_ini($ini) or { ini => $ini, ini_data => default_ini() }; } # save_ini # returns boolean 1 on success, 0 on failure sub save_ini($) { my ( $ini_struct ) = @_; # this is a hash slice, btw... my ( $ini, $ini_data ) = %{ $ini_struct }{ qw/ini ini_data/ }; # check for a valid filename # (could be undef, see load_ini_safe for details) return 0 unless $ini; # If we keep the filename around from load_ini_safe, # then we have to perform the same checks as in # load_ini(). # If we only keep the filename if we could open it, # then we don't need to perform these tests return 0 if ( -d $ini or ! -r $ini or ! -f $ini ); # Now, try to open the file for writing. my $fh = new FileHandle(">$ini"); return 0 unless defined $fh; # now write the contents out to the filehandle # presumably, this returns 1 on success, 0 on failure return write_ini( $fh, $ini_data ); } # get_default_ini_data # provide default INI hashref sub default_ini() {} # parse_ini # converts text from filehandle into INI hashref sub parse_ini($) {} # write_ini # writes in-memory INI data structure to file as text sub write_ini($\%) {}
    So, depending on what you want (i.e. load_ini or load_ini_safe), you could keep those separate or combine them or get rid of load_ini_safe. I broke them up to show a division of labor, since I was unclear about just which steps were important to you. Note that this has a small advantage over opening a file in read+update mode -- if it's read-only, then you can load it. If it's not writable, then writing fails (maybe silently in your app). If you only have write permissions for the file, you could load defaults internally and write those out (not very useful, but there it is). You get to completely avoid the error handling for trying to open a file for read+update when loading and/or saving, which would fail for both the load and the save cases if the file were read-only or write-only. Hope this helps... :)
      What I've learned, through various approaches, is that you want to depend on the result of open() (or sysopen()). A file might exist, for instance, because -e "file" returns true, but you still might not be able to open it
      But you then go on to use -r in your code to check if a file is readable with the current effective UID/GID. I was going to point you at exactly that operator to solve your "file exists, can I read it" problem. The various -X operators cover all of the various situations the OP mentioned.
        True, that.

        In practice, I usually dispense with such filetests and just go straight for the open(). But then, I usually program on win32, where that's okay.

        I seem to remember, though, that it's perfectly legal to open directory entries, block/char special files, etc on a UNIX filesystem. I included those specific tests to cover those cases. You (probably) don't want to read your persistent INI from a serial port, nor save it to a directory inode.

        I didn't check for read/write permissions, because open() will do that. Also, it encourages checking the return value of open(), which was most of my point -- it avoids code like this:
        if ( -r $file ) { open(FILE, $file); # hmmm... is the file open or not? my @contents = <FILE>; close FILE; # process @contents; }

      So, when I read your problem statement, I'm left with the impression that you might be working with an INI file, which you'll create if missing, update if writable, use if read-only, and ignore if none of the above. (If that's not right, feel free to take or leave the following as you see fit).

      close ;) it isn't an INI file and i'm in UNIX, and i want to bail if none of the above. from reading above and the Camel, sysopen limits things sufficiently that I shall not have to test for all those various possibilities. To make sure that the $file wasn't a socket or directory or link and that the script's UID can write it ... or that the script's UID can create the file if it doesn't exist was turning into a waterfall of if elsif statements that would work, if i needed to be real specific. for example, if i needed to make sure that i'm opening up and fork, or something (i've never done that, i'm not of legal age for that sort of thing in my country) ... then i would appreciate all that precise testing. But for just opening up a file, I can rely on the fact that sysopen can't open up a directory, or a pipe (yet still allows me easy access to the resulting file's permissions). Hey, that's gravvy :)

      that said, i'm still digesting the code sample and write up. Thank you so much for taking the time to explain. Thanks also to PodMaster for directing me to check out sysopen. I've learned something new :)

      jamgill

Re: file testing is hard
by ysth (Canon) on Nov 30, 2003 at 20:33 UTC
    If you only want to read or append to the file, open my $fh, "+>>", "filename" will work. (Update: Don't forget to seek before reading.)

      if i only wanted to open the file for reading or writing, yes. the open part isn't really my hurdle so much as the if, elsif, elsif, .. else structure surrounding it which must test the file (does it exist? can i write here? is it zero bytes? is it a directory? a plaintext file? am i opening the wrong thing?)

      I want to catch any possibility (at least on my platform*) and handle it responsibly.

      *nix

      thanks,
      jamgill

        Can you not just try to open it and see if you get an error? The only tricky part is making sure you don't allow a race condition (e.g. if the file is not there, then opening it with "+>"; but having another process create it inbetween the if and then parts).

        Update: s/is there/is not there/

Re: file testing is hard
by Jaime (Initiate) on Dec 01, 2003 at 10:33 UTC
    Well, with sysopen you can do that. Or with open. Check http://www.perldoc.com/perl5.8.0/pod/func/open.html to see how can you do it with open.