Why does repeatedly opening, appending and closing a text file cause the lines to be written in reverse order?

Viewed 170

Why does the following perl code:

for(open(my $fh, ">>", "test.txt")){
    print $fh "one\n";
}
for(open(my $fh, ">>", "test.txt")){
    print $fh "two\n";
}
for(open(my $fh, ">>", "test.txt")){
    print $fh "three\n";
}

Write the following to test.txt?

three
two
one

Why is the order is getting reversed? My understanding is that each for block will automatically close the file when the block exits. Shouldn't this cause perl to flush any buffers before the the next block starts? I expected this code to open the file, write a line, close the file, then repeat all of those steps two more times. What am I missing?

I tested this with Perl 5.26.1, running on Ubuntu 18.04.3.

(Yes, I know I could easily get the lines to be written in the correct order by just putting all the print statements in the same block. That's not the question here. I want to understand why this behavior is happening.)

For bonus weirdness, when I run the following code:

for my $val (qw/ one two three /) {
    for(open(my $fh, ">>", "test.txt")){
        print $fh "$val\n";
    }
}

It gives me the following output:

one
two
three

This code seems like it should be functionally identical to the previous code. Why is it behaving differently?

2 Answers

That's just a complicated version of

open(my $fh, ">>", "test.txt");
print $fh "one\n";
open(my $fh, ">>", "test.txt");
print $fh "two\n";
open(my $fh, ">>", "test.txt");
print $fh "three\n";

Let's make small change to make things more readable and easier to discuss:

open(my $fh1, ">>", "test.txt");
print $fh1 "one\n";
open(my $fh2, ">>", "test.txt");
print $fh2 "two\n";
open(my $fh3, ">>", "test.txt");
print $fh3 "three\n";

This is equivalent because each my creates a new variable.

So what's happening?

open(my $fh1, ">>", "test.txt");  # You create a file handle.
print $fh1 "one\n";               # You write to the file handle's buffer.
open(my $fh2, ">>", "test.txt");  # You create a file handle.
print $fh2 "two\n";               # You write to the file handle's buffer.
open(my $fh3, ">>", "test.txt");  # You create a file handle.
print $fh3 "three\n";             # You write to the file handle's buffer.
# Implicit close($fh3);           # You close the file handle, flushing its buffer.
# Implicit close($fh2);           # You close the file handle, flushing its buffer.
# Implicit close($fh1);           # You close the file handle, flushing its buffer.

Since Perl doesn't guarantee the order in which variable are destroyed, you could easily get the output in any order.

The solution is to flush the handles after printing to them ($fh->flush;, or $fh->autoflush(1);), or close the handles earlier.

{ open(my $fh, ">>", "test.txt");  print $fh "one\n";   }
{ open(my $fh, ">>", "test.txt");  print $fh "two\n";   }
{ open(my $fh, ">>", "test.txt");  print $fh "three\n"; }

As already explained by ikegami, the reason for this behaviour lies in the different scopes of variable visibility and existence. Here is a small example visualizing this:

#!/usr/bin/perl

use v5.16;
use warnings;
use experimental 'signatures';

package MyScalar;

sub TIESCALAR ($class, $val) {
    my $self = \$val;
    bless $self, $class;
}

sub FETCH ($self) {
    $$self;
}

sub DESTROY ($self) {
    warn "destroying $$self\n";
}


package main;

for (tie my $val, 'MyScalar', 'first') {
    say $val;
}
for (tie my $val, 'MyScalar', 'second') {
    say $val;
}
for (tie my $val, 'MyScalar', 'third') {
    say $val;
}

The variables are destroyed in an unpredictable order. Here the output is:

destroying second
destroying first
destroying third
first
second
third
Related