Writing A Shell In C

Problem Statement:

Now comes the question - Is it simpler to make use of the available shells in the current market, or is developing your own shell not too challenging? Indeed, crafting a shell is not just a beneficial endeavor that empowers you to expand or improve the capabilities of your command line interface; it is also an excellent method to gain knowledge about operating systems overall. In this specific scenario, our attention will be directed towards the C programming language since it facilitates the development of a comprehensive comprehension of various OS aspects such as child processes, command and message interpretation, and input/output redirection.

In our particular emphasis on a solitary shell application, users will have the capability to input specific commands, which will undergo initial processing before executing the intended actions. This tutorial will delve into the steps involved in creating a basic shell application starting from the ground up utilizing the C programming language.

Development

The primary objective for us is to implement a simplistic shell, which is designed with the following constraints:

  • Accepts inputs from users, and returns appropriate responses when called upon.
  • Interacts with the user via a command line interface and accepts textual commands.
  • Exits simply when the user enters exit command.
  • Supports error handling and interacts with external command line programs.

Extended goals include areas that can be improved in the future or further developed:

  • Simultaneous execution of inputs to an application for modifying sequential execution outputs.
  • Execution of background processes.
  • Application of logic that enables combining several commands and using variables.

Overall, the limitations are lacking in various crucial aspects concerning application development. However, due to logistical constraints, the focus should be on constructing the fundamental components of the application's functionality, which serve as the heart or central part of the app.

Implementation

A shell can be defined as an application or software that acts as a middleman by interpreting and executing user commands. In order for users to interact with the shell the following features should be incorporated:

  • Collecting data from the user.
  • Understanding commands and their contexts.
  • Missing texts and executing functionality.
  • Error detection and overcoming errors gracefully.
  • Step-by-Step Implementation

  1. Setting Up the Main Loop

A shell operates in an infinite loop until explicitly terminated. Inside the loop, it:

  • Displays a prompt.
  • Reads input.
  • Processes the input.

Program:

Example

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

#define MAX_INPUT 1024
#define MAX_ARGS 64
#define DELIMITERS " \t\r\n"

/* Function prototypes */
void shell_prompt();
char* read_input();
char** parse_input(char* input);
int execute_command(char** args, int background, int input_fd, int output_fd);
void handle_redirection_and_pipes(char* input);
void handle_cd(char** args);

/* Main function */
int main() {
    while (1) {
        shell_prompt();
        char* input = read_input();
        if (!input) {  // End of input (Ctrl+D)
            printf("\n");
            break;
        }
        handle_redirection_and_pipes(input);
        free(input);
    }
    return 0;
}

/* Display shell prompt */
void shell_prompt() {
    printf("my_shell> ");
    fflush(stdout);
}

// Function to read user input
char* read_input() {
    char* buffer = (char*)malloc(MAX_INPUT); // Explicit cast for C++
    if (!buffer) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }
    if (fgets(buffer, MAX_INPUT, stdin) == NULL) {
        free(buffer);
        return NULL;
    }
    return buffer;
}

// Function to parse user input into arguments
char** parse_input(char* input) {
    char** args = (char**)malloc(MAX_ARGS * sizeof(char*)); // Explicit cast for C++
    if (!args) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    int i = 0;
    char* token = strtok(input, " \t\r\n");
    while (token != NULL && i < MAX_ARGS - 1) {
        args[i++] = token;
        token = strtok(NULL, " \t\r\n");
    }
    args[i] = NULL;
    return args;
}

/* Execute a single command */
int execute_command(char** args, int background, int input_fd, int output_fd) {
    if (args[0] == NULL) {
        return -1;  // Empty command
    }

    // Handle built-in commands
    if (strcmp(args[0], "exit") == 0) {
        exit(0);
    }
    if (strcmp(args[0], "cd") == 0) {
        handle_cd(args);
        return 0;
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return -1;
    }

    if (pid == 0) {
        // Child process
        if (input_fd != STDIN_FILENO) {
            dup2(input_fd, STDIN_FILENO);
            close(input_fd);
        }
        if (output_fd != STDOUT_FILENO) {
            dup2(output_fd, STDOUT_FILENO);
            close(output_fd);
        }

        if (execvp(args[0], args) == -1) {
            perror("execvp");
            exit(EXIT_FAILURE);
        }
    } else {
        // Parent process
        if (!background) {
            int status;
            waitpid(pid, &status, 0);
        } else {
            printf("[Background] PID: %d\n", pid);
        }
    }
    return 0;
}

