perl-diddler has asked for the wisdom of the Perl Monks concerning the following question:

I am trying to get the current number of rows and columns of the TTY I am attached to under linux.

Originally, I was calling the "/bin/stty size" prog and grabbing the output, but decided I didn't want to have to call an outside program.

I looked at the source of 'stty', and found it using the TIOCGWINSZ call to 'ioctl' on stdout and getting back a struct of 4 shorts (16-bit).

So, converting that to perl, I end up with:

#!/usr/bin/perl -w use strict; my $need_update; my ($maxcols, $maxrows); sub getwinsize () { sub TIOCGWINSZ () {0x5413} #from ioctls.ph, which wouldn't include return ($maxcols,$maxrows) if $maxcols && $maxrows && !$need_update; my $winsize=0; my $err = ioctl (STDOUT, TIOCGWINSZ, $winsize); if ($err) { print "ioctl err: $?\n"; } my ($rows, $cols, $xpix, $ypix) = unpack "s!*", $winsize; printf STDERR "r=%d, c=%d, xp=%d, yp=%d\n", $rows//0, $cols//0, $xpix//0, $ypix//0; $need_update = undef; ($maxcols, $maxrows) = ($cols ? $cols : -1, $rows ? $rows : -1); } #main my ($rows,$cols) = getwinsize; print "rows=$rows, cols=$cols\n";
But this doesn't work -- returns all zeros for the sizes.

Why? The ioctl works in 'C', why not in Perl?

Thanks!

