How to check if stdin is a tty in an escript?

I need to know if the standard input of my escript is a terminal or not. io:getopts() is almost there, except that it checks is the standard output is a tty or not.

    $ cat x.escript
    #!/usr/bin/env escript

    main(_) ->
      io:format("~p~n", [proplists:get_value(terminal, io:getopts(), unknown)]).

This is how it works:

    $ ./x.escript         # stdin and stdout are both ttys
    $ echo | ./x.escript  # stdin is not a tty, but stdout is
    $ ./x.escript | cat   # stdin is a tty, but stdout goes to a pipe => it is not a tty

This clearly checks the stdout. Is there a way to check if the stdin is a terminal?

There is no official API to do it. But in Erlang/OTP 26 and later you can call the undocumented function prim_tty:isatty(stdin) to get that information.

Note that since it is undocumented, it may very will be removed/changed in a future release without notice, so if you want this functionality I would suggest making a PR exposing it similarly how stdout is exposed through io:getopts/0 today.


Presumably this is something you check once, so it doesn’t have to be ultra-fast.

{ok,F} = file:open(“/dev/stdin”, [read]),
Opts = io:getopts(F),
ok = file:close(F),

should do the trick. You’d think.
In an interactive shell, running Erlang 24, both /dev/stdin and /dev/tty yield
Opts = [{binary,false},{encoding,latin1}]
What am I doing wrong here?

The documentation states:

the file I/O server returns all available options for a file, which are the expected ones, encoding and binary.

So you are getting the options that are available for anything opened using the file API.

Using /dev/stdin was however a good idea, you just need to do it with file:read_file_info("/dev/stdin"). If you do it on Linux you will get:

%% When stdin is a terminal
> file:read_file_info("/dev/stdin").
{ok,#file_info{size = 0,type = device,access = read_write,
               atime = {{2024,4,12},{9,11,28}},
               mtime = {{2024,4,12},{9,11,28}},
               ctime = {{2024,4,11},{12,18,37}},
               mode = 8592,links = 1,major_device = 24,
               minor_device = 34816,inode = 3,uid = 107466,gid = 5}}
%% When stdin is a pipe
> file:read_file_info("/dev/stdin").
{ok,#file_info{size = 0,type = other,access = read_write,
               atime = {{2024,4,12},{9,12,10}},
               mtime = {{2024,4,12},{9,12,10}},
               ctime = {{2024,4,12},{9,12,10}},
               mode = 4480,links = 1,major_device = 14,minor_device = 0,
               inode = 1755087,uid = 107466,gid = 64000}}

You’ll notice that mode is different on these two. If we look at: The file type and mode it states:

           S_IFCHR    0020000   character device
           S_IFIFO    0010000   FIFO

So by looking at the mode of /dev/stdin we can see if it is a terminal or not (On Linux, not sure about other UNIXs and definitely not on Windows).