/* Handle input/output redirection and pipelines */
void handle_redirection_and_pipes(char* input) {
    char* commands[64];
    int num_commands = 0;

    // Split input into commands separated by '|'
    char* token = strtok(input, "|");
    while (token != NULL && num_commands < 64) {
        commands[num_commands++] = token;
        token = strtok(NULL, "|");
    }

    int input_fd = STDIN_FILENO;
    for (int i = 0; i < num_commands; i++) {
        int pipe_fd[2];
        int output_fd = STDOUT_FILENO;

        if (i < num_commands - 1) {
            pipe(pipe_fd);
            output_fd = pipe_fd[1];
        }

        char* cmd = commands[i];
        char* args[MAX_ARGS];
        int background = 0;
        int redirection_in = 0, redirection_out = 0;

        // Handle background execution and redirection
        char* out_file = NULL;
        char* in_file = NULL;

        int arg_idx = 0;
        token = strtok(cmd, DELIMITERS);
        while (token != NULL) {
            if (strcmp(token, "&") == 0) {
                background = 1;
            } else if (strcmp(token, ">") == 0) {
                token = strtok(NULL, DELIMITERS);
                if (token) {
                    redirection_out = 1;
                    out_file = token;
                }
            } else if (strcmp(token, "<") == 0) {
                token = strtok(NULL, DELIMITERS);
                if (token) {
                    redirection_in = 1;
                    in_file = token;
                }
            } else {
                args[arg_idx++] = token;
            }
            token = strtok(NULL, DELIMITERS);
        }
        args[arg_idx] = NULL;

        // Handle redirection
        if (redirection_out) {
            output_fd = open(out_file, O_WRONLY | O_CREAT | O_TRUNC, 0644);
            if (output_fd < 0) {
                perror("open");
                return;
            }
        }
        if (redirection_in) {
            input_fd = open(in_file, O_RDONLY);
            if (input_fd < 0) {
                perror("open");
                return;
            }
        }

        // Execute the command
        execute_command(args, background, input_fd, output_fd);

        // Close unnecessary file descriptors
        if (output_fd != STDOUT_FILENO) close(output_fd);
        if (input_fd != STDIN_FILENO) close(input_fd);

        if (i < num_commands - 1) {
            close(pipe_fd[1]);
            input_fd = pipe_fd[0];
        }
    }
}

/* Handle the 'cd' built-in command */
void handle_cd(char** args) {
    if (args[1] == NULL) {
        fprintf(stderr, "cd: missing argument\n");
    } else if (chdir(args[1]) != 0) {
        perror("cd");
    }
}

Input/Output for the C++ Code:

Example

Scenario 1: Executing a Basic Command
my_shell> ls
main.cpp  shell  Makefile
Scenario 2: Handling Input Redirection
my_shell> cat < input.txt
Hello, World!
This is a test file.
Scenario 3: Handling Output Redirection
my_shell> ls > output.txt
my_shell> cat output.txt
main.cpp  shell  Makefile
Scenario 4: Executing a Command in the Background
my_shell> sleep 10 &
[Background] PID: 12345
my_shell> 
Scenario 5: Using a Pipeline
my_shell> ls | grep cpp
main.cpp
Scenario 6: Changing Directory with cd
my_shell> cd ..
my_shell> pwd
/home/user/projects
Scenario 7: Exit
my_shell> exit

Explanation:

  • Basic Commands: The shell successfully runs external commands using execvp.
  • Input/Output Redirection: Redirection operators (<, >) are processed to read/write files.
  • Background Execution: The & allows commands to run in the background, with the shell returning to the prompt immediately.
  • Pipelines: The shell chains multiple commands using pipes (|), connecting the output of one to the input of the next.
  • Built-in Commands (cd, exit): These are handled directly within the shell without using execvp.
  • Conclusion

Developing a shell in the C programming language offers practical exposure to system calls such as fork, execvp, and waitpid. This endeavor enhances your comprehension of Unix-like operating systems and readies you for more intricate subjects like inter-process communication and multithreading.

Enhance your shell functionality by adding capabilities such as pipelines, maintaining a history of commands, and supporting tab completion to increase its resilience and functionality.

Input Required

This code uses input(). Please provide values below: