The Shell Lab in Computer Systems: A Programmer's Perspective (CSAPP) at CMU is one of the most interesting and rewarding labs in the course. It involves writing a Unix-like shell that can handle job control, including running jobs in the foreground or background, handling signals, and managing jobs (processes).

Step 1: Understanding Shell Lab

A shell is a command-line interpreter that allows users to interact with the operating system. It runs user commands, manages job control (foreground/background processes), and handles signals like SIGINT (Ctrl-C) and SIGTSTP (Ctrl-Z).

Key concepts to implement:

Step 2: Set Up Your Environment

Start by copying the provided files and reviewing tsh_helper.h and csapp.h. These files provide useful functions and constants for job management and signal handling.

Step 3: Parse the Command Line

The first task is to parse user commands. The function parseline provided in the lab splits the input command into tokens and returns whether it should be run in the background or foreground. Your shell will have to handle both built-in and external commands.

Here's a simple command-line parser (eval) that decides whether to execute a built-in command (like quit, jobs, bg, or fg) or an external command:

void eval(const char *cmdline) {
    parseline_return parse_result;
    struct cmdline_tokens token;

    // Parse command line
    parse_result = parseline(cmdline, &token);

    if (parse_result == PARSELINE_ERROR || parse_result == PARSELINE_EMPTY) {
        return;
    }

    // Determine the type of command and handle it accordingly
    if (token.builtin == BUILTIN_QUIT) {
        exit(0);  // Quit the shell
    } else if (token.builtin == BUILTIN_NONE) {
        run_external_command(cmdline, token, parse_result);  // External command
    } else if (token.builtin == BUILTIN_JOBS) {
        handle_jobs(token);  // List jobs
    } else {  // 'fg' or 'bg' commands
        handle_fg_bg(token);
    }
}

Step 4: Running External Commands

To run external commands, you need to fork a child process and then use execve to run the command in the child. You also need to handle I/O redirection if the user specifies input/output files with > or <.

Here’s how to implement the function that runs external commands:

void run_external_command(const char *cmdline, struct cmdline_tokens token, parseline_return parse_result) {
    pid_t pid;
    sigset_t mask_all, prev_mask;
    sigfillset(&mask_all);
    sigprocmask(SIG_BLOCK, &mask_all, &prev_mask);  // Block all signals

    if ((pid = fork()) == 0) {  // Child process
        setpgid(0, 0);  // Set process group ID to avoid terminal issues

        // Handle I/O redirection
        if (token.infile) {
            int in_file = open(token.infile, O_RDONLY);
            if (in_file < 0) {
                perror(token.infile);
                exit(1);
            }
            dup2(in_file, STDIN_FILENO);
            close(in_file);
        }
        if (token.outfile) {
            int out_file = open(token.outfile, O_WRONLY | O_CREAT | O_TRUNC, DEF_MODE);
            if (out_file < 0) {
                perror(token.outfile);
                exit(1);
            }
            dup2(out_file, STDOUT_FILENO);
            close(out_file);
        }

        sigprocmask(SIG_SETMASK, &prev_mask, NULL);  // Unblock signals

        if (execve(token.argv[0], token.argv, environ) < 0) {
            perror(token.argv[0]);
            exit(1);
        }
    }

    // Parent process: manage jobs based on foreground/background
    if (parse_result == PARSELINE_FG) {
        add_job(pid, FG, cmdline);  // Add to foreground
        jid_t jid = job_from_pid(pid);
        while (job_exists(jid) && job_get_state(jid) == FG) {
            sigsuspend(&prev_mask);  // Wait for the job to finish
        }
    } else {
        add_job(pid, BG, cmdline);  // Add to background
        jid_t jid = job_from_pid(pid);
        printf("[%d] (%d) %s\\\\n", jid, pid, cmdline);
    }

    sigprocmask(SIG_SETMASK, &prev_mask, NULL);  // Unblock all signals
}

This function: