Every time you type a command in a terminal, a shell does the same short dance: read a line, split it into words, fork a copy of itself, exec the program in the copy, and wait for it to finish. That dance is the foundation of process management on Unix, and it is surprisingly little code. Building tsh ties together almost the whole track: strings and tokenizing, heap buffers, error handling with errno, file descriptors, and the process system calls from the systems module. After this project, the behavior of real shells (why cd is special, how > works, what $? really holds) stops being folklore.
When stdin is not a terminal (for example, ./tsh < script.txt), the prompt is not printed, so the shell can also run scripts. isatty(STDIN_FILENO) tells the two cases apart, the same way real shells decide whether they are interactive.
The read-eval loop
+--------------------------------------------+
v |
print prompt (if interactive) |
| |
getline --EOF--> exit with last status |
| |
split into argv[] |
| |
builtin? --yes--> run it in the shell itself ------+
| no |
fork --> child: redirect, execvp, (_exit 127) |
| |
parent: waitpid, record status --------------------+
Step 1: splitting the line
execvp wants an array of char * terminated by NULL, exactly the shape of main's argv. strtok produces it in place: each word is terminated where the separator was, and argv holds pointers into the line buffer.
The capacity check reserves one slot for the terminating NULL, the same off-by-one discipline as reserving a byte for a string's '\0'. This simple splitter does not understand quotes, so echo "a b" passes "a and b" as two words; handling quoting properly is a parser of its own and a good extension.
Step 2: fork, exec, wait
fflush(stdout);
pid_t pid = fork();
if (pid == 0) { /* child */
execvp(argv[0], argv); /* only returns on failure */
fprintf(stderr, "tsh: %s: command not found\n", argv[0]);
_exit(127);
}
int status;
waitpid(pid, &status, 0); /* parent */
The key facts, from the systems module:
fork creates a nearly identical copy of the process. Both continue from the same point; the return value tells them apart: 0 in the child, the child's process ID in the parent, -1 on failure.
execvp replaces the current program with a new one, searching the directories in PATH (the p) for argv[0]. The process ID, open file descriptors and working directory survive; the code, data, heap and stack are replaced. If it returns at all, it failed, and errno says why: ENOENT (no such program) or EACCES (not executable).
The child must not return into the shell's loop after a failed exec, or you would have two shells reading the same input. It ends with _exit(127); 127 is the conventional "command not found" status that bash also uses.
_exit rather than exit in the child: exit flushes stdio buffers, and the child's buffers are a copy of the parent's, so anything not yet written would be written twice. The fflush(stdout) before fork empties the buffer so there is nothing to duplicate at all.
waitpid blocks until the child ends and fills in status, an encoded integer. Decode it with macros: WIFEXITED(status) and WEXITSTATUS(status) for a normal exit, WIFSIGNALED and WTERMSIG for death by a signal. Shells report signal deaths as 128 plus the signal number, so a program killed by SIGTERM (15) gives 143 and a segfault (SIGSEGV, 11) gives 139. You have been seeing that 139 since the debugging module.
If the parent never waited, finished children would linger as zombies: process table entries holding an exit status nobody collected.
Step 3: builtins
A child process cannot change its parent. The working directory belongs to each process separately, so a cd program would change only its own directory and then exit, leaving the shell where it was. cd must therefore run inside the shell, by calling chdir directly. The same goes for exit, which must end the shell itself. These are builtins: checked by name before forking.
cd with no argument uses getenv("HOME"). The environment is a list of NAME=value strings every process inherits from its parent; the sandbox sets HOME=/home/learner.
Step 4: redirection
> and < are implemented entirely with file descriptors. Every process starts with fd 0 (stdin), 1 (stdout) and 2 (stderr). Programs like sort just read fd 0 and write fd 1; they have no idea what those refer to. So, in the child, between fork and exec:
int f = open("sorted.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(f, STDOUT_FILENO); /* fd 1 now refers to the same open file as f */
close(f); /* the extra descriptor is no longer needed */
execvp(argv[0], argv); /* sort inherits fd 1 = sorted.txt */
Doing it in the child is what keeps the shell's own stdout pointing at the terminal. This "fork, rearrange descriptors, exec" pattern is how every shell implements >, <, 2> and pipes: a pipeline a | b is two children whose fd 1 and fd 0 are joined with pipe and dup2.
The redirection words must be removed from argv before exec, or sort would try to open a file named <. Compacting the array with a read index and a write index does that in place.
Common mistakes
Forgetting _exit after a failed execvp, so the child continues as a second shell. Symptom: commands after a typo run twice.
Using exit in the child, duplicating buffered output.
Not flushing stdout before fork, so text printed by the shell appears twice or out of order when output is redirected to a file or pipe.
Redirecting in the parent, which permanently sends the shell's own output to the file.
Forgetting O_TRUNC: writing "hi" over a longer file leaves the old tail behind.
Treating status as the exit code. It is encoded; status for exit(1) is 256. Always use the W... macros.
Leaking descriptors: not closing the original after dup2 gives the program extra open files.
Recap
A shell is a loop: read, split, builtin or fork; exec in the child; wait in the parent.
execvp replaces the program but keeps the process, its descriptors and its working directory; it returns only on failure.
Decode waitpid status with WIFEXITED/WEXITSTATUS and WIFSIGNALED/WTERMSIG; shells use 128 + signal.
Builtins exist because a child cannot change its parent's state.
Redirection is open plus dup2 onto fd 0 or 1, done in the child between fork and exec.
Going further: add pipelines (ls | wc -l) with pipe, two forks and dup2; then 2> for stderr, & for background jobs (and reaping them with waitpid(-1, &st, WNOHANG)), and quoting in the splitter. Compare your shell's behavior with bash on the same scripts. The classic reading is the "Shell" chapter of Advanced Programming in the UNIX Environment by Stevens and Rago.
Exercises
Exercise 1 · write code
Step 1: split a command line
Write int split_line(char *line, char **argv, int max). It splits linein place into words separated by spaces, tabs and newlines, stores a pointer to each word in argv[0], argv[1], ..., sets the entry after the last word to NULL (as execvp requires), and returns the number of words. max is the size of the argv array, so at most max - 1 words fit; if there are more, return -1.
main prints each word in brackets, for example for the input line ls -l /tmp:
3: [ls] [-l] [/tmp]
File: main.c
Starting point
#include <stdio.h>
#include <string.h>
#define MAX_ARGS 8
int split_line(char *line, char **argv, int max)
{
argv[0] = NULL;
return 0;
}
int main(void)
{
char line[256];
char *argv[MAX_ARGS];
while (fgets(line, sizeof line, stdin) != NULL) {
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
printf("too many words\n");
continue;
}
printf("%d:", n);
for (int i = 0; argv[i] != NULL; i++)
printf(" [%s]", argv[i]);
printf("\n");
}
return 0;
}
Example
input
ls -l /tmp
expected output
3: [ls] [-l] [/tmp]
Example
input
echo hi
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7
expected output
2: [echo] [hi]
0:
too many words
7: [1] [2] [3] [4] [5] [6] [7]
Hint 1
strtok(line, " \t\n") returns the first word and terminates it in place; strtok(NULL, " \t\n") returns the next.
Hint 2
Before storing a word, check that there is still room for it AND the final NULL: n must be less than max - 1.
Hint 3
After the loop, argv[n] = NULL; return n;
Reference solution
#include <stdio.h>
#include <string.h>
#define MAX_ARGS 8
int split_line(char *line, char **argv, int max)
{
int n = 0;
for (char *w = strtok(line, " \t\n"); w != NULL; w = strtok(NULL, " \t\n")) {
if (n == max - 1)
return -1;
argv[n++] = w;
}
argv[n] = NULL;
return n;
}
int main(void)
{
char line[256];
char *argv[MAX_ARGS];
while (fgets(line, sizeof line, stdin) != NULL) {
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
printf("too many words\n");
continue;
}
printf("%d:", n);
for (int i = 0; argv[i] != NULL; i++)
printf(" [%s]", argv[i]);
printf("\n");
}
return 0;
}
Exercise 2 · write code
Step 2: run commands
Make the shell run programs. For each non-empty input line (lines starting with # are comments), run_external must:
fflush(stdout) (so nothing buffered is duplicated into the child),
fork(); in the child, execvp(argv[0], argv). If exec fails, print tsh: NAME: command not found to stderr when errno is ENOENT (otherwise tsh: NAME: followed by strerror(errno)), and end the child with _exit(127);
in the parent, waitpid for the child and return its status: the exit code if it exited normally (WIFEXITED/WEXITSTATUS), or 128 plus the signal number if a signal killed it (WIFSIGNALED/WTERMSIG).
When input ends, the shell exits with the status of the last command (0 if there was none). The prompt is only printed when stdin is a terminal, so the checks see no prompt.
$ printf 'echo hello
nosuch
' | ./prog
hello
tsh: nosuch: command not found
$ echo $?
127
File: main.c
Starting point
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 64
int split_line(char *line, char **argv, int max)
{
int n = 0;
for (char *w = strtok(line, " \t\n"); w != NULL; w = strtok(NULL, " \t\n")) {
if (n == max - 1)
return -1;
argv[n++] = w;
}
argv[n] = NULL;
return n;
}
/* Run argv as a child process and return its status. */
static int run_external(char **argv)
{
(void)argv;
return 0;
}
int main(void)
{
char *line = NULL;
size_t cap = 0;
char *argv[MAX_ARGS];
int last = 0;
int interactive = isatty(STDIN_FILENO);
for (;;) {
if (interactive) {
printf("tsh> ");
fflush(stdout);
}
if (getline(&line, &cap, stdin) == -1)
break;
if (line[0] == '#')
continue;
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
fprintf(stderr, "tsh: too many arguments\n");
last = 2;
continue;
}
if (n == 0)
continue;
last = run_external(argv);
}
free(line);
return last;
}
Example
input
echo hello
# a comment
echo a b
expected output
hello
a b
Example
input
echo before
nosuchcommand
expected output
before
Hint 1
fork() returns 0 in the child, the child's pid in the parent, and -1 on failure.
Hint 2
execvp only returns if it failed. Everything after it in the child is error handling, and the child must end with _exit(127) so it does not fall back into the shell's loop.
Hint 3
Use _exit, not exit, in the child: exit would flush the parent's stdio buffers a second time.
Hint 4
In the parent: int status; waitpid(pid, &status, 0); then decode with WIFEXITED(status) ? WEXITSTATUS(status) : ...
Reference solution
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 64
int split_line(char *line, char **argv, int max)
{
int n = 0;
for (char *w = strtok(line, " \t\n"); w != NULL; w = strtok(NULL, " \t\n")) {
if (n == max - 1)
return -1;
argv[n++] = w;
}
argv[n] = NULL;
return n;
}
/* Run argv as a child process and return its status. */
static int run_external(char **argv)
{
fflush(stdout);
pid_t pid = fork();
if (pid < 0) {
perror("tsh: fork");
return 1;
}
if (pid == 0) {
execvp(argv[0], argv);
if (errno == ENOENT)
fprintf(stderr, "tsh: %s: command not found\n", argv[0]);
else
fprintf(stderr, "tsh: %s: %s\n", argv[0], strerror(errno));
_exit(127);
}
int status;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
perror("tsh: waitpid");
return 1;
}
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status))
return 128 + WTERMSIG(status);
return 1;
}
int main(void)
{
char *line = NULL;
size_t cap = 0;
char *argv[MAX_ARGS];
int last = 0;
int interactive = isatty(STDIN_FILENO);
for (;;) {
if (interactive) {
printf("tsh> ");
fflush(stdout);
}
if (getline(&line, &cap, stdin) == -1)
break;
if (line[0] == '#')
continue;
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
fprintf(stderr, "tsh: too many arguments\n");
last = 2;
continue;
}
if (n == 0)
continue;
last = run_external(argv);
}
free(line);
return last;
}
Exercise 3 · write code
Step 3: cd and exit
Some commands cannot be separate programs. Add two builtins, handled in main before run_external:
cd DIR changes the shell's own working directory with chdir. Plain cd goes to the directory in the HOME environment variable (getenv("HOME")). On failure print tsh: cd: DIR: REASON to stderr (REASON from strerror(errno)) and set the status to 1; on success set it to 0.
exit ends the shell with the last status; exit N ends it with status N.
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 64
int split_line(char *line, char **argv, int max)
{
int n = 0;
for (char *w = strtok(line, " \t\n"); w != NULL; w = strtok(NULL, " \t\n")) {
if (n == max - 1)
return -1;
argv[n++] = w;
}
argv[n] = NULL;
return n;
}
static int run_external(char **argv)
{
fflush(stdout);
pid_t pid = fork();
if (pid < 0) {
perror("tsh: fork");
return 1;
}
if (pid == 0) {
execvp(argv[0], argv);
if (errno == ENOENT)
fprintf(stderr, "tsh: %s: command not found\n", argv[0]);
else
fprintf(stderr, "tsh: %s: %s\n", argv[0], strerror(errno));
_exit(127);
}
int status;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
perror("tsh: waitpid");
return 1;
}
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status))
return 128 + WTERMSIG(status);
return 1;
}
int main(void)
{
char *line = NULL;
size_t cap = 0;
char *argv[MAX_ARGS];
int last = 0;
int interactive = isatty(STDIN_FILENO);
for (;;) {
if (interactive) {
printf("tsh> ");
fflush(stdout);
}
if (getline(&line, &cap, stdin) == -1)
break;
if (line[0] == '#')
continue;
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
fprintf(stderr, "tsh: too many arguments\n");
last = 2;
continue;
}
if (n == 0)
continue;
/* TODO: builtins exit and cd */
last = run_external(argv);
}
free(line);
return last;
}
Example
input
cd /tmp
pwd
cd /
pwd
expected output
/tmp
/
Example
input
cd /tmp
cd
pwd
expected output
/home/learner
Hint 1
Compare argv[0] with "exit" and "cd" using strcmp before calling run_external.
Hint 2
For exit, break out of the loop (so line is still freed) after setting last from atoi(argv[1]) when an argument is given.
Hint 3
For cd: const char *dir = argv[1] ? argv[1] : getenv("HOME"); then if (chdir(dir) != 0) report with strerror(errno).
Reference solution
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 64
int split_line(char *line, char **argv, int max)
{
int n = 0;
for (char *w = strtok(line, " \t\n"); w != NULL; w = strtok(NULL, " \t\n")) {
if (n == max - 1)
return -1;
argv[n++] = w;
}
argv[n] = NULL;
return n;
}
static int run_external(char **argv)
{
fflush(stdout);
pid_t pid = fork();
if (pid < 0) {
perror("tsh: fork");
return 1;
}
if (pid == 0) {
execvp(argv[0], argv);
if (errno == ENOENT)
fprintf(stderr, "tsh: %s: command not found\n", argv[0]);
else
fprintf(stderr, "tsh: %s: %s\n", argv[0], strerror(errno));
_exit(127);
}
int status;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
perror("tsh: waitpid");
return 1;
}
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status))
return 128 + WTERMSIG(status);
return 1;
}
static int builtin_cd(char **argv)
{
const char *dir = argv[1] != NULL ? argv[1] : getenv("HOME");
if (dir == NULL) {
fprintf(stderr, "tsh: cd: HOME not set\n");
return 1;
}
if (chdir(dir) != 0) {
fprintf(stderr, "tsh: cd: %s: %s\n", dir, strerror(errno));
return 1;
}
return 0;
}
int main(void)
{
char *line = NULL;
size_t cap = 0;
char *argv[MAX_ARGS];
int last = 0;
int interactive = isatty(STDIN_FILENO);
for (;;) {
if (interactive) {
printf("tsh> ");
fflush(stdout);
}
if (getline(&line, &cap, stdin) == -1)
break;
if (line[0] == '#')
continue;
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
fprintf(stderr, "tsh: too many arguments\n");
last = 2;
continue;
}
if (n == 0)
continue;
if (strcmp(argv[0], "exit") == 0) {
if (argv[1] != NULL)
last = atoi(argv[1]);
break;
}
if (strcmp(argv[0], "cd") == 0) {
last = builtin_cd(argv);
continue;
}
last = run_external(argv);
}
free(line);
return last;
}
Exercise 4 · write code
Step 4: redirection
Add < FILE and > FILE for external commands. The operators are separate words (sort < data.txt > sorted.txt), may appear anywhere after the command name, and are removed from argv before execvp.
In the child, before execvp: for < FILE open it with O_RDONLY and dup2 it onto standard input (fd 0); for > FILE open it with O_WRONLY | O_CREAT | O_TRUNC and mode 0644 and dup2 it onto standard output (fd 1). Close the original descriptor after dup2. If a file cannot be opened, print tsh: FILE: REASON to stderr and _exit(1). An operator with no file name after it prints tsh: syntax error: missing file after > (or <) to stderr and gives status 2 without running anything.
The workspace contains data.txt with two lines, line two and line one.
$ printf 'echo hi > out.txt
cat out.txt
sort < data.txt
' | ./prog
hi
line one
line two
File: main.c
Starting point
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 64
int split_line(char *line, char **argv, int max)
{
int n = 0;
for (char *w = strtok(line, " \t\n"); w != NULL; w = strtok(NULL, " \t\n")) {
if (n == max - 1)
return -1;
argv[n++] = w;
}
argv[n] = NULL;
return n;
}
/* TODO: remove "< file" and "> file" from argv, remembering the file names */
static int run_external(char **argv)
{
fflush(stdout);
pid_t pid = fork();
if (pid < 0) {
perror("tsh: fork");
return 1;
}
if (pid == 0) {
/* TODO: set up redirections here */
execvp(argv[0], argv);
if (errno == ENOENT)
fprintf(stderr, "tsh: %s: command not found\n", argv[0]);
else
fprintf(stderr, "tsh: %s: %s\n", argv[0], strerror(errno));
_exit(127);
}
int status;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
perror("tsh: waitpid");
return 1;
}
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status))
return 128 + WTERMSIG(status);
return 1;
}
static int builtin_cd(char **argv)
{
const char *dir = argv[1] != NULL ? argv[1] : getenv("HOME");
if (dir == NULL) {
fprintf(stderr, "tsh: cd: HOME not set\n");
return 1;
}
if (chdir(dir) != 0) {
fprintf(stderr, "tsh: cd: %s: %s\n", dir, strerror(errno));
return 1;
}
return 0;
}
int main(void)
{
char *line = NULL;
size_t cap = 0;
char *argv[MAX_ARGS];
int last = 0;
int interactive = isatty(STDIN_FILENO);
for (;;) {
if (interactive) {
printf("tsh> ");
fflush(stdout);
}
if (getline(&line, &cap, stdin) == -1)
break;
if (line[0] == '#')
continue;
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
fprintf(stderr, "tsh: too many arguments\n");
last = 2;
continue;
}
if (n == 0)
continue;
if (strcmp(argv[0], "exit") == 0) {
if (argv[1] != NULL)
last = atoi(argv[1]);
break;
}
if (strcmp(argv[0], "cd") == 0) {
last = builtin_cd(argv);
continue;
}
last = run_external(argv);
}
free(line);
return last;
}
Example
input
echo hi > out.txt
cat out.txt
sort < data.txt
expected output
hi
line one
line two
Example
input
wc -l < data.txt
expected output
2
Hint 1
Scan argv with two indexes: i reads every word, j writes the words you keep. When argv[i] is "<" or ">", record argv[i + 1] as the file and skip both words.
Hint 2
Do the redirection in the child only, after fork and before execvp, so the shell's own stdin and stdout are untouched.
Hint 3
open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644) then dup2(fd, STDOUT_FILENO) makes fd 1 refer to the file; then close(fd).
Hint 4
execvp keeps open file descriptors, so the new program inherits fd 0 and 1 already pointing at the files.
Reference solution
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 64
int split_line(char *line, char **argv, int max)
{
int n = 0;
for (char *w = strtok(line, " \t\n"); w != NULL; w = strtok(NULL, " \t\n")) {
if (n == max - 1)
return -1;
argv[n++] = w;
}
argv[n] = NULL;
return n;
}
/* Remove "< file" and "> file" from argv, remembering the file names.
Returns 0, or -1 (after printing a message) if an operator has no file. */
static int find_redirects(char **argv, const char **in, const char **out)
{
int j = 0;
*in = NULL;
*out = NULL;
for (int i = 0; argv[i] != NULL; i++) {
if (strcmp(argv[i], "<") == 0 || strcmp(argv[i], ">") == 0) {
if (argv[i + 1] == NULL) {
fprintf(stderr, "tsh: syntax error: missing file after %s\n", argv[i]);
return -1;
}
if (argv[i][0] == '<')
*in = argv[i + 1];
else
*out = argv[i + 1];
i++; /* skip the file name too */
} else {
argv[j++] = argv[i]; /* keep: compact the array */
}
}
argv[j] = NULL;
return 0;
}
/* In the child: make fd refer to path. Does not return on failure. */
static void redirect(const char *path, int flags, int fd)
{
int f = open(path, flags, 0644);
if (f < 0) {
fprintf(stderr, "tsh: %s: %s\n", path, strerror(errno));
_exit(1);
}
if (dup2(f, fd) < 0) {
perror("tsh: dup2");
_exit(1);
}
close(f);
}
static int run_external(char **argv)
{
const char *in, *out;
if (find_redirects(argv, &in, &out) != 0)
return 2;
if (argv[0] == NULL)
return 0;
fflush(stdout);
pid_t pid = fork();
if (pid < 0) {
perror("tsh: fork");
return 1;
}
if (pid == 0) {
if (in != NULL)
redirect(in, O_RDONLY, STDIN_FILENO);
if (out != NULL)
redirect(out, O_WRONLY | O_CREAT | O_TRUNC, STDOUT_FILENO);
execvp(argv[0], argv);
if (errno == ENOENT)
fprintf(stderr, "tsh: %s: command not found\n", argv[0]);
else
fprintf(stderr, "tsh: %s: %s\n", argv[0], strerror(errno));
_exit(127);
}
int status;
while (waitpid(pid, &status, 0) < 0) {
if (errno != EINTR) {
perror("tsh: waitpid");
return 1;
}
}
if (WIFEXITED(status))
return WEXITSTATUS(status);
if (WIFSIGNALED(status))
return 128 + WTERMSIG(status);
return 1;
}
static int builtin_cd(char **argv)
{
const char *dir = argv[1] != NULL ? argv[1] : getenv("HOME");
if (dir == NULL) {
fprintf(stderr, "tsh: cd: HOME not set\n");
return 1;
}
if (chdir(dir) != 0) {
fprintf(stderr, "tsh: cd: %s: %s\n", dir, strerror(errno));
return 1;
}
return 0;
}
int main(void)
{
char *line = NULL;
size_t cap = 0;
char *argv[MAX_ARGS];
int last = 0;
int interactive = isatty(STDIN_FILENO);
for (;;) {
if (interactive) {
printf("tsh> ");
fflush(stdout);
}
if (getline(&line, &cap, stdin) == -1)
break;
if (line[0] == '#')
continue;
int n = split_line(line, argv, MAX_ARGS);
if (n < 0) {
fprintf(stderr, "tsh: too many arguments\n");
last = 2;
continue;
}
if (n == 0)
continue;
if (strcmp(argv[0], "exit") == 0) {
if (argv[1] != NULL)
last = atoi(argv[1]);
break;
}
if (strcmp(argv[0], "cd") == 0) {
last = builtin_cd(argv);
continue;
}
last = run_external(argv);
}
free(line);
return last;
}
Exercise 5 · quiz
Why must cd be a builtin?
Suppose cd were an ordinary program, /usr/bin/cd, that calls chdir and exits. What would happen when you type cd /tmp in the shell, then pwd?
Answer
pwd still prints the old directory: the child process changed its own working directory and then exited; the shell's working directory was never touched
The working directory is a per-process attribute, like the open file table and the environment. fork gives the child a copy; changes in the child never flow back to the parent. Anything that must change the shell's own state has to run inside the shell process: cd, exit, export (which changes the shell's environment for future children), ulimit, and the redirections on exec. That is why every shell has builtins, and why bash -c 'cd /tmp' cannot change the directory of the terminal you typed it in.