Skip to content

Exam Rank 04: Process & Parsing

Download Subject Files:

Exam Rank 04 tests your ability to work with Unix system calls. These skills are essential for writing shells, daemons, and any program that needs to spawn other processes.

IMPORTANT: Exam Rank 04 is pure C, not C++. These exercises test Unix system programming fundamentals: processes, pipes, file descriptors, and parsing.

The exam has two levels: Level 1 tests process management, Level 2 tests parsing. You’ll get one exercise from each level.

Exam Structure:

  • Level 1: picoshell, ft_popen, or sandbox (process-focused)
  • Level 2: argo or vbc (parsing-focused)

Key Concepts:

  • Process creation with fork()
  • Inter-process communication with pipe()
  • File descriptor manipulation with dup2()
  • Signal handling with sigaction()
  • Recursive descent parsing
  • Expression evaluation with operator precedence

This exam covers Unix system programming. Here’s the C operators you’ll use:

pid_t pid = fork();
if (pid == -1) { /* error */ }
else if (pid == 0) { /* child */ }
else { /* parent - pid is child's ID */ }
  • fork() returns different values in parent and child
  • -1 means error
  • 0 in child means “I am the child”
  • Positive number in parent is the child’s process ID (PID)
  • This is how you differentiate parent and child code paths
int pipefd[2];
pipe(pipefd);
// pipefd[0] = read end
// pipefd[1] = write end
  • pipefd[2] is an array of two file descriptors
  • pipefd[0] is the read end (0 looks like an O - “open to read”)
  • pipefd[1] is the write end (1 looks like a pen - “write”)
  • Mnemonic: 0 = input, 1 = output
dup2(pipefd[1], STDOUT_FILENO); // Redirect stdout to pipe
  • dup2(old, new) makes file descriptor new point to the same file as old
  • After this, writing to new actually writes to old’s destination
  • STDOUT_FILENO = 1 (standard output)
  • STDIN_FILENO = 0 (standard input)
  • STDERR_FILENO = 2 (standard error)
wait(NULL); // Wait for ANY child
waitpid(pid, &status, 0); // Wait for SPECIFIC child
  • wait(NULL) pauses parent until any child process finishes
  • waitpid(pid, &status, 0) waits for specific child with PID pid
  • &status stores information about how the child ended
  • Without wait(), child processes become “zombies” (finished but not cleaned up)
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
}
  • WIFEXITED(status) checks if child exited normally (via exit() or return from main())
  • WEXITSTATUS(status) extracts the exit code (0-255)
  • WIFSIGNALED(status) checks if child was killed by a signal
  • WTERMSIG(status) extracts which signal killed the child
struct_ptr->member; // Same as (*struct_ptr).member
  • -> combines pointer dereference with member access
  • Used with pointers to structures
  • ptr->field is equivalent to (*ptr).field
  • The * dereferences the pointer, . accesses the member
char **argv; // Array of strings (array of char pointers)
  • ** means “pointer to pointer”
  • char** is an array of strings (each string is char*)
  • argv[0] is the first string (program name)
  • argv[1] is the second string (first argument)
  • Common in main(int argc, char **argv)
*s++; // Move pointer forward in recursive descent parsing
  • *s dereferences the pointer to get the current character
  • (*s)++ gets current char then advances pointer
  • Used in parsing to consume characters one by one

fork() creates an exact copy of the current process.

#include <unistd.h>
#include <sys/wait.h>
pid_t pid = fork();
if (pid == -1) {
// Error - fork failed
perror("fork");
exit(1);
}
else if (pid == 0) {
// Child process
// pid == 0 means "I am the child"
printf("I am the child (PID: %d)\n", getpid());
exit(0);
}
else {
// Parent process
// pid == child's PID
printf("I am the parent, child PID: %d\n", pid);
wait(NULL); // Wait for child to finish
}
ParentChild
Returns child’s PIDReturns 0
Continues executionStarts at same point
Has original file descriptorsGets copies of file descriptors
Must wait() for childrenShould exit() when done

A pipe creates a unidirectional data channel:

  • pipefd[0] = read end
  • pipefd[1] = write end
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe");
exit(1);
}
// pipefd[0] - read from pipe
// pipefd[1] - write to pipe

To connect cmd1 | cmd2:

  1. Create pipe
  2. Fork for cmd1: redirect stdout to pipe write end
  3. Fork for cmd2: redirect stdin from pipe read end
  4. Close unused pipe ends in all processes
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
// Child 1 (cmd1): writes to pipe
close(pipefd[0]); // Close read end
dup2(pipefd[1], STDOUT_FILENO); // Redirect stdout to pipe
close(pipefd[1]); // Close original write end
execvp(cmd1[0], cmd1);
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Child 2 (cmd2): reads from pipe
close(pipefd[1]); // Close write end
dup2(pipefd[0], STDIN_FILENO); // Redirect stdin from pipe
close(pipefd[0]); // Close original read end
execvp(cmd2[0], cmd2);
exit(1);
}
// Parent: close both ends and wait
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);

Why? A pipe’s read end returns EOF only when ALL write ends are closed. If you forget to close the write end in the reader process, it will hang forever waiting for input.

// WRONG - cmd2 will hang!
pid_t pid2 = fork();
if (pid2 == 0) {
dup2(pipefd[0], STDIN_FILENO);
// Forgot to close pipefd[1]!
execvp(cmd2[0], cmd2);
}
// CORRECT
pid_t pid2 = fork();
if (pid2 == 0) {
close(pipefd[1]); // MUST close write end!
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
execvp(cmd2[0], cmd2);
}

dup2(oldfd, newfd) makes newfd point to the same file as oldfd.

// Redirect stdout to a file
int fd = open("output.txt", O_WRONLY | O_CREAT, 0644);
dup2(fd, STDOUT_FILENO); // Now stdout writes to output.txt
close(fd); // Close original fd (stdout still works)
printf("This goes to output.txt\n");
// Redirect stdout to pipe write end
dup2(pipefd[1], STDOUT_FILENO);
// Redirect stdin from pipe read end
dup2(pipefd[0], STDIN_FILENO);
// Redirect stderr to stdout
dup2(STDOUT_FILENO, STDERR_FILENO);

FunctionPath ResolutionEnvironment
execve(path, argv, envp)Must be full pathYou provide envp
execvp(file, argv)Searches PATHUses current environ
// execvp - easier, searches PATH
char *argv[] = {"ls", "-la", NULL};
execvp("ls", argv); // Will find /bin/ls automatically
// execve - more control
char *envp[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, envp);

Important: execvp Never Returns on Success!

Section titled “Important: execvp Never Returns on Success!”
execvp(cmd[0], cmd);
// If we reach here, execvp failed!
perror("execvp");
exit(1); // Child must exit on exec failure

#include <signal.h>
void handler(int sig) {
// Handle signal
write(1, "Caught signal\n", 14);
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, NULL);
alarm(5); // Send SIGALRM in 5 seconds
pause(); // Wait for signal
return 0;
}
alarm(timeout); // Schedule SIGALRM in 'timeout' seconds
// ... do work ...
alarm(0); // Cancel the alarm
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
// Process exited normally
int exit_code = WEXITSTATUS(status);
printf("Exited with code %d\n", exit_code);
}
else if (WIFSIGNALED(status)) {
// Process killed by signal
int sig = WTERMSIG(status);
printf("Killed by signal: %s\n", strsignal(sig));
}

For expression parsing, define a grammar:

expr = term (('+') term)*
term = factor (('*') factor)*
factor = '(' expr ')' | NUMBER
int parse_expr(const char **s);
int parse_term(const char **s);
int parse_factor(const char **s);
// Factor: number or (expr)
int parse_factor(const char **s) {
if (**s == '(') {
(*s)++; // Skip '('
int result = parse_expr(s);
if (**s != ')') error("Expected ')'");
(*s)++; // Skip ')'
return result;
}
if (isdigit(**s)) {
int num = **s - '0';
(*s)++;
return num;
}
error("Unexpected token");
return 0;
}
// Term: factor (* factor)*
int parse_term(const char **s) {
int result = parse_factor(s);
while (**s == '*') {
(*s)++;
result *= parse_factor(s);
}
return result;
}
// Expr: term (+ term)*
int parse_expr(const char **s) {
int result = parse_term(s);
while (**s == '+') {
(*s)++;
result += parse_term(s);
}
return result;
}
  • * has higher precedence than +
  • By parsing * in term (called first), it binds tighter
  • 3+4*5 parses as 3+(4*5) = 23, not (3+4)*5 = 35

value = string | number | object
object = '{' (pair (',' pair)*)? '}'
pair = string ':' value
string = '"' characters '"'
number = digits
int parse_value(FILE *f, json *dst) {
int c = getc(f);
if (c == '"') {
return parse_string(f, dst);
}
else if (isdigit(c)) {
ungetc(c, f);
return parse_number(f, dst);
}
else if (c == '{') {
return parse_object(f, dst);
}
else if (c == EOF) {
printf("Unexpected end of input\n");
return -1;
}
else {
printf("Unexpected token '%c'\n", c);
return -1;
}
}
int parse_string(FILE *f, json *dst) {
// Already consumed opening "
char buffer[1024];
int i = 0;
int c;
while ((c = getc(f)) != EOF && c != '"') {
if (c == '\\') {
c = getc(f);
if (c == '"' || c == '\\') {
buffer[i++] = c;
} else {
// Invalid escape
printf("Unexpected token '%c'\n", c);
return -1;
}
} else {
buffer[i++] = c;
}
}
if (c == EOF) {
printf("Unexpected end of input\n");
return -1;
}
buffer[i] = '\0';
// Store string in dst...
return 1;
}

// WRONG - leaking fds
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
dup2(pipefd[1], 1);
execvp(cmd[0], cmd);
}
// Parent forgot to close pipefd[0] and pipefd[1]!
// CORRECT
if (pid == 0) {
close(pipefd[0]);
dup2(pipefd[1], 1);
close(pipefd[1]);
execvp(cmd[0], cmd);
exit(1);
}
close(pipefd[0]);
close(pipefd[1]);
// WRONG - creates zombies
for (int i = 0; i < n; i++) {
if (fork() == 0) {
execvp(cmds[i][0], cmds[i]);
exit(1);
}
}
// Parent exits without waiting!
// CORRECT
for (int i = 0; i < n; i++) {
pids[i] = fork();
if (pids[i] == 0) {
execvp(cmds[i][0], cmds[i]);
exit(1);
}
}
for (int i = 0; i < n; i++) {
waitpid(pids[i], NULL, 0);
}
// WRONG - child continues after failed exec
if (fork() == 0) {
execvp(cmd[0], cmd);
// If execvp fails, child continues running parent's code!
}
// CORRECT
if (fork() == 0) {
execvp(cmd[0], cmd);
perror("execvp");
exit(1); // MUST exit!
}


Implement a function that executes a pipeline of commands:

  • Input: array of commands (each command is array of strings)
  • Connect stdout of each command to stdin of the next
  • Return 0 on success, 1 on error
  • Close all file descriptors, wait for all children

Example pipeline: ls | grep .c | wc -l

char *cmd1[] = {"ls", NULL};
char *cmd2[] = {"grep", ".c", NULL};
char *cmd3[] = {"wc", "-l", NULL};
char **cmds[] = {cmd1, cmd2, cmd3, NULL};
picoshell(cmds);

