Skip to content

Exam Rank 04 Solutions

Download Subject Files:



Execute a pipeline of commands, connecting stdout of each to stdin of the next.

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 of 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++)
{
// 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;
}
if (pid == 0)
{
// Child process
// Redirect stdin from previous pipe (if not first command)
if (prev_fd != -1)
{
dup2(prev_fd, STDIN_FILENO);
close(prev_fd);
}
// Redirect stdout to current pipe (if not last command)
if (cmds[i + 1])
{
close(pipefd[0]); // Close read end
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
execvp(cmds[i][0], cmds[i]);
exit(1); // exec failed
}
// Parent process
if (prev_fd != -1)
close(prev_fd);
if (cmds[i + 1])
{
close(pipefd[1]); // Close write end
prev_fd = pipefd[0]; // Save read end for next command
}
}
// Wait for all children
int status;
for (i = 0; i < cmd_count; i++)
{
wait(&status);
}
return 0;
}

Key Points:

  • Create pipe BEFORE fork (so both processes have access)
  • Child: redirect stdin from previous pipe, stdout to current pipe
  • Parent: close write end, keep read end for next command
  • Close ALL unused file descriptors in BOTH parent and child
  • Wait for ALL children to avoid zombies

Implement a simplified version of the standard popen() function.

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;
}
if (pid == 0)
{
// Child process
if (type == 'r')
{
// Parent wants to READ child's output
// Child writes to pipe (redirect stdout)
close(pipefd[0]); // Close read end
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
}
else // type == 'w'
{
// Parent wants to WRITE to child's input
// Child reads from pipe (redirect stdin)
close(pipefd[1]); // Close write end
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
}
execvp(file, argv);
exit(1); // exec failed
}
// Parent process
if (type == 'r')
{
// Return read end, close write end
close(pipefd[1]);
return pipefd[0];
}
else // type == 'w'
{
// Return write end, close read end
close(pipefd[0]);
return pipefd[1];
}
}

Key Points:

  • ‘r’ mode: parent reads from child’s stdout -> return pipe read end
  • ‘w’ mode: parent writes to child’s stdin -> return pipe write end
  • Child redirects the OPPOSITE end (if parent reads, child writes)
  • Always close the unused end in both parent and child
  • Return -1 for invalid type or any error

Test if a function is “nice” (exits 0) or “bad” (signal, non-zero exit, timeout).

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 alarm_handler(int sig)
{
(void)sig;
g_timeout = 1;
}
int sandbox(void (*f)(void), unsigned int timeout, bool verbose)
{
pid_t pid;
int status;
struct sigaction sa;
// Validate input
if (!f)
return -1;
// Setup alarm handler
sa.sa_handler = alarm_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGALRM, &sa, NULL) == -1)
return -1;
g_timeout = 0;
pid = fork();
if (pid == -1)
return -1;
if (pid == 0)
{
// Child: run the function
f();
exit(0);
}
// Parent: set timeout and wait
if (timeout > 0)
alarm(timeout);
if (waitpid(pid, &status, 0) == -1)
{
alarm(0);
return -1;
}
alarm(0); // Cancel alarm
// Check if timed out
if (g_timeout)
{
kill(pid, SIGKILL);
waitpid(pid, NULL, 0); // Reap the killed child
if (verbose)
printf("Bad function: timed out after %u seconds\n", timeout);
return 0;
}
// Check exit status
if (WIFEXITED(status))
{
int exit_code = WEXITSTATUS(status);
if (exit_code == 0)
{
if (verbose)
printf("Nice function!\n");
return 1;
}
else
{
if (verbose)
printf("Bad function: exited with code %d\n", exit_code);
return 0;
}
}
else if (WIFSIGNALED(status))
{
int sig = WTERMSIG(status);
if (verbose)
printf("Bad function: %s\n", strsignal(sig));
return 0;
}
return -1; // Should not reach here
}

Key Points:

  • Fork child to run function in isolation
  • Use alarm() to schedule timeout
  • Use sigaction() (not signal()) for reliable signal handling
  • Check WIFEXITED vs WIFSIGNALED to determine how child terminated
  • Use strsignal() to get human-readable signal description
  • Cancel alarm with alarm(0) after wait returns
  • Must waitpid() for killed child to avoid zombie


Parse a simplified JSON format (numbers, strings, objects only, no whitespace).

