name: "Compound commands: coproc" # NOTE: Coproc tests must defend against TWO race conditions caused by bash's # SIGCHLD handler: # # 1. The handler clears the COPROC array when the coproc child exits, so # ${COPROC[0]} can become empty before the parent captures it. # 2. The handler ALSO closes the underlying pipe FDs, so even after capturing # the integer FD, reading from it returns EOF immediately. # # We defend against both by: # (a) Having the coproc body block on `read -r _ < _sync` first, so it can't # exit before the parent dups the FD (no SIGCHLD can fire yet). # (b) Using `exec 9<&${COPROC[0]}` to dup the pipe to a fresh FD that bash # does NOT manage as part of the coproc, so it survives SIGCHLD cleanup. # (c) Then `echo x > _sync` releases the coproc to do its real work. # # We use explicit FD 9 (rather than `{fd}<&...` auto-assignment) for portability. cases: # # Basic invocation # - name: "Unnamed coproc with brace group" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; echo "hello from coproc"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Unnamed coproc with subshell" test_files: - path: "test.sh" contents: | mkfifo _sync coproc ( read -r _ < _sync; echo "hello from subshell coproc" ) exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Named coproc with brace group" test_files: - path: "test.sh" contents: | mkfifo _sync coproc MYCOPROC { read -r _ < _sync; echo "named coproc"; } exec 9<&${MYCOPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Named coproc with subshell" test_files: - path: "test.sh" contents: | mkfifo _sync coproc MYSUB ( read -r _ < _sync; echo "named subshell coproc" ) exec 9<&${MYSUB[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc with simple command" skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O test_files: - path: "test.sh" contents: | coproc cat rfd=${COPROC[0]} wfd=${COPROC[1]} echo "input line" >&$wfd exec {wfd}>&- read -r line <&$rfd echo "got: $line" wait args: ["./test.sh"] - name: "Coproc with multi-statement brace group" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo "line1" echo "line2" echo "line3" } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] # # COPROC and COPROC_PID variable validation via functional use # - name: "COPROC array read FD is functional" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; echo "via read fd"; } exec 9<&${COPROC[0]} echo x > _sync read -r line <&9 echo "got: $line" wait args: ["./test.sh"] - name: "Named coproc FDs are functional" test_files: - path: "test.sh" contents: | mkfifo _sync coproc MYPROC { read -r _ < _sync; echo "named fd test"; } exec 9<&${MYPROC[0]} echo x > _sync read -r line <&9 echo "got: $line" wait args: ["./test.sh"] # # Reading from coproc stdout # - name: "Read coproc output with cat" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; echo "output from coproc"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Read coproc output with read builtin" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; echo "readline test"; } exec 9<&${COPROC[0]} echo x > _sync read -r line <&9 echo "got: $line" wait args: ["./test.sh"] - name: "Read multiple lines from coproc" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo "first" echo "second" echo "third" } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Read multiple lines with read builtin" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo "line1" echo "line2" } exec 9<&${COPROC[0]} echo x > _sync { read -r first read -r second } <&9 echo "first=$first" echo "second=$second" wait args: ["./test.sh"] - name: "Named coproc read" test_files: - path: "test.sh" contents: | mkfifo _sync coproc READER { read -r _ < _sync; echo "from named"; } exec 9<&${READER[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] # # Writing to coproc stdin # - name: "Write to coproc stdin and read back" skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O test_files: - path: "test.sh" contents: | coproc cat rfd=${COPROC[0]} wfd=${COPROC[1]} echo "hello coproc" >&$wfd exec {wfd}>&- read -r line <&$rfd echo "got: $line" wait args: ["./test.sh"] - name: "Bidirectional communication" skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O test_files: - path: "test.sh" contents: | coproc { while read -r line; do echo "processed: $line" done } rfd=${COPROC[0]} wfd=${COPROC[1]} echo "msg1" >&$wfd echo "msg2" >&$wfd exec {wfd}>&- { read -r out1 read -r out2 } <&$rfd echo "$out1" echo "$out2" wait args: ["./test.sh"] - name: "Named coproc bidirectional" skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O test_files: - path: "test.sh" contents: | coproc BIDIR { while read -r line; do echo "echo: $line" done } rfd=${BIDIR[0]} wfd=${BIDIR[1]} echo "test1" >&$wfd echo "test2" >&$wfd exec {wfd}>&- { read -r out1 read -r out2 } <&$rfd echo "$out1" echo "$out2" wait args: ["./test.sh"] - name: "Close write FD to signal EOF" skip: true # TODO(coproc): hangs - wait never completes after bidirectional coproc I/O test_files: - path: "test.sh" contents: | coproc { count=0 while read -r line; do count=$((count + 1)) done echo "read $count lines" } rfd=${COPROC[0]} wfd=${COPROC[1]} echo "a" >&$wfd echo "b" >&$wfd echo "c" >&$wfd exec {wfd}>&- read -r result <&$rfd echo "$result" wait args: ["./test.sh"] - name: "Bidirectional with cat for EOF" skip: true # TODO(coproc): hangs - stdout pipe not fully closed after coproc exits test_files: - path: "test.sh" contents: | coproc { while read -r line; do echo "processed: $line" done } rfd=${COPROC[0]} wfd=${COPROC[1]} echo "msg1" >&$wfd echo "msg2" >&$wfd exec {wfd}>&- cat <&$rfd wait echo "DONE" args: ["./test.sh"] # # Coproc with compound command bodies # - name: "Coproc running a for loop" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync for i in 1 2 3; do echo "iteration $i" done } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc running a while loop" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync n=1 while [ $n -le 3 ]; do echo "count $n" n=$((n + 1)) done } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc with conditionals" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync if true; then echo "true branch" else echo "false branch" fi } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc with case statement" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync val="hello" case $val in hello) echo "matched hello";; *) echo "no match";; esac } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc body with pipeline" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo -e "banana\napple\ncherry" | sort } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc body with command substitution" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync result=$(echo "hello" | tr a-z A-Z) echo "$result" } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc with arithmetic operations" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync a=5 b=3 echo $((a + b)) echo $((a * b)) } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] # # Exit status and lifecycle # - name: "Coproc statement returns 0" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; echo done; } status=$? exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait echo "exit status: $status" args: ["./test.sh"] - name: "Coproc with failing body returns 0 immediately" test_files: - path: "test.sh" contents: | coproc { exit 42; } echo "coproc start status: $?" wait args: ["./test.sh"] - name: "Wait with no args waits for coproc" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; echo "coproc done"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait echo "after wait" args: ["./test.sh"] # # Error cases # - name: "Invalid coproc name" ignore_stderr: true test_files: - path: "test.sh" contents: | coproc 123bad { echo x; } echo "exit: $?" args: ["./test.sh"] # # Coproc in different contexts # - name: "Coproc inside a function" test_files: - path: "test.sh" contents: | mkfifo _sync start_coproc() { coproc { read -r _ < _sync; echo "from function coproc"; } } start_coproc exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc inside a conditional" test_files: - path: "test.sh" contents: | mkfifo _sync if true; then coproc { read -r _ < _sync; echo "conditional coproc"; } fi exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc inside a loop body" test_files: - path: "test.sh" contents: | mkfifo _sync for i in 1; do coproc { read -r _ < _sync; echo "loop coproc $i"; } done exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Named coproc inside a function" test_files: - path: "test.sh" contents: | mkfifo _sync f() { coproc FUNCPROC { read -r _ < _sync; echo "named in func"; } } f exec 9<&${FUNCPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] # # Multiple coprocs and variable lifecycle # - name: "Second unnamed coproc replaces first" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; echo "first"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 exec 9<&- wait coproc { read -r _ < _sync; echo "second"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Sequential named coprocs with different names" test_files: - path: "test.sh" contents: | mkfifo _sync coproc PROC1 { read -r _ < _sync; echo "proc1"; } exec 9<&${PROC1[0]} echo x > _sync cat <&9 exec 9<&- wait coproc PROC2 { read -r _ < _sync; echo "proc2"; } exec 9<&${PROC2[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] # # Environment and variable scope # - name: "Coproc inherits parent environment" test_files: - path: "test.sh" contents: | export MY_VAR="inherited" mkfifo _sync coproc { read -r _ < _sync; echo "MY_VAR=$MY_VAR"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc inherits non-exported variables" test_files: - path: "test.sh" contents: | LOCAL_VAR="visible" mkfifo _sync coproc { read -r _ < _sync; echo "LOCAL_VAR=$LOCAL_VAR"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc cannot modify parent variables" test_files: - path: "test.sh" contents: | PARENT_VAR="original" mkfifo _sync coproc { read -r _ < _sync; PARENT_VAR="modified"; echo "in coproc: $PARENT_VAR"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait echo "in parent: $PARENT_VAR" args: ["./test.sh"] - name: "Coproc inherits functions" test_files: - path: "test.sh" contents: | greet() { echo "hello $1"; } export -f greet mkfifo _sync coproc { read -r _ < _sync; greet "world"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc inherits aliases" test_files: - path: "test.sh" contents: | shopt -s expand_aliases alias myecho='echo ALIASED:' mkfifo _sync coproc { read -r _ < _sync; myecho "test"; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] # # Coproc with redirections in body # - name: "Coproc body with stderr redirection" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo "stdout msg" echo "stderr msg" >&2 } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] - name: "Coproc body writing to a file" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo "file content" > /tmp/coproc_test_$$ echo "wrote file" } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait cat /tmp/coproc_test_$$ rm -f /tmp/coproc_test_$$ args: ["./test.sh"] # # Coproc with various data patterns # - name: "Coproc producing empty output" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync; true; } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait echo "after empty coproc" args: ["./test.sh"] - name: "Coproc producing multiple lines of output" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync for i in 1 2 3 4 5 6 7 8 9 10; do echo "line $i" done } exec 9<&${COPROC[0]} echo x > _sync count=0 while read -r line; do count=$((count + 1)) done <&9 echo "read $count lines" wait args: ["./test.sh"] - name: "Coproc with multiline string output" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync cat <<'HEREDOC' first line second line third line HEREDOC } exec 9<&${COPROC[0]} echo x > _sync cat <&9 wait args: ["./test.sh"] # # Coproc output transformation # - name: "Transform coproc output to uppercase" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo "hello" echo "world" } exec 9<&${COPROC[0]} echo x > _sync while read -r line; do echo "$line" | tr a-z A-Z done <&9 wait args: ["./test.sh"] - name: "Coproc output filtered with case" test_files: - path: "test.sh" contents: | mkfifo _sync coproc { read -r _ < _sync echo "apple" echo "banana" echo "avocado" echo "cherry" } exec 9<&${COPROC[0]} echo x > _sync while read -r line; do case $line in a*) echo "starts with a: $line";; esac done <&9 wait args: ["./test.sh"] # # Tests requiring wait with job specs (known failures) # - name: "Wait for specific coproc PID" known_failure: true # TODO(wait): wait with job specs not implemented test_files: - path: "test.sh" contents: | coproc { echo "done"; } fd=${COPROC[0]} cat <&$fd wait $COPROC_PID echo "wait status: $?" args: ["./test.sh"] - name: "Coproc with failing body - wait captures exit status" known_failure: true # TODO(wait): wait with job specs not implemented test_files: - path: "test.sh" contents: | coproc { exit 42; } wait $COPROC_PID echo "exit status: $?" args: ["./test.sh"] - name: "Wait for named coproc PID" known_failure: true # TODO(wait): wait with job specs not implemented test_files: - path: "test.sh" contents: | coproc MYPROC { echo "done"; } fd=${MYPROC[0]} cat <&$fd wait $MYPROC_PID echo "status: $?" args: ["./test.sh"]