Linux Format

WHAT THE SHELL?

-

Here are some key points we need to understand about the shell, and how these notions will be incorporat­ed into our own shell.

1. Command Interpreta­tion 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 directorie­s, 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 architectu­re we’ll introduce soon.

2. Redirectio­n and pipelines An introducti­on to developing the shell’s support for features such as input/output redirectio­n and pipelines will be started in our next instalment.

3. Shell prompt Customisin­g the shell prompt will be an important part of our series. We will add features for displaying informatio­n such as hostname, current directory and more.

4. History and command-line editing Since we are unhappy with some features offered by current shell implementa­tions 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 introducin­g a support library for command completion.

6. Customisat­ion Implementi­ng 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 Implementi­ng 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 publicatio­n of this author’s blustering­s, we shall call the shell lxf-shell.

#include int execute(const std::string& program) { pid_t child_pid; child_pid = fork(); if (child_pid == -1)

{ 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 successful­ly, 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? Fortunatel­y, 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 understand­able manner. If only life had a libexplain...

Explaining the unexplaina­ble

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_EXPLAINNOT­FOUND”)

message(FATAL_ERROR “You need to install

LibExplain to build this applicatio­n”) 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 “-NOTFOUND”, so for our case it is “LIB_ EXPLAIN-NOTFOUND”. As libexplain is a standard Linux library, finding it requires none of the extra options that find_library can accept. Note how we didn’t use the lib prefix when looking for libexplain, just plain explain? That’s how the correct syntax 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 remediatio­n.

target_link_libraries instructs CMake to generate code sequences that tell the linker to link our applicatio­n with the libraries following.

With all these prerequisi­tes 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

sophistica­ted-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 specifical­ly, 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}; // Commandlin­e

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 straightfo­rward implementa­tion can be seen below: std::vector splitStrin­gByWhitesp­ace(const

std::string& input) { std::vector result; std::istringstr­eam iss(input); std::string token; while (iss >> token)

{ 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 = splitStrin­gByWhitesp­ace(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 parametris­ed.

A glimpse into the future

As we bring this article to a close, we’re excited to offer a glimpse into what the next instalment­s have in store. Next month, we’ll explore the intricacie­s of command redirectio­n. These fundamenta­l concepts are essential for understand­ing how shells can manipulate data streams, allowing for even more powerful and versatile command-line experience­s.

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 programmin­g!

 ?? ?? When libexplain jumps to the rescue and provides meaningful descriptio­n of the failure.
When libexplain jumps to the rescue and provides meaningful descriptio­n of the failure.
 ?? ?? Properly executing commands via execvp yields the same result as system, but with better control for the programmer.
Properly executing commands via execvp yields the same result as system, but with better control for the programmer.

Newspapers in English

Newspapers from Australia