通过 bash 输入重定向为多个 read(stdin) 调用提供输入

通过 bash 输入重定向为多个 read(stdin) 调用提供输入

假设我们有以下程序,它调用read()两次:

#include <stdio.h>
#include <unistd.h>

#define SIZE 0x100

int main(void)
{
    char buffer1[SIZE];
    char buffer2[SIZE];

    printf("Enter first input: \n");
    fflush(stdout);
    read(STDIN_FILENO, buffer1, SIZE);

    printf("Enter second input: \n");
    fflush(stdout);
    read(STDIN_FILENO, buffer2, SIZE);

    printf("\nFirst input:\n%s", buffer1);
    printf("\nSecond input:\n%s", buffer2);

    return 0;
}

当我们直接调用它时,我们可以输入1第一个输入和2第二个输入,以便打印:

First input:
1

Second input:
2

使用输入重定向时如何实现这一点?

以下方法不起作用,因为第一个方法read消耗了两个输入:

管道重定向:

$ { echo "1"; echo "2"; } | ./main_read
Enter first input:
Enter second input:

First input:
1
2

Second input:

定界符重定向:

$ ./main_read << EOF
1
2
EOF
Enter first input:
Enter second input:

First input:
1
2

Second input:

假设源代码无法更改,并且输入有时必须短于SIZE.

有什么方法可以通知第一个read()停止读取,以便第二个read()消耗剩余的输入?

答案1

这可能不会为您提供可接受的解决方案,但考虑到:

  • 源代码无法更改

  • shell 无法更改正在运行的程序的打开文件描述符指向的位置,也无法使正在运行的程序停止从文件描述符读取

您剩下的一些替代方案(除了利用竞争条件)是:

  • 尝试确保您的程序始终SIZE一次输入字节:

    {
      echo foo | dd bs=256 conv=sync
      echo bar | dd bs=256 conv=sync
    } 2>/dev/null | ./main_read
    

    输出:

    Enter first input: 
    Enter second input: 
    
    First input:
    foo
    
    Second input:
    bar
    

    这至少假设它SIZE小于管道缓冲区的大小。

  • 将程序的调用包装在expect(或等效的)脚本中:

    expect <<'EOT'
    spawn ./main_read
    expect "Enter first input:"
    send "foo\n"
    expect "Enter second input:"
    send "bar\n"
    expect eof
    EOT
    

    或者,以允许您将其他命令的输出通过管道传输给它的方式,单独读取(假设您的操作系统为进程提供文件描述符/dev/fd/n):

    echo foo | {
      echo bar |
        expect 4<&0 <<'EOT'
        spawn ./main_read
        set chan [open "/dev/fd/3"]
        gets $chan line
        expect "Enter first input:"
        send "$line\r"
        close $chan
        set chan [open "/dev/fd/4"]
        gets $chan line
        expect "Enter second input:"
        send "$line\r"
        close $chan
        expect eof
    EOT
    } 3<&0
    

    在这两种情况下,输出都是:

    spawn ./main_read
    Enter first input: 
    foo
    Enter second input: 
    bar
    
    First input:
    foo
    
    Second input:
    bar
    
  • 在允许以非阻塞方式打开管道的系统(例如 Linux)上,您可以使用 FIFO 使 shell 读取和写入您的程序。例如:

    makefifo fifo
    {
      exec 3<>fifo
      ./main_read 0<&3
    } |
    sh -c '
      # read a line from the pipe
      # read from a file or a different pipe, write to fifo
      # repeat ...
      # echo the output from the pipe
    ' mysh
    

    不过,如果expect你可以的话,我认为没有令人信服的理由去重新发明它。

但请注意,正如其他人指出的那样,没有保证您的所有程序read实际上都会获取SIZE字节。

答案2

假设源代码不能更改

你应该主要致力于改变这个假设。

通常无法保证read()调用返回多少字节当从常规文件读取时,它通常返回请求的字节数(最多有可用的字节数),但并非所有类型的文件描述符都是如此。系统上运行的进程之间的调度以及其他此类计时问题也可能会影响一次调用可用的数据量。

执行一次read()调用而不检查读取的数据量几乎总是错误的。即使像这样的情况dd(明确地应该公开调用的行为read())和从数据报套接字读取(每个人read()给出一条消息)也需要程序知道它获得了多少数据。