argo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
typedef enum { JSON_NUMBER, JSON_STRING, JSON_OBJECT } json_type;
typedef struct json json;
typedef struct json_pair json_pair;
struct json_pair {
char *key;
json *value;
json_pair *next;
};
struct json {
json_type type;
union {
int number;
char *string;
json_pair *pairs; // For objects
};
};
// Forward declarations
static int parse_value(FILE *f, json *dst);
static int parse_number(FILE *f, json *dst)
{
int num;
if (fscanf(f, "%d", &num) != 1)
return -1;
dst->type = JSON_NUMBER;
dst->number = num;
return 1;
}
static int parse_string(FILE *f, json *dst)
{
// Opening " already consumed
char buffer[4096];
int i = 0;
int c;
while ((c = getc(f)) != EOF && c != '"')
{
if (c == '\\')
{
c = getc(f);
if (c == '"' || c == '\\')
buffer[i++] = c;
else if (c == EOF)
{
printf("Unexpected end of input\n");
return -1;
}
else
{
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';
dst->type = JSON_STRING;
dst->string = strdup(buffer);
return 1;
}
static int parse_object(FILE *f, json *dst)
{
// Opening { already consumed
dst->type = JSON_OBJECT;
dst->pairs = NULL;
json_pair **tail = &dst->pairs;
int c = getc(f);
if (c == '}')
return 1; // Empty object
ungetc(c, f);
while (1)
{
// Parse key (must be string)
c = getc(f);
if (c != '"')
{
if (c == EOF)
printf("Unexpected end of input\n");
else
printf("Unexpected token '%c'\n", c);
return -1;
}
json key_json;
if (parse_string(f, &key_json) == -1)
return -1;
// Expect colon
c = getc(f);
if (c != ':')
{
free(key_json.string);
if (c == EOF)
printf("Unexpected end of input\n");
else
printf("Unexpected token '%c'\n", c);
return -1;
}
// Parse value
json *value = malloc(sizeof(json));
if (parse_value(f, value) == -1)
{
free(key_json.string);
free(value);
return -1;
}
// Add pair to list
json_pair *pair = malloc(sizeof(json_pair));
pair->key = key_json.string;
pair->value = value;
pair->next = NULL;
*tail = pair;
tail = &pair->next;
// Check for comma or closing brace
c = getc(f);
if (c == '}')
break;
if (c != ',')
{
if (c == EOF)
printf("Unexpected end of input\n");
else
printf("Unexpected token '%c'\n", c);
return -1;
}
}
return 1;
}
static int parse_value(FILE *f, json *dst)
{
int c = getc(f);
if (c == '"')
return parse_string(f, dst);
if (isdigit(c))
{
ungetc(c, f);
return parse_number(f, dst);
}
if (c == '{')
return parse_object(f, dst);
if (c == EOF)
{
printf("Unexpected end of input\n");
return -1;
}
printf("Unexpected token '%c'\n", c);
return -1;
}
int argo(json *dst, FILE *stream)
{
return parse_value(stream, dst);
}

Key Points:

  • Recursive descent: parse_value calls parse_string, parse_number, or parse_object
  • Objects recursively contain values
  • Use getc() to read one character at a time
  • Use ungetc() to “peek” and put character back
  • Handle escape sequences \" and \\ only
  • Exact error messages: "Unexpected token '%c'\n" or "Unexpected end of input\n"

Evaluate math expressions with +, *, and parentheses, respecting operator precedence.

vbc.c
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
// Forward declarations
static int parse_expr(const char **s);
static void error_token(char c)
{
if (c == '\0')
printf("Unexpected end of input\n");
else
printf("Unexpected token '%c'\n", c);
exit(1);
}
// factor = '(' expr ')' | DIGIT
static int parse_factor(const char **s)
{
if (**s == '(')
{
(*s)++; // Skip '('
int result = parse_expr(s);
if (**s != ')')
error_token(**s);
(*s)++; // Skip ')'
return result;
}
if (isdigit(**s))
{
int num = **s - '0';
(*s)++;
return num;
}
error_token(**s);
return 0; // Never reached
}
// term = factor (('*') factor)*
static int parse_term(const char **s)
{
int result = parse_factor(s);
while (**s == '*')
{
(*s)++; // Skip '*'
result *= parse_factor(s);
}
return result;
}
// expr = term (('+') term)*
static int parse_expr(const char **s)
{
int result = parse_term(s);
while (**s == '+')
{
(*s)++; // Skip '+'
result += parse_term(s);
}
return result;
}
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage: %s 'expression'\n", argv[0]);
return 1;
}
const char *s = argv[1];
int result = parse_expr(&s);
// Check for trailing characters
if (*s != '\0')
error_token(*s);
printf("%d\n", result);
return 0;
}

Key Points:

  • Operator Precedence: * binds tighter than +
    • 3+4*5 = 3+(4*5) = 23
    • Achieved by parsing * in term (inner level)
  • Grammar:
    expr = term (('+') term)*
    term = factor (('*') factor)*
    factor = '(' expr ')' | DIGIT
  • Recursive for parentheses: factor calls expr for (expr)
  • Single digits only: 0-9, no multi-digit numbers
  • Pass string pointer by reference (const char **s) to advance position
  • Check for unexpected trailing characters after parsing completes

Terminal window
# Test picoshell
./picoshell echo hello "|" cat
./picoshell /bin/ls "|" grep .c "|" wc -l
# Check for fd leaks
lsof -c picoshell
# Check for zombies
ps aux | grep defunct
Terminal window
# Test vbc
./vbc '3+4*5' # Should be 23
./vbc '(3+4)*5' # Should be 35
./vbc '1+2+3+4+5' # Should be 15
# Test argo
echo -n '{"a":1}' | ./argo /dev/stdin
echo -n '"hello"' | ./argo /dev/stdin
echo -n '{"nested":{"deep":42}}' | ./argo /dev/stdin

  1. Forgetting exit(1) after failed execvp()
  2. Not closing all pipe ends (causes hangs)
  3. Wrong operator precedence in vbc (* before +)
  4. Wrong error message format (must match exactly)
  5. Not handling empty input / EOF
  6. Zombie processes (always wait() for all children)