How Do I Point a File Handle to STDOUT (or Another File Handle) In Perl?
Asked Answered
B

5

26

I want to make a quick script that writes to a file if a file is given, or stdout if no file is given. It would be much easier for me to do this if I could start the script by pointing a new file handle, OUTFILE, to whichever of these is appropriate.

I know I can accomplish what I want by just pointing STDOUT to my output file when appropriate, but I'd rather not do that as I don't want to confuse anyone using the script later and wondering why their print statements don't work. Also, I'd still like to use the occasional print statement myself for troubleshooting purposes, and never want this print to the output file.

All of this said, what I'm looking for is something along the lines of:
open( OUTFILE, ($output ? ">$output" : STDOUT );
except that doesn't work.

Ideas?

Buggs answered 9/6, 2011 at 17:0 Comment(0)
G
38

Some ways (globs):

# Requires 2-arg open, unfortunately
open(OUTPUT, "> ".($output || '-'))  or die;

if ($output) {
   open(OUTPUT, '>', $output) or die;
} else {
   *OUTPUT = *STDOUT;
}

if ($output) {
   open(OUTPUT, '>', $output) or die;
} else {
   open(OUTPUT, '>&', \*STDOUT) or die;
}

Some ways (lexical var):

# Requires 2-arg open, unfortunately
open(my $fh, "> ".($output || '-'))  or die;

my $fh;
if ($output) {
   open($fh, '>', $output) or die;
} else {
   $fh = \*STDOUT;
}

my $fh;
if ($output) {
   open($fh, '>', $output) or die;
} else {
   open($fh, '>&', \*STDOUT) or die;
}

Some ways (other):

# Changes the default handle for <print "foo">.
if ($output) {
   open(OUTPUT, '>', $output) or die;
   select(OUTPUT);
}
Garofalo answered 9/6, 2011 at 18:33 Comment(1)
... } else { open($fh, '>&' . fileno(STDOUT)); }Germann
C
11

For the reader's sake (since OP already "got it working"):

The Perl documentation, "perlopentut" (perldoc perlopentut) gives examples of opening an alternate filehandle directed to STDOUT, but it uses bareword filehandles, and the two-arg version of open, both of which are somewhat ancient. Damian Conway's book "Perl Best Practices" (as well as Perl::Critic, which is based on Damian's book) emphasizes using three arg opens and lexical filehandles (my $foo style filehandles). chromatic's "Modern Perl" also briefly mentions using lexical filehandles, and discourages barewords where practical. The three arg open is considered to be a safer form, as it limits exposure to shell injections. While the specific case here is low risk, it's still a good habit to default to the three-arg open.

It takes careful reading of the examples shown in Perl's open documentation (perldoc -f open), and a little interpretation to notice the correct placement of the ampersand character in opening a duplicate filahandle directed to STDOUT when using the three-arg version of open. One might errantly assume that this should work: open my $fh, '>', '&STDOUT' or die $!;, mostly because it looks like a format we're used to seeing (ampersand preceding a symbol name). But that syntax isn't what open needs, and we're used to seeing it in an entirely different context: certain subroutine calls.

The correct way to open a dupe to STDOUT with the three arg open is to combine the > and the & into the same second argument, and leave STDOUT bare, like this:

use Modern::Perl;

open my $fh, '>&', STDOUT or die "Bleah: $!";

say $fh 'Printing alternate filehandle "$fh" worked.';
say 'Implicitly printing to "STDOUT" also works.';

Or passing STDOUT to open as a reference to the typeglob:

open my $fh, '>&', \*STDOUT or ...
Ciaphus answered 9/6, 2011 at 17:57 Comment(0)
E
7

- is magic. Follow the convention or you violate the principle of least surprise.

use autodie qw(:all);
use Getopt::Long qw(GetOptions);
GetOptions(\my %opt, 'out=s') or die 'usage';
die 'usage' unless $opt{out};
open my $handle, ">$opt{out}";

perl foo -o blahblah        # opens file blahblah for writing
perl foo -o -               # goes to STDOUT

Related: Are there reasons to ever use the two-argument form of open(...) in Perl?

Equestrian answered 9/6, 2011 at 17:20 Comment(0)
B
4

Nevermind. Figured it out. Throwing in an ampersand does the trick. Full working code is:
open( OUTPUT, ( $output ? ">$output" : ">&STDOUT" ) );

Buggs answered 9/6, 2011 at 17:12 Comment(1)
What would make this answer great is an explanation of how this works.Viscometer
C
3

You could always just write the regular output to STDOUT and your debug statements to STDERR. Then if the user wants the output to go to a file they can just redirect STDOUT to the file on the command line.

Chlorate answered 9/6, 2011 at 17:4 Comment(3)
Like I said, I know I can redirect the file to STDOUT, but this is really something I'd rather avoid doing as I find it confusing and annoying when others do it and I have to deal with the code later. It'll work as a last ditch effort, but I'd much rather just direct a different file handle to an output file or STDOUT as I think this would be much cleaner.Buggs
@Buggs - You don't have to do it in your code. Just write your script to always print to STDOUT. When a user runs the script normally they'll see the output on the screen. But they can also do scriptname.pl > filename.txt in which case the script will run and the output will end up in the file filename.txt. Any debug code on STDERR will still appear on the console. This is the way most *nix utilities and scripts work.Chlorate
@Buggs - This is the simplest approach all round. You don't have any code in your script that knows about different output paths. All you do in the code is write to STDOUT. The user decides when they use the script whether they want to see the output in the console, or put it into a file, or even pipe it into the STDIN of another command.Chlorate

© 2022 - 2024 — McMap. All rights reserved.