Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stream redirection and pipes when making a Linux shell

Tags:

c

redirect

io

pipe

I have an assignment to create a Linux shell in C. Currently, I am stuck on implementing redirections and pipes. The code that I have so far is below. The main() parses user's input. If the command is built in, then that command is executed. Otherwise, the tokenized input is passed to execute() (I know that I should probably pull the built-in commands into their own function).

What execute() does is loop through the array. If it encounters <, >, or | it should take appropriate action. The first thing I am trying to get to work correctly is piping. I am definitely doing something wrong, though, because I cannot get it to work for even one pipe. For example, a sample input/output:

/home/ad/Documents> ls -l | grep sh
|: sh: No such file or directory
|

My idea was to get each of the directions and piping work for just one case, and then by making the function recursive I could hopefully use multiple redirections/pipes in the same command line. For example, I could do program1 < input1.txt > output1.txt or ls -l | grep sh > output2.txt.

I was hoping that someone can point out my errors in trying to pipe and perhaps offer some pointers in how to approach the case where multiple redirections/pipes are inputted by the user.

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

int MAX_PATH_LENGTH = 1024; //Maximum path length to display.
int BUF_LENGTH = 1024; // Length of buffer to store user input
char * delims = " \n"; // Delimiters for tokenizing user input.
const int PIPE_READ = 0;
const int PIPE_WRITE = 1;

void execute(char **argArray){

  char **pA = argArray;
  int i = 0;
  while(*pA != NULL) {
    if(strcmp(argArray[i],"<") == 0) { 
        printf("<\n"); 
    }
    else if(strcmp(argArray[i],">") == 0) { 
        printf(">\n"); 
    }
    else if(strcmp(argArray[i],"|") == 0) {
        int fds[2];
        pipe(fds);
        pid_t pid;
        if((pid = fork()) == 0) {
            dup2(fds[PIPE_WRITE], 1);
            close(fds[PIPE_READ]);
            close(fds[PIPE_WRITE]);
            char** argList;
            memcpy(argList, argArray, i);
            execvp(argArray[0], argArray);            
        }
        if((pid = fork()) == 0) {
            dup2(fds[PIPE_READ], 0);
            close(fds[PIPE_READ]);
            close(fds[PIPE_WRITE]);
            execvp(argArray[i+1], pA);            
        }
        close(fds[PIPE_READ]);
        close(fds[PIPE_WRITE]);
        wait(NULL);
        wait(NULL);
        printf("|\n");
    }
    else { 
        if(pid == 0){
            execvp(argArray[0], argArray);
            printf("Command not found.\n");
        }
        else
            wait(NULL);*/
    }
    *pA++;
    i++;
  }
}

int main () {

  char path[MAX_PATH_LENGTH];
  char buf[BUF_LENGTH];
  char* strArray[BUF_LENGTH];
  /**
   * "Welcome" message. When mash is executed, the current working directory
   * is displayed followed by >. For example, if user is in /usr/lib/, then
   * mash will display :
   *      /usr/lib/> 
   **/
  getcwd(path, MAX_PATH_LENGTH);
  printf("%s> ", path);
  fflush(stdout);

  /**
   * Loop infinitely while waiting for input from user.
   * Parse input and display "welcome" message again.
   **/ 
  while(1) {
    fgets(buf, BUF_LENGTH, stdin);
    char *tokenPtr = NULL;
    int i = 0;
    tokenPtr = strtok(buf, delims);

    if(strcmp(tokenPtr, "exit") == 0){

        exit(0);
    }
    else if(strcmp(tokenPtr, "cd") == 0){
        tokenPtr = strtok(NULL, delims);
        if(chdir(tokenPtr) != 0){
            printf("Path not found.\n");
        }
        getcwd(path, MAX_PATH_LENGTH);
    }
    else if(strcmp(tokenPtr, "pwd") == 0){
        printf("%s\n", path);
    }
    else {
        while(tokenPtr != NULL) {
            strArray[i++] = tokenPtr;
            tokenPtr = strtok(NULL, delims);
        }
        execute(strArray);
    }

    bzero(strArray, sizeof(strArray)); // clears array
    printf("%s> ", path);
    fflush(stdout);
  }

}
like image 318
A D Avatar asked Oct 10 '22 22:10

A D


1 Answers

Part of the problem is in the pipe handling code - as you suspected.

else if (strcmp(argArray[i], "|") == 0) {
    int fds[2];
    pipe(fds);
    pid_t pid;
    if ((pid = fork()) == 0) {
        dup2(fds[PIPE_WRITE], 1);
        close(fds[PIPE_READ]);
        close(fds[PIPE_WRITE]);
        char** argList;
        memcpy(argList, argArray, i);
        execvp(argArray[0], argArray);            
    }
    if ((pid = fork()) == 0) {
        dup2(fds[PIPE_READ], 0);
        close(fds[PIPE_READ]);
        close(fds[PIPE_WRITE]);
        execvp(argArray[i+1], pA);            
    }
    close(fds[PIPE_READ]);
    close(fds[PIPE_WRITE]);
    wait(NULL);
    wait(NULL);
    printf("|\n");
}

The first execvp() was probably intended to use argList since you've just copied some material there. However, you've copied i bytes, not i character pointers, and you've not ensured that the pipe is zapped and replaced with a null pointer.

memcpy(argList, argArray, i * sizeof(char *));
argList[i] = 0;
execvp(argList[0], argList);

Note that this has not verified that there is no buffer overflow on argList; Note that there is no space allocated for argList; if you use it, you should allocate the memory before doing the memcpy().

Alternatively, and more simply, you can do without the copy. Since you're in a child process, you can simply zap replace argArray[i] with a null pointer without affecting either the parent or the other child process:

argArray[i] = 0;
execvp(argArray[0], argArray);

You might also note that the second invocation of execvp() uses a variable pA which cannot be seen; it is almost certainly incorrectly initialized. As a moderately good rule of thumb, you should write:

execvp(array[n], &array[n]);

The invocations above don't conform to this schema, but if you follow it, you won't go far wrong.

You should also have basic error reporting and a exit(1) (or possibly _exit(1) or _Exit(1)) after each execvp() so that the child does not continue if it fails to execute. There is no successful return from execvp(), but execvp() most certainly can return.

Finally for now, these calls to execvp() should presumably be where you make your recursive call. You need to deal with pipes before trying to deal with other I/O redirection. Note that in a standard shell, you can do:

> output < input command -opts arg1 arg2

This is aconventional usage, but is actually permitted.

One good thing - you have ensured that the original file descriptors from pipe() are closed in all three processes (parent and both children). This is a common mistake which you have avoided making; well done.

like image 121
Jonathan Leffler Avatar answered Oct 18 '22 16:10

Jonathan Leffler