4

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).

1
  • One answer returns true for prog </dev/null, the other returns false. Depends what you want. Commented Nov 4, 2018 at 2:40

3 Answers 3

8

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.

Sign up to request clarification or add additional context in comments.

3 Comments

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.
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.
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.
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."
}

3 Comments

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?
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.
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.
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).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.