THE EXTRACTOR
The extraction of redirection targets happens with the following C++ function: std::vector
{ std::vector
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 communication channel between parent and child processes, allowing a unidirectional flow of data from one to the other.
The redirecting shell
With all this in mind, we can start putting all the information 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 redirection and combine it with the existing functionality. The implementation of the redirection feature will work in the following way: •
Extract the redirection destinations from the command line. •
Set up required redirection structures to support the redirection to the destination. • Execute the application, which is the remainder of the command line after the redirection targets were extracted.
The function extractWordsAfterSequence 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 occurrences of the sequence using find. For each occurrence found, it extracts the word following the sequence, considering 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 functionality to implement identifying the redirection targets from a command line, by invoking it with the parameters >> for appending, and > for simple redirection, thus at some point in our application, we will get two pairs of vectors, like: std::vector
extractWordsAfterSequence(command, “2>>”); std::vector
extractWordsAfterSequence(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 descriptors, as the following code does: const int stdoutNumOutputs = stdoutAppendRedirects. size() + stdoutOverwriteRedirects.size(); int stdoutFds[stdoutNumOutputs]; bool stdoutGoesToStderr = false; i = 0; for (; i < stdoutOverwriteRedirects.size(); i++) { if(stdoutOverwriteRedirects[i].empty()) { stdoutFds[i] = dup(STDOUT_FILENO);
} else if(stdoutOverwriteRedirects[i] == “&2”) { stdoutGoesToStderr = true; stdoutFds[i] =-1;
} else { stdoutFds[i] = open(stdoutOverwriteRedirects [i].c_str(), O_WRONLY | O_CREAT, 0666); if (stdoutFds[i] == -1)
{ std::cerr << “Redirect overwrite failed for: [“<< stdoutOverwriteRedirects[i] << “] “<< explain_open( stdoutOverwriteRedirects[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 descriptors. It calculates the required number of output file descriptors based on the sizes of the vectors mentioned above, appendRedirects and overwriteRedirects
(for both stderr and stdout). It then iterates through
overwriteRedirects, 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 redirection to a file).
In case we find the standalone > , we use the
dup function for duplication 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 descriptors, which will contain the file descriptors 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 descriptors 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(stdoutOverwriteRedirects[i] == “&2”) { stdoutGoesToStderr = true; stdoutFds[i] =-1; } , but this has the explanation 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 destination as being unused (hence the
-1 ) and also set a flag for later usage in the execute process to act accordingly, 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 functionality to redirect the outputs of the application it currently executes. Its declaration has changed, too – now it looks like:
int execute(const std::string& program,
int* stdoutFds, int numStdoutFds, int*
stderrFds, int numStderrFds, bool stderrGoesToStdout, bool
stdoutGoesToStderr)
The following is a short description of the new parameters (the way they were created can be found in the code snippet a few paragraphs above): • stdoutFds and numStdoutFds: These parameters are used for capturing the standard output of the executed program – stdoutFds is an array of file descriptors where the stdout of the executed program is redirected, and numStdoutFds specifies the number of file descriptors in the array. • stderrFds and numStderrFds: Similar to the stdout
par ameters, these are used for capturing the standard error (stderr) of the executed program. • stderrGoesToStdout and stdoutGoesToStderr:
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 redirection 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 needsStdoutRedirect = numStdoutFds > 0;
If we need, we set up the require pipe functionality:
int pipeStdoutFd[2] = {-1, -1}; if (pipe(pipeStdoutFd) == -1)
{
std::cerr << explain_pipe(pipeStdoutFd) << std::endl; return 1; }
Then we proceed further down along the lines of the application we presented in the pipe creation section, namely to fork the application, then in the child process: •
Close the unwanted descriptors. •
Duplicate the necessary file descriptors into their proper place. •
Then execute the application, following the same logic as in the previous instalment of the series, and upon its turn the child process (the executed application) starts producing some output.
In the parent process, however: •
We start reading from the pipe descriptor, representing the write end of the child process into a buffer •
And we write the data from the buffer into the respective file descriptors we have received as parameters to the program, representing 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 comprehensive understanding of the entire process.
With everything properly configured, it’s time to validate the shell’s functionality, 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 expectations, exhibiting the anticipated redirecting functionality 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 intricacies of implementing input redirection, 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 architecture. So stay tuned and happy coding!