Think before coding:

  1. What is a pipe?

    • pipe(fd) creates two file descriptors
    • fd[0] = read end, fd[1] = write end
    • Data written to fd[1] can be read from fd[0]
  2. Pipeline pattern:

    cmd1 stdout --> [pipe1] --> stdin cmd2 stdout --> [pipe2] --> stdin cmd3
  3. For each command:

    • Fork a child process
    • In child: redirect stdin/stdout, exec the command
    • In parent: track file descriptors, wait for children
  4. Critical rule: Close ALL unused file descriptors in both parent AND child!

Stage 1: Basic structure

picoshell.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int picoshell(char **cmds[])
{
int i = 0;
int prev_fd = -1; // Read end from previous pipe
int pipefd[2];
pid_t pid;
// Count commands
while (cmds[i])
i++;
int cmd_count = i;
// Process each command
for (i = 0; cmds[i]; i++)
{
// ... implementation here
}
// Wait for all children
for (i = 0; i < cmd_count; i++)
wait(NULL);
return 0;
}

Stage 2: Create pipes and fork

picoshell.c - pipe and fork
for (i = 0; cmds[i]; i++)
{
// Create pipe if not the last command
if (cmds[i + 1] && pipe(pipefd) == -1)
{
if (prev_fd != -1)
close(prev_fd);
return 1;
}
pid = fork();
if (pid == -1)
{
if (prev_fd != -1)
close(prev_fd);
if (cmds[i + 1])
{
close(pipefd[0]);
close(pipefd[1]);
}
return 1;
}
// Child or parent handling...
}

Stage 3: Child process - redirect and exec

