3

Can I detect if a Perl script is being run from a terminal for sure?

I'd rather default to assume that it's been run from a browser if I'm not sure. But if there's a way to be sure that it has 100% been run from a terminal I would be happy (for debugging purposes).

brian d foy
  • 129,424
  • 31
  • 207
  • 592
Daniel Kaplan
  • 701
  • 7
  • 19

3 Answers3

5

This is taken directly from the source code of ExtUtils::MakeMaker's prompt function. It's possible, I suppose, that someone could go to lengths to trick it. But at some point the breakage must be owned by the breaker.

For most purposes this ought to be adequate:

 my $isa_tty = -t STDIN && (-t STDOUT || !(-f STDOUT || -c STDOUT)) ;

First it checks if STDIN is opened to a TTY. If so, check if STDOUT is. If STDOUT is not, it must also neither be opened to a file nor a character special file.

Update:

IO::Prompt::Tiny uses the following:

# Copied (without comments) from IO::Interactive::Tiny by Daniel Muey,
# based on IO::Interactive by Damian Conway and brian d foy

sub _is_interactive {
    my ($out_handle) = ( @_, select );
    return 0 if not -t $out_handle;
    if ( tied(*ARGV) or defined( fileno(ARGV) ) ) {
        return -t *STDIN if defined $ARGV && $ARGV eq '-';
        return @ARGV > 0 && $ARGV[0] eq '-' && -t *STDIN if eof *ARGV;
        return -t *ARGV;
    }
    else {
        return -t *STDIN;
    }
}

And IO::Interactive::Tiny adds comments to explain what's going on:

sub is_interactive {
    my ($out_handle) = (@_, select);    # Default to default output handle

    # Not interactive if output is not to terminal...
    return 0 if not -t $out_handle;

    # If *ARGV is opened, we're interactive if...
    if ( tied(*ARGV) or defined(fileno(ARGV)) ) { # IO::Interactive::Tiny: this is the only relavent part of Scalar::Util::openhandle() for 'openhandle *ARGV'
        # ...it's currently opened to the magic '-' file
        return -t *STDIN if defined $ARGV && $ARGV eq '-';

        # ...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;

        # ...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.
    else {
        return -t *STDIN;
    }
}

And I've verified that the logic in IO::Interactive mirrors that of IO::Interactive::Tiny. So, if your goal is to prompt where appropriate, consider using IO::Prompt::Tiny. And if your needs are more nuanced than IO::Prompt::Tiny supports, you can use IO::Interactive::Tiny to provide this specific functionality.

While you're probably mostly safe using your own solution, an advantage to using one of these CPAN modules is that they are presumably actively maintained and would receive but reports and eventual updates if they turn out to be inadequate to their advertised purpose.

DavidO
  • 13,812
  • 3
  • 38
  • 66
  • The overall purpose of this heuristic being pretty straightforward: can I output a prompt and expect the user to be able to respond to it. – Grinnz Nov 04 '18 at 02:52
  • Yes, and in fact this is precisely why ExtUtils::MakeMaker puts this test in its `prompt` function. A similar test is also embedded within `IO::Prompt::Tiny`. – DavidO Nov 04 '18 at 02:52
  • Actually that last statement is not true: IO::Prompt::Tiny uses logic borrowed from IO::Interactive::Tiny. I'll update my answer with that as an alternative. – DavidO Nov 04 '18 at 02:54
3

Use the file test operator -t which tests whether a file handle is attached to a terminal. For example:

if (-t STDIN) {
  print "Running with a terminal as input."
}
Tim
  • 9,171
  • 33
  • 51
  • Thanks. Since I've read some links that state sometimes a browser may not set the proper ENV variable, is STDIN a safe solution. Could there ever be a false positive for the above? – Daniel Kaplan Nov 03 '18 at 18:38
  • 4
    It's a fairly good solution. Of course, depending on the program, it could be running from a terminal, but with standard input redirected from a file, in which case this wouldn't work perfectly. You might want to test STDOUT and STDERR as well - if one of them is a terminal, you know there's a terminal there. You can get some confidence by trying it in the ways you expect your program to be used, and see what it does. – Tim Nov 03 '18 at 18:41
  • 3
    Also note that for a program run from the terminal, but as the receiving end of a pipe, for example if you type `cat data.txt | prog.pl` in the terminal, the STDIN of `prog.pl` will not be a tty. – Håkon Hægland Nov 03 '18 at 19:56
1

The device file /dev/tty represents the controlling terminal for the process. If your process isn't attached to a terminal (is a daemon, runs out of cron/at, etc) then it can't "open" this special device. So the following tests for that

sub isatty {
    no autodie;
    return open(my $tty, '+<', '/dev/tty');
}

The /dev/tty can represent a virtual console device (/dev/ttyN), pty (xterm, ssh), serial port (COM1), etc, and is not affected by redirections, so this should be reliable.

If this runs very often perhaps use this version

use feature qw(state);

sub isatty { 
    no autodie; 
    state $isatty = open(my $tty, '+<', '/dev/tty'); 
    return $isatty;
}

which should be more efficient (over an order of magnitude in my simple benchmark).

These only work on Unix-y systems (or in an POSIX application running on top of Windows, or in Window's POSIX subsystem).

zdim
  • 64,580
  • 5
  • 52
  • 81