1

I'm creating a shell in C, and I need help implementing input and output redirection.

When I try to create a file using ">" I get an error message saying the file does not exist. When I try to do something like ls > test.txt; it won't create a new file.

I updated the code with the suggestions provided to me, but now I got different errors. However, a new file is still not created for the output redirection.

This is my full code:

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

#define MAX_BUF 160
#define MAX_TOKS 100

int main(int argc, char **argv) 
{
    char *pos;
    char *tok;
    char *path;
    char s[MAX_BUF];
    char *toks[MAX_TOKS];
    time_t rawtime;
    struct tm *timeinfo;
    static const char prompt[] = "msh> ";
    FILE *infile;
    int in;
    int out;
    int fd0;
    int fd1;
    in = 0;
    out = 0;

 /* 
 * process command line options
*/

  if (argc > 2) {
    fprintf(stderr, "msh: usage: msh [file]\n");
    exit(EXIT_FAILURE);
  }
  if (argc == 2) {
    /* read from script supplied on the command line */
    infile = fopen(argv[1], "r");
    if (infile == NULL) 
    {
       fprintf(stderr, "msh: cannot open script '%s'.\n", argv[1]);
       exit(EXIT_FAILURE);
    }
  } else {
      infile = stdin;
  }

  while (1) 
  {
    // prompt for input, if interactive input
     if (infile == stdin) {
     printf(prompt);
  }

/*
 * read a line of input and break it into tokens 
 */

  // read input 
  char *status = fgets(s, MAX_BUF-1, infile);

  // exit if ^d or "exit" entered
  if (status == NULL || strcmp(s, "exit\n") == 0) {
       if (status == NULL && infile == stdin) {
              printf("\n");
        }
        exit(EXIT_SUCCESS);
  }

  // remove any trailing newline
  if ((pos = strchr(s, '\n')) != NULL) {
    *pos = '\0';
   }

   // break input line into tokens 
    char *rest = s;
    int i = 0;

  while((tok = strtok_r(rest, " ", &rest)) != NULL && i < MAX_TOKS) 
  {
      toks[i] = tok;
      if(strcmp(tok, "<") == 0)
      {
          in = i + 1;
           i--;
       }
       else if(strcmp(tok, ">")==0)
       {
          out = i + 1;
          i--;
       }
       i++;
  }

  if (i == MAX_TOKS) {
      fprintf(stderr, "msh: too many tokens");
      exit(EXIT_FAILURE);
  }
  toks[i] = NULL;

/*
 * process a command
 */

  // do nothing if no tokens found in input
  if (i == 0) {
     continue;
  }


  // if a shell built-in command, then run it 
  if (strcmp(toks[0], "help") == 0) {
      // help 
       printf("enter a Linux command, or 'exit' to quit\n");
       continue;
   } 
  if (strcmp(toks[0], "today") == 0) {
       // today
       time(&rawtime);
       timeinfo = localtime(&rawtime);
       printf("Current local time: %s", asctime(timeinfo));
      continue;
  }
  if (strcmp(toks[0], "cd") == 0) 
  {
     // cd 
     if (i == 1) {
         path = getenv("HOME");
     } else {
         path = toks[1];
     }
     int cd_status = chdir(path);
     if (cd_status != 0) 
     {
         switch(cd_status) 
         {
            case ENOENT:
                printf("msh: cd: '%s' does not exist\n", path);
                break;
            case ENOTDIR:
                printf("msh: cd: '%s' not a directory\n", path);
                break;
            default:
                printf("msh: cd: bad path\n");
          }
      }
     continue;
  }

  // not a built-in, so fork a process that will run the command
  int rc = fork();
  if (rc < 0) 
  {
     fprintf(stderr, "msh: fork failed\n");
      exit(1);
   }
   if (rc == 0) 
   {
        if(in)
        {
            int fd0;
            if((fd0 = open(toks[in], O_RDONLY, 0)) == -1)
            {
                perror(toks[in]);
                exit(EXIT_FAILURE);
            }
            dup2(fd0, 0);
            close(fd0);
         }

        if(out)
        {
           int fd1;
           if((fd1 = open(toks[out], O_WRONLY | O_CREAT | O_TRUNC | O_CREAT, 
            S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
            { 
               perror (toks[out]);
               exit( EXIT_FAILURE);
             }
            dup2(fd1, 1);
            close(fd1);
        }
        // child process: run the command indicated by toks[0]
        execvp(toks[0], toks);
        /* if execvp returns than an error occurred */
        printf("msh: %s: %s\n", toks[0], strerror(errno));
        exit(1);
     } 
    else 
    {
        // parent process: wait for child to terminate
       wait(NULL);
    }
  }
}
Gray
  • 373
  • 1
  • 3
  • 10
  • Presumably `in` and `out` are declared at least at the scope of `int num_toks = 0;` and are validly initialized. [**A Minimal, Complete, and Verifiable example**](http://stackoverflow.com/help/mcve) would help. – David C. Rankin Feb 25 '18 at 05:19
  • What is `dup2` doing? – Lee Gaines Feb 25 '18 at 05:21
  • I don't think you want those calls to `close()`. `dup2` will close the old descriptors that it's overwriting. You would only see calls to `close` when there were other file descriptors open in the shell that you didn't want to have copied into the forked process. At the very least, it seems wrong to close `fd0` before the `dup2` – lockcmpxchg8b Feb 25 '18 at 05:24
  • @DavidC.Rankin Yes, I just updated the code. `int in` and `int out` are declared at the beginning. – Gray Feb 25 '18 at 05:25
  • See [Redirecting exec output to a buffer or file](https://stackoverflow.com/questions/2605130/redirecting-exec-output-to-a-buffer-or-file) If you are still having trouble, then drop another comment and we will go through it, but that answer is fairly complete. – David C. Rankin Feb 25 '18 at 05:29
  • @DavidC.Rankin I had actually looked at that prior to posting this, but I'm still having trouble with the redirection. I tried to use the `open` call that they had, but a new file doesn't get created. If I try to run a output redirection with a file that already exists, that doesn't work either. – Gray Feb 25 '18 at 05:41
  • Ok, give me a minute (or a couple figuratively speaking) and I'll work something up. – David C. Rankin Feb 25 '18 at 05:42
  • @DavidC.Rankin Okay. Thank you – Gray Feb 25 '18 at 07:20
  • See my comment on your use of `strtok_r` following my answer. You need `NULL` for all subsequent calls instead of `rest`. (the `saveptr` is OK), but it must be `strtok_r(NULL, ...` for calls 2 and thereafter. That would be a big problem for parsing... (`see man 3 strtok`) – David C. Rankin Feb 25 '18 at 08:12

1 Answers1

0

On first glance, other than your close and dup2 being out of order in your toks[in] case, there isn't anything readily apparent that explains why you do not create an output file when redirecting (e.g. cat somefile > newfile). However, there are a number of subtleties that you are not checking.

For example, you need to check whether your call to open succeeds in each case before calling dup2 and close. (otherwise, you are attempting to redirect a file-descriptor that is not open). Simple basic checking will do, e.g.

if (in) {
    int fd0;
    if ((fd0 = open(toks[in], O_RDONLY)) == -1) {
        perror (toks[in]);
        exit (EXIT_FAILURE);
    }
    dup2(fd0, 0);
    close(fd0);
}

if (out)
{
    int fd1;
    if ((fd1 = open(toks[out], 
                O_WRONLY | O_CREAT | O_TRUNC | O_CREAT, 
                S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {            
        perror (toks[out]);
        exit (EXIT_FAILURE);
    }
    dup2(fd1, 1);
    close(fd1);
}

(note: I've tweaked the permission to write the file as 0644 (user 'rw', group 'r' and world 'r'. You should also check the returns of dup2 and in the pedantic case close)

The bigger issues come in how you rearrange toks before your call to execvp. The reason you use dup2 or a pipe is that the exec.. function cannot handle redirection (e.g. it doesn't know what to do with '>' or '<'). So you are manually handling the redirection of input or output to a file by redirecting either the file to stdin on the input case or stdout (and/or stderr) to the file on the output case. In either case, you must remove the < filename or > filename tokens from toks before calling execvp or you will get an error.

If you insure that set each pointer in toks to NULL and you read no more than MAXTOKS - 1 (preserving a terminating NULL as required), then you can iterate over toks shifting the pointers to insure you do not send the < > and filename to execvp. After you find < or > in toks at index i and insure there is a toks[i+1] filename, something like:

            while (toks[i]) {
                toks[idx] = toks[i+2];
                i++; 
            }

Then passing toks to execvp will not generate the error (that I suspect is what you are experiencing)

There is also another corner-case nit you should be aware of. If your executable has any registered calls to atexit or other desctructors, the references are not part of your call to execvp. So if the call to execvp fails, you cannot call exit (which can invoke undefined behavior in a call to any post-exit function), so the proper call is to _exit which will not attempt any such calls.

A bare minimum of the working redirection would be something like the following. Not there are many other aspects of parsing and redirection not addressed below, but for your basic file creation problem, it provides a framework, e.g.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

enum {ARGSIZE = 20, BUF_SIZE = 1024};    /* constants */

void execute (char **args);

int main (void) {

    while (1) {

        char line[BUF_SIZE] = "",
            *args[ARGSIZE],
            *delim = " \n",
            *token;
        int argIndex = 0;

        for (int i = 0; i < ARGSIZE; i++)  /* set all pointers NULL */
            args[i] = NULL;

        printf ("shell> ");             /* prompt */

        if (!fgets (line, BUF_SIZE, stdin)) {
            fprintf (stderr, "Input canceled - EOF received.\n");
            return 0;
        }
        if (*line == '\n')              /* Enter alone - empty line */
            continue;

        for (token = strtok (line, delim);        /* parse tokens */
                token && argIndex + 1 < ARGSIZE; 
                token = strtok (NULL, delim)) {
            args[argIndex++] = token;
        }

        if (!argIndex) continue;        /* validate at least 1 arg */

        if (strcmp (args[0], "quit") == 0 || strcmp (args[0], "exit") == 0)
            break;

        execute (args);  /* call function to fork / execvp */

    }
    return 0;
}

void execute (char **args)
{
    pid_t pid, status;
    pid = fork ();

    if (pid < 0) {
        perror ("fork");
        return;
    }
    else if (pid > 0) {
        while (wait (&status) != pid)
            continue;
    }
    else if (pid == 0) {
        int idx = 0,
            fd;
        while (args[idx]) {   /* parse args for '<' or '>' and filename */
            if (*args[idx] == '>' && args[idx+1]) {
                if ((fd = open (args[idx+1], 
                            O_WRONLY | O_CREAT, 
                            S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1) {
                    perror (args[idx+1]);
                    exit (EXIT_FAILURE);
                }
                dup2 (fd, 1);
                dup2 (fd, 2);
                close (fd);
                while (args[idx]) {
                    args[idx] = args[idx+2];
                    idx++; 
                }
                break;
            }
            else if (*args[idx] == '<' && args[idx+1]) {
                if ((fd = open (args[idx+1], O_RDONLY)) == -1) {
                    perror (args[idx+1]);
                    exit (EXIT_FAILURE);
                }
                dup2 (fd, 0);
                close (fd);
                while (args[idx]) {
                    args[idx] = args[idx+2];
                    idx++; 
                }
                break;
            }
            idx++;
        }
        if (execvp (args[0], args) == -1) {
            perror ("execvp");
        }
        _exit (EXIT_FAILURE);   /* must _exit after execvp return, otherwise */
    }                           /* any atext calls invoke undefine behavior  */
}

Example Use/Output

Minimally working both > filename and < filename,

$ ./bin/execvp_wredirect
shell> ls -al tmp.txt
ls: cannot access 'tmp.txt': No such file or directory
shell> cat dog.txt
my dog has fleas
shell> cat dog.txt > tmp.txt
shell> ls -al tmp.txt
-rw-r--r-- 1 david david 17 Feb 25 01:52 tmp.txt
shell> cat < tmp.txt
my dog has fleas
shell> quit

Let me know if this solves the error issue. The only other creation issue would be you don't have write permission where you are attempting to create the file. If this doesn't solve the issue, please post all your code in a MCVE so I can insure that problems are not created in other areas of the code.


After Post of Your Complete Code

Your biggest issue was in your use of strtok_r and not removing the filename (or setting it NULL before calling execvp), and in using i + 1 instead of i in your assignment to in and out, e.g.

tok = strtok_r(rest, delim, &rest);
while(tok != NULL && i < MAX_TOKS) 
{
    toks[i] = tok;
    if(strcmp(tok, "<") == 0)
    {
        in = i;
        i--;
    }
    else if(strcmp(tok, ">")==0)
    {
        out = i;
        i--;
    }
    i++;
    tok = strtok_r(NULL, delim, &rest);
}

When you used i + 1, you set the index for tok[in] or tok[out] to one past the filename prompting the Bad Address error. It's one of those Doah! (or "id10t") errors... (rewrite the quote all-caps)

Further, before your call to execvp you must set tok[in] or tok[out] to NULL as you have removed the < and > and the file descriptor has already been duped, e.g.

            dup2(fd0, 0);
            close(fd0);
            toks[in] = NULL;

and

            dup2(fd1, 1);
            close(fd1);
            toks[out] = NULL;

You had also forgotten to reset your loop variables, e.g.

while (1) 
{
    in = out = 0;       /* always reset loop variables */
    for (int i = 0; i < MAX_TOKS; i++)
        toks[i] = NULL; /* and NULL all pointers */

Cleaning what you had up a bit, you could do something like the following:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>  /* missing headers */
#include <sys/wait.h>

#define MAX_BUF 160
#define MAX_TOKS 100

int main(int argc, char **argv) 
{
    char *delim = " \n";    /* delimiters for strtok_r (including \n) */
//     char *pos;           /* no longer used */
    char *tok;
    char *path;
    char s[MAX_BUF];
    char *toks[MAX_TOKS];
    time_t rawtime;
    struct tm *timeinfo;
    static const char prompt[] = "msh> ";
    FILE *infile;
    int in;
    int out;
//     int fd0;     /* unused and shadowed declarations below */
//     int fd1;     /* always compile with -Wshadow */
    in = 0;
    out = 0;

   /* 
    * process command line options
    */

    if (argc > 2) {
        fprintf(stderr, "msh: usage: msh [file]\n");
        exit(EXIT_FAILURE);
    }
    if (argc == 2) {
        /* read from script supplied on the command line */
        infile = fopen(argv[1], "r");
        if (infile == NULL) {
            fprintf(stderr, "msh: cannot open script '%s'.\n", argv[1]);
            exit(EXIT_FAILURE);
        }
    } else {
        infile = stdin;
    }

    while (1) 
    {
        in = out = 0;       /* always reset loop variables */
        for (int i = 0; i < MAX_TOKS; i++)
            toks[i] = NULL;

        // prompt for input, if interactive input
        if (infile == stdin) {
            printf(prompt);
        }

    /*
     * read a line of input and break it into tokens 
     */

        // read input 
        char *status = fgets(s, MAX_BUF-1, infile);

        // exit if ^d or "exit" entered
        if (status == NULL || strcmp(s, "exit\n") == 0) {
            if (status == NULL && infile == stdin) {
                printf("\n");
            }
            exit(EXIT_SUCCESS);
        }


        // break input line into tokens 
        char *rest = s;
        int i = 0;

        tok = strtok_r(rest, delim, &rest);
        while(tok != NULL && i < MAX_TOKS) 
        {
            toks[i] = tok;
            if(strcmp(tok, "<") == 0)
            {
                in = i;     /* only i, not i + 1, you follow with i-- */
                i--;
            }
            else if(strcmp(tok, ">")==0)
            {
                out = i;    /* only i, not i + 1, you follow with i-- */
                i--;
            }
            i++;
            tok = strtok_r(NULL, delim, &rest);
        }

        if (i == MAX_TOKS) {
            fprintf(stderr, "msh: too many tokens");
            exit(EXIT_FAILURE);
        }
        toks[i] = NULL;

    /*
     * process a command
     */

        // do nothing if no tokens found in input
        if (i == 0) {
            continue;
        }

        // if a shell built-in command, then run it 
        if (strcmp(toks[0], "help") == 0) {
            // help 
            printf("enter a Linux command, or 'exit' to quit\n");
            continue;
        } 
        if (strcmp(toks[0], "today") == 0) {
            // today
            time(&rawtime);
            timeinfo = localtime(&rawtime);
            printf("Current local time: %s", asctime(timeinfo));
            continue;
        }
        if (strcmp(toks[0], "cd") == 0) 
        {
            // cd 
            if (i == 1) {
                path = getenv("HOME");
            } else {
                path = toks[1];
            }
            int cd_status = chdir(path);
            if (cd_status != 0) 
            {
                switch(cd_status) 
                {
                    case ENOENT:
                        printf("msh: cd: '%s' does not exist\n", path);
                        break;
                    case ENOTDIR:
                        printf("msh: cd: '%s' not a directory\n", path);
                        break;
                    default:
                        printf("msh: cd: bad path\n");
                }
            }
            continue;
        }

        // not a built-in, so fork a process that will run the command
        pid_t rc = fork(), rcstatus;       /* use type pid_t, not int */
        if (rc < 0) 
        {
            fprintf(stderr, "msh: fork failed\n");
            exit(1);
        }
        if (rc == 0) 
        {
            if(in)
            {
                int fd0;
                if((fd0 = open(toks[in], O_RDONLY, 0)) == -1)
                {
                    perror(toks[in]);
                    exit(EXIT_FAILURE);
                }
                dup2(fd0, 0);
                close(fd0);
                toks[in] = NULL;
            }

            if(out)
            {
                int fd1;
                if((fd1 = open(toks[out], O_WRONLY | O_CREAT | O_TRUNC | O_CREAT, 
                    S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) == -1)
                { 
                    perror (toks[out]);
                    exit( EXIT_FAILURE);
                }
                dup2(fd1, 1);
                close(fd1);
                toks[out] = NULL;
            }

            // child process: run the command indicated by toks[0]
            execvp(toks[0], toks);
            /* if execvp returns than an error occurred */
            printf("msh: %s: %s\n", toks[0], strerror(errno));
            exit(1);
        } 
        else 
        {
            // parent process: wait for child to terminate
            while (wait (&rcstatus) != rc)
                continue;
        }
    }
}

You will need to verify there are no additional issues, but it certainly has no problems with cat file1 > file2.

David C. Rankin
  • 81,885
  • 6
  • 58
  • 85
  • I just updated my question to include my entire code. I looked at what you had provided earlier and tried that, but got errors. – Gray Feb 25 '18 at 08:02
  • I'll check your code, look at the example I just provided. – David C. Rankin Feb 25 '18 at 08:03
  • @Gray you are using `strtok` incorrectly. Only the **first** call to `strtok` uses the pointer -- eg `strtok_r(rest, " ", &rest))` all subsequent calls to `strtok` use `NULL`, e.g. `strtok_r(NULL, " ", &rest))` (and `rest` must be unchanged from the previous call) – David C. Rankin Feb 25 '18 at 08:09
  • @Gray - I cleaned up your code and found several issues that were preventing it from working (there are more you can clean up), but it now redirects. – David C. Rankin Feb 25 '18 at 09:15
  • They were subtle. Compiling with `-Wshadow` caught the `fd0, fd1` issue, but the rest, I added debug blocks and dumped what was in `toks` after the initial parse and immediately before `execvp` to find the other issues. The rest was clean up stuff, like `int` to `pid_t` and adding the header files (your compiler should have complained about implicit declaration of `wait` there). For learning, you were not in bad shape, but with larger programs, it shows how much more important it is to make sure all the small parts work `:)` – David C. Rankin Feb 25 '18 at 09:27
  • The first example code `while (args[idx]) { args[idx] = args[idx+2]; idx++; }` I don't get this part. `cat upper.c > tmp.txt` feels like args[2] = args[4] since there is no args[4] so, the final comand is like `cat upper.c null tmp.txt`? – jian Nov 22 '22 at 06:08
  • `else if (*args[idx] == '<' && args[idx+1]) { ...` what you are missing is the `execvp()` array is allways terminated by a `NULL` after the last token, see `for (int i = 0; i < ARGSIZE; i++) args[i] = NULL;`. So `idx + 2` is `NULL` not some uninitialized value. (also note, the way that works was cleaned up in the actual versions) – David C. Rankin Nov 22 '22 at 07:23
  • @DavidC.Rankin the last code example. I feel like I get most of it. But in ` if(out)` code block, I am not sure the reason to use `close(fd1)`. – jian Nov 22 '22 at 09:17
  • `in` and `out` are controlled by the block `if(strcmp(tok, "<") == 0) { ...` which just tells whether you are redirecting *from* or *to* a file. In the first case you have to `dup2` the `stdin` file descriptor and `stdout` for the second. The `if (in) ...` and `if (out) ...` blocks just handle those cases individually where `'<'` is set to read from the given file and `'>'` (`if (out) ...`) is set to write to the given file. The `close(fd1)` closes the open file descriptor you have duped (the file being read on `stdout`). – David C. Rankin Nov 22 '22 at 09:23
  • Also note: that part of the answer was after the OP posted his code, that was a clean-up of his code -- not that it is the optimal way to write it. It isn't bad to go through as you are to understand the file opening and duping of the descriptors and closing the original so your command in `execvp()` will either read from that file on `stdin` or write to that file on `stdout` (and `stderr` in the minimal example). You do this because shell-commands read and write to `stdin`/`stdout` (like redirecting the file list from `ls -al` to a file with `>`) – David C. Rankin Nov 22 '22 at 09:37