Hack 13. Interact Correctly on the Command Line


Be kind to other programs.

Command-line programs that expect input from the keyboard are easy, right? Certainly they're easier than writing good GUI applications, right? Not necessarily. The Unix command line is flexible and powerful, but that flexibility can break naively written programs.

Prompting for interactive input in Perl typically looks like:

print "> "; while (my $next_cmd = <>) {     chomp $next_cmd;     process($next_cmd);     print "> "; }

If your program needs to handle noninteractive situations as well, things get a whole lot more complicated. The usual solution is something like:

print "> " if -t *ARGV && -t select; while (my $next_cmd = <>) {     chomp $next_cmd;     process($next_cmd);     print "> " if -t *ARGV && -t select; }

The -t test checks whether its filehandle argument is connected to a terminal. To handle interactive cases correctly, you need to check both that you're reading from a terminal (-t *ARGV) and that you're writing to one (-t select). It's a common mistake to mess those tests up, and write instead:

print "> " if -t *STDIN && -t *STDOUT;

The problem is that the <> operator doesn't read from STDIN; it reads from ARGV. If there are filenames specified on the command line, those two filehandles aren't the same. Likewise, although print usually writes to STDOUT, it won't if you've explicitly select-ed some other destination. You need to call select with no arguments to get the filehandle which each print will currently target.

Worse, still, even the correct version:

print "> " if -t *ARGV && -t select;

doesn't always work correctly. That's because the ARGV filehandle is magically self-opening, but only magically self-opens during the first read operation on it. If you haven't already done at least one <> before you start prompting for input, then the ARGV handle won't be open yet, so the first -t *ARGV test (the one before the while loop) won't be true, and the first prompt won't print.

To accurately test if an application is running interactively in all possible circumstances, you need an elaborate nightmare:

use Scalar::Util qw( openhandle ); sub is_interactive {     # Not interactive if output is not to terminal...     return 0 if not -t select;     # If *ARGV is opened, we're interactive if...     if (openhandle *ARGV)     {         # ...it's currently opened to the magic '-' file         #    and the standard input is interactive...         return -t *STDIN if defined $ARGV && $ARGV eq '-';         # ...or it's at end-of-file and the next file         #    is the magic '-' file...         return @ARGV>0 && $ARGV[0] eq '-' && -t *STDIN if eof *ARGV;         # ...or it's directly attached to the terminal         return -t *ARGV;     }     # If *ARGV isn't opened, it will be interactive if *STDIN is     # attached to a terminal and either there are no files specified     # on the command line or if there are files and the first is the     # magic '-' file...     else     {         return -t *STDIN && (@ARGV= =0 || $ARGV[0] eq '-');     } }

The Hack

Of course, no one wants to reinvent that for each project, so there's a CPAN module that does it for you:

use IO::Interactive qw( is_interactive ); print "> " if is_interactive; while (my $next_cmd = <>) {     chomp $next_cmd;     process($next_cmd);     print "> " if is_interactive; }

The Hack

The module has a second interface that's even Lazier. Instead of an explicit interactivity test, it can provide you with a writable filehandle that implicitly tests for interactivity:

use IO::Interactive qw( interactive ); print {interactive} "> "; while (my $next_cmd = <>) {     chomp $next_cmd;     process($next_cmd);     print {interactive} "> "; }



Perl Hacks
Perl Hacks: Tips & Tools for Programming, Debugging, and Surviving
ISBN: 0596526741
EAN: 2147483647
Year: 2004
Pages: 141

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net