Step 7: Put it together, and compare with Go
The final program combines everything: tokenizer, built-ins (pwd, cd, exit), running programs with status reporting, and > redirection, driven by a script of commands.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_ARGS 16
int tokenize(char *line, char *argv[]) {
int argc = 0;
char *p = line;
while (*p != '\0' && argc < MAX_ARGS - 1) {
while (*p == ' ') p++;
if (*p == '\0') break;
argv[argc++] = p;
while (*p != '\0' && *p != ' ') p++;
if (*p != '\0') *p++ = '\0';
}
argv[argc] = NULL;
return argc;
}
int run(char *argv[], const char *redirect_to) {
fflush(stdout);
pid_t pid = fork();
if (pid == 0) {
if (redirect_to) {
int fd = open(redirect_to, O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
}
execvp(argv[0], argv);
_exit(127);
}
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}
int main(void) {
char *script[] = {
"cd /tmp", "pwd", "echo hello > /tmp/tiny-final.txt", "cat /tmp/tiny-final.txt",
"nosuchprogram", "false", "exit"
};
int last_status = 0;
for (int i = 0; i < 7; i++) {
char line[100];
strcpy(line, script[i]);
printf("tiny> %s\n", line);
// look for "> file" and split it off
char *redirect = NULL;
char *gt = strstr(line, " > ");
if (gt) { *gt = '\0'; redirect = gt + 3; }
char *argv[MAX_ARGS];
int argc = tokenize(line, argv);
if (argc == 0) continue;
if (strcmp(argv[0], "pwd") == 0) { char b[256]; printf("%s\n", getcwd(b, sizeof b)); last_status = 0; }
else if (strcmp(argv[0], "cd") == 0) { last_status = chdir(argc > 1 ? argv[1] : "/") == 0 ? 0 : 1; }
else if (strcmp(argv[0], "exit") == 0) { printf("bye (last status %d)\n", last_status); break; }
else { last_status = run(argv, redirect); if (last_status != 0) printf("[status %d]\n", last_status); }
}
remove("/tmp/tiny-final.txt");
return 0;
}tiny> cd /tmp tiny> pwd /tmp tiny> echo hello > /tmp/tiny-final.txt tiny> cat /tmp/tiny-final.txt hello tiny> nosuchprogram [status 127] tiny> false [status 1] tiny> exit bye (last status 1)
The same ideas look much shorter in a higher-level language. Go's os/exec package wraps fork, exec and wait, and hands you the output and the exit status:
package main
import (
"fmt"
"os/exec"
"strings"
)
func main() {
// The same ideas in Go: start a program, capture its output and exit status.
out, err := exec.Command("echo", "hello from Go").Output()
fmt.Printf("%q %v\n", strings.TrimSpace(string(out)), err)
err = exec.Command("false").Run()
if exitErr, ok := err.(*exec.ExitError); ok {
fmt.Println("false exited with status", exitErr.ExitCode())
}
_, err = exec.Command("nosuchprogram").Output()
fmt.Println("missing program:", err != nil)
// a pipeline by hand: the output of one command becomes the input of the next
first, _ := exec.Command("echo", "hello pipes").Output()
cmd := exec.Command("tr", "a-z", "A-Z")
cmd.Stdin = strings.NewReader(string(first))
second, _ := cmd.Output()
fmt.Print(string(second))
}"hello from Go" <nil> false exited with status 1 missing program: true HELLO PIPES
You have built the heart of a shell and seen the system calls behind every terminal: fork, exec, wait, chdir, dup2 and pipe. Where next: support >> and <, more than two commands in a pipeline, environment variables ($HOME), background jobs with &, and handling Ctrl-C with signals. Reading a real shell's source (such as dash) is a great next step.
Which system call replaces a process with a new program?
Finish the quick check (and any code exercise) to complete this lesson, or mark it done yourself.
Tip: use the left and right arrow keys to move between lessons.