2025/10/15: Wrapping in a terminal

The interface (on UNIX-like systems) of a CLI program is pretty rich: the command-line arguments, the stream on stdin, the environment variables, the file system, etc. One of the things a process can ask for is whether a certain file descriptor is attached to a terminal and, if this is the case, various parameters of that terminal, including size. And, of course, if something can be done, there are programs that do it, including those that, like bazel, do not interactively work with the user but simply produce a human-readable progress stream on stderr.

This means, in order to build—in a programmatic way—accurate output samples for documentation/presentation/testing, the program needs to be connected to a terminal, which is typically not the case for actions called from a build tool. Fortunately, for every part of interface, there is also a way to set it. In this case, it is openpty(3). So there we are with the wrapper program of the day: withPty (can be used under the terms of the Apache-2.0 license).


#include <pty.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <termios.h>
#include <unistd.h>

/* fork a process that executes argv+1 with stdin/out/err
   connected to a (pseudo)terminal of size 80x25 and forward
   stdin to the terminal and terminal output to stdout; exit
   with the exit code of the child. */
int main(int argc, char **argv) {
  int master, slave;
  pid_t pid;
  int child_status;
  int pos, size, write_size, input_done;
  fd_set rfds;
  struct timeval timeout;
  struct termios tmios;
  struct winsize winsz;
  char buf[4096];

  if (openpty(&master, &slave, NULL, NULL, NULL) != 0) {
    /* terminal allocation failed */
    return 66;
  }

  /* create and set up child process */
  pid = fork();
  if (pid < 0) {
    /* fork failed */
    return 65;
  }
  if (pid == 0) {
    /* child */
    close(master);

    /* set terminal size */
    if (ioctl(slave, TIOCGWINSZ, &winsz) == 0) {
      winsz.ws_row = 25;
      winsz.ws_col = 80;
      ioctl(slave, TIOCSWINSZ, &winsz);
      setenv("COLUMNS", "80", 1);
    }

    /* disable echo */
    if (tcgetattr(slave, &tmios) == 0) {
      tmios.c_lflag &= ~(ECHO);
      tcsetattr(slave, TCSANOW, &tmios);
    }

    /* connect stdin, stdout, and stderr to the slave end of the pty */
    dup2(slave, 0);
    dup2(slave, 1);
    dup2(slave, 2);
    close(slave);

    /* set a meaningful value of TERM, if not set already */
    setenv("TERM", "xterm-256color", 0);

    execvp(argv[1], argv + 1);
    /* exec failed*/
    return 64;
  }

  close(slave);
  input_done = 0;

  while(1) {
    /* if child exited, we will exit with the same status code */
    if (waitpid(pid, &child_status, WNOHANG) == pid) {
      if (WIFEXITED(child_status)) {
        break;
      }
    }
    /* forward any output */
    FD_ZERO(&rfds);
    if (!input_done) {
      FD_SET(0, &rfds);
    }
    FD_SET(master, &rfds);
    timeout.tv_sec = 1;
    timeout.tv_usec = 0;
    if (select(master + 1, &rfds, NULL, NULL, &timeout) > 0) {
      if (FD_ISSET(master, &rfds)) {
        size = read(master, buf, 4096);
        pos = 0;
        while (size > 0) {
          write_size = write(1, &buf[pos], size);
          size -= write_size;
          pos += write_size;
        }
      }
      if (FD_ISSET(0, &rfds)) {
        size = read(0, buf, 4096);
        if (size == 0) {
          /* end of input on stdin; signal as EOT to the terminal */
          buf[0] = '\x04';
          write(master, buf, 1);
          close(0);
          input_done = 1;
        } else {
          pos = 0;
          while (size > 0) {
            write_size = write(master, &buf[pos], size);
            size -= write_size;
            pos += write_size;
          }
        }
      }
    }
  }

  /* handle any remaining output */
  size = read(master, buf, 4096);
  while(size > 0) {
    pos = 0;
    while (size > 0) {
      write_size = write(1, &buf[pos], size);
      size -= write_size;
      pos += write_size;
    }
    size = read(master, buf, 4096);
  }

  return WEXITSTATUS(child_status);
}
download