(I'm not sure if I chose the right wording in the heading so correct me if it sounds wrong, same about tags)
I'm writing a shell implementation which must be able to carry out piped commands. Here's a simplified piece of code which executes ls | wc -l
(wc
can be replaced with sleep
or date
):
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t family[2];
int pipefd[2];
extern char **environ;
void ft_exec(int i)
{
char **ls = malloc(sizeof(char *) * 2);
ls[0] = "/bin/ls";
ls[1] = NULL;
char **wc = malloc(sizeof(char *) * 3);
wc[0] = "/usr/bin/wc";
wc[1] = "-l";
wc[2] = NULL;
printf("ENTERED BY %d\n", getpid());
if (i == 0)
{
dup2(pipefd[1], 1);
close(pipefd[0]);
close(pipefd[1]);
execve(ls[0], ls, environ);
}
if (i == 1)
{
dup2(pipefd[0], 0);
close(pipefd[0]);
close(pipefd[1]);
execve(wc[0], wc, environ);
}
}
void ft_block_main_process(void)
{
int i;
pid_t terminated;
int status;
i = 0;
while (i < 2)
{
terminated = waitpid(-1, &status, 0);
printf("%d TERMINATED; WEXITSTATUS:%d, WTERMSIG:%d, STATUS:%d\n", terminated, WEXITSTATUS(status), WTERMSIG(status), status);
if (terminated == family[0])
close(pipefd[1]);
else if (terminated == family[1])
close(pipefd[0]);
i++;
}
}
void ft_interpret(void)
{
int i;
i = 0;
while (i < 2)
{
family[i] = fork();
if (family[i] == 0)
ft_exec(i);
i++;
}
ft_block_main_process();
}
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
pipe(pipefd);
ft_interpret();
}
The output is as follows:
ENTERED BY 1511
ENTERED BY 1512
1511 TERMINATED; WEXITSTATUS:0, WTERMSIG:0, STATUS:0
3
1512 TERMINATED; WEXITSTATUS:0, WTERMSIG:0, STATUS:0
which is correct.
However, if I try changing the second command to, say, printenv
, it blows up:
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
pid_t family[2];
int pipefd[2];
extern char **environ;
void ft_exec(int i)
{
char **ls = malloc(sizeof(char *) * 2);
ls[0] = "/bin/ls";
ls[1] = NULL;
char **printenv = malloc(sizeof(char *) * 2);
printenv[0] = "/usr/bin/printenv";
printenv[1] = NULL;
printf("ENTERED BY %d\n", getpid());
if (i == 0)
{
dup2(pipefd[1], 1);
close(pipefd[0]);
close(pipefd[1]);
execve(ls[0], ls, environ);
}
if (i == 1)
{
dup2(pipefd[0], 0);
close(pipefd[0]);
close(pipefd[1]);
execve(printenv[0], printenv, environ);
}
}
void ft_block_main_process(void)
{
int i;
pid_t terminated;
int status;
i = 0;
while (i < 2)
{
terminated = waitpid(-1, &status, 0);
printf("%d TERMINATED; WEXITSTATUS:%d, WTERMSIG:%d, STATUS:%d\n", terminated, WEXITSTATUS(status), WTERMSIG(status), status);
if (terminated == family[0])
close(pipefd[1]);
else if (terminated == family[1])
close(pipefd[0]);
i++;
}
}
void ft_interpret(void)
{
int i;
i = 0;
while (i < 2)
{
family[i] = fork();
if (family[i] == 0)
ft_exec(i);
i++;
}
ft_block_main_process();
}
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
pipe(pipefd);
ft_interpret();
}
The output is:
ENTERED BY 1937
ENTERED BY 1938
TMPDIR=/var/folders/zz/zyxvpxvq6csfxvn_n000ccj800334_/T/
<...> // environ stuff
_=/Users/aisraely/testing/./a.out
1938 TERMINATED; WEXITSTATUS:0, WTERMSIG:0, STATUS:0
1937 TERMINATED; WEXITSTATUS:0, WTERMSIG:13, STATUS:13
Now if the same is run in bash
, it exits with 0:
bash-3.2$ ls | printenv
TERM_PROGRAM=iTerm.app
<...> // environ stuff
_=/usr/bin/printenv
bash-3.2$ echo $?
0
I do realize that, according to the manpage of pipe()
, writing on a widowed pipe forces the writing process to terminate by receiving a SIGPIPE (whose value is properly extracted by the WTERMSIG()
macro), and that printenv
exits earlier, ls
tries to write to it but sees no reader and catches SIGPIPE. But why in some cases this is not true? Why my executing program doesn't exit with a 0 when it should? Is it because of ls
?
I think this is normal behavior. The exit status of the shell pipeline is usually the exit status of the rightmost command on the pipeline.
The GNU Bash shell has a pipefail
option that can be enabled. When enabled, the exit status of the pipeline is the exit status of the rightmost command that exited with a non-zero exit status.
For example:
bash$ set -o pipefail # turn pipefail option on
bash$ seq 0 1000000 | date
Thu Aug 26 14:49:51 UTC 2021
bash$ echo $?
141
bash$ set +o pipefail # turn pipefail option off
bash$ seq 0 1000000 | date
Thu Aug 26 14:50:10 UTC 2021
bash$ echo $?
0
bash$
The exit status 141
is due to the seq
command being terminated by a SIGPIPE
signal.