Replies are listed 'Best First'.
Re: Failing to get current TTY's rows & columns...
by Anonymous Monk on Apr 13, 2011 at 20:32 UTC
    See ioctl, if the return value is true, its not an error.

    See also Pack/Unpack Tutorial (aka How the System Stores Data), s!* looks suspicious, what I'd usually do is try every option :)

    update: Oh look, this is a perlfaq8 item

    How do I get the screen size?

    If you have Term::ReadKey module installed from CPAN, you can use it to fetch the width and height in characters and in pixels:

    use Term::ReadKey; ($wchar, $hchar, $wpixels, $hpixels) = GetTerminalSize();
    This is more portable than the raw ioctl, but not as illustrative:
    require 'sys/ioctl.ph'; die "no TIOCGWINSZ " unless defined &TIOCGWINSZ; open(TTY, "+</dev/tty") or die "No tty: $!"; unless (ioctl(TTY, &TIOCGWINSZ, $winsize='')) { die sprintf "$0: ioctl TIOCGWINSZ (%08x: $!)\n", &TIOCGWIN +SZ; } ($row, $col, $xpixel, $ypixel) = unpack('S4', $winsize); print "(row,col) = ($row,$col)"; print " (xpixel,ypixel) = ($xpixel,$ypixel)" if $xpixel || $ypixe +l; print "\n";
      So the answer here is that I have to open "/dev/tty" each time I call it rather than reference my already-open "STDOUT"?

      Hmmm....I'm disappointed that I need to open another file handle and can't get the rows & columns associated with my already open FH, "STDOUT", directly...

      As for using 'Term::ReadKey', I thought of using something along those lines (or a Curses func), but wanted something that was minimal overhead, as this was called every loop, and portability isn't a major consideration, considering the nature of the program this is being used in is showing linux process and mounted-dev I/O.

      If I look at /proc/$$/fd/1, I see a softlink to /dev/pts/1 (i.e. a character, tty type device). Indeed, if I use your 2nd prog and substitute in "/dev/pts/1" instead of "/dev/tty", I get the same result (ie. it works).

      Shouldn't STDIN, STDOUT map to fd[0], fd1 => /dev/pts/1 on some level?

      I.e. Shouldn't I be able to use some mapping function on STDOUT to get a file descriptor that's suitable for ioctl?

        So the answer here is that I have to open "/dev/tty" each time I call it rather than reference my already-open "STDOUT"?

        I don't have linux, but no, I don't think that is the answer.

        I think part of the answer is to do error checking correctly like the faq entry

        I also think using S4 is part of the answer, not being an expert on pack/unpack and not having a /dev/tty, I can't run any tests.

        Hmmm....I'm disappointed that I need to open another file handle and can't get the rows & columns associated with my already open FH, "STDOUT", directly...

        Sounds premature if you ask me, avoiding abstractions always involves some growing pains :)

        Shouldn't STDIN, STDOUT map to fd[0], fd[1] => /dev/pts/1 on some level?

        They do

        $ perl -le " print fileno($_) for STDIN, STDOUT, STDERR " 0 1 2
        See, fd 0,1,2. Now regarding tty, using operator -t to test if filehandle is opened to a tty, you can see 0,1,2 are connected to a tty
        $ perl -le " print 0+-t $_ for STDIN, STDOUT, STDERR " 1 1 1
        but not when you do redirection, here is STDIN not connected to a tty
        $ perl -le " print 0+-t $_ for STDIN, STDOUT, STDERR " < NUL 0 1 1
        here is STDIN and STDERR not connected to a tty
        $ perl -le " print 0+-t $_ for STDIN, STDOUT, STDERR " < NUL 2> NUL 0 1 0
        here is STDOUT not connected to a tty
        $ perl -le " print 0+-t $_ for STDIN, STDOUT, STDERR " > out.txt $ cat out.txt 1 0 1
        I.e. Shouldn't I be able to use some mapping function on STDOUT to get a file descriptor that's suitable for ioctl?

        ioctl says you don't need to, the faq item doesn't, and neither does Term::Size::Any

      See ioctl, if the return value is true, its not an error.
      My manpage says:
      RETURN VALUE Usually, on success zero is returned. A few ioctl() requests +use the return value as an output parameter and return a non-negative +value on success. On error, -1 is returned, and errno is set appropriat +ely.
      Note the phrase: "Usually, on success, zero is returned."... Some return a value as an output param, but this call isn't one of them. My error check code appears to be valid according to the above, though I noticed an oddity if you print the value returning from ioctl in this code:
      require 'sys/ioctl.ph'; die "no TIOCGWINSZ " unless defined &TIOCGWINSZ; open(TTY, "+</dev/tty") or die "No tty: $!"; my $ret=ioctl(TTY, &TIOCGWINSZ, $winsize=''); printf "ret num=%d, retstring=%s\n",$ret,$ret; unless ($ret) { die sprintf "$0: ioctl TIOCGWINSZ (%08x: $!)\n", &TIOCGWIN +SZ; } ($row, $col, $xpixel, $ypixel) = unpack('S4', $winsize); print "(row,col) = ($row,$col)"; print " (xpixel,ypixel) = ($xpixel,$ypixel)" if $xpixel || $ypixe +l; print "\n";
      The output is:
      ret num=0, retstring=0 but true (row,col) = (79,80)
      That is bizarre! 0 = true?!

      In any event, that's a red herring, as my code works when I use "/dev/tty" as a device instead of STDOUT. Same goes for the unpack format.

      I used S!*, which is likely "more correct" than S4 shown above.

      The "!" after the "S" says to use the platform's native size for a short instead of fixed a fixed 16-bit size. Since the struct is declared as a short on my platform, I figured it was best to use my platform's native "short" size rather than a fixed 16 bit value -- wouldn't you agree??

      The "*" vs. "4", makes no difference in this case as there are only 4 values to receive the output. "*" tells unpack to unpack as many as are in the struct -- so if the struct was 6 shorts long instead of 4, it would return 6 values, the last 2 of which would be thrown away as I only have an array of 4 vars to receive it. It would be like doing:

      my @ar=(1,2,3,4,5,6); my ($x,$y,$z)=@ar;
      $x-$z get values 1 through 3, and values 4,5,6 get ignored.

      All of the above covers the stuff that "doesn't make a difference". Then comes the part that doesn't make sense.

      As you note, fileno() on STD(IN,OUT,ERR) all return 0,1,2 as expected. Then why can't I use ioctl(STDOUT) with ioctl and have it work?

        My manpage says:

        Manpage? I already linked to ioctl before, perldoc -f ioctl

Re: Failing to get current TTY's rows & columns...
by eff_i_g (Curate) on Apr 13, 2011 at 20:40 UTC