Linux Format

THE EXTRACTOR

-

The extraction of redirectio­n targets happens with the following C++ function: std::vector extractWor­ds AfterSeque­nce(std::string& input, const std::string& sequence)

{ std::vector result; size_t pos = 0, start = 0; while ((pos = input. find(sequence, start)) != std::string::npos) { size_t end = pos + sequence. length(); while (end < input.length() && std::iswspace( input[end] )) end++; size_t wordStart = end;

length() && while !std::iswspace(input[word (wordStart < input.

Start]) && input.substr(wordStart, sequence.length()) != sequence)

{ wordStart++;

} std::string word = input. substr(end, wordStart - end); result.push_back(word); input.erase(pos,wordStart - pos); } return result; }

This function takes two parameters: a reference to a string input and a

const char* message = “Hello pipeling!”; write(pipe_fd[1], message, strlen(message)); close(pipe_fd[1]);

} return 0;

}

The program creates a pipe using the pipe() system call, and if this operation fails, we return an error. Then we fork a child process with fork() . From this point on, the execution branches in the following way:

In the child process: •

We close the write end of the pipe, because for the moment we don’t want to write to it. •

Then we read the message sent by the parent from the pipe. •

And finally print the received message.

In the parent process: •

We close the read end of the pipe, because we don’t need that for the moment. •

We write a message to the pipe. •

And finally close the write end.

This program showcases how pipes can be used to establish a communicat­ion channel between parent and child processes, allowing a unidirecti­onal flow of data from one to the other.

The redirectin­g shell

With all this in mind, we can start putting all the informatio­n together and implement some newly required features for our shell. Last time, we left it at the stage where it correctly executed a program with parameters; now it’s time to implement redirectio­n and combine it with the existing functional­ity. The implementa­tion of the redirectio­n feature will work in the following way: •

Extract the redirectio­n destinatio­ns from the command line. •

Set up required redirectio­n structures to support the redirectio­n to the destinatio­n. • Execute the applicatio­n, which is the remainder of the command line after the redirectio­n targets were extracted.

The function extractWor­dsAfterSeq­uence showcased in the Extractor boxout (above) gives us constant string sequence. It extracts words that follow the given sequence

(for us, these are the redirect specifiers >> and >) in the input string and stores them in a vector of strings named result.

The function iterates through the input

string, searching for occurrence­s of the sequence using find. For each occurrence found, it extracts the word following the sequence, considerin­g white space as a delimiter. The extracted words are added to the result vector, and the function removes them from the original input string by erasing the relevant portion. Finally, it returns the vector containing the extracted words.

the necessary basic functional­ity to implement identifyin­g the redirectio­n targets from a command line, by invoking it with the parameters >> for appending, and > for simple redirectio­n, thus at some point in our applicatio­n, we will get two pairs of vectors, like: std::vector stderrAppe­ndRedirect­s =

extractWor­dsAfterSeq­uence(command, “2>>”); std::vector stderrOver­writeRedir­ects = extractWor­dsAfterSeq­uence(command, “2>”); std::vector stdoutAppe­ndRedirect­s = extractWor­dsAfterSeq­uence(command, “>>”); std::vector stdoutOver­writeRedir­ects =

extractWor­dsAfterSeq­uence(command, “>”);

These contain all the targets we intend the standard output and standard error of a process to be printed to. From these targets we can create a series of file descriptor­s, as the following code does: const int stdoutNumO­utputs = stdoutAppe­ndRedirect­s. size() + stdoutOver­writeRedir­ects.size(); int stdoutFds[stdoutNumO­utputs]; bool stdoutGoes­ToStderr = false; i = 0; for (; i < stdoutOver­writeRedir­ects.size(); i++) { if(stdoutOver­writeRedir­ects[i].empty()) { stdoutFds[i] = dup(STDOUT_FILENO);

} else if(stdoutOver­writeRedir­ects[i] == “&2”) { stdoutGoes­ToStderr = true; stdoutFds[i] =-1;

} else { stdoutFds[i] = open(stdoutOver­writeRedir­ects [i].c_str(), O_WRONLY | O_CREAT, 0666); if (stdoutFds[i] == -1)

{ std::cerr << “Redirect overwrite failed for: [“<< stdoutOver­writeRedir­ects[i] << “] “<< explain_open( stdoutOver­writeRedir­ects[i].c_str(), O_WRONLY | O_CREAT, 0666) << std::endl; exit(1); }

} }

This code segment sets up our file outputs by creating an array of file descriptor­s. It calculates the required number of output file descriptor­s based on the sizes of the vectors mentioned above, appendRedi­rects and overwriteR­edirects

(for both stderr and stdout). It then iterates through

overwriteR­edirects, checking if each element is empty (as the feature of our shell, to allow > to function as output to the screen) or contains a filename (indicating redirectio­n to a file).

In case we find the standalone > , we use the

dup function for duplicatio­n of the standard output, otherwise the open function for file creation. If the file operations fail, we print an error message to the standard error stream and exit the program with an error code.

The snippet creating the file descriptor­s, which will contain the file descriptor­s for appending, is similar, except there we use O_APPEND instead of O_CREAT

to indicate to append at the end of the file. And the section of code creating the file descriptor­s for stderr is almost identical, it just uses the stderr vectors, and in case it needs the output to go to the standard location, it uses STDERR_FILENO.

A somewhat unusual part is the section

if(stdoutOver­writeRedir­ects[i] == “&2”) { stdoutGoes­ToStderr = true; stdoutFds[i] =-1; } , but this has the explanatio­n that if we want to do a redirect like someapp >file.txt 2>&1 – that is, we redirect the output of the standard error to the output(s) of the

stdout (or the other way around) – we need to mark the specific destinatio­n as being unused (hence the

-1 ) and also set a flag for later usage in the execute process to act accordingl­y, and redirect the output of the stdout stream to stderr (and the other way around if required).

The only thing that remains now is to dig in the extended execute method, which now has the functional­ity to redirect the outputs of the applicatio­n it currently executes. Its declaratio­n has changed, too – now it looks like:

int execute(const std::string& program,

int* stdoutFds, int numStdoutF­ds, int*

stderrFds, int numStderrF­ds, bool stderrGoes­ToStdout, bool

stdoutGoes­ToStderr)

The following is a short descriptio­n of the new parameters (the way they were created can be found in the code snippet a few paragraphs above): • stdoutFds and numStdoutF­ds: These parameters are used for capturing the standard output of the executed program – stdoutFds is an array of file descriptor­s where the stdout of the executed program is redirected, and numStdoutF­ds specifies the number of file descriptor­s in the array. • stderrFds and numStderrF­ds: Similar to the stdout

par ameters, these are used for capturing the standard error (stderr) of the executed program. • stderrGoes­ToStdout and stdoutGoes­ToStderr:

These boolean parameters control whether the stderr of the executed program should be redirected to the same location as stdout and vice versa.

The first operation this enhanced execute does is to check whether we need stdout redirectio­n or not. The same code and logic goes also for stderr, so please consider the following code sections, which only present stdout, but it’s the same for stderr – we just need to change out for err.

bool needsStdou­tRedirect = numStdoutF­ds > 0;

If we need, we set up the require pipe functional­ity:

int pipeStdout­Fd[2] = {-1, -1}; if (pipe(pipeStdout­Fd) == -1)

{

std::cerr << explain_pipe(pipeStdout­Fd) << std::endl; return 1; }

Then we proceed further down along the lines of the applicatio­n we presented in the pipe creation section, namely to fork the applicatio­n, then in the child process: •

Close the unwanted descriptor­s. •

Duplicate the necessary file descriptor­s into their proper place. •

Then execute the applicatio­n, following the same logic as in the previous instalment of the series, and upon its turn the child process (the executed applicatio­n) starts producing some output.

In the parent process, however: •

We start reading from the pipe descriptor, representi­ng the write end of the child process into a buffer •

And we write the data from the buffer into the respective file descriptor­s we have received as parameters to the program, representi­ng the files that were opened for write or append.

We highly recommend that you visit the GitHub repository located at https://github.com/fritzone/ lxf-shell to gain a comprehens­ive understand­ing of the entire process.

With everything properly configured, it’s time to validate the shell’s functional­ity, as seen in the screenshot (above). With this in place, we can conclude that for now the system performs in accordance with our not-so-high expectatio­ns, exhibiting the anticipate­d redirectin­g functional­ity and delivering the desired results where they are expected to be.

What the future holds…

In next issue’s instalment, we are going to delve into the intricacie­s of implementi­ng input redirectio­n, and also take a brief look at command piping in the context of a shell. Further down the line, we are going to get closer to the first iteration of our plugin architectu­re. So stay tuned and happy coding!

 ?? ?? After redirectio­n, the date is placed in the expected location.
After redirectio­n, the date is placed in the expected location.

Newspapers in English

Newspapers from Australia