What does local do on the standard streams / bareword word filehandles: STDIN?
Asked Answered
Q

2

6

I was thinking about doing

open(*STDIN, "<", "/dev/null" );
open(my $fh, "-|", "/bin/bash", "/tmp/foo");
print for <$fh>;'

However, I would like *STDIN restored, afterward, so I tried.

{
  open(local *STDIN, "<", "/dev/null" );
  open(my $fh, "-|", "/bin/bash", "/tmp/foo");
  print for <$fh>;
}

You can try this with cat with and without the local keyword like this,

{
  # remove local and it works,
  open(local *STDIN, "<", "/dev/null" );
  open(my $fh, "-|", "/bin/cat");
  print for <$fh>;
}

Only without local will cat read from /dev/null. So what does local actually do on a bareword filehandle?

Quinquennium answered 2/9, 2020 at 17:7 Comment(0)
U
4

There are system files handles (called file descriptors, or "fd"), and there are Perl file handles. Perl file handles usually wrap a system file handles, but this isn't always the case. For example, open(my $fh, '<', \$buf) creates a Perl handle that isn't associated with any system file handle.

Other processes don't know anything about your process's variables, so they don't know anything about Perl file handles. Whatever handle is opened as fd 0 will be used as STDIN, fd 1 as STDOUT, and fd 2 as STDERR.[1]

