Samrat Man Singh

Email: mail@samrat.me

Hi! I’m Samrat, a programmer from Nepal.

Piping output to a pager

2019-10-28


When you run git diff on a project where you’ve made more than a screenful of changes, the output is automatically redirected to a pager and you can browse through your changes using the familiar less interface. This post shows a minimal example piping output to less.

To achieve this, git creates a pipe and spawns a new process, whose purpose is to exec the pager.

int fds[2];
if (pipe(fds) == -1) {
  perror_die("pipe");
}
int child_pid = fork();

Let’s start by looking at the parent process, even though in the code this case is handled last. The STDOUT of the parent process is aliased to the write end of the pipe, so any further printing in the parent process writes stuff to fds[WRITE_END]. Once we’re done, we close the write end of the pipe to signal EOF to the child process.

We wait for the child process to exit since otherwise control returns to the shell.

switch (child_pid)
// ...
  default: { /* Parent */
    /* STDOUT is now fds[WRITE_END] */
    dup2(fds[WRITE_END], STDOUT_FILENO);
    /* parent doesn't read from pipe */
    close(fds[READ_END]);

    /* "Business" logic which determines what to actually print */
    int num_lines = 1024;
    if (argc > 1) {
      num_lines = atoi(argv[1]);
    }
    print_n_lines(num_lines);


    fflush(stdout);

    /* Signal EOF to the pager process */
    close(STDOUT_FILENO);

    int stat_loc;
    waitpid(child_pid, &stat_loc, 0);
    break;
  }

The STDIN of the new process is aliased to the read end of the pipe. Anything being printed in the parent process is thus now an input to the pager process. After setting up STDIN, this process runs less.

switch (child_pid) {
case 0: {    /* Child(pager) */
    /* Pager process doesn't write to pipe */
    close(fds[WRITE_END]);

    /* Make READ_END of pipe pager's STDIN */
    dup2(fds[READ_END], STDIN_FILENO);

    /* F -> quit-if-one-screen */
    /* R -> preserve color formatting */
    /* X -> don't send some special instructions eg. to clear terminal screen before starting */
    char *less_argv[] = {"less", "-FRX", NULL};
    int exec_status = execvp(less_argv[0], less_argv);

    fprintf(stderr,
            "execvp failed with status: %d and errno: %d\n", exec_status, errno);
    break;
  }
  
// ...
} // switch

The full example can be found in this gist. You can also check out the Ruby and Rust implementations of this.

Tags: c  programming 

If you have comments or feedback, please send me an email or reach out on Twitter.