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; }