How to get the real name of the controlling terminal?

The "controlling terminal" aka. ctty, is distincted from "the terminal a process is interacting with".

Standard way of getting the path of ctty is ctermid(3). Upon calling this, In freebsd since release 10, an actual path is looked up[1], while older freebsd and glibc implementations[2] unconditionally returns "/dev/tty"].

ps(1) from the linux procps 3.2.8 package, read the numerical entry in /proc/*/stat[3], and then deduct the pathname partially by guessing[4, 5] due to lack of system support[6].

However if we are not strictly interested in the ctty but any terminal associated with stdio, tty(1) prints the terminal path connected to stdin, which is identical to ttyname(fileno(stdin)) in c, and an alternative is readlink /proc/self/fd/0.


Less important thought regarding the unconditional "/dev/tty" behavior: Specs merely say the string returned by ctermid "when used as a path name, refer to the current controlling terminal", instead of some straightforward "is the path name of the current controlling terminal". It might be interpreted as that "/dev/tty" is not the controlling terminal, but only refer to the controlling terminal if the same process open(3) it. Thus not violating the "a terminal may be ctty for at most one session" rule[7].

Another consequence is that when I am without any controlling terminal, ctermid does not fail -- such failing is allowed by specs[8] --, so only can I become aware of my ctty'lessness until failing a subsequent open(3), which is okay since specs also say calling open(3) on it is not guarranteed to succeed.


The POSIX spec really hedges its bets where the Controlling Terminal is concerned, and which it defines thus:

  • Controlling Terminal
    • The question of which of possibly several special files referring to the terminal is meant is not addressed in POSIX.1. The pathname /dev/tty is a synonym for the controlling terminal associated with a process.

That's in the Definitions list - and that's all there is there. But in General Terminal Interface, some more is said:

  • A terminal may belong to a process as its controlling terminal. Each process of a session that has a controlling terminal has the same controlling terminal. A terminal may be the controlling terminal for at most one session. The controlling terminal for a session is allocated by the session leader in an implementation-defined manner. If a session leader has no controlling terminal, and opens a terminal device file that is not already associated with a session without using the O_NOCTTY option (see open()), it is implementation-defined whether the terminal becomes the controlling terminal of the session leader.

  • The controlling terminal is inherited by a child process during a fork() function call. A process relinquishes its controlling terminal when it creates a new session with the setsid() function; other processes remaining in the old session that had this terminal as their controlling terminal continue to have it. Upon the close of the last file descriptor in the system (whether or not it is in the current session) associated with the controlling terminal, it is unspecified whether all processes that had that terminal as their controlling terminal cease to have any controlling terminal. Whether and how a session leader can reacquire a controlling terminal after the controlling terminal has been relinquished in this fashion is unspecified. A process does not relinquish its controlling terminal simply by closing all of its file descriptors associated with the controlling terminal if other processes continue to have it open.

There's a lot there left unspecified - and honestly I think it makes sense. While the terminal is a key user interface, it's also all kinds of other things in some cases - like actual hardware, or even a kind of printer - but in a lot of cases it's practically nothing at all - like an xterm which is just an emulator. It's hard to get specific there - and I don't think it would be much in the interest of Unix anyway, because terminals do a lot more than Unix.

Anyway, POSIX is also pretty iffy on how ps should behave where the ctty is concerned.

There's the -a switch:

  • Write information for all processes associated with terminals. Implementations may omit session leaders from this list.

Great. Session leaders may be omitted. That's not very helpful.

And -t:

  • Write information for processes associated with terminals given in termlist. The application shall ensure that the termlist is a single argument in the form of a <blank> or comma-separated list. Terminal identifiers shall be given in an implementation-defined format.

...which is another let-down. But it does go on to say this about XSI systems:

  • On XSI-conformant systems, they shall be given in one of two forms: the device's filename (for example, tty04) or, if the device's filename starts with tty, just the identifier following the characters tty (for example, 04 ).

That's a little better, but is not a path. Also on XSI systems there is the -d switch:

  • Write information for all processes, except session leaders.

...which is at least clear. You can specify the -output switch as well with the tty format string, but, as you've noted, its output format is implementation-defined. Still, I think it is as good as it gets. I think that - with a lot of work - the above switches in combination with some other utilities can get you a pretty good ballpark. To be quite honest though, I don't know when/how it breaks for you - and I haven't been able to imagine a situation in which it would. But, I think probably if we add fuser and find we can verify the path.

exec 2<>/dev/null
ctty=$(sh -c 'ps -p "$$" -o tty=' <&2)
sid=$(sh -c 'ps -Ao pid= -o tty=|
      grep '"$ctty$"' | 
      grep -Fv "$(ps -do pid=)"'  <&2)
find / -type c -name "*${ctty##*/}*" \
       -exec fuser -uv {} \; 2>&1  |
grep ".*$ctty.*${sid%%"$ctty"*}"

The /dev/null stuff was just to show that it could work when none of the searching subshells had any of 0,1,2 connected to the ctty. Anyway, that prints:

/dev/pts/3:          mikeserv   3342 F.... (mikeserv)zsh

Now the above gets the full path on my machine, and I imagine it would for most people in most cases. I can also imagine it could fail. It's just rough heuristics.

This is could fail for many other reasons probably, but if you're on a system which allows the session leader to relinquish all descriptors to the ctty and yet remain the sid then as the spec allows, then this is definitely not going to help. That said, I think this can get a pretty good estimate in most cases.

Of course the easiest thing to do if you have any descriptors connected to your ctty is just...

tty <&2

...or similar.