Hello bash team! I found a bug in my beloved bash and hope that you can fix it! Everything else is described in the attached bashbug.txt
Cheers, Justus
From: justus (justuskohl at gmail.com) To: bug-bash@gnu.org Subject: Backticked, nested command will steal piped stdin Configuration Information [Automatically generated, do not change]: Machine: x86_64 OS: linux-gnu Compiler: gcc Compilation CFLAGS: -march=x86-64 -mtune=generic -O2 -pipe -fno-plt -fexceptions -Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security -fstack-clash-protection -fcf-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -g -ffile-prefix-map=/build/bash/src=/usr/src/debug/bash -flto=auto -DDEFAULT_PATH_VALUE='/usr/local/sbin:/usr/local/bin:/usr/bin' -DSTANDARD_UTILS_PATH='/usr/bin' -DSYS_BASHRC='/etc/bash.bashrc' -DSYS_BASH_LOGOUT='/etc/bash.bash_logout' -DNON_INTERACTIVE_LOGIN_SHELLS uname output: Linux archkohlio 6.9.10-arch1-1 #1 SMP PREEMPT_DYNAMIC Thu, 18 Jul 2024 18:06:13 +0000 x86_64 GNU/Linux Machine Type: x86_64-pc-linux-gnu Bash Version: 5.2 Patch Level: 26 Release Status: release Description: Original problem: $ seq 3 | head -n $(calc -dp "1+1") $ seq 3 | head -n 2 # expected behaviour should print "1 \n 2 \n" instead calls: $ head -n $(seq 3 | calc -dp "1+1") $ head -n 2 1 2 3 # actual behaviour, tries opening files 1,2,3 $(calc -p) manages to steal the piped stdin from head! This is not a problem with apcalc, I couldn't manage to reproduce the expected behaviour from steal.cpp at all ... $ seq 3 | echo $(tee out.txt) tee steals seq 3 in this case as well, instead of reading from the terminal $ echo $(tee out.txt) # like so Maybe I am ignorant and this is actually the expected behaviour, but I doubt it. Also, I am not sure if this should be fixed at all, as bash is at the core of so many machines and a fix for this might break some scripts in a subtle way; but that's not for me to decide. Neither `backticks` nor $(eval calc) make a difference in any case. Both bash and sh let the $(inner process) steal piped stdin. zsh and fish act as expected, some other shells I tested can't manage $() properly due to "2\n" as arg to head etc. It looks like the pipe is passed to the first process that is running, instead of the "correct" one. Maybe this is even intended ...? fish and zsh save themselves by (incorrectly) asserting that steal.cpp is getting stdin input from a tty ( isatty(stdin) == true ) Repeat-By: Attached below are bash_bug.sh (running bash, sh, zsh, fish) with the line: $ bash -c 'seq 3 | ./print_args -n $(./steal arg)' as well as two short C++ programs print_args.cpp and steal.cpp, reading command line arguments as well as piped stdin input and printing them to stdout as a demonstration. Most programs I tested in place of ./steal will also take the output of "seq 3", with the exception of calc (without -p flag). print_args.cpp and steal.cpp are different files for clarity, although "bash -c 'seq 3 | ./print_args $(./print_args)'" does the same trick -------#######------- bash_bug.sh -------#######------- #!/bin/bash echo " -------------- Broken behaviour, stealing 'seq 3 |' --------------------" echo 'seq 3 | ./print_args -n $(./steal arg)' echo -------------------------------- bash ---------------------------------- bash -c 'seq 3 | ./print_args -n $(./steal arg)' echo --------------------------------- sh ----------------------------------- sh -c 'seq 3 | ./print_args -n $(./steal arg)' echo -------------------------------- fish ---------------------------------- fish -c 'seq 3 | ./print_args -n $(./steal arg)' # works echo --------------------------------- zsh ---------------------------------- zsh -c 'seq 3 | ./print_args -n $(./steal arg)' # works echo " ---------- Expected behaviour, including /dev/null fix ----------------" echo 'seq 3 | ./print_args -n $(./steal arg </dev/null)' echo -------------------------------- bash ---------------------------------- bash -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)' echo --------------------------------- sh ----------------------------------- sh -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)' echo -------------------------------- fish ---------------------------------- fish -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)' echo --------------------------------- zsh ---------------------------------- zsh -c 'seq 3 | ./print_args -n $(./steal arg </dev/null)' # Various user side fixes # $ bash -c 'seq 3 | head -n $(calc -dp 2 </dev/null)' # $ bash -c 'seq 3 | head -n $(echo -n "" | calc -dp 2)' # $ bash -c 'seq 3 | head -n $(seq 0 | calc -dp 2)' # $ bash -c 'seq 3 | head -n $(calc -dp "reinitialize()" 2)' # without the calc -p flag , everything works as expected, whyever # $ bash -c 'seq 3 | head -n $(calc -d 2)' -------#######------- print_args.cpp -------#######------- #include <sstream> #include <stdio.h> #include <unistd.h> #include <sys/stat.h> int main(int argc, char *argv[]) { /* Output command line args */ std::stringstream arg_stream; arg_stream << "Command line args:\n"; for(int i = 0; i < argc; ++i) arg_stream << " " << i << ": " << argv[i] << "\n"; printf(arg_stream.str().c_str()); /* Read->output if input comes from a pipe */ bool stdin_pipe = false; if (not isatty(fileno(stdin))) { struct stat stats; int r = fstat(fileno(stdin), &stats); if (r < 0) printf("print_args:fstat failed!\n"); else if (S_ISFIFO(stats.st_mode)) stdin_pipe = true; } else { printf("print_args:stdin is a tty!\n"); } if (not stdin_pipe) { printf("print_args:stdin is not a pipe!\n"); return 0; } /* stdin is a pipe! just cat it to stdout */ printf("Reading from pipe:\n+"); /* Both versions (fgetc and fread) produce identical results */ #if 0 while((feof(stdin) == 0) && (ferror(stdin) == 0)) { char ch = fgetc(stdin); if (ch == EOF) break; else putchar(ch); } #else const size_t BUF_SIZE = 1; /* no rush */ char buffer[BUF_SIZE]; size_t read_bytes; while( (read_bytes = fread(buffer, 1, BUF_SIZE, stdin)) == BUF_SIZE ) for(int i = 0; i < read_bytes; ++i) putchar(buffer[i]); for(int i = 0; i < read_bytes; ++i) putchar(buffer[i]); #endif putchar('+'); if (feof(stdin) != 0) printf("\nEnd of stdin reached!\n"); if (ferror(stdin) != 0) printf("\nERROR encountered for stdin!\n"); return 0; } -------#######------- steal.cpp -------#######------- #include <sstream> #include <stdio.h> #include <unistd.h> #include <sys/stat.h> int main(int argc, char *argv[]) { /* Output command line args */ std::stringstream arg_stream; for(int i = 1; i < argc; ++i) arg_stream << argv[i] << "\n"; printf(arg_stream.str().c_str()); /* Read->output if input comes from a pipe */ bool stdin_pipe = false; if (not isatty(fileno(stdin))) { struct stat stats; int r = fstat(fileno(stdin), &stats); if (r < 0) printf("steal:fstat_failed!\n"); else if (S_ISFIFO(stats.st_mode)) stdin_pipe = true; } else { printf("steal:stdin_is_a_tty!\n"); } if (not stdin_pipe) { printf("steal:stdin_is_not_a_pipe!\n"); return 0; } /* stdin is a pipe! just cat it to stdout */ while((feof(stdin) == 0) && (ferror(stdin) == 0)) { char ch = fgetc(stdin); if (ch == EOF) break; else putchar(ch); } return 0; } Fix: I guess pipe() - fork() - dup2(.. STDIN/OUT_FILENO) - execvp() would have to be shuffled around somewhat, I didn't read too deep into the bash sources. Looking forward to a reply whether this is even worthy a fix, expected behaviour, or actually a bug. Take care!