在 C 中创建 shell。我将如何实现输入和输出重定向?

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

我正在用 C 语言创建一个 shell,我需要帮助来实现输入和输出重定向。

当我尝试使用“>”创建文件时,我收到一条错误消息,指出该文件不存在。当我尝试做类似 ls > test.txt; 的事情时它不会创建新文件。

我根据提供给我的建议更新了代码,但现在出现了不同的错误。但是,仍未为输出重定向创建新文件。

这是我的完整代码:

#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 = '[=11=]';
   }

   // 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);
    }
  }
}

乍一看,除了您的 closedup2 在您的 toks[in] 案例中出现问题外,没有任何显而易见的东西可以解释您为什么不创建重定向时的输出文件(例如 cat somefile > newfile)。但是,有许多细节您没有检查。

例如,在调用 dup2close 之前,您需要检查对 open 的调用是否成功。 (否则,您正试图重定向未打开的 file-descriptor)。简单的基本检查就可以了,例如

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);
}

(注意: 我调整了将文件写入 0644 的权限(用户 'rw'、组 'r' 和世界'r'。你还应该检查 dup2 的 returns 和迂腐的 close)

更大的问题在于如何在调用 execvp 之前重新排列 toks。您使用 dup2 或管道的原因是 exec.. 函数无法处理重定向(例如,它不知道如何处理 '>''<')。因此,您通过将文件重定向到输入案例上的 stdin 或将 stdout (and/or stderr) 重定向到文件来手动处理输入或输出到文件的重定向在输出情况下。在任何一种情况下,您都必须在调用 execvp 之前从 toks 中删除 < filename> filename 标记,否则您将收到错误消息。

如果您确保将 toks 中的每个指针设置为 NULL 并且您读取的内容不超过 MAXTOKS - 1(根据需要保留终止 NULL),那么您可以迭代 toks 移动指针以确保您不会将 < >filename 发送到 execvp。在索引 i 处的 toks 中找到 <> 并确保有一个 toks[i+1] 文件名后,类似于:

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

然后将 toks 传递给 execvp 将不会产生错误(我怀疑是您遇到的错误)

还有一个 corner-case 您应该注意的问题。如果您的可执行文件有任何已注册的对 atexit 或其他析构函数的调用,则这些引用不是您对 execvp 的调用的一部分。因此,如果对 execvp 的调用失败,则无法调用 exit(它可以在对任何 post-exit 函数的调用中调用未定义的行为),因此正确的调用是 _exit它不会尝试任何此类调用。

工作重定向的最低限度类似于以下内容。不是下面没有解决解析和重定向的许多其他方面,但是对于您的基本文件创建问题,它提供了一个框架,例如

#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  */
}

示例Use/Output

最低限度地工作 > 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

让我知道这是否解决了错误问题。唯一的其他创建问题是您在尝试创建文件时没有写权限。如果这不能解决问题,请 post MCVE 中的所有代码,这样我可以确保代码的其他区域不会产生问题。


在您的完整代码Post之后

您最大的问题是您使用了 strtok_r 而没有删除文件名(或在调用 execvp 之前将其设置为 NULL),并且使用了 i + 1分配给 inouti,例如

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);
}

当您使用 i + 1 时,您将 tok[in]tok[out] 的索引设置为 文件名后的索引 提示 Bad Address 错误。这是其中一个 Doah!(或 "id10t")错误...(重写引用 all-caps)

此外,在调用 execvp 之前,您必须将 tok[in]tok[out] 设置为 NULL,因为您已经删除了 <> 并且文件描述符已经被欺骗,例如

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

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

您还忘记了重置循环变量,例如

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

稍微清理一下,您可以执行以下操作:

#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;
        }
    }
}

您需要确认没有其他问题,但 cat file1 > file2.

肯定没有问题