简体   繁体   English

用C创建外壳。如何实现输入和输出重定向?

[英]Creating a shell in C. How would I implement input and output redirection?

I'm creating a shell in C, and I need help implementing input and output redirection. 我正在用C创建外壳,我需要帮助来实现输入和输出重定向。

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; 当我尝试做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);
    }
  }
}

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 (eg cat somefile > newfile ). 乍一看,除了closedup2toks[in]出现问题外,没有任何明显的东西可以解释为什么在重定向时不创建输出文件(例如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 . 例如,您需要在调用dup2close之前检查每种情况下的open调用是否成功。 (otherwise, you are attempting to redirect a file-descriptor that is not open). (否则,您尝试重定向未打开的文件描述符)。 Simple basic checking will do, eg 简单的基本检查就可以了,例如

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 ) 注意:我已经调整了将文件写为0644的权限(用户'rw' ,组'r''r' 。您还应该检查dup2的返回值,并且在学究的情况下请close

The bigger issues come in how you rearrange toks before your call to execvp . 更大的问题在于如何在调用execvp之前重新排列toks The reason you use dup2 or a pipe is that the exec.. function cannot handle redirection (eg it doesn't know what to do with '>' or '<' ). 使用dup2或管道的原因是exec..函数无法处理重定向(例如,它不知道如何处理'>''<' )。 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. 因此,通过将文件重定向到输入案例上的stdin或将stdout (和/或stderr )重定向到输出案例上的文件,您正在手动处理输入或输出到文件的重定向。 In either case, you must remove the < filename or > filename tokens from toks before calling execvp or you will get an error. 无论哪种情况,都必须在调用execvp之前从toks删除< filename> filename令牌, toks会出现错误。

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 . 如果确保将toks每个指针都toksNULL并且读取的内容不超过MAXTOKS - 1 (根据需要保留终止NULL ),则可以遍历toks移动指针,以确保不将< >filename发送给execvp After you find < or > in toks at index i and insure there is a toks[i+1] filename, something like: 在索引i toks中找到<> ,并确保有toks[i+1]文件名后,类似:

            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) 然后将toks传递给execvp不会生成错误(我怀疑这是您遇到的问题)

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 . 如果您的可执行文件具有对atexit或其他析构函数的任何已注册调用,则这些引用不是您对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. 因此,如果对execvp的调用失败,则无法调用exit (它可以在对任何后退出函数的调用中调用未定义的行为),因此正确的调用是_exit ,它将不尝试任何此类调用。

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, eg 解析和重定向没有很多其他方面在下面没有解决,但是对于您的基本文件创建问题,它提供了一个框架,例如

#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 , 最少使用> filename< 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. 如果这样做不能解决问题,请在MCVE中发布所有代码,以便确保在代码的其他区域不会出现问题。


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 , eg 最大的问题是使用strtok_r而不删除文件名(或在调用execvp之前将其设置为NULL ),以及在分配inout使用i + 1而不是i ,例如

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. 使用i + 1 ,将tok[in]tok[out]的索引设置为文件名后一个,提示出现Bad Address错误。 It's one of those Doah! 这就是这些多哈之一! (or "id10t") errors... (rewrite the quote all-caps) (或“ id10t”)错误...(改写全大写的引号)

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, eg 此外,在调用execvp之前,必须将tok[in]tok[out]NULL因为您已删除<>并且文件描述符已被复制,例如

            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, eg 您还忘记了重置循环变量,例如

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 . 您将需要验证没有其他问题,但是cat file1 > file2当然没有问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM