How I Built a Simple Shell in C – A Beginner's Guide to System Programming (2/3)

Ertugrul - Feb 18 - - Dev Community

Part 2: Parsing User Input in a Custom Shell (C Programming)

In this part of my custom shell project, I will explain how to parse user input dynamically in C. Parsing the input correctly is crucial in a shell environment, as commands often include multiple arguments. Instead of relying on fixed-size buffers, I implemented a dynamic memory allocation approach for better flexibility.


Understanding the parser Function

The function parser() is responsible for:
✅ Splitting the user command into arguments dynamically

✅ Managing memory allocation and reallocation efficiently

✅ Returning an array of arguments for further processing

Breaking Down the Code

#ifndef PARSER_H
#define PARSER_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define arg_buffer_size 4

char **parser(char *command, int *argc);
void free_args(char **args, int argc);

#endif
Enter fullscreen mode Exit fullscreen mode
  • Header Guard (#ifndef PARSER_H): Prevents multiple inclusions.
  • Constant Definition (arg_buffer_size): Sets an initial buffer size for argument storage.
  • Function Prototypes: Declares the parser() function for splitting the input and free_args() for memory cleanup.

Implementation of parser()

char **parser(char *command, int *argc)
{
    int size = arg_buffer_size;
    int length = 0;
    char **args = malloc(size * sizeof(char *));
Enter fullscreen mode Exit fullscreen mode
  • Memory Allocation (malloc): Allocates memory for an array of strings.
  • Error Handling: Although not explicitly shown, malloc should be checked to prevent failures.
    *argc = 0;
    char *token = strtok(command, " ");
    while (token)
    {
        if (*argc >= size)
        {
            size *= 2;
            args = realloc(args, size * sizeof(char *));
        }
        args[*argc] = strdup(token);
        (*argc)++;
        token = strtok(NULL, " ");
    }
    args[*argc] = NULL;
    return args;
}
Enter fullscreen mode Exit fullscreen mode
  1. Tokenizing the Input

    • Uses strtok() to break the input into separate arguments.
    • Iterates over each token and stores it dynamically.
  2. Expanding Memory Dynamically (realloc)

    • If the argument count exceeds the allocated size, the buffer is doubled.
    • realloc() attempts to resize the buffer safely.
  3. Storing Arguments

    • Each token is duplicated using strdup() to avoid memory corruption.
    • The last argument is set to NULL for proper command execution.

Cleaning Up Memory

void free_args(char **args, int argc)
{
    for (int i = 0; i < argc; i++)
    {
        free(args[i]);
    }
    free(args);
}
Enter fullscreen mode Exit fullscreen mode
  • Prevents Memory Leaks: Frees each argument and the overall array to ensure efficient memory management.

Integrating the Parser with the Shell

The parser() function is integrated into the shell’s main loop, allowing commands to be properly processed.

int main(void)
{
    Command commands[] = {
        {"help", help_action},
        {"clr", clear_action},
        {"say", say_action},
        {"exit", exit_action},
        {"cwp", cwp_command},
        {"date", date_command},
        {"clone", clone_file_command},
        {"cut", cut_file_command},
        {"read", read_file_command},
        {"del", delete_file_command},
        {"open", open_program_command},
        {"list", list_of_directory}};

    int command_count = sizeof(commands) / sizeof(commands[0]);
    while (1)
    {
        int argc;
        printf("shell_of_mine => ");
        char *command = read_command();
        char **args = parser(command, &argc);
        execute_command(args, argc, commands, command_count);
        free_args(args, argc);
        free(command);
    }
    return 0;
}
Enter fullscreen mode Exit fullscreen mode
  • Main Loop: Continuously reads, parses, and executes user commands.
  • Dynamic Parsing: The parser extracts arguments dynamically, allowing flexible command handling.
  • Memory Cleanup: free_args() ensures allocated memory is released after execution.

Why This Approach?

📌 Handles Arbitrary Command Lengths: Unlike static arrays, dynamic allocation ensures flexibility.

📌 Efficient Memory Management: Uses realloc() to optimize memory usage dynamically.

📌 Prevents Buffer Overflow: Eliminates fixed-size limitations that could lead to errors.


Next Part: Executing Commands 🚀

In the next part, I will explain how the shell executes parsed commands using system calls and function pointers. Stay tuned! 😊

📂 GitHub: https://github.com/Ertugrulmutlu/shell_of_mine

What do you think about this approach? Would you handle parsing differently? Let me know in the comments! 👇

. . . . . .