On Tue, 9 Sept 2025 at 00:05, Chet Ramey <[email protected]> wrote:

> That association seems kind of tenuous, but it's better than nothing.

Attached is a patch for suggested docs and tests updates, to go alongside the
original ${;cmd;} patch.  Shouldn't be too hard to update if something other
than ';' is chosen instead.  Let me know if there's anything else that you'd
like regarding any of this.

Kev
diff --git a/doc/bash.1 b/doc/bash.1
index cdb2e809..39afb814 100644
--- a/doc/bash.1
+++ b/doc/bash.1
@@ -4146,7 +4146,7 @@ which executes \fIcommand\fP in the current execution environment
 and captures its output, again with trailing newlines removed.
 .PP
 The character \fIc\fP following the open brace must be a space, tab,
-newline, or \fB|\fP, and the close brace must be in a position
+newline, \fB;\fP, or \fB|\fP, and the close brace must be in a position
 where a reserved word may appear (i.e., preceded by a command terminator
 such as semicolon).
 \fBBash\fP allows the close brace to be joined to the remaining characters in
@@ -4166,6 +4166,13 @@ however, the rest of the execution environment,
 including the positional parameters, is shared with the caller.
 .PP
 If the first character following the open brace
+is a \fB;\fP, the construct behaves like the regular form above but
+preserves any trailing newlines in the output of \fIcommand\fP
+rather than removing them.
+This form is useful when the trailing newlines are significant
+and should not be stripped from the command's output.
+.PP
+If the first character following the open brace
 is a \fB|\fP, the construct expands to the
 value of the \fBREPLY\fP shell variable after \fIcommand\fP executes,
 without removing any trailing newlines,
diff --git a/doc/bashref.texi b/doc/bashref.texi
index fefe5dd9..7ec0ffdf 100644
--- a/doc/bashref.texi
+++ b/doc/bashref.texi
@@ -2860,7 +2860,7 @@ which executes @var{command} in the current execution environment
 and captures its output, again with trailing newlines removed.
 
 The character @var{c} following the open brace must be a space, tab,
-newline, or @samp{|}, and the close brace must be in a position
+newline, @samp{;}, or @samp{|}, and the close brace must be in a position
 where a reserved word may appear (i.e., preceded by a command terminator
 such as semicolon).
 Bash allows the close brace to be joined to the remaining characters in
@@ -2879,6 +2879,13 @@ function is executing, and the @code{return} builtin forces
 however, the rest of the execution environment,
 including the positional parameters, is shared with the caller.
 
+If the first character following the open brace
+is a @samp{;}, the construct behaves like the regular form above but
+preserves any trailing newlines in the output of @var{command}
+rather than removing them.
+This form is useful when the trailing newlines are significant
+and should not be stripped from the command's output.
+
 If the first character following the open brace
 is a @samp{|}, the construct expands to the
 value of the @code{REPLY} shell variable after @var{command} executes,
diff --git a/tests/comsub2.right b/tests/comsub2.right
index 87359788..1ecaa89b 100644
--- a/tests/comsub2.right
+++ b/tests/comsub2.right
@@ -1,8 +1,31 @@
 aa bb cc dd
 AAaa bb cc ddBB
+AAaa
+bb
+cc
+ddBB
+aa bb cc dd
+AAaa bb cc dd BB
+AAaa
+bb
+cc
+dd
+BB
+aa bb cc dd
+aa bb cc dd
 aa bb cc dd
 aa bb cc dd
 DDDDDaa bb cc ddEEEEE
+DDDDDaa bb cc dd EEEEE
+DDDDDaa
+bb
+cc
+ddEEEEE
+DDDDDaa
+bb
+cc
+dd
+EEEEE
 aa bb cc dd
 outside: 42
 aa bb cc dd
@@ -13,20 +36,23 @@ func ()
     echo func-inside
 }
 abcde
+abcde
+abcde
+
 67890
 12345
 argv[1] = <>
 argv[1] = <>
 aa,bb
 JOBaa bb cc ddCONTROL
-./comsub2.tests: line 68: p: command not found
+./comsub2.tests: line 90: p: command not found
 NOTFOUND
-./comsub2.tests: line 75: p: command not found
-./comsub2.tests: line 75: p: command not found
+./comsub2.tests: line 97: p: command not found
+./comsub2.tests: line 97: p: command not found
 expand_aliases      	off
 expand_aliases      	off
 outside:
-./comsub2.tests: line 79: alias: p: not found
+./comsub2.tests: line 101: alias: p: not found
 alias e='echo inside redefine'
 expand_aliases      	off
 1
@@ -34,7 +60,7 @@ expand_aliases      	on
 2
 expand_aliases      	on
 outside:
-./comsub2.tests: line 89: alias: p: not found
+./comsub2.tests: line 111: alias: p: not found
 expand_aliases      	on
 1
 xx
@@ -50,6 +76,9 @@ newlines
 
 
 outside: 42
+newlines
+
+
 before: 1 2
 after: 2
 before: 1 2
@@ -58,23 +87,60 @@ before: 1 2
 after: 1 2
 XnestedY
 a nested b
+a
+nested
+
+b
+
+a
+nested
+b
+
+a
+nested
+
+b
 one two
 42
 42
 42
+42
+42
+42
+
 123
 123
+123
+0
 0
 123
 123
+123
 0
+0
+Mon Aug 29 20:03:02 EDT 2022
 Mon Aug 29 20:03:02 EDT 2022
 Mon Aug 29 20:03:02 EDT 2022
+
+Mon Aug 29 20:03:02 EDT 2022
+
 Mon Aug 29 20:03:02 EDT 2022
 Mon Aug 29 20:03:02 EDT 2022
+Mon Aug 29 20:03:02 EDT 2022
+
+Mon Aug 29 20:03:02 EDT 2022
+
+123
 123
+
 before 123
+before
+123
+
 in for 123
+in for
+123
+
 outside before: value
 inside before: value
 inside after: funsub
@@ -83,26 +149,38 @@ outside after: funsub
 =====posix mode=====
 outside before: value
 .
+.
 declare -a a=([0]="1" [1]="2" [2]="3" [3]="4")
+declare -a a=([0]="1" [1]="2" [2]="3" [3]=$'4\n')
 declare -- int="2"
 after here-doc: 1
 [1]-  Running                    sleep 1 &
 [2]+  Running                    sleep 1 &
 [1]-  Running                    sleep 1 &
 [2]+  Running                    sleep 1 &
+[1]-  Running                    sleep 1 &
+[2]+  Running                    sleep 1 &
+
 17772 26794
 17772 26794
 we should try rhs
 comsub
 and
 funsub
+and
+newline funsub
+
 in here-documents
 after all they work here
 and work here
+and also here
+
 after for
 uname
+uname
 after arith for
 1) a[${ break;}]
+2) a[${;break;}]
 #? after select
 a b c == 1 2 3
  == 1 2 3
@@ -190,6 +268,8 @@ AFTER
 unbalanced braces}}
 combined comsubs
 combined comsubs
+combined comsubs
+
 inside
 after: var = inside
 after: 42 var = inside
diff --git a/tests/comsub2.tests b/tests/comsub2.tests
index 73d378ff..a5689108 100644
--- a/tests/comsub2.tests
+++ b/tests/comsub2.tests
@@ -18,13 +18,32 @@
 
 echo ${ printf '%s\n' aa bb cc dd; }
 echo AA${ printf '%s\n' aa bb cc dd; }BB
+echo "AA${ printf '%s\n' aa bb cc dd; }BB"
+
+echo ${;printf '%s\n' aa bb cc dd; }
+echo AA${;printf '%s\n' aa bb cc dd; }BB
+echo "AA${;printf '%s\n' aa bb cc dd; }BB"
 
 echo ${ printf '%s\n' aa bb cc dd; return; echo ee ff; }
+echo ${;printf '%s\n' aa bb cc dd; return; echo ee ff; }
 echo ${ printf '%s\n' aa bb cc dd
 	}
+echo ${;printf '%s\n' aa bb cc dd
+	}
+
 echo DDDDD${
 	printf '%s\n' aa bb cc dd
 }EEEEE
+echo DDDDD${;
+	printf '%s\n' aa bb cc dd
+}EEEEE
+echo "DDDDD${
+	printf '%s\n' aa bb cc dd
+}EEEEE"
+echo "DDDDD${;
+	printf '%s\n' aa bb cc dd
+}EEEEE"
+
 unset x
 echo ${ printf '%s\n' aa bb cc dd; x=42 ; return 12; echo ee ff; }
 echo outside: $x
@@ -37,8 +56,9 @@ unset xx
 
 declare -i x
 y=${ :;}
+yy=${;:;}
 declare -i z
-unset -v x y z
+unset -v x y yy z
 
 # variables can be local, but all function declarations are global
 func() { echo func-outside; }
@@ -48,6 +68,8 @@ xx=${ unset -f func; }
 declare -f func
 
 echo ${ ( echo abcde );}
+echo ${;( echo abcde );}
+echo "${;( echo abcde );}"
 
 echo ${| echo 67890;  REPLY=12345; }		# works in mksh
 x=${| REPLY= ;}
@@ -106,11 +128,13 @@ a=${| local b ; a=12 ; b=22 ; REPLY=42 ; echo inside: $a $b $REPLY; }
 echo outside: $a $b
 unset a b
 
-# this form doesn't remove the trailing newlines
+# these forms don't remove the trailing newlines
 REPLY=42
 a=${| REPLY=$'newlines\n\n'; }
 echo "$a"
 echo outside: $REPLY
+a=${; printf '%s' $'newlines\n\n'; }
+echo "$a"
 
 # how do we handle shift with these weird ksh93 function-like semantics?
 # ksh93 doesn't reset the positional parameters here
@@ -133,6 +157,10 @@ echo after: "$@"
 echo ${ echo X${ echo nested; }Y; }
 echo ${ echo a ; echo ${ echo nested; }; echo b; }
 
+echo "${;echo a ; echo "${;echo nested; }"; echo b; }"
+echo "${;echo a ; echo "${ echo nested; }"; echo b; }"
+echo "${ echo a ; echo "${;echo nested; }"; echo b; }"
+
 # nested funsubs/comsubs
 x=${
 	echo ${ echo one;} $(echo two)
@@ -144,6 +172,10 @@ echo $(( ${ echo 24 + 18; }))
 echo $(( ${ echo 14 + 18; }+ 10))
 echo ${ echo $(( 24+18 )); }
 
+echo $(( "${;echo 24 + 18; }"))
+echo $(( "${;echo 14 + 18; }+" 10))
+echo "${;echo $(( 24+18 )); }"
+
 # alias expansion and nested funsubs in other constructs
 ${THIS_SH} ./comsub21.sub
 ${THIS_SH} ./comsub22.sub
diff --git a/tests/comsub21.sub b/tests/comsub21.sub
index 0837f723..2d7f51ed 100644
--- a/tests/comsub21.sub
+++ b/tests/comsub21.sub
@@ -22,12 +22,16 @@ alias number="echo 123"
 
 echo ${ number; }
 echo $(( ${ number; } ))
+echo $(( "${;number; }" ))
 (( ${ number; } )) ; echo $?
+(( "${;number; }" )) ; echo $?
 
 set -o posix
 echo ${ number; }
 echo $(( ${ number; } ))
+echo $(( "${;number; }" ))
 (( ${ number; } )) ; echo $?
+(( "${;number; }" )) ; echo $?
 set +o posix
 
 # have to turn it back on after leaving posix mode
@@ -37,10 +41,14 @@ alias my_alias='echo $DATE'
 
 echo ${ eval my_alias; }
 echo ${ my_alias; }
+echo "${;eval my_alias; }"
+echo "${;my_alias; }"
 
 set -o posix
 echo ${ eval my_alias; }
 echo ${	my_alias; }
+echo "${;eval my_alias; }"
+echo "${;my_alias; }"
 set +o posix ; shopt -s expand_aliases
 
 alias e=echo
@@ -48,18 +56,25 @@ alias v='e 123'
 
 set -o posix
 echo ${ v; }
+echo "${;v; }"
 echo ${ echo before ; v; }
+echo "${;echo before ; v; }"
 echo ${	for f in 0; do
 echo in for
 done; v; }
+echo "${;for f in 0; do
+echo in for
+done; v; }"
 set +o posix ; shopt -s expand_aliases
 
 alias let='let --'
 
 let '1 == 1'
 : ${ let '1 == 1'; }
+: "${;let '1 == 1'; }"
 
 set -o posix
 let '1 == 1'
 : ${ let '1 == 1'; }
+: "${;let '1 == 1'; }"
 set +o posix ; shopt -s expand_aliases
diff --git a/tests/comsub23.sub b/tests/comsub23.sub
index 5c90d513..0f251a46 100644
--- a/tests/comsub23.sub
+++ b/tests/comsub23.sub
@@ -17,15 +17,21 @@
 # make sure parsing is right within conditional commands
 [[ ${ echo -n "[${ echo -n foo; }]" ; } == '[foo]' ]] || echo bad 1
 [[ "${ echo -n "[${ echo -n foo; }]" ; }" == '[foo]' ]] || echo bad 1
+[[ "${;echo -n "[${;echo -n foo; }]" ; }" == '[foo]' ]] || echo bad 1
 
 # mix multiple calls to parse_and_execute
 got=$(eval 'x=${ for i in test; do case $i in test) echo .;; esac; done; }' ; echo $x)
 echo $got
+got=$(eval 'x=${;for i in test; do case $i in test) echo .;; esac; done; }' ; echo $x)
+echo $got
 
 # mix compound assignment and nofork command substitution
 : ${ a=(1 2 3 ${ echo 4;} ); }
 declare -p a
 unset a
+: ${;a=(1 2 3 "${;echo 4;}" ); }
+declare -p a
+unset a
 
 # function execution with side effects
 int=0
@@ -55,6 +61,9 @@ printf '%s\n' "$jl1"
 jl2=${ jobs; }
 printf '%s\n' "$jl2"
 
+jl3="${;jobs; }"
+printf '%s\n' "$jl3"
+
 # nofork command substitution doesn't affect the shell's random number sequence
 RANDOM=42
 echo $RANDOM ${ echo $RANDOM; }
@@ -69,6 +78,8 @@ we should try rhs
 ${word-$(echo comsub)}
 and
 ${word-${ echo funsub; }}
+and
+${word-${;echo newline funsub; }}
 in here-documents
 EOF
 
@@ -77,20 +88,22 @@ exec 4<&-
 
 echo after all they ${word-$(echo work here)}
 echo and ${word-${ echo work here; }}
+echo and ${word-"${;echo also here; }"}
 
 unset x
 
-set -- 'a[${ break;}]'
+set -- 'a[${ break;}]' 'a[${;break;}]'
 declare -in x
 
 for x do :; done
 echo after for
 
 true; for (( ; $? == 0; ${ ! break;} )); do echo uname; done
+true; for (( ; $? == 0; ${;! break;} )); do echo uname; done
 echo after arith for
 
 declare -i z
-select z in 'a[${ break;}]'
+select z in 'a[${ break;}]' 'a[${;break;}]'
 do
 	echo $z
 done <<<1
diff --git a/tests/comsub26.sub b/tests/comsub26.sub
index eb17fa3e..5bdc9498 100644
--- a/tests/comsub26.sub
+++ b/tests/comsub26.sub
@@ -24,6 +24,7 @@ echo ${ echo unbalanced braces; }}}
 
 echo $(echo combined ${| REPLY=comsubs; })
 echo ${ echo $(echo combined ${| REPLY=comsubs; }); }
+echo "${;echo $(echo combined ${| REPLY=comsubs; }); }"
 
 var=outside
 echo ${ var=inside; echo $var; }

Reply via email to