WHAT THE SHELL?
Here are some key points we need to understand about the shell, and how these notions will be incorporated into our own shell.
1. Command Interpretation There are two categories of commands that we need to support:
a. External commands (ls, date) are executable files that are already residing in the directories, and running them is the theme of the current tutorial.
b. Support for built-in commands (cd, alias, etc) will be provided using the backbone plugin architecture we’ll introduce soon.
2. Redirection and pipelines An introduction to developing the shell’s support for features such as input/output redirection and pipelines will be started in our next instalment.
3. Shell prompt Customising the shell prompt will be an important part of our series. We will add features for displaying information such as hostname, current directory and more.
4. History and command-line editing Since we are unhappy with some features offered by current shell implementations concerning history, we will introduce a rather novel approach to how history is handled by our shell.
5. Tab completion Pressing the Tab key has a special place in our hearts and we will present an innovative approach to making this feature even more widely used by introducing a support library for command completion.
6. Customisation Implementing variables and aliases will be introduced very soon in this tutorial series, however support for functions will be delayed until we have the scripting feature in place.
7. Scripting Implementing a scripting language specific to our shell will be revisited at a later stage due to the sheer size of the project.
As a final touch, in homage to the magazine that accepted the publication of this author’s blusterings, we shall call the shell lxf-shell.
#include
{ std::perror(“fork”); return 1;
} if (child_pid == 0)
{ const char* const arguments[] = {program.c_ str(), nullptr}; if (execvp(program.c_str(), (char* const*) arguments) == -1)
{ std::perror(“execvp”); return 1;
}
} else
{ int status; waitpid(child_pid, &status, 0); } return 0; } int main() { while(true)
{ std::cout << “lxfsh$ “; std::string command; std::getline(std::cin, command); if(command == “exit”)
{ exit(0);
} execute(command);
} }
The execute function executes a given command by forking a new process. It starts by creating a child process using fork(). If the fork operation fails, it prints an error message and returns 1. In the child process (the true branch of the child_pid == 0 check), it prepares the command and its arguments, and tries to execute it using execvp(). If execvp() encounters an issue, such as the command not being found, it prints an error message and returns 1 to indicate an error.
In the parent process, the function waits for the child process to complete using waitpid(). This ensures the parent doesn’t continue executing until the child has finished. If the command executes successfully, the function returns 0. Now, if you compile and execute the program above, you are greeted with the familiar lxfsh$ prompt and you can again execute programs.
Oops, that didn’t go as well as expected, did it? What is the problem with the execvp call when you pass in a parameter to it? Fortunately, Linux comes with a handy library for such a complex situation as this. Its name is libexplain and it explains stuff to us, so the reasons for our failures will be written in a nice and understandable manner. If only life had a libexplain...
Explaining the unexplainable
To install the explainer library, run the following (or the equivalent for your distro):
$ sudo apt install libexplain-dev
Now we need to tell CMake to find an external library. Add the following lines in your CMakeLists.txt, just after the add_executable command: find_library(LIB_EXPLAIN explain) if(${LIB_EXPLAIN} STREQUAL “LIB_EXPLAINNOTFOUND”)
message(FATAL_ERROR “You need to install
LibExplain to build this application”) else() message(STATUS “LibExplain found at: ${LIB_
EXPLAIN}”) endif() target_link_libraries(${PROJECT_NAME} ${LIB_
EXPLAIN})
With these lines we have introduced several new commands for the aspiring CMake user:
find_library tries to find a library. The first parameter for this invocation is the name of a variable that CMake stores the result in, while the second is the name of the library. In case CMake fails to find the library in question, the value of this variable is “
if behaves like a classic if – it has a true branch from the if until the matching else, and a false branch from the else to endif. However, not all ifs must have an else. Please note how the value of the LIB_ EXPLAIN variable is compared to the literal value of
“LIB_EXPLAIN-NOTFOUND”. This is how you compare strings in CMake... Ahem, did we mention that some developers find the syntax of CMake a peculiar beauty? Finally, the endif command closes the if.
message prints a message to the screen. Depending on the optional first parameter – which in our case is
FATAL_ERROR, used for really fatal situations, such as the inability to find a library, or STATUS, as in the situation of we just feel like saying something – the upcoming parameters are printed to the screen, and if there is not FATAL_ERROR for the first parameter, the execution just continues. Otherwise CMake stops processing and waits for the developer’s remediation.
target_link_libraries instructs CMake to generate code sequences that tell the linker to link our application with the libraries following.
With all these prerequisites in place, we can head to our main.cpp, and it’s time to ask the newly introduced libexplain to explain what the problem with execvp is. Let’s add the following to the beginning of main.cpp:
#include
In the body of the execute method, let’s change the simplistic std::perror(“execvp”) to a more
sophisticated-looking: std::cerr << explain_execvp(program.c_str(), (char*
const*)arguments) << std::endl;
Once you compile and run again, you can see in the screenshot (left) the response of the shell if you try again to convince it to run ls -l :
This is a lot more detailed, and it helps us identify what the real problem is: execvp thought we wanted to execute a program named ls -l instead of the program ls with the parameter -l, so obviously we have a few logic errors in our program.
Splitting up is easy to do
Very specifically, for our situation, trying to execute ls -l , execvp expects the following parameters: char *program = “ls”; // Name of the program char *arguments[] = {“ls”, “-l”, nullptr}; // Commandline
arguments
However, we are providing instead: char *program = “ls -l”; // Name of the program char *arguments[] = {“ls -l”, nullptr}; // Command-line
arguments
And now we see why it fails. There is just one resolution to our problem: we need to split the incoming command string, and build up a decent char* array from it that execvp can work with. Splitting a string in C++ is not such a tedious task and it can be done in a few easy steps. A naive and straightforward implementation can be seen below: std::vector
std::string& input) { std::vector
{ result.push_back(token);
} return result; }
For the current iteration of the shell, this will do, and in order to integrate the newly acquired function, we shall modify our execute function to use it. The following piece of code goes into the if (child_pid == 0) branch – in other words, the place where we start our child process after a successful fork: auto split = splitStringByWhitespace(program); const char** arguments = new const char*[split.size() + 1]; size_t i = 0; for(; i -1) { std::cerr << explain_execvp(program.c_str(), (char* const*)(arguments)) << std::endl; return 1; } This iteration uses a vector called split to store the command’s components, converts them to C-style strings, and places them in an array of const char* called arguments. A nullptr is added as the last element to terminate the argument list, and execvp() is called to execute the command. If execvp() fails, it prints an error message using a custom function (explain_execvp()) and returns an error code (1). And with this in place, we can finally build our shell again, and after starting it, we can verify whether it accepts parameters to the commands as expected (see screenshot, below). And yes, it works – finally our commands can be properly parametrised. As we bring this article to a close, we’re excited to offer a glimpse into what the next instalments have in store. Next month, we’ll explore the intricacies of command redirection. These fundamental concepts are essential for understanding how shells can manipulate data streams, allowing for even more powerful and versatile command-line experiences. So, be sure to stay tuned for the upcoming chapters in our journey. In the meantime, keep those coding spirits high, and happy coding, as you continue to explore the world of Linux programming!A glimpse into the future