Pseudoterminals The script(1) Program

 

Chapter 64: Pseudoterminals
Part 4 of 4 โ€” The script(1) Program
script
classic tool
typescript
output file
proxy
design pattern

๐Ÿ“‚ Tutorial Series Navigation
Part 1: Intro & PTY I/O Part 2: ptyFork() Part 3: Packet Mode โ–ถ Part 4: script(1) (this file)

๐Ÿท Key Terms in This Part
script(1) typescript file session recording proxy process raw mode ptyFork() echo password not captured stdin relay stdout relay waitpid() tcsetattr()

What is the script(1) Program?

script is a standard Unix utility that records everything that happens in a terminal session โ€” every command you type and every output produced โ€” and saves it to a file called typescript.

When you run script, it starts a new shell. Everything you do in that shell is recorded. When you type exit, the recording stops and you get a full transcript of the session.

script is historically important: many of the shell sessions shown in the TLPI book were captured using it. Understanding how script works is one of the best practical exercises for mastering pseudoterminals.

$ script          # Start recording to ./typescript
Script started, file is typescript

$ ls /tmp
file1  file2  file3

$ echo "hello"
hello

$ exit           # Stop recording
Script done, file is typescript

$ cat typescript  # View the recording
Script started...
$ ls /tmp
file1  file2  file3
...

๐Ÿ“ˆ How script Works โ€” The Big Picture

In a normal login session, the shell is connected directly to the user’s real terminal. When script runs, it inserts itself in between the user and the shell using a PTY pair.

๐Ÿ‘ค User
at terminal
โ†‘
โ†“
๐Ÿ“บ Real
Terminal
(stdin/stdout)
โ†‘
โ†“
๐Ÿ“‹ script
(parent)
proxy
โ†’
write
PTY
MASTER
KERNEL
PTY
PTY
SLAVE
(stdin/out/err)
โ†‘
โ†“
๐Ÿ  bash
(child)
โ†
read
script also writes everything read from master to typescript file

The script process acts as a proxy:

  • It reads from the real terminal (user’s keyboard input) and writes it to the PTY master โ†’ shell reads it from slave.
  • It reads from the PTY master (shell’s output, echoed input) and writes it to the real terminal โ†’ user sees it on screen.
  • It also copies everything read from master into the typescript file.

โš™ Why Commands You Type Are Also Recorded

You might wonder: the typescript file records both output and the commands you typed. How?

The answer is terminal echo. Recall from Part 1:

  • script writes your keypress to the PTY master.
  • The kernel’s PTY driver copies it to the slave input queue.
  • Because echo is ON, the kernel also copies the same byte to the slave output queue.
  • script reads this echo back from the PTY master.
  • script writes this echoed byte to both the screen AND the typescript file.
๐Ÿ’ก Key insight: script does not specially intercept your typing. It simply records all output from the master โ€” and that naturally includes echoed input.

When echo is disabled (e.g., for password entry), the input is NOT echoed to the output queue, so passwords do not appear in the typescript file. This is correct and intentional behaviour.

โš™ Why script Puts the Real Terminal in Raw Mode

When script starts, it changes the real terminal (the one the user is sitting at) to raw mode. Here is why:

Without raw mode (bad)
  • Real terminal processes Ctrl-C itself โ†’ sends SIGINT to script
  • Real terminal does line editing (backspace, Ctrl-U)
  • Double line buffering: real terminal AND PTY slave both buffer
  • Shell gets confused, double-echo, broken behaviour
With raw mode (correct)
  • Real terminal passes every byte directly to script unchanged
  • Ctrl-C goes through PTY to shell, which handles it correctly
  • No double buffering
  • All line editing happens inside the PTY slave (shell’s tty)
#include <termios.h>
#include <unistd.h>

/* Save original terminal settings */
struct termios orig_tios;
tcgetattr(STDIN_FILENO, &orig_tios);

/* Copy and modify for raw mode */
struct termios raw = orig_tios;
raw.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
raw.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR
                | INPCK | ISTRIP | IXON | PARMRK);
raw.c_oflag &= ~OPOST;
raw.c_cc[VMIN]  = 1;
raw.c_cc[VTIME] = 0;

/* Apply raw mode to real terminal */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);

/* ... run script recording loop ... */

/* Restore original settings when script exits */
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_tios);

๐Ÿ“„ Simplified script Implementation (Core Loop)

Below is a simplified but complete implementation showing how the main relay loop works. Error handling is simplified for clarity.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/select.h>
#include <sys/wait.h>
#include "pty_fork.h"   /* ptyFork() from TLPI */

#define SCRIPT_FILE "typescript"
#define BUF_SIZE    256