picoshell.c - child
if (pid == 0)
{
// Child process
// Redirect stdin from previous pipe (if not first)
if (prev_fd != -1)
{
dup2(prev_fd, STDIN_FILENO);
close(prev_fd);
}
// Redirect stdout to current pipe (if not last)
if (cmds[i + 1])
{
close(pipefd[0]); // Close read end (won't use)
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
execvp(cmds[i][0], cmds[i]);
exit(1); // exec failed - MUST exit!
}

Stage 4: Parent process - manage fds

picoshell.c - parent
// Parent process
if (prev_fd != -1)
close(prev_fd); // Done with previous read end
if (cmds[i + 1])
{
close(pipefd[1]); // Close write end (child uses it)
prev_fd = pipefd[0]; // Save read end for next command
}

Understanding dup2:

CallEffect
dup2(pipefd[1], STDOUT_FILENO)stdout now writes to pipe
dup2(prev_fd, STDIN_FILENO)stdin now reads from previous pipe

Pipe data flow:

Before dup2:
stdin (0) --> keyboard
stdout (1) --> terminal
After dup2 in middle command:
stdin (0) --> previous pipe read end
stdout (1) --> current pipe write end

Why close after dup2?

dup2(pipefd[1], STDOUT_FILENO); // Creates copy at fd 1
close(pipefd[1]); // Original no longer needed
// Now: stdout (1) points to pipe write
// pipefd[1] is closed (not leaked)

1. Forgetting to close pipe ends:

// WRONG: Pipe read end never closed
if (fork() == 0) {
dup2(pipefd[1], 1);
execvp(cmd, argv);
}
// RIGHT: Close all unused ends
if (fork() == 0) {
close(pipefd[0]); // Not reading
dup2(pipefd[1], 1);
close(pipefd[1]); // Closed after dup2
execvp(cmd, argv);
exit(1);
}

2. Not exiting after failed exec:

// WRONG: Child continues as parent!
execvp(cmd, argv);
// If exec fails, continues here...
// RIGHT: Exit on failure
execvp(cmd, argv);
exit(1); // Only reached if exec failed

3. Creating zombie processes:

// WRONG: Not waiting for children
for (i = 0; cmds[i]; i++) {
fork(); // No wait!
}
// RIGHT: Wait for all
for (i = 0; i < cmd_count; i++)
wait(NULL);

4. Parent not closing write end:

// WRONG: Child blocks reading because parent holds write end
// Parent keeps pipefd[1] open -> child's read never gets EOF
// RIGHT: Parent closes write end
close(pipefd[1]);
prev_fd = pipefd[0];
Terminal window
# Compile
gcc -Wall -Wextra -Werror picoshell.c main.c -o picoshell
# Test simple pipeline
./picoshell echo hello "|" cat
# Test multiple pipes
./picoshell ls "|" grep .c "|" wc -l
# Check for fd leaks
lsof -c picoshell
# Check for zombies
ps aux | grep defunct
picoshell.c
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int picoshell(char **cmds[]) {
int n = 0, pfd[2], prev = -1;
while (cmds[n]) n++;
for (int i = 0; cmds[i]; i++) {
if (cmds[i + 1] && pipe(pfd) == -1) return 1;
pid_t pid = fork();
if (pid == -1) return 1;
if (pid == 0) {
if (prev != -1) { dup2(prev, 0); close(prev); }
if (cmds[i + 1]) { close(pfd[0]); dup2(pfd[1], 1); close(pfd[1]); }
execvp(cmds[i][0], cmds[i]);
exit(1);
}
if (prev != -1) close(prev);
if (cmds[i + 1]) { close(pfd[1]); prev = pfd[0]; }
}
while (n--) wait(NULL);
return 0;
}

Memory Pattern:

prev = -1
for each cmd:
if (not last) pipe()
fork()
child: dup2(prev→0), dup2(pfd[1]→1), exec
parent: close prev, prev = pfd[0]
wait all

Implement a simplified popen():

  • Take command path, arguments, and mode (‘r’ or ‘w’)
  • Return file descriptor connected to command’s stdin or stdout
  • ‘r’ mode: return fd to read command’s output
  • ‘w’ mode: return fd to write to command’s input

Think before coding:

  1. What does ‘r’ mode mean?

    • “I want to READ the command’s output”
    • Child redirects stdout to pipe write end
    • Parent gets pipe read end
  2. What does ‘w’ mode mean?

    • “I want to WRITE to the command’s input”
    • Child redirects stdin from pipe read end
    • Parent gets pipe write end
  3. Visualization:

    'r' mode: parent <--read-- pipe <--write-- child stdout
    'w' mode: parent --write--> pipe --read--> child stdin

Stage 1: Setup and validation

ft_popen.c
#include <unistd.h>
#include <stdlib.h>
int ft_popen(const char *file, char *const argv[], char type)
{
int pipefd[2];
pid_t pid;
// Validate type
if (type != 'r' && type != 'w')
return -1;
// Create pipe
if (pipe(pipefd) == -1)
return -1;
pid = fork();
if (pid == -1)
{
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
// ... child/parent handling
}

Stage 2: Child process

ft_popen.c - child
if (pid == 0)
{
if (type == 'r')
{
// Parent reads -> child writes to stdout
close(pipefd[0]); // Child won't read
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
else // type == 'w'
{
// Parent writes -> child reads from stdin
close(pipefd[1]); // Child won't write
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
}
execvp(file, argv);
exit(1);
}

Stage 3: Parent process

ft_popen.c - parent
// Parent process
if (type == 'r')
{
close(pipefd[1]); // Parent won't write
return pipefd[0]; // Return read end
}
else // type == 'w'
{
close(pipefd[0]); // Parent won't read
return pipefd[1]; // Return write end
}

Mode direction table:

ModeParentChildParent returns
’r’reads from pipewrites to pipe (stdout)pipefd[0]
’w’writes to pipereads from pipe (stdin)pipefd[1]

Memory trick:

  • ‘r’ = “I Read” = parent reads = child outputs = child’s stdout
  • ‘w’ = “I Write” = parent writes = child inputs = child’s stdin

1. Reversed direction:

// WRONG: 'r' mode but returning write end
if (type == 'r')
return pipefd[1]; // Wrong!
// RIGHT: 'r' mode returns read end
if (type == 'r')
return pipefd[0]; // Correct

2. Child redirects wrong fd:

// WRONG: 'r' mode but child redirects stdin
if (type == 'r')
dup2(pipefd[0], STDIN_FILENO); // Wrong!
// RIGHT: 'r' mode, child redirects stdout
if (type == 'r')
dup2(pipefd[1], STDOUT_FILENO); // Correct
ft_popen.c
#include <unistd.h>
#include <stdlib.h>
int ft_popen(const char *file, char *const argv[], char type) {
if (type != 'r' && type != 'w') return -1;
int pfd[2];
if (pipe(pfd) == -1) return -1;
pid_t pid = fork();
if (pid == -1) { close(pfd[0]); close(pfd[1]); return -1; }
if (pid == 0) {
if (type == 'r') { close(pfd[0]); dup2(pfd[1], 1); close(pfd[1]); }
else { close(pfd[1]); dup2(pfd[0], 0); close(pfd[0]); }
execvp(file, argv);
exit(1);
}
if (type == 'r') { close(pfd[1]); return pfd[0]; }
else { close(pfd[0]); return pfd[1]; }
}

Memory Pattern:

'r' = I Read → child writes stdout → return pfd[0]
'w' = I Write → child reads stdin → return pfd[1]

Test if a function is “nice” or “bad”:

  • Nice: exits with code 0, no signal, no timeout
  • Bad: crashes (signal), exits non-zero, or times out
  • Return 1 for nice, 0 for bad, -1 for sandbox error

Think before coding:

  1. Why fork?

    • Run untrusted function in isolated process
    • If it crashes, parent survives
  2. How to detect problems?

    • WIFEXITED(status) - did it exit normally?
    • WEXITSTATUS(status) - what exit code?
    • WIFSIGNALED(status) - killed by signal?
    • WTERMSIG(status) - which signal?
  3. How to handle timeout?

    • alarm(seconds) sends SIGALRM after delay
    • In handler, set flag
    • After wait, kill child if timed out
sandbox.c
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
static volatile sig_atomic_t g_timeout = 0;
static void handler(int s) { (void)s; g_timeout = 1; }
int sandbox(void (*f)(void), unsigned int timeout, bool verbose) {
if (!f) return -1;
struct sigaction sa = {.sa_handler = handler};
sigemptyset(&sa.sa_mask);
if (sigaction(SIGALRM, &sa, NULL) == -1) return -1;
g_timeout = 0;
pid_t pid = fork();
if (pid == -1) return -1;
if (pid == 0) { f(); exit(0); }
if (timeout) alarm(timeout);
int st;
if (waitpid(pid, &st, 0) == -1) { alarm(0); return -1; }
alarm(0);
if (g_timeout) {
kill(pid, SIGKILL); waitpid(pid, NULL, 0);
if (verbose) printf("Bad function: timed out after %u seconds\n", timeout);
return 0;
}
if (WIFEXITED(st)) {
int code = WEXITSTATUS(st);
if (verbose) printf(code ? "Bad function: exited with code %d\n" : "Nice function!\n", code);
return code == 0;
}
if (WIFSIGNALED(st)) {
if (verbose) printf("Bad function: %s\n", strsignal(WTERMSIG(st)));
return 0;
}
return -1;
}

Memory Pattern:

fork() → child runs f(), exit(0)
parent: alarm(), waitpid(), alarm(0)
check: g_timeout? WIFEXITED? WIFSIGNALED?


Evaluate mathematical expressions:

  • Operators: + and *
  • Parentheses: ( and )
  • Numbers: single digits 0-9
  • Operator precedence: * before +

Examples:

3+4*5 = 23 (not 35!)
(3+4)*5 = 35
1+2+3 = 6

Think before coding:

  1. Why does * bind tighter?

    • 3+4*5 should be 3+(4*5) = 23
    • Not (3+4)*5 = 35
  2. Grammar for precedence:

    expr = term (('+') term)* // + is lowest
    term = factor (('*') factor)* // * is higher
    factor = '(' expr ')' | DIGIT // () and numbers highest
  3. Recursive descent:

    • parse_expr handles +
    • parse_term handles *
    • parse_factor handles () and digits
    • Higher precedence = deeper in call stack
vbc.c
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
static int expr(const char **s);
static void err(char c) {
printf(c ? "Unexpected token '%c'\n" : "Unexpected end of input\n", c);
exit(1);
}
static int factor(const char **s) {
if (**s == '(') {
(*s)++;
int r = expr(s);
if (**s != ')') err(**s);
(*s)++;
return r;
}
if (isdigit(**s)) return *(*s)++ - '0';
err(**s);
return 0;
}
static int term(const char **s) {
int r = factor(s);
while (**s == '*') { (*s)++; r *= factor(s); }
return r;
}
static int expr(const char **s) {
int r = term(s);
while (**s == '+') { (*s)++; r += term(s); }
return r;
}
int main(int ac, char **av) {
if (ac != 2) { printf("Usage: %s 'expression'\n", av[0]); return 1; }
const char *s = av[1];
int r = expr(&s);
if (*s) err(*s);
printf("%d\n", r);
return 0;
}

Memory Pattern:

expr = term ('+' term)* ← + is lowest
term = factor ('*' factor)* ← * is higher
factor = '(' expr ')' | digit ← () highest
3+4*5 = 3+(4*5) = 23
Terminal window
./vbc '3+4*5' # 23
./vbc '(3+4)*5' # 35
./vbc '1+2+3+4+5' # 15
./vbc '2*3*4' # 24
./vbc '((1+2))' # 3
./vbc '1+2)' # Error: Unexpected token ')'
./vbc '(1+2' # Error: Unexpected end of input
./vbc '1 + 2' # Error: Unexpected token ' '

Parse simplified JSON:

  • Types: numbers, strings, objects (no arrays, booleans, null)
  • No whitespace handling (spaces are errors)
  • Handle escape sequences: \" and \\ only
  • Print exact error messages
Terminal window
# Test numbers
echo -n '42' | ./argo /dev/stdin
# Test strings
echo -n '"hello"' | ./argo /dev/stdin
# Test objects
echo -n '{"a":1}' | ./argo /dev/stdin
echo -n '{"name":"test","value":42}' | ./argo /dev/stdin
# Test nested
echo -n '{"outer":{"inner":1}}' | ./argo /dev/stdin
# Test escapes
echo -n '"hello\"world"' | ./argo /dev/stdin
# Test errors
echo -n '{"a":}' | ./argo /dev/stdin # Missing value
echo -n '{a:1}' | ./argo /dev/stdin # Key not string

FunctionPurpose
fork()Create child process
wait(NULL)Wait for any child
waitpid(pid, &status, 0)Wait for specific child
exit(code)Terminate process
FunctionPurpose
pipe(int fd[2])Create pipe (fd[0]=read, fd[1]=write)
dup2(old, new)Redirect new to old
close(fd)Close file descriptor
FunctionPurpose
execvp(file, argv)Execute with PATH search
execve(path, argv, envp)Execute with full path
FunctionPurpose
sigaction(sig, &sa, NULL)Set signal handler
alarm(seconds)Schedule SIGALRM
kill(pid, sig)Send signal to process
MacroPurpose
WIFEXITED(status)True if exited normally
WEXITSTATUS(status)Get exit code
WIFSIGNALED(status)True if killed by signal
WTERMSIG(status)Get signal number

LevelExerciseKey Concept
1picoshellPipe chains, fork, exec, fd management
1ft_popenPipe direction based on mode
1sandboxSignals, timeout, process status
2vbcRecursive descent, operator precedence
2argoRecursive parsing, escape sequences

Common mistakes to avoid:

  1. Not closing all pipe ends
  2. Not exiting after failed execvp
  3. Wrong pipe direction for read/write mode
  4. Wrong operator precedence in vbc
  5. Wrong error message format (must be exact!)