: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:
- `-Wall` β enables most common warning messages.
- `-Wextra` β enables additional warnings taht wall doesn't have
- `-pedantic` β enforces strict ISO C compliance. Better to write clean code
- `-std=c99` β uses the C99 standard for compiling (THE BEST FUCKING STANDARD FOR C. THE NEW WOKE LIBERAL STANDARDS AER ASS. C99 SUPREMACY).
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