Search code examples
cunixpipe

Output of popen() is different from execute "sh -c cmdstring"


Description

I tested an example of APUE(Advanced Programming in the UNIX Environment, 3ed), 15.3 popen and plcose Function.
Example code is as below

#include "apue.h"
#include <sys/wait.h>

#define PAGER   "${PAGER:-more}" /* environment variable, or default */

int
main(int argc, char *argv[])
{
    char    line[MAXLINE];
    FILE    *fpin, *fpout;

    if (argc != 2)
        err_quit("usage: a.out <pathname>");
    if ((fpin = fopen(argv[1], "r")) == NULL)
        err_sys("can't open %s", argv[1]);

    if ((fpout = popen(PAGER, "w")) == NULL)
        err_sys("popen error");

    /* copy argv[1] to pager */
    while (fgets(line, MAXLINE, fpin) != NULL) {
        if (fputs(line, fpout) == EOF)
            err_sys("fputs error to pipe");
    }
    if (ferror(fpin))
        err_sys("fgets error");
    if (pclose(fpout) == -1)
        err_sys("pclose error");

    exit(0);
}

And my input file is "temp.in", we have

$ cat temp.in
test sentence

And the output of example is as below

$ ./popen2 temp.in 
test sentence

Problem

As showed in APUE, executing fpout = popen(cmdstring, "w") is similiar to its child process execute sh -c cmdstring. Thus the popen of code showed in the last section should execte

sh -c "${PAGER:-more} test sentence"

My OS(Ubuntu 22.04.3 LTS) doesn't have an environmental value named PAGER, thus it should execute

sh -c "more test sentence"

However, order more's option should be filename as far as I know. Test below order in my system and get the output, which is different from what I inferred above:

$ ${PAGER:-more} temp.in 
test sentence
$ ${PAGER:-more} test sentence
more: can't open test: no such file or directory
more: can't open sentence: no such file or directory

What's wrong with my deduce?


Solution

  • Well, the step by step working principle of popen() is as below(we ignore some irrelevant checks):
    First: the currency process call pipe() to create a pipe

    int pfd[2];  
    pipe(pfd);
    

    Second: the current process call fork() to create a child process.

    if (fork() == 0)
        /* child process work */
    else if(fork() > 0)
        /* parent process work */
    

    Third: the child and parent process close their own fileno to create an unidirectional pipe.
    e.d. popen(cmdstring, "r"), the parent process close its write fileno, and the child process close its read fileno.

    /* child process work */
        close(pfd[0]);  // pfd[0] is read port
    /* parent process work */
        close(pfd[1]);  // pfd[0] is write port
    

    Forth: The parent process and child process redirect their own fileno.
    e.d. popen(cmdstring, "r"), the parent redirect its STDIN_FILENO to read port, and the child process redirect its STDOUT_FILENO to write port.

    /* child process work */
        dup2(pfd[1], STDOUT_FILENO);  
        close(pfd[1]);  
    /* parent process work */
        dup2(pfd[0], STDIN_FILENO);  
        close(pfd[0]);
    

    Then, the child process could write its output to the pipe, and the parent process could read messages from its child process through its stdin.

    Back to the problem. popen({PAGER:-more}, "w") connect the currency process's stdout to more process's stdin.
    Thus fgets(line, MAXLINE, fpin) read from argv[1] and transfer the string to more through pipe, and more print it to the terminate. The effect of example is simmiliar to

    cat $argv1 | ${PAGER:-more}