When open is passed an existing Perl handle, this handle will be closed. If creating a new system file handle, it will be either be given the same number as the original fd (if the original handle had one) or the lowest available number (if it didn't).[2]

So, when you use open(*STDIN, ...), the new handle associated with *STDIN{IO} will also be fd 0.

$ perl -e'
   CORE::say fileno(*STDIN);
   open(*STDIN, "<", "/dev/null") or die $!;
   CORE::say fileno(*STDIN);
'
0
0

cat, reading from fd 0, will therefore notice the change.

local *STDIN creates a backup of the glob and associated *STDIN with a fresh glob. The original *STDIN is still in memory, so no resources associated with *STDIN are freed. This means that any file handle associated with *STDIN is still open.

When you use open(local *STDIN, ...), the new fd will have the lowest number available. fd 0 is still being used by the original *STDIN somewhere in memory, so fd 0 is not available. Maybe fd 3 will be the first available fd this time (1 and 2 being used by STDOUT and STDERR).

$ perl -e'
   CORE::say fileno(*STDIN);
   {  
      open(local *STDIN, "<", "/dev/null") or die $!;
      CORE::say fileno(*STDIN);
   }
   CORE::say fileno(*STDIN);
'
0
3
0

cat, reading from fd 0, will read from the original handle.

Perl could make it so whatever handles are associated with STDIN, STDOUT and STDERR become fd 0, 1 and 2 before executing cat, but it doesn't. This is left to you.


  1. While I'm describing how things work in unixy systems, things work similarly in Windows.

  2. In the situation where open is passed a handle that wraps a system file handle and a new system file handle is also being created, the internal mechanism used on unixy system is the following:

    1. A new fd is created using open. It will have the lowest available number.
    2. dup2 is used to create a duplicate of the new fd with the same number as the original fd.
    3. The fd created by open is closed.

    This means the original fd isn't closed if an error occurs.

Unpeopled answered 2/9, 2020 at 22:34 Comment(2)
"Perl could make it so whatever handles are associated with STDIN, STDOUT and STDERR become fd 0, 1 and 2 before executing cat, but it doesn't. This is left to you." That beautifully and succinctly shows the misunderstanding. That certainly would make for a better user interface, imho.Quinquennium
What you're describing in the 3rd paragraph is complete fantasy. When you use open(*STDIN, "file"), perl will first open(2) "file", then dup2(2) the returned file descriptor into fd 0. It makes no sense to first close the file descriptor associated with stdin, since opening the new file may fail. And in fact, perl is not doing that, which a simple strace or source code perusal would show you.Rhnegative
D
4

open ...,'-|' and system calls that read from standard input will try to read from file descriptor 0. If you don't mess that up, the external programs will read from the same input stream as perl's standard input.

# reads from standard input
open my $fh, '-|', "/bin/cat";
while (<fh>) { print }

# reads from /tmp/foo
open STDIN, "<", "/tmp/foo";       # replaces fd0 with handle to /tmp/foo
open my $fh, '-|', "/bin/cat";
while (<$fh>) { print }

# reads from standard input
open local *STDIN, "<", "/tmp/foo";  # doesn't close fd0, creates new fd
open my $fh, '-|',  "/bin/cat";
while (<$fh>) { print }

# reads from /tmp/foo
close STDIN;
open FOO, "<", "/tmp/foo";       # fileno(FOO) should be 0 now
open my $fh, '-|', "/bin/cat";
while (<$fh>) { print }
Deliciadelicious answered 2/9, 2020 at 19:23 Comment(2)
That's just a really weird unintuitive undocumented behavior of local *STDIN. I would expect it to stash the old value, and return it after scope (like it does with everything else). Though at least you've told me what it's doing.Quinquennium
*STDIN is a typeglob (holding package data in the STDIN namespace), not just a fileglob. local *STDIN creates a new typeglob but doesn't affect anything in the original *STDIN namespace, so it doesn't affect what I/O stream file descriptor 0 is attached to.Deliciadelicious
U
4

There are system files handles (called file descriptors, or "fd"), and there are Perl file handles. Perl file handles usually wrap a system file handles, but this isn't always the case. For example, open(my $fh, '<', \$buf) creates a Perl handle that isn't associated with any system file handle.

Other processes don't know anything about your process's variables, so they don't know anything about Perl file handles. Whatever handle is opened as fd 0 will be used as STDIN, fd 1 as STDOUT, and fd 2 as STDERR.[1]

When open is passed an existing Perl handle, this handle will be closed. If creating a new system file handle, it will be either be given the same number as the original fd (if the original handle had one) or the lowest available number (if it didn't).[2]

So, when you use open(*STDIN, ...), the new handle associated with *STDIN{IO} will also be fd 0.

$ perl -e'
   CORE::say fileno(*STDIN);
   open(*STDIN, "<", "/dev/null") or die $!;
   CORE::say fileno(*STDIN);
'
0
0

cat, reading from fd 0, will therefore notice the change.

local *STDIN creates a backup of the glob and associated *STDIN with a fresh glob. The original *STDIN is still in memory, so no resources associated with *STDIN are freed. This means that any file handle associated with *STDIN is still open.

When you use open(local *STDIN, ...), the new fd will have the lowest number available. fd 0 is still being used by the original *STDIN somewhere in memory, so fd 0 is not available. Maybe fd 3 will be the first available fd this time (1 and 2 being used by STDOUT and STDERR).

$ perl -e'
   CORE::say fileno(*STDIN);
   {  
      open(local *STDIN, "<", "/dev/null") or die $!;
      CORE::say fileno(*STDIN);
   }
   CORE::say fileno(*STDIN);
'
0
3
0

cat, reading from fd 0, will read from the original handle.

Perl could make it so whatever handles are associated with STDIN, STDOUT and STDERR become fd 0, 1 and 2 before executing cat, but it doesn't. This is left to you.


  1. While I'm describing how things work in unixy systems, things work similarly in Windows.

  2. In the situation where open is passed a handle that wraps a system file handle and a new system file handle is also being created, the internal mechanism used on unixy system is the following:

    1. A new fd is created using open. It will have the lowest available number.
    2. dup2 is used to create a duplicate of the new fd with the same number as the original fd.
    3. The fd created by open is closed.

    This means the original fd isn't closed if an error occurs.

Unpeopled answered 2/9, 2020 at 22:34 Comment(2)
"Perl could make it so whatever handles are associated with STDIN, STDOUT and STDERR become fd 0, 1 and 2 before executing cat, but it doesn't. This is left to you." That beautifully and succinctly shows the misunderstanding. That certainly would make for a better user interface, imho.Quinquennium
What you're describing in the 3rd paragraph is complete fantasy. When you use open(*STDIN, "file"), perl will first open(2) "file", then dup2(2) the returned file descriptor into fd 0. It makes no sense to first close the file descriptor associated with stdin, since opening the new file may fail. And in fact, perl is not doing that, which a simple strace or source code perusal would show you.Rhnegative

© 2022 - 2024 — McMap. All rights reserved.