Fundamentals 19 min read

Build Your Own Unix Shell in C: A Step‑by‑Step Tutorial

This tutorial walks you through creating a simple Unix shell in C, covering its lifecycle, input handling, command parsing, process launching, built‑in commands, and full source compilation, while providing complete code examples and explanations.

21CTO
21CTO
21CTO
Build Your Own Unix Shell in C: A Step‑by‑Step Tutorial

Many programmers doubt they are "real" developers, but building a simple Unix shell in C (named lsh) is an excellent way to prove otherwise; the full source is available on GitHub.

Shell Basic Lifecycle

A shell typically performs three stages:

Initialization : load configuration files (the tutorial’s shell skips this).

Read‑execute : read commands from standard input, parse them, and execute.

Termination : clean up memory and exit.

Main Function

int main(int argc, char **argv) {
    // If a config file existed we would load it here.
    lsh_loop();
    return EXIT_SUCCESS;
}

Read a Line

Reading an arbitrary‑length line from stdin requires dynamic buffer allocation. The implementation grows the buffer as needed and returns a null‑terminated string.

#define LSH_RL_BUFSIZE 1024
char *lsh_read_line(void) {
    int bufsize = LSH_RL_BUFSIZE;
    int position = 0;
    char *buffer = malloc(sizeof(char) * bufsize);
    int c;
    if (!buffer) {
        fprintf(stderr, "lsh: allocation error
");
        exit(EXIT_FAILURE);
    }
    while (1) {
        c = getchar();
        if (c == EOF || c == '
') {
            buffer[position] = '\0';
            return buffer;
        } else {
            buffer[position] = c;
        }
        position++;
        if (position >= bufsize) {
            bufsize += LSH_RL_BUFSIZE;
            buffer = realloc(buffer, bufsize);
            if (!buffer) {
                fprintf(stderr, "lsh: allocation error
");
                exit(EXIT_FAILURE);
            }
        }
    }
}

Parse a Line

The line is split into tokens using whitespace as delimiters. This simple parser does not handle quotes or escape characters.

#define LSH_TOK_BUFSIZE 64
#define LSH_TOK_DELIM " \t
\a"
char **lsh_split_line(char *line) {
    int bufsize = LSH_TOK_BUFSIZE, position = 0;
    char **tokens = malloc(bufsize * sizeof(char*));
    char *token;
    if (!tokens) {
        fprintf(stderr, "lsh: allocation error
");
        exit(EXIT_FAILURE);
    }
    token = strtok(line, LSH_TOK_DELIM);
    while (token != NULL) {
        tokens[position] = token;
        position++;
        if (position >= bufsize) {
            bufsize += LSH_TOK_BUFSIZE;
            tokens = realloc(tokens, bufsize * sizeof(char*));
            if (!tokens) {
                fprintf(stderr, "lsh: allocation error
");
                exit(EXIT_FAILURE);
            }
        }
        token = strtok(NULL, LSH_TOK_DELIM);
    }
    tokens[position] = NULL;
    return tokens;
}

Launch a Process

To run external commands the shell forks a child process and uses execvp to replace the child’s image. The parent waits for the child to finish.

int lsh_launch(char **args) {
    pid_t pid, wpid;
    int status;
    pid = fork();
    if (pid == 0) { // child
        if (execvp(args[0], args) == -1) {
            perror("lsh");
        }
        exit(EXIT_FAILURE);
    } else if (pid < 0) { // fork error
        perror("lsh");
    } else { // parent
        do {
            wpid = waitpid(pid, &status, WUNTRACED);
        } while (!WIFEXITED(status) && !WIFSIGNALED(status));
    }
    return 1;
}

Built‑in Commands

Some commands must be handled by the shell itself because they affect the shell’s own state (e.g., cd, exit, help).

int lsh_cd(char **args) {
    if (args[1] == NULL) {
        fprintf(stderr, "lsh: expected argument to \"cd\"
");
    } else {
        if (chdir(args[1]) != 0) {
            perror("lsh");
        }
    }
    return 1;
}

int lsh_help(char **args) {
    int i;
    printf("Stephen Brennan's LSH
");
    printf("Type program names and arguments, and hit enter.
");
    printf("The following are built in:
");
    for (i = 0; i < lsh_num_builtins(); i++) {
        printf("  %s
", builtin_str[i]);
    }
    printf("Use the man command for information on other programs.
");
    return 1;
}

int lsh_exit(char **args) {
    return 0;
}

Execute Command

The executor checks whether the command matches a built‑in; if not, it launches an external process.

int lsh_execute(char **args) {
    int i;
    if (args[0] == NULL) {
        return 1; // empty command
    }
    for (i = 0; i < lsh_num_builtins(); i++) {
        if (strcmp(args[0], builtin_str[i]) == 0) {
            return (*builtin_func[i])(args);
        }
    }
    return lsh_launch(args);
}

Putting It All Together

The complete shell consists of the loop ( lsh_loop) that repeatedly reads a line, splits it, executes it, and frees allocated memory. Compile with the required headers ( <stdio.h>, <stdlib.h>, <unistd.h>, <sys/wait.h>, <string.h>) and run the resulting binary.

Compilation example:

gcc -o lsh main.c
./lsh
Source: https://brennan.io/2015/01/16/write-a-shell-in-c/ (original article by "万能的大雄")
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CShellUnixTutorialsystem calls
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.