Shell 重定向

2020-05-21T11:35:17+08:00

Shell 重定向是 POSIX Shell 的一个功能,作为 Shell 语法的一部分。要理解它是如何工作的就需要了解 Unix 系统编程,包括进程、fork(2)、文件描述符、open(2) 和 dup(2)。稍后依次演示以下命令是如何工作的:

ls > dirlist 重定向标准输出(stdout)到文件

ls > dirlist 等于 ls 1> dirlist,其中:

要实现 1> dirlist 就是把文件描述符 1 和文件 dirlist 绑定起来,有两种方法做到这一点,第一种方法利用 SUSv3/POSIX.1-2001 对 open(2) 返回值的规定:

Upon successful completion, the function shall open the file and return a non-negative integer representing the lowest numbered unused file descriptor.

所以首先关闭文件描述符 1,然后打开 dirlist 所得到的文件描述符必然就是 1:

/* ls-1.c --- 实现 ls > dirlist,法一 */
#include <stdio.h>              /* printf */
#include <unistd.h>             /* execlp */
#include <stdlib.h>             /* exit */
#include <fcntl.h>              /* open */
#include <sys/stat.h>           /* S_IRUSR */

int
main(void)
{
    if (close(STDOUT_FILENO) == -1) {
        perror("close stdout");
        exit(EXIT_FAILURE);
    }

    int fd = open("dirlist", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open dirlist");
        exit(EXIT_FAILURE);
    }

    if (fd != STDOUT_FILENO) {
        fprintf(stderr, "ERROR: got fd %d, want 1\n", fd);
        exit(EXIT_FAILURE);
    }

    execlp("ls", "ls", NULL);
    perror("execlp ls");
    exit(EXIT_FAILURE);
}

这个方法的问题是 SUSv4/POSIX.1-2008、macOS、Linux 对 open(2) 的返回值均没有像 SUSv3 那样规定,所以或许依赖它不太靠谱。第二种(更靠谱)方法是 dup2(2):

/* ls-2.c --- 实现 ls > dirlist,法二 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int
main(void)
{
    int fd = open("dirlist", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open dirlist");
        exit(EXIT_FAILURE);
    }

    if (dup2(fd, STDOUT_FILENO) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    execlp("ls", "ls", NULL);
    perror("execlp ls");
    exit(EXIT_FAILURE);
}

int dup2(int oldfd, int newfd); 会复制一份文件描述符 oldfd,并这个新的文件描述符会使用数字 newfd,大致等于 close(newfd); dup(oldfd)

tr a-z A-Z < dirlist 重定向文件到标准输入(stdin)

tr a-z A-Z < dirlist 等于 tr a-z A-Z 0< dirlist,即用文件描述符 0 以可读模式打开 dirlist,可用 dup2(2) 实现:

/* tr.c --- 实现 tr a-z A-Z < dirlist */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int
main(void)
{
    int fd = open("dirlist", O_RDONLY | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open dirlist");
        exit(EXIT_FAILURE);
    }

    if (dup2(fd, STDIN_FILENO) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    execlp("tr", "tr", "a-z", "A-Z", NULL);
    perror("execlp tr");
    exit(EXIT_FAILURE);
}

date >> output 重定向标准输出(stdout)到文件结尾

date >> output 等于 date 1>> output,其中 >> 表示以 appending 方式重定向输出,而不是先清空 output,相当于 open(2) 使用了 O_APPEND 模式

/* date.c --- 实现 date >> output */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int
main(void)
{
    int fd = open("output", O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open output");
        exit(EXIT_FAILURE);
    }

    if (dup2(fd, STDOUT_FILENO) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    execlp("date", "date", NULL);
    perror("execlp date");
    exit(EXIT_FAILURE);
}

echo “stdout to stderr” >&2 重定向标准输出(stdout)到标准错误(stderr)

>&2 等于 1>&2,其中:

/* echo.c --- 实现 echo "stdout to stderr" >&2 */
#include <stdio.h>              /* perror */
#include <stdlib.h>             /* exit */
#include <unistd.h>             /* execlp */

int
main(void)
{
    if (dup2(STDERR_FILENO, STDOUT_FILENO) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    execlp("echo", "echo", "stdout to stderr", NULL);
    perror("execlp echo");
    exit(EXIT_FAILURE);
}

cmd > x 2>&1 对比 cmd 2>&1 > x

> x 表示重定向 fd 1 到文件 x,2>&1 表示重定向 fd 2 到 fd 1。所以这两个命令不同在于重定向的顺序,而 SUSv4 规定:

If more than one redirection operator is specified with a command, the order of evaluation is from beginning to end.

int x = open("x", ...);

/* cmd > x 2>&1 */

dup2(x, 1); // 1 是 x 的 copy
dup2(1, 2); // 2 是 1 的 copy,所以 x, 1, 2 指向同一个文件

/* cmd 2>&1 > x */

dup2(1, 2); // 2 是 1 的 copy,所以 2 指向 /dev/tty
dup2(x, 1); // 1 是 x 的 copy,所以 x, 1 指向同一个文件,2 依旧指向 /dev/tty
/* cmd-1.c --- 实现 cmd > x 2>&1*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int
main(void)
{
    int x = open("x", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
    if (x == -1) {
        perror("open dirlist");
        exit(EXIT_FAILURE);
    }

    /* 1 > x */
    if (dup2(x, 1) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    /* 2 >& 1 */
    if (dup2(1, 2) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    execlp("cmd", "cmd", NULL);
    perror("execlp cmd");
    exit(EXIT_FAILURE);
}
/* cmd-2.c --- 实现 cmd 2>&1 > 1 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int
main(void)
{
    int x = open("x", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
    if (x == -1) {
        perror("open dirlist");
        exit(EXIT_FAILURE);
    }

    /* 2 >& 1 */
    if (dup2(1, 2) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    /* 1 > x */
    if (dup2(x, 1) == -1) {
        perror("dup2");
        exit(EXIT_FAILURE);
    }

    execlp("cmd", "cmd", NULL);
    perror("execlp cmd");
    exit(EXIT_FAILURE);
}