如果程序要读取行,则应该使用fgets()orgetline()而不是 raw read()。如果它应该读取其他类型的块,它应该实现一些其他方式来区分这些块。这可以是在它们前面加上一个长度,或者使用单独的文件描述符,或者使用一些分隔符(比如换行符,但它可能比一个字节长)。

也就是说,除非您安排stdin连接到数据报套接字,但这将是一个非常不寻常的设置,并且您无法真正使用常规输入重定向来提供数据。

另外,在将缓冲区传递给 之前printf("%s"),程序应确保它们包含终止字符串的 NUL 字节。现在,如果其中一个提供的数据不包含 NUL,程序会产生未定义的行为,包括第二个根本不返回数据的read()情况。read()

答案3

正如下面评论中所建议的,你可以这样做:

{ echo a & sleep 0.1; echo b; } | ./main

请注意,您可能需要调整睡眠时间。该命令的要点是使 read() 的第一次调用认为这a 是它获得的整个输入。这假设是 C 程序将在echo a &(注意&- 它被发送到后台)已经完成并由第一个 read() 处理后到达第二个 read()。但由于 Linux 是一个真正的多用户多任务操作系统,它还执行惰性虚拟内存分配,因此sleep 0.1 可能不足以在所有情况下使这一假设成立。

它起作用的原因和

{ echo a && echo b; } | ./main

唯一不同的是, read() 第一次读取时会读取整个可用的标准输入,最多可达 SIZE 个字符,第二次读取时不会留下任何字符。如果您检查了 read() 返回的值:

#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

#define SIZE 0x100

int main(void)
{
    char buffer1[SIZE];
    char buffer2[SIZE];

    printf("Enter first line of input: \n");

    ssize_t read_bytes = read(STDIN_FILENO, buffer1, SIZE);
    buffer1[read_bytes] = '\0';
    printf("First input - count of read bytes: %jd\n", (intmax_t) read_bytes);

    printf("Enter second line of input: \n");

    read_bytes = read(STDIN_FILENO, buffer2, SIZE);
    printf("Second input - count of read bytes: %jd\n", (intmax_t) read_bytes);
    buffer2[read_bytes] = '\0';

    printf("\nFirst input:\n%s", buffer1);
    printf("\nSecond input:\n%s", buffer2);

    return 0;
}

你会发现它第二次没有读取任何字符:

$ ./main << EOF
1
2
EOF
Enter first line of input:
First input - count of read bytes: 4

First input:
1
2
Enter second line of input:
Second input - count of read bytes: 0

Second input:

First input:
1
2

Second input:

要完成{ echo a && echo b; } | ./main工作,您必须切换到 getline() 或将两个输入保存到单个缓冲区并使用 strtok() 通过换行符解析缓冲区。 getline() 版本可能如下所示:

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

int main(void)
{
    size_t size = 0x100;
    char *buffer1 = malloc(size);
    if (buffer1 == NULL)
        {
            perror("malloc");
        }


    char *buffer2 = malloc(size);
    if (buffer2 == NULL)
        {
            perror("malloc");
        }

    printf("Enter first line of input: \n");
    getline(&buffer1, &size, stdin);

    printf("Enter second line of input: \n");
    getline(&buffer2, &size, stdin);

    printf("\nFirst input:\n%s", buffer1);
    printf("\nSecond input:\n%s", buffer2);

    free(buffer1);
    free(buffer2);

    return 0;
}

例子:

$ ./main << EOF
1
2
EOF
Enter first line of input:
Enter second line of input:

First input:
1

Second input:
2

我在这里还想讨论3点:

  • 你不需要,fflush(stdout);因为标准输出总是在换行符后刷新

  • 你不需要在互联网上查找手册页,因为你本地就有它们 - 只需man 2 read在终端中输入或在编辑器中打开它们(例如 Emacs 就可以做到这一点)

  • 您在问题中发布的代码中有一个错误 -read() 不会自动添加 nul 字节,您必须自己执行此操作以避免 UB。它应该是:

      printf("Enter first line of input: \n");
      ssize_t read_bytes = read(STDIN_FILENO, buffer1, SIZE - 1);
      buffer1[read_bytes] = '\0';
    

相关内容