In Linux a shell is a command line interface, most modern systems use a graphical user interface. While a GUI (graphical user interface) is easier to use, a CLI (command line interface) is more powerful. To use a CLI, a terminal must be opened, the terminal is where commands are entered, the shell is the software that interprets those commands. How this is accomplished will be covered in detail below.
First we should cover what command we will be using for this example, let us use ls -l as our example command. The ls -l command list all the files in the current directory, in the long format. We will not be covering how commands are written, that is beyond the scope of this article. However we will cover how the shell interpreters those commands. The shell expects a command that may be followed by arguments. In this case the command is ls and the argument is -l.
To execute the command ls -l the shell needs to find the command in the path, and check if its actually a command. We can check if a command is an actual command with stat, if stat comes back true its a real command, if not its not a known command. We also have to check for aliases, aliases are another name for a command, if an alias is used we need to check for it. In order to understand this process we need to explain what the path is, the path is an environment variable that tells the shell where to look for executables. An environment variable is a value that is set outside of the program, generally by the operating system. The path can be stored in two ways, a linked list, or an array. It depends on which shell you decide to use. A linked list is a variable and an assigned memory address for that value, this way each item in the list points the the next item in the list via the memory address. An array is stored contiguously in memory, this allows for faster read times but potentially slower write times. The path contains all the directories that normally contain common commands and executables. This allows the shell to get the path and search through it for the specified command. If the command is not found the shell will say so and return to the prompt. Once the command is found in the path it is passed off to execve system call, that executes commands, it may or may not be passed with arguments. In our example of ls -l we are using the -l argument. System calls are a function of the operating system, when one is called it takes control from the program and executes the system call with the arguments that are passed to it, this can be slow so it is good practice to use as few system calls as possible.
The execve system call, replaces the current process, overwriting its heap, stack, and variables, generally everything but the environment variables. The heap and stack are different parts of the memory reserved for a program when it starts. The stack is used for memory that’s size is known at the start of the program, the heap is for dynamically allocated memory. These are overwritten by the execve system call. This is why it is important to fork, forking is when a program creates a duplicate of itself that is identical except for the process id. The process id of a process is always unique. We fork the program because otherwise once a command is executed the shell would exit, and would not exit properly. If we use the wait command the parent, (the program that called the fork process) will wait until the child (the program created by the parent) exits, this allows us to call a command and then have the prompt reappear. Since we are passing ls -l to the execve system call we have to also handle the -l argument. To do this we use strtok.
strtok takes a string and turns its members into tokens, these tokens are smaller strings that can be passed to system calls and functions such as execve. So in the case ls -l we have strtok break the string up on spaces, so ls will be one token and -l will be another this way we can pass ls to execve, and -l as an argument of ls.
So in order to run a command we first get the path form the environment variables, then we print the prompt. Once we do that the user can enter the command, in this case ls -l, once the command is entered we fork, wait for the child process to finish, and tokenize the command so we can pass it to execve after finding it in the path. Once the command is completed the prompt will be printed again.