WeCodeAcademy

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.

ExampleC
#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;
}
Output
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:

ExampleGo
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))
}
Output
"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.

Quick check

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.