tags:

views:

151

answers:

5

I am trying to determine if a given scalar holds a filehandle. It could have been passed to me from a bareword filehandle (i.e. \*FH), a lexical filehandle, an IO::Handle, an IO::File, etc. So far I the only thing that seem to be consistent amongst the various flavors is that they all have a reftype of "GLOB".

A: 

Perhaps you can try some operation on this filehandle (like seek) inside an eval block. If there was no error, then it was a true filehandle :-)

eugene y
I think this is probably the best approach - what really matters is not what it looks like, but what it does. However, a filehandle could be valid but still fail on a seek, so there might be a better interface to call on it?
Ether
The code currently tries to read a line from the handle inside of a block `eval`. This is for a check before trying that.
Chas. Owens
+6  A: 

Remember that you can do this:

$ perl -le '$fh = "STDOUT"; print $fh "Hi there"'
Hi there

That's an ordinary string but still useful as a filehandle.

Looking at the source of IO::Handle, its opened is a thin wrapper around fileno, which has a handy property:

Returns the file descriptor for a filehandle, or undefined if the filehandle is not open.

But there is one caveat:

Filehandles connected to memory objects via new features of open may return undefined even though they are open.

It appears then that a test along the lines of

$@ = "";
my $fd = eval { fileno $maybefh };
my $valid = !$@ && defined $fd;

will do what you want.

The code below checks representatives of

  • in-memory objects
  • named filehandles
  • globs
  • glob references
  • glob names
  • the standard input
  • FileHandle instances
  • IO::File instances
  • pipes
  • FIFOs
  • sockets

Run it yourself:

#! /usr/bin/perl

use warnings;
use strict;

use Fatal qw/ open /;
use FileHandle;
use IO::File;
use IO::Socket::INET;

my $SLEEP = 5;
my $FIFO  = "/tmp/myfifo";

unlink $FIFO;
my $pid = fork;
die "$0: fork" unless defined $pid;
if ($pid == 0) {
  system("mknod", $FIFO, "p") == 0 or die "$0: mknod failed";
  open my $fh, ">", $FIFO;
  sleep $SLEEP;
  exit 0;
}
else {
  sleep 1 while !-e $FIFO;
}

my @ignored = (\*FH1,\*FH2);
my @handles = (
  [0, "1",           1],
  [0, "hashref",     {}],
  [0, "arrayref",    []],
  [0, "globref",     \*INC],
  [1, "in-memory",   do {{ my $buf; open my $fh, "<", \$buf; $fh }}],
  [1, "FH1 glob",    do {{ open FH1, "<", "/dev/null"; *FH1 }}],
  [1, "FH2 globref", do {{ open FH2, "<", "/dev/null"; \*FH2 }}],
  [1, "FH3 string",  do {{ open FH3, "<", "/dev/null"; "FH3" }}],
  [1, "STDIN glob",  \*STDIN],
  [1, "plain read",  do {{ open my $fh, "<", "/dev/null"; $fh }}],
  [1, "plain write", do {{ open my $fh, ">", "/dev/null"; $fh }}],
  [1, "FH read",     FileHandle->new("< /dev/null")],
  [1, "FH write",    FileHandle->new("> /dev/null")],
  [1, "I::F read",   IO::File->new("< /dev/null")],
  [1, "I::F write",  IO::File->new("> /dev/null")],
  [1, "pipe read",   do {{ open my $fh, "sleep $SLEEP |"; $fh }}],
  [1, "pipe write",  do {{ open my $fh, "| sleep $SLEEP"; $fh }}],
  [1, "FIFO read",   do {{ open my $fh, "<", $FIFO; $fh }}],
  [1, "socket",      IO::Socket::INET->new(PeerAddr => "localhost:80")],
);

sub valid {
  local $@;
  my $fd = eval { fileno $_[0] };
  !$@ && defined $fd;
}

for (@handles) {
  my($expect,$desc,$fh) = @$_;
  print "$desc: ";

  my $valid = valid $fh;
  if (!$expect) {
    print $valid ? "FAIL\n" : "PASS\n";
    next;
  }

  if ($valid) {
    close $fh;
    $valid = valid $fh;
    print $valid ? "FAIL\n" : "PASS\n";
  }
  else {
    print "FAIL\n";
  }
}

print "Waiting for sleeps to finish...\n";

All passes on an Ubuntu 9.10 box, so the caveat concerning in-memory objects does not seem to be a concern on that platform at least.

1: PASS
hashref: PASS
arrayref: PASS
globref: PASS
in-memory: PASS
FH1 glob: PASS
FH2 globref: PASS
FH3 string: PASS
STDIN glob: PASS
plain read: PASS
plain write: PASS
FH read: PASS
FH write: PASS
I::F read: PASS
I::F write: PASS
pipe read: PASS
pipe write: PASS
FIFO read: PASS
socket: PASS
Greg Bacon
It looks like `tell` is not portable and fails to detect valid things: "The return value of tell() for the standard streams like the STDIN depends on the operating system: it may return -1 or something else. tell() on pipes, fifos, and sockets usually returns -1."
Chas. Owens
@Chas Nice catch. See updated answer.
Greg Bacon
Really cool. Makes me wonder why there isn't something like this on CPAN already.
Robert P
How about a tied filehandle?
runrig
@Robert P, there is. It's called [openhandle in Scalar::Util](http://search.cpan.org/perldoc?Scalar%3a%3aUtil#openhandle). See [my answer](http://stackoverflow.com/questions/3214647/what-is-the-best-way-to-determine-if-a-scalar-holds-a-filehandle/3807592#3807592).
cjm
+2  A: 

Here's an excerpt from File::Copy determining whether or not a variable is a file handle:

my $from_a_handle = (ref($from)
  ? (ref($from) eq 'GLOB'
      || UNIVERSAL::isa($from, 'GLOB')
      || UNIVERSAL::isa($from, 'IO::Handle'))
  : (ref(\$from) eq 'GLOB'));
runrig
Eww. `UNIVERSAL::isa` is a bad bad bad way of doing isa. I'd prefer `eval { $from->isa('GLOB') }` so that inheritance and overriding work correctly. So sayeth [the isa documentation.](http://search.cpan.org/~jesse/perl-5.12.1/lib/UNIVERSAL.pm). I mean, the example they give for what NOT to do is `$is_io = UNIVERSAL::isa($fd, "IO::Handle"); # BAD!`
Robert P
@Robert P: Actually for GLOB (and other basic reference types) the latest recommendation is to use Scalar::Util::reftype(). While I agree somewhat with all this, using the above code will give you the same results as File::Copy, a core module, and you'd have to go through some effort to make it not work.
runrig
@Robert P: And I experienced a discussion of this on PerlMonks (http://www.perlmonks.org/?node_id=615015) with no real conclusive answer for what is current best practice.
runrig
+4  A: 

Use the openhandle function from Scalar::Util:

openhandle FH

Returns FH if FH may be used as a filehandle and is open, or FH is a tied handle. Otherwise undef is returned.

  $fh = openhandle(*STDIN);           # \*STDIN
  $fh = openhandle(\*STDIN);          # \*STDIN
  $fh = openhandle(*NOTOPEN);         # undef
  $fh = openhandle("scalar");         # undef

The current implementation is similar to Greg Bacon's answer, but it has some additional tests.

cjm
A: 

io_from_any from IO::Handle::Util takes care of upgrading anything to something sane.

daxim