:toc: [[TOC]{.smallcaps}]{.tag tag-name=”TOC”} {#toc}

Hoi.

So I kinda promised to write this tutorial almost half a year ago but I seem to have sadly forgotten to write it.πŸ˜” My apolocheese. (insert image of nazrin here.)

So this is a tutorial to write a minimal shell.

Setting stuff up.

The libraries.

You will need the gnu readline library. install it through your package manager or smthn IDK. We basically need it to handle our input. Though there are other alternatives, I prefer this since it's super easy. ALSO IT EFFIN SUPPORTS VIM MODE HELL YEAH. Other than that you don't really need anything.

Setting up the files.

So we need one single C file. Just name it shell.c for simplcity. Other than that we just need a makefile. And that's all :3 Now put this in the makefile:

shell: shell.c
    gcc shell.c -o shell -Wall -Wextra -pedantic -std=c99 -lreadline

for those who don't get makefiles, we're just running the gcc compiler with some flags. They ar:

Writing the actual code.

beginning

So we're just gonna import some libraries for now.

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <readline/readline.h>

First of all, We're defining GNUSOURCE as the macro. It just enables some non standard macros since we're going to be writing substandard ass code rn. :3 The rest are pretty obvious I think. Stdio, stdlib and string are for the usual reasons. and Sys/unistd are for lower level stuff. ANd I already explained what we need readline for.

Now we need to define some macros.

#define MAXLTR 1000 // max letter count
#define MAXLIST 100 // max comand amont
#define commandCount 4

they are for some stuff that will come up later. Let's just ignore those for now.

#define clear() printf("\033[H\033[J")

And this is for the clear() function. Yes I'm just turning it into a macro since we don't need more than that. So that is tje escape sequence to clean the terminal screen. that's what gets run when you do a clear in the terminal.

Init and basic input

Now we need to write an init function. It will get executed the moment you opne the shell.

void init() {
  // should add some shit later but CBA. For now it just clears teh screen and greets you
  clear();
  char *username = getenv("USER");
  printf("Hoi 0/\n");
  printf("USER: %s", username);
}

it's pretty rudemantay. Just a basic outline for now. We're just greetings the user and then showing the username taht we got from getenv. It's a function to grab envirorment variables from the main/user shell.

//input bugfer
int takeInput(char *str) {
  char *buf;

  buf = readline("\n  ");
  if (buf == NULL || strlen(buf) == 0) {
    return 1;
  }

  // TODO: fuckin make this more secure. strcpy can be pretty insecure
  strcpy(str, buf);
  return 0;
}

so this is the main function to handle inputs. It takes the input via readline. And it renders the prompt too. I'm just using a nerd font symbol for the arrow for now. Now we need to move on to process management.

execution.

Now we need to handle threads and processes. So that they don't collide.

void execArguments(char **parsed_input) {
  // Forking a child
  // -1 is bad 0 and other shit is good. <-  pls remember
  pid_t pid = fork();

  if (pid == -1) {
    printf("\nFailed forking..");
    return;
  } else if (pid == 0) {
    if (execvp(parsed_input[0], parsed_input) < 0) {
      printf("\n executionfailed");
    }
    exit(0);
  } else {
    // waiting for child to terminate
    wait(NULL);
    return;
  }
}

this basically assigns a PID to each parsed command that gets executed. This is pretty good in practice. Other than that it just does some basic error handling in case something fails.

Shell builtins.

What's one thing that makes different shells stand out from each other? The answer is shell builtins. All shells ship with some extra tools and coomands that are native to that shell and that shell only. Like cd, help and other commands. We're gonna do this too to get basic functionality

// shell builtin handler
int shellBuiltinHandler(char **parsed) {
  int i, switchOwnArgument = 0;
  char *ListOfOwnCmds[commandCount] = {"exit", "cd", "help", "test_cmd"};
  //parses for shell builtins
  for (i = 0; i < commandCount; i++) {
    if (strcmp(parsed[0], ListOfOwnCmds[i]) == 0) {
      switchOwnArgument = i + 1; // for the switch cases stuff. Adding 1 since
                                 // indexing that way seems easier
      break;
    }
  }
  // the switch case. we can add more shit by just expanding it. but tht would
  // be a bit annoying NGL I'll show y'all how to do a better version next time.
  switch (switchOwnArgument) {
  case 1:
    printf("\nBAI BAI\n");
    exit(0);
  case 2:
    if (chdir(parsed[1]) != 0) {
      printf("Error: Could not change directory to %s\n", parsed[1]);
    } //since we don't have reguar cd we;re using chdir from unistd
    return 1;
  case 3:
    printf("NO HELP FOR YOU HOE\n");
    return 1;
  case 4:
    printf("test works hoe");
    return 1;
  default:
    break;
  }
  return 0;
}

So I'm getting the number of shell builtins from the macro that we declared in the beginning. and then making an array of that length. And we then fill it up with the commands we want as a builtin. Then the for loop goes through the command and looks for the shell builtins in the command. If it finds them then assigns a value from 1 to n to them. I'm not using 0 indexes because why not :3. Then based on that indexing it does switch statements. So for example if it finds "exit" then it will assign it the index 1 and do what is in case 1. In our case it is the exit() function to quit the shell after saying bye. Btw the regular cd command won't work in the shell since oftentimes they're actually a shell builtin. so for our case we're using the chdir command from unistd. Ignore the hoe part I was talking with a friend while writing the code so that affected me :sob:

Now we need to parse the different parts of the command via whitespce detection.

//  for parsing command words
void parseSpace(char *str, char **parsed) {
  int i;

  for (i = 0; i < MAXLIST; i++) {
    parsed[i] = strsep(&str, " ");

    if (parsed[i] == NULL)
      break;
    if (strlen(parsed[i]) == 0)
      i--;
  }
}

I'm not really sure what to say. I assume the code here is self evident enough. if it isn't then just give up this tutorial and learn more C πŸ’€

// main parsing and classivication function.
int processString(char *str, char **parsed) {
  parseSpace(str, parsed);

  if (shellBuiltinHandler(parsed))
    return 0;
  else
    return 1;
}

this just does the final parsing/formatting and stuff. And checks if there are shell builtins in the inputted command.

Main function.

So now we finish everything with the main function.

int main() {
  char inputString[MAXLTR], *parsedArgs[MAXLIST];
  char *parsedArgsPiped[MAXLIST];
  int execFlag = 0;
  init();

  while (1) {
    // take input
    if (takeInput(inputString))
      continue;
    // process
    execFlag = processString(inputString, parsedArgs);
    // execflag returns zero if there is no command
    // or it is a builtin command,
    // 1 if it is a simple command

    // execute
    if (execFlag == 1)
      execArguments(parsedArgs);

    // if (execFlag == 2)
    //   execPipedArguments(parsedArgs, parsedArgsPiped);
  }
  return 0;
}

here we're taking our input. we limit the character and word limit using the macros we pre defined in the beginning. and then run the init function. Then run the init function. And then just run a while loop to keep taking and parsing inputs. It also keeps track of if the command is a shell builtin or a regular command.

The end

So uhhh. That's all. the shell should compile the make command and work when you run it. To exit the shell simply do a ctrl+c.

BAI BAI :3