How to modify content of a file using single file handle - perl

I'm trying to modify content of a file using Perl.
The following script works fine.
#!/usr/bin/perl
use strict;
use warnings;
open(FH,"test.txt") || die "not able to open test.txt $!";
open(FH2,">","test_new.txt")|| die "not able to opne test_new.txt $!";
while(my $line = <FH>)
{
$line =~ s/perl/python/i;
print FH2 $line;
}
close(FH);
close(FH2);
The content of test.txt:
im learning perl
im in File handlers chapter
The output in test_new.txt:
im learning python
im in File handlers chapter
If I try to use same file handle for modifying the content of file, then I'm not getting expected output. The following is the script that attempts to do this:
#!/usr/bin/perl
use strict;
use warnings;
open(FH,"+<","test.txt") || die "not able to open test.txt $!";
while(my $line = <FH>)
{
$line =~ s/perl/python/i;
print FH $line;
}
close(FH);
Incorrect output in test.txt:
im learning perl
im learning python
chapter
chapter
How do I modify the file contents using single file handle?

You can't delete from a file (except at the end).
You can't insert characters into a file (except at the end).
You can replace a character in a file.
You can append to a file.
You can shorten a file.
That's it.
You're imagining you can simply replace "Perl" with "Python" in the file. Those aren't of the same length, so it would require inserting characters into the file, and you can't do that.
You can effectively insert characters into a file by loading the rest of the file into memory and writing it back out two characters further. But doing this gets tricky for very large files. It's also very slow since you end up copying a (possibly very large) portion of the file every time you want to insert characters.
The other problem with in-place modifications is that you can't recover from an error. If something happens, you'll be left with an incomplete or corrupted file.
If the file is small and you're ok with losing the data if something goes wrong, the simplest approach is to load the entire file into memory.
open(my $fh, '<+', $qfn)
or die("Can't open \"$qfn\": $!\n");
my $file = do { local $/; <$fh> };
$file =~ s/Perl/Python/g;
seek($fh, 0, SEEK_SET)
or die $!;
print($fh $file)
or die $!;
truncate($fh)
or die $!;
A safer approach is to write the data to a new file, then rename the file when you're done.
my $new_qfn = $qfn . ".tmp";
open(my $fh_in, '<', $qfn)
or die("Can't open \"$qfn\": $!\n");
open(my $fh_out, '<', $new_qfn)
or die("Can't create \"$new_qfn\": $!\n");
while (<$fh_in>) {
s/Perl/Python/g;
print($fh_out $_);
}
close($fh_in);
close($fh_out);
rename($qfn_new, $qfn)
or die $!;
The downside of this approach is it might change the file's permissions, and hardlinks will point to the old content instead of the new file. You also need permissions to create a file.

As #Сухой27 answered
it's typical situation that perl onliner pleasingly used.
perl -i -pe 's/perl/python/i'
perl takes below options
-p : make line by line loop(every line assign into $_ and print after evaluated $_)
-e : evaluate code block in above loop ( regex take $_ as default operand )
-i : in plcae file edit (if you pass arguments for -i, perl preserve original files with that extention)
if you run below script
perl -i.bak -pe 's/perl/python/i' test.txt
you will get modified test.txt
im learning python
im in File handlers chapter
and get original text files named in test.txt.bak
im learning perl
im in File handlers chapter

Related

Perl - a way to run a script multiple time and append output files

I would like to run a perl script multiple times and append output files in a file.
Let's say I have a perl script (sparc_sub.pl) that generate an output file, sparc_sub.output. I would like to run it multiple times. In every time run, it retunes the same output file (sparc_sub.output), but the content is different.
I know '>>' is the sign if I would like to append to the end of the file, so I used the sign to open the output files in a loop. However, the output file, sparc_sub.output always stores the result of the last run. (not appending)
Any help is appreciated.
sparc_top.pl
use strict;
use Math::BigInt;
use 5.010;
use List::Util qw(sum);
my $s;
my $done=3;
my $script="sparc_sub.pl";
my #output;
my $filename ='sparc_sub.output';
for ($s=0; $s < $done; $s++) {
system('perl',$script)==0 or die "failed to execute $script: \$?=$?";
open(my $fh, '>>', $filename) or die "Could not open file '$filename' $!";
close $fh;
}
You have opened the file and then immediately closed it without even writing anything to it.
open(my $fh, '>>', $filename) or die "Could not open file '$filename' $!";
print $fh "this data goes to file";
close $fh;
I have a perl script sparc_sub.pl that generate an output file, sparc_sub.output
So with each run the same file will be generated. I think you want to create a new file which will keep appending the data from sparc_sub.output. If that's the case then do below.
open(my $fh2, '>>', 'all_outputs') or die "Could not open file 'all_outputs' $!";
for ($s=0; $s < $done; $s++) {
system('perl',$script)==0 or die "failed to execute $script: \$?=$?";
#sparc_sub.output has been generated.
#Now take the data from this file and append it to a final file
open(my $fh, '<', 'sparc_sub.output') or die "Could not open file '$filename' $!";
my #data = <$fh>;
print $fh2 #data;
}
Now all_outputs file contains the data from sparc_sub.output file which was generated multiple times in the loop.

Are there any gotchas with open(my $f, '<:encoding(UTF-8)', $n)

I am having a problem that I am unable to reproduce in a manner suitable for Stackoverflow although it's reproducable in my production environment.
The problem occors in a Perl script that, among others, iterates over a file that looks like so:
abc-4-9|free text, possibly containing non-ascii characters|
cde-3-8|hällo wörld|
# comment
xyz-9-1|and so on|
qrs-2-8|and so forth|
I can verify the correctness of the file with this Perl script:
use warnings;
use strict;
open (my $f, '<:encoding(UTF-8)', 'c:\path\to\file') or die "$!";
while (my $s = <$f>) {
chomp($s);
next unless $s;
next if $s =~ m/^#/;
$s =~ m!(\w+)-(\d+)-(\d+)\|([^|]*)\|! or die "\n>$s<\n didn't match on line $.";
}
print "Ok\n";
close $f;
When I run this script, it won't die on line 10 and consequently print Ok.
Now, I use essentially the same construct in a huge Perl script (hence irreproducable for Stackoverflow) and it will die on line 2199 of the input file.
If I change the first line (which is completely unrelated to line 2199) from something like
www-1-1|A line with some words|
to
www-1-1|x|
the script will process line 2199 (but fail later).
Interestingly, this behaviour was introduced when I changed
open (my $f, '<', 'c:\path\to\file') or die "$!";
to
open (my $f, '<:encoding(UTF-8)', 'c:\path\to\file') or die "$!";
Without the :encoding(UTF-8) directive, the script does not fail. Of course, I need the encoding directive since the file contains non-ascii characters.
BTW, the same script runs without problems on Linux.
On Windows, where it fails, I use Strawberry Perl 5.24
I do not have a full and correct explanation of why this is necessary, but you can try opening the file with
'<:unix:encoding(UTF-8)'
This may be related to my question "Why is CRLF set for the unix layer on Windows?" which I noticed when I was trying to figure out stuff which I ended up never figuring out.

Perl not able to tail rotated log file

I am using property based filters of rsyslog to send specific logs to seperate file where those logs will be parsed using perl.
This is my rsyslog entry
$template SPLIT,"/home/shivam/hello-%$YEAR%%$MONTH%%$DAY%%$HOUR%%$MINUTE%"
:msg, contains, "hello" -?SPLIT
So rsyslog will create separate files for logs coming after every minute. Files will be created like this hello-201505281139.
My perl script to parse these files is
use strict;
use warnings;
use POSIX qw(strftime);
my $date = strftime "%Y%m%d%H%M", localtime;
my $file = '/root/defer-'.$date;
open(my $fh,'<',$file) or die "unable to open file $!\n";
while(1) {
while(my $line = <$fh>){
print "$date\n";
print "$line";
}
sleep(2);
unless ($date == strftime "%Y%m%d%H%M", localtime) {
close($fh);
$date = strftime "%Y%m%d%H%M", localtime;
$file = '/root/defer-'. $date;
system("touch $file");
open(my $fh,'<',$file) or die "unable to open file $!\n";
}
}
In unless block i am checking that if minute has changed then i close previous file and open new file.
The reason i am creating new file from script and not waiting for rsyslog to create file is that the frequency of logs coming is not that much. So i create file and just start reading on it in hope that when any new log will come i will be able to read that.
But what is happening is that i am able to create new file but not able to read anything from that file.
This is what i am getting as warning
readline() on closed filehandle $fh at test.pl line 14.
Line 14 in my code is this line
while(my $line = <$fh>){
I am not able to see anything wrong in my code. Please suggest what is the mistake.
You have two different $fh lexical (my) variables,
So instead declaring new one
open(my $fh,'<',$file) or die "unable to open file $!\n";
keep using previously declared one,
open($fh,'<',$file) or die "unable to open file $!\n";

perl unable to copy contents of file and print it

I need to read/copy the contents of a file(test.pl) just as the way it is formatted and email it.
I am using the following code but I am unable to print anything out.
I get this error even though the file exists in the same directory.
Failed: No such file or directory
Code:
#!/usr/bin/perl
use strict;
use warnings;
use DBI;
open my $fh, '<', 'test.pl '
or die "Failed: $!\n";
my $text = do {
local $/;
<$fh>
};
close $fh
or die "Failed again: $!\n";
print $text, "\n";
It looks like there is an extra space in the filename you are trying to open. In your open statement, try changing 'test.pl ' to 'test.pl'.
if you are going to read files names from STDIN (user's input), you may want to trim them either by using regex (s/^\s+//....) or Text::Trim among other validations.

Perl : Cannot open a file with filename passed as argument

I am passing two filenames from a DOS batch file to a Perl script.
my $InputFileName = $ARGV[0];
my $OutputFileName = $ARGV[1];
Only the input file physically exists while the Outputfile must be created by the script.
open HANDLE, $OutputFileName or die $!;
open (HANDLE, ">$OutputFileName);
open HANDLE, ">$OutputFileName" or die $!;
All three fail.
However the following works fine.
open HANDLE, ">FileName.Txt" or die $!;
What is the correct syntax?
Edit : Error message is : No such file or directory at Batchfile.pl at line nn
The proper way is to use the three-parameter form of open (with the mode as a separate parameter) with lexical file handles. Also die doesn't have a capital D.
Like this
open my $out, '>', $OutputFileName or die $!;
but your last example should work assuming you have spelled die properly in your actual code.
If you are providing a path to the filename that doesn't exist then you also need to create the intermediate directories.
The die string will tell you the exact problem. What message do you get when this fails?
code:
$file_name = $ARGV[1];
open (OUTPUT "> $file_name") or error("unable to create or open $file_name");
print OUTPUT "hello world";
close(OUTPUT);
command to execute:
perl perl_file.pl data.txt
it will work try