How can I reopen a closed file handle in Perl? - perl

Once a file handle is closed is it possible to reopen it without creating a new file handle?
ex (This doesn't work):
use strict;
use warnings;
# Open and write to a file
open my $fh, '>', 'file.txt' or die $!;
print $fh 'Hi!';
close $fh;
# Reopen the file and read its contents
open $fh, '<' or die $!;
my #contents = <$fh>;
close $fh;
print #contents;
I'm trying to avoid doing this:
open my $fh2, '<', 'file.txt' or die $!;

You cannot "reopen" it. The best solution is to avoid closing, if you
need it later. Remember that you can alway use seek if you need
to rewind to some portion of the file.
# open for read/write
open my $fh, '+<', 'file.txt' or die $!;
# do something to file, like writing
# go to the beginning
seek FILEHANDLE, 0, 0;
# do something else, like reading

I don't know that you can reopen a closed handle. But, you can reuse the $fh variable as a filehandle since it's been closed. But, you're missing the name of the file when you open again.
open $fh, '<', 'file.txt' or die $!;

Related

Most efficient way to write over file after reading

I'm reading in some data from a file, manipulating it, and then overwriting it to the same file. Until now, I've been doing it like so:
open (my $inFile, $file) or die "Could not open $file: $!";
$retString .= join ('', <$inFile>);
...
close ($inFile);
open (my $outFile, $file) or die "Could not open $file: $!";
print $outFile, $retString;
close ($inFile);
However I realized I can just use the truncate function and open the file for read/write:
open (my $inFile, '+<', $file) or die "Could not open $file: $!";
$retString .= join ('', <$inFile>);
...
truncate $inFile, 0;
print $inFile $retString;
close ($inFile);
I don't see any examples of this anywhere. It seems to work well, but am I doing it correctly? Is there a better way to do this?
The canonical way to read an entire file contents is to (temporarily) set the input record separator $/ to undef, after which the next readline will return all of the rest of the file. But I think your original technique is much clearer and less involved than just reopening the file for write.
Note that all the following examples make use of the autodie pragma, which avoids the need to explicitly test the status of open, close, truncate, seek, and many other related calls that aren't used here.
Opening in read/write mode and using truncate would look like this
use strict;
use warnings;
use autodie;
use Fcntl 'SEEK_SET';
my ($file) = #ARGV;
open my $fh, '+<', $file;
my $ret_string = do { local $/; <$fh>; };
# Modify $ret_string;
seek $fh, 0, SEEK_SET;
truncate $fh, 0;
print $fh $ret_string;
close $fh;
whereas a simple second open would be
use strict;
use warnings;
use autodie;
my ($file) = #ARGV;
open my $fh, '<', $file;
my $ret_string = do { local $/; <$fh>; };
# Modify $ret_string;
open $fh, '>', $file;
print $fh $ret_string;
close $fh;
There is a third way, which is to use the equivalent of the edit-in-place -i command-line option. If you set the built-in variable $^I to anything other than undef and pass a file on the command line to the program then Perl will transparently rename by appending the value of $^I and open a new output file with the original name.
If you set $^I to the empty string '' then the original file will be deleted from the directory and will disappear when it is closed (note that Windows doesn't support this, and you have to specify a non-null value). But while you are testing your code it is best to set it to something else so that you have a route of retreat if you succeed in destroying the data.
That mode would look like this
use strict;
use warnings;
$^I = '.old';
my $ret_string = do { local $/; <>; };
# Modify $ret_string;
print $ret_string;
Note that the new output file is selected as the default output, and if you want to print to the console you have to write an explicit print STDOUT ....
I would recommend using $INPLACE_EDIT:
use strict;
use warnings;
my $file = '...';
local #ARGV = $file;
local $^I = '.bak';
while (<>) {
# Modify the line;
print;
}
# unlink "$file$^I"; # Optionally delete backup
For additional methods for editing a file, just read perlfaq5 - How do I change, delete, or insert a line in a file, or append to the beginning of a file?.
I would change the way you're reading the file, not how you open it. Joining lines is less efficient than reading whole file at once,
$retString .= do { local $/; <$inFile> };
As for truncate you might want to seek to begining of the file first as perldoc suggests
The position in the file of FILEHANDLE is left unchanged. You may want to call seek before writing to the file.
The Tie::File module may help if you are changing some lines in a file.
Like this
use strict;
use warnings;
use Tie::File;
tie my #source, 'Tie::File', 'file.txt' or die $!;
for my $line (#source) {
# Modify $line here
}
untie #source;

why can I open undef?

The following does not die:
open my $in, '<', undef or
die q{couldn't open undef};
print <$in>;
Neither does this:
open my $in, '>', undef or
die q{couldn't open undef};
print $in 'hello';
I don't understand why neither of these die. How could opening undef possibly be successful?
The reason I found this was that a guy I work with had done this:
open my $in, '>', $ARGV[0] or die q{couldn't open $ARGV[0]};
He thought that this would kill the script if no arguments were passed in (I know this isn't the cleanest way to do that but I didn't think it wouldn't work).
I'm using Strawberry 5.16.1.
See perldoc -f open:
As a special case the three-argument form with a read/write mode and
the third argument being undef:
open(my $tmp, "+>", undef) or die ...
opens a filehandle to an anonymous temporary file.

Open and read a lot of files with the same ending using Perl

I have a folder called ALLEQData in which I have 100 files that end in '.ndk' for example, 'jan05.ndk', 'feb05.ndk' etc.
Using a Perl script I would like to open every single file ending in '.ndk', read the information contained in that file and place it in an output file.
Before I only needed to open one file and read it, for which I used:
my $filename = "jan76_dec10.ndk";
open FILEEQ, "<$filename"
or die "can't open '$filename' for reading: $!";
close FILEEQ;
$icount = 0;
for ($j=0; $j<#equ_file; $j++) .....etc
Then read over the information. I can read and sort the information into the output that I want.
What I am not sure how to do, is how to open all of the file that end in '.ndk', one by one, do the reading and sorting, close that file, then move onto the next one?
Hope this is clear enough.
Use glob:
my #filenames = glob('*.ndk');
for my $filename (#filenames) {
open my $fh, '<', $filename
or die "can't open '$filename' for reading: $!";
# read/sort file
close $fh;
}
Take a look at Perl's globbing mechanism.
You could also read filenames from the command line:
while ($#ARGV > -1) {
my $filename = shift;
open my $fh, '<', $filename
or die "can't open $filename for reading: $!";
# ...
close $fh;
}
Then call your perl script with a wild card expression:
your-script.pl *.ndk

flock: the right way to truncate with sysopen

Are these three versions equivalent?
#!/usr/bin/env perl
use warnings;
use strict;
use 5.10.0;
use Fcntl qw(:flock :seek);
my $fh;
sysopen $fh, $file, O_WRONLY | O_CREAT | O_TRUNC or die $!;
flock $fh, LOCK_EX or die $!;
say $fh 'something';
close $fh;
sysopen $fh, $file, O_WRONLY | O_CREAT or die $!;
flock $fh, LOCK_EX or die $!;
seek $fh, 0, SEEK_SET or die $!;
truncate $fh, 0 or die $!;
say $fh 'something';
close $fh;
sysopen $fh, $file, O_WRONLY | O_CREAT or die $!;
flock $fh, LOCK_EX or die $!;
truncate $fh, 0 or die $!;
say $fh 'something';
close $fh;
It seems you want to open a file, lock it, and then truncate it without any possibility that another process that is competing for that lock might see a truncated file. This poses several restrictions:
A lock has to be obtained before truncation. This rules out the first solution, as it does the truncation before the lock can be acquired.
The file handle cannot be reopened with another mode, or the lock would be lost. This means that the truncation can't be done by reopening.
Thus the ideal solution has the following steps:
The file is opened without truncating, e.g. with sysopen, or the open modes >>, +<. The sysopen option O_TRUNC or the open modes >, +> etc. must not be used here.
A lock is acquired.
Truncation is performed. As open would create a new filehandle, the truncate function must be used here.
If the file was opened in append mode, it is neccessary to seek to the beginning of the file.
Your first solution truncates before aquiring the lock, and can therefore be ruled out.
Your second and third solution are both possible, although the seek is unneccessary.
Other solutions could use regular open:
open my $fh, "+<", $file; # Achtung: fails if the file doesn't exist
flock $fh, LOCK_EX;
truncate $fh;
# or:
open my $fh, ">>", $file;
flock $fh, LOCK_EX;
truncate $fh;
seek 0, 0;
In many cases, it may be preferable to use an unrelated file as lock, thus avoiding difficulties with reopening. This would look like:
# adapted from perldoc -f flock
sub lock {
my $file = shift;
open my $fh, "<", ".$file.lock";
flock $fh, LOCK_EX;
return $fh;
}
sub unlock {
my $fh = shift;
flock $fh, LOCK_UN;
}
my $lock = lock($file);
open my $fh, ">", $file;
...
unlock($lock);
Of course, all related processes have to honour this interface and create an appropriate lockfile.

Creating a file name using variables in Perl

I am trying to write out to a file, where the file name is created from a variable(the name + the user id + the date and time + file extension).
I have read various things on Stackoverflow which I have based my code off.
my $windowsfile = "winUserfile-$User_ID-$datetime.csv";
open winUserfile, ">>", $windowsfile) or die "$!";
print winUserfile "User_ID, Expression\n";
close winUserfile;
I would assumed this would work, but I am getting a syntax error. Would anyone be able to help?
Your second line has a close-paren without the preceeding open:
open winUserfile, ">>", $windowsfile) or die "$!";
You likely want to open it first
open(winUserfile, ">>", $windowsfile) or die "$!";
Or just not bother with them entirely here, as they're optional in this case
open winUserfile, ">>", $windowsfile or die "$!";
Also, it's bad style to use a bareword filehandle, as this creates becomes global. Better to use a lexical one:
open my $winUserfile, ">>", $windowsfile or die "$!";
print $winUserfile "User_ID, Expression\n";
You don't then need to close it; the close will be automatic when the $winUserfile variable goes out of scope.
I like using the IO::All module for file io.
use IO::All
my $windowsfile = "winUserfile-$User_ID-$datetime.csv";
io($windowsfile) > "User_ID, Expression\n";
my $windowsfile = "winUserfile-$User_ID-$datetime.csv";
open (winUserfile, ">>$windowsfile") or die "$!";
print winUserfile "User_ID, Expression\n";
close winUserfile;