Linux shell under the hood !
By: Thomas Montoya
Tags: Linux, Programming

1. Check if the shell should work in interactive or non-interactive mode:

For this, we use the isatty() function. isatty() tests whether fd (file descriptor) is an open file descriptor referring to a terminal. isatty() returns 1 if fd is an open file descriptor referring to a terminal; otherwise, 0 is returned, and errno is set to indicate the error.

2. If the shell should work in an interactive mode:

Show the prompt ($ ) and wait for the user input.

3. After the user input:

After the user writes something (ls -l ) in this case and hits enter, we need to store that input somewhere, for that purpose we use the _getline() function which is our implementation of the getline() function.

getline() reads an entire line from the stream, storing the address of the buffer containing the text into *lineptr. The buffer is null-terminated and includes the newline character if one was found.

If *lineptr is NULL, then getline() will allocate a buffer to store the line, which should be freed by the user program. (In this case, the value in *n is ignored)

4. more than one-word input:

As you may know, usually the shell commands and builtins have flags, those flags are the options for that command or builtin. (-l) in this case, which means long format. The user can also write the path, where they want to perform that action. Lets see an example: list (ls) in long format (-l) in some folder (/home/thomas).

5. Split the input:

To check for those options we need to split all the input in single words, so we implemented our own strtok() function.

strtok() divides an entire string into substrings depending on the delimiter (more of that in the step by step process).

The strtok() function parses a string into a sequence of tokens. On the first call to strtok() the string to be parsed should be specified in str. In each subsequent call that should parse the same string, str should be NULL.

The delim argument (space in this case)specifies a set of bytes that delimit the tokens in the parsed string. The caller may specify different strings in delim in successive calls that parse the same string. So every time that strtok() finds a delimiter (space), the string is divided.

with _getline() we just get one string (ls -l). After strtok() we get:

String 1 = ls.

string 2 = -l.

6. Analyze the first string.

We need to check if that first substring is a command or a builtin, in this case, our first substring or token is (ls) which is a command.

A builtin is a command or a function, called from a shell, that is executed directly in the shell itself, instead of an external executable program which the shell would load and execute(more of that in about the process section of this article):

env builtin example:

We compare that first string against our builtins functions, our builtins are:

cd - Change the directory.

Change the directory. help - Print the shell help.

Print the shell help. exit - Exit from the shell.

Exit from the shell. env- Print the environment variables.

As you can see ls isn’t a builtin, now we have to find out if it is a command. We don’t know where that command is (it resides on /bin/ls, ok we know but the computer doesn’t) so we need to find it, for that we built the path management. The path management searches in the computer for the command.

Without the path management:

And we don’t want to write where the command resides every single time because is a lot of effort!

With the path management:

7. About the procces.

now we are ready to run the command….. But we can’t run it in the same shell process so we created a child process using the fork() function.

fork() creates a new process by duplicating the calling process(the shell in this case). The new process is referred to as the child process (where ls is going to be executed). The calling process is referred to as the parent process.

The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes, file mappings, and unmapping performed by one of the processes do not affect the other.The child process is an exact duplicate of the parent.

On success, the PID (process id) of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.

8. Exucting the command.

Now that we have the child process (a copy of the shell at this moment). The computer has the green light to run the command. We use the execve() function for this purpose. execve() executes the program pointed to by filename. filename must be either a binary executable.

execve() does not return on success, and the text, data, bss, and stack of the calling process are overwritten by that of the program loaded (now the child process isn’t a copy of the shell anymore, the command ls -l has been executed into that child process)

Tadaa by this time the command has been executed successfully and you should see in the screen something like this:

9. Clean everything

Remember the getline() and strtok() functions ? we occupied some memory that we don’t need anymore. So we have to use free() to release it and the system can use that resources in something else.

10. Start again.

If the command isn’t exit, and we were working in the interactive mode, we show the prompt again and the process restart.