int main(void)
{
    int masterFd, scriptFd;
    pid_t childPid;
    char slaveName[64];
    char buf[BUF_SIZE];
    int nread;
    struct termios orig_tios, raw_tios;
    fd_set readfds;

    /* --- Open typescript output file --- */
    scriptFd = open(SCRIPT_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (scriptFd == -1) { perror("open typescript"); exit(1); }

    /* --- Get current terminal settings and window size --- */
    struct termios tios;
    struct winsize ws;
    tcgetattr(STDIN_FILENO, &tios);
    ioctl(STDIN_FILENO, TIOCGWINSZ, &ws);
    tcgetattr(STDIN_FILENO, &orig_tios);   /* save for restore later */

    /* --- Fork child with PTY, passing real terminal config --- */
    childPid = ptyFork(&masterFd, slaveName, sizeof(slaveName),
                       &tios, &ws);
    if (childPid == -1) { perror("ptyFork"); exit(1); }

    if (childPid == 0) {
        /* ======= CHILD: run a shell ======= */
        execlp("bash", "bash", NULL);
        perror("execlp bash");
        exit(1);
    }

    /* ======= PARENT: the script process ======= */
    printf("Script started, file is %s\n", SCRIPT_FILE);

    /* Put real terminal in raw mode so every byte passes through unchanged */
    raw_tios = orig_tios;
    raw_tios.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
    raw_tios.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
    raw_tios.c_oflag &= ~OPOST;
    raw_tios.c_cc[VMIN]  = 1;
    raw_tios.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw_tios);

    /* === Main relay loop === */
    while (1) {
        FD_ZERO(&readfds);
        FD_SET(STDIN_FILENO, &readfds);   /* real terminal (user input) */
        FD_SET(masterFd,     &readfds);   /* PTY master (shell output) */

        if (select(masterFd + 1, &readfds, NULL, NULL, NULL) == -1)
            break;

        /* --- User typed something on real terminal --- */
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
            nread = read(STDIN_FILENO, buf, BUF_SIZE);
            if (nread <= 0) break;
            /* Relay to PTY master: shell will see it as keyboard input */
            write(masterFd, buf, nread);
        }

        /* --- Shell produced output (or echoed input) --- */
        if (FD_ISSET(masterFd, &readfds)) {
            nread = read(masterFd, buf, BUF_SIZE);
            if (nread <= 0) break;  /* shell exited */

            /* Write to real terminal (user sees it) */
            write(STDOUT_FILENO, buf, nread);

            /* ALSO write to typescript file (this is the recording) */
            write(scriptFd, buf, nread);
        }
    }

    /* === Cleanup === */
    /* Restore real terminal to original settings */
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_tios);

    /* Wait for child (shell) to finish */
    waitpid(childPid, NULL, 0);
    close(masterFd);
    close(scriptFd);

    printf("\nScript done, file is %s\n", SCRIPT_FILE);
    return 0;
}

๐Ÿ”„ Complete Data Flow in script
Event Source script reads from script writes to Goes to typescript?
User types a character Real terminal (stdin) STDIN_FILENO masterFd (PTY) โŒ (not directly)
PTY echoes typed char PTY slave echo masterFd STDOUT + typescript โœ” YES
Shell prints output Shell writes to slave masterFd STDOUT + typescript โœ” YES
User types password (echo off) Real terminal (stdin) STDIN_FILENO masterFd only โŒ NO (no echo)
Shell exits PTY master masterFd returns EIO/0 Loop exits, cleanup โ€”

๐Ÿ”’ Why Passwords Are NOT Recorded by script

This is one of the most important design points of the PTY system. When a program like sudo or ssh asks for a password:

  1. It calls tcsetattr() to disable echo on the slave tty (clears the ECHO flag in c_lflag).
  2. The user types the password โ†’ script relays it to masterFd โ†’ kernel puts it in slave input queue.
  3. Because echo is OFF, the kernel does NOT copy those bytes to the slave output queue.
  4. script reads from masterFd โ†’ sees nothing (no echo output).
  5. Nothing is written to the typescript file.
๐Ÿ”’ Security benefit: This is not a bug or oversight. It is the correct and intended behaviour. The PTY echo mechanism naturally protects passwords from being captured by session recording tools.

๐Ÿ“ˆ Understanding Figure 64-4 from TLPI

TLPI’s Figure 64-4 shows the layers of the script architecture. Let’s walk through each connection:

Connection in Figure What it represents
User terminal โ†” script process The real terminal’s stdin/stdout. script reads keypresses from here and sends shell output back to here for display.
script process โ†” PTY master script reads all output (data + echoed input) from the master, and writes user input to the master to relay to shell.
PTY master โ†” PTY slave (kernel) Kernel PTY driver. Data flows bidirectionally with terminal line discipline applied on slave side.
PTY slave โ†” shell (child) Shell’s stdin/stdout/stderr are all the PTY slave. Shell reads user commands from here and prints results to here.
script โ†’ typescript file Everything script reads from the PTY master is copied to this file. This is the recording.
fork() + exec() arrow script calls ptyFork() which internally forks. The child execs bash. The parent becomes the script proxy.

โš™ Compile and Test the Simplified script

To build and test (assuming you have TLPI source available for ptyFork):

# Using the TLPI library:
gcc -o myscript myscript.c pty_fork.c error_functions.c -I../lib

# Or implement ptyFork inline and compile standalone:
gcc -o myscript myscript_standalone.c

# Run it:
./myscript

# You'll get a new bash shell.
# Type some commands:
echo "hello from script"
ls /tmp

# Exit the shell:
exit

# Check the recording:
cat typescript
๐Ÿ”Ž What you’ll see in typescript: All commands you typed (because of echo) and all output your commands produced. No passwords.

โš  Signal Handling in script

A production script implementation also needs to handle SIGWINCH: the signal sent when the terminal window is resized.

#include <signal.h>
#include <sys/ioctl.h>

static int masterFdGlobal;   /* for use in signal handler */

/* Called when user resizes the terminal window */
void sigwinch_handler(int sig)
{
    struct winsize ws;

    /* Get the new size from the real terminal */
    if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == -1)
        return;

    /* Apply the new size to the PTY slave via the master */
    if (ioctl(masterFdGlobal, TIOCSWINSZ, &ws) == -1)
        return;

    /* The kernel will also send SIGWINCH to the shell's process group */
}

/* In main: */
masterFdGlobal = masterFd;
signal(SIGWINCH, sigwinch_handler);

Without this handler, programs like vim and less running inside the script session will not reflow their display when you resize the window.

๐Ÿ“š Chapter 64 Complete Summary
Part Topic Key Concept
Part 1 PTY I/O basics Master/slave pair; 4kB buffer; close behaviour (SIGHUP/EIO); canonical mode echo
Part 2 ptyFork() posix_openpt + fork + setsid + dup2; child gets slave as stdin/out/err
Part 3 Packet Mode TIOCPKT; control byte protocol; POLLPRI/exceptfds; flow control for telnet/rlogin
Part 4 script(1) program Proxy pattern; raw mode on real terminal; record via master output; passwords not captured

๐ŸŽ“ Interview Questions โ€” script(1) Program
Q1. What does the script(1) program do and how does it use a pseudoterminal?
script records a complete terminal session to a file called typescript. It uses ptyFork() to create a PTY pair and fork a child shell. The child’s stdin/stdout/stderr are connected to the PTY slave. script (parent) relays data between the real terminal and the PTY master, and copies everything it reads from the master into the typescript file.
Q2. Why does script put the real terminal into raw mode?
In raw mode, the real terminal passes every byte to script unchanged without any processing. This prevents double line buffering (both real terminal and PTY slave would otherwise buffer), ensures Ctrl-C is forwarded to the shell through the PTY (not caught by the real terminal), and avoids double echo. All terminal processing happens in the PTY slave’s line discipline.
Q3. How does script record user input in the typescript file even though it only writes master output to the file?
Terminal echo. When script writes keypress data to the PTY master, the kernel PTY driver echoes those bytes back to the master output. script reads this echo and writes it to both the screen and the typescript file. So input is captured indirectly via echo, not by reading stdin directly.
Q4. Why are passwords not captured in the typescript file?
Programs reading passwords disable echo on the PTY slave using tcsetattr(). With echo disabled, the kernel does not copy slave input bytes to the output queue. Since script reads the master (which only sees output), it never sees the password bytes. The recording file never contains passwords.
Q5. How does script know when the shell has exited?
When the child shell exits, all slave fds are closed. The next read() from masterFd returns EIO (or 0 on some systems). script’s main loop detects this, breaks out, restores the real terminal settings with tcsetattr(), waits for the child with waitpid(), and closes the typescript file.
Q6. What is the role of select() in script’s main loop?
script must simultaneously watch two fds: STDIN_FILENO (user typing) and masterFd (shell output). select() allows it to block until either one is ready, then act on whichever one has data. Without select(), script would have to block on one fd and miss data from the other.
Q7. What is the “proxy” design pattern in the context of script?
script acts as a transparent proxy between the user’s real terminal and the shell. It relays all data in both directions while also recording a copy. From the shell’s perspective, it is talking to a normal terminal (the PTY slave). From the user’s perspective, nothing changes. The proxy is invisible to both sides.
Q8. How should script handle terminal window resize (SIGWINCH)?
script should install a SIGWINCH handler. When the real terminal is resized, SIGWINCH is delivered to script. The handler reads the new window size from the real terminal using ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) and applies it to the PTY master using ioctl(masterFd, TIOCSWINSZ, &ws). The kernel then sends SIGWINCH to the shell’s process group so programs like vim can reflow their display.
Q9. How does the script program record both the commands typed AND their output in a single file?
Because of how the PTY slave’s echo works. Input bytes arrive at the slave input queue AND (when echo is on) are also placed on the slave output queue. The output queue flows to the master, where script reads them. So script reads both echoed input and program output from the same master fd, and writes everything to the typescript file in one unified stream.
Q10. Why must script call tcsetattr() to restore the real terminal before exiting?
script put the real terminal in raw mode at startup. If it exits without restoring the terminal, the user’s shell (outside script) will still be in raw mode. This means Ctrl-C, Enter, backspace, and all editing will be broken. The terminal restoration is critical for leaving the user’s session in a usable state.

๐ŸŽ‰ Chapter 64 Complete!
You have covered all key topics: PTY I/O, ptyFork(), Packet Mode, and the script program. Great work!

โ† Back to Part 1: Intro EmbeddedPathashala Home

Leave a Reply

Your email address will not be published. Required fields are marked *