- Item 51. Don't ignore the file test operators.
- Item 52. Always use the three-argument open.
- Item 53. Consider different ways of reading from a stream.
- Item 54. Open filehandles to and from strings.
- Item 55. Make flexible output.
- Item 56. Use File::Spec or Path::Class to work with paths.
- Item 57. Leave most of the data on disk to save memory.
Item 55. Make flexible output.
When you use hard-coded (or assumed) filehandles in your code, you limit your program and frustrate your users. Some culprits look like these:
print "This goes to standard output\n"; print STDOUT "This goes to standard output too\n"; print STDERR "This goes to standard error\n";
When you put those sorts of statements in your program, you reduce the flexibility of the code, causing people to perform acrobatics and feats of magic to work around it. They shouldn't have to localize any filehandles or redefine standard filehandles to change where the output goes. Despite that, people still code like that because it's quick, it's easy, and mostly, they don't know how easy it is to do it better.
You don't need an object-oriented design to make this work, but it's a lot easier that way. When you need to output something in a method, get the output filehandle from the object. In this example, you call get_output_fh to fetch the destination for your data:
sub output_method {
my ( $self, @args ) = @_;
my $output_fh = $self->get_output_fh
;
print $output_fh @args;
}
To make that work, you need a way to set the output filehandle. That can be a set of regular accessor methods. get_output_fh returns STDOUT if you haven't set anything:
sub get_output_fh { my ($self) = @_; return $self->{output_fh} || *STDOUT{IO}; } sub set_output_fh { my ( $self, $fh ) = @_ ; $self->{output_fh} = $fh; }
With this as part of the published interface for your code, the other programmers have quite a bit of flexibility when they want to change how your program outputs data:
$obj->output_method("Hello stdout!\n"); # capture the output in a string open my ($str_fh), '>', \$string; $obj->set_output_fh($str_fh); $obj->output_method("Hello string!\n"); # send the data over the network socket( my ($socket), ... ); $obj->set_output_fh($socket); $obj->output_method("Hello socket!\n"); # output to a string and STDOUT at the same time use IO::Tee; my $tee = IO::Tee->new( $str_fh, *STDOUT{IO} ); $obj->set_output_fh($tee); $obj->output_method("Hello all of you!\n"); # send the data nowhere use IO::Null; my $null_fh = IO::Null->new; $obj->set_output_fh($null_fh); $obj->output_method("Hello? Anyone there?\n"); # decide at run time: interactive sessions use stdout, # non-interactive session use a null filehandle use IO::Interactive; $obj->set_output_fh( interactive() ); $obj->output_method("Hello, maybe!\n");
It gets even better, though. You almost get some features for free. Do you want to have another method that returns the output as a string? You've already done most of the work! You just have to shuffle some filehandles around as you temporarily make a filehandle to a string (Item 54) as the output filehandle:
sub as_string { my ( $self, @args ) = @_; my $string = ''; open my ($str_fh), '>', \$string; my $old_fh = $self->get_output_fh; $self->set_output_fh($str_fh); $self->output_method(@args); # restore the previous fh $self->set_output_fh($old_fh); $string; }
If you want to have a feature to turn off all output, that's almost trivial now. You just use a null filehandle to suppress all output:
$obj->set_output_fh( IO::Null->new
)
if $config->{be_quiet};
Things to remember
- For flexibility, don't hard-code your filehandles.
- Give other programmers a way to change the output filehandle.
- Use IO::Interactive to check if someone will see your output.