about summary refs log tree commit diff
diff options
context:
space:
mode:
authorBart Schaefer <schaefer@zsh.org>2024-03-17 14:28:28 -0700
committerBart Schaefer <schaefer@zsh.org>2024-03-17 14:28:28 -0700
commit25182cc2e69ab1cfeeb3f0faa1d28d774393043b (patch)
treeaef59e3f6da6763142c40cc6139c9b1edac21175
parent45b0a838aa6e05131523dee291c561cf86f04771 (diff)
downloadzsh-25182cc2e69ab1cfeeb3f0faa1d28d774393043b.tar.gz
zsh-25182cc2e69ab1cfeeb3f0faa1d28d774393043b.tar.xz
zsh-25182cc2e69ab1cfeeb3f0faa1d28d774393043b.zip
52759: ${ ... } trims one trailing newline; "${ ... }" preserves that newline.
-rw-r--r--ChangeLog5
-rw-r--r--Doc/Zsh/expn.yo3
-rw-r--r--Etc/FAQ.yo21
-rw-r--r--Src/subst.c8
-rw-r--r--Test/D10nofork.ztst41
5 files changed, 61 insertions, 17 deletions
diff --git a/ChangeLog b/ChangeLog
index c3f770477..7e5f68059 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,10 @@
 2024-03-14  Bart Schaefer  <schaefer@zsh.org>
 
+	* 52759: Doc/Zsh/expn.yo, Etc/FAQ.yo, Src/subst.c,
+	Test/D10nofork.ztst: change ${ ... } substitution to trim one
+	trailing newline; instead "${ ... }" (with quotes) preserves that
+	newline.  All trailing newlines are still trimmed in emulations.
+
 	* unposted: Etc/BUGS: HIST_IGNORE_DUPS mishandles quoted whitespace.
 
 	* 52752: Src/params.c, Test/B02typeset.ztst: more typeset -p fixes
diff --git a/Doc/Zsh/expn.yo b/Doc/Zsh/expn.yo
index 183ca6e03..0e121e784 100644
--- a/Doc/Zsh/expn.yo
+++ b/Doc/Zsh/expn.yo
@@ -1950,6 +1950,9 @@ the braces by whitespace, like `tt(${ )...tt( })', is replaced by its
 standard output.  Like `tt(${|)...tt(})' and unlike
 `tt($LPAR())...tt(RPAR())', the command executes in the current shell
 context with function local behaviors and does not create a subshell.
+Word splitting does not apply unless tt(SH_WORD_SPLIT) is set, but a
+single trailing newline is stripped unless the substitution is enclosed
+in double quotes.
 
 Note that because the `tt(${|)...tt(})' and `tt(${ )...tt( })' forms
 must be parsed at once as both string tokens and commands, all other
diff --git a/Etc/FAQ.yo b/Etc/FAQ.yo
index 4a86050e6..4d71c8f30 100644
--- a/Etc/FAQ.yo
+++ b/Etc/FAQ.yo
@@ -1091,20 +1091,23 @@ sect(Comparisons of forking and non-forking command substitution)
   mytt(set -- pos1 pos2 etc).  Nothing that happens within mytt($(command))
   affects the caller.
 
-  mytt($(command)) removes trailing newlines from the output of mytt(command)
-  when substituting, whereas mytt(${ command }) and its variants do not.
-  The latter is consistent with mytt(${|...}) from mksh but differs from
-  bash and ksh, so in emulation modes, newlines are stripped from command
-  output (not from tt(REPLY) assignments).
-
   When not enclosed in double quotes, the expansion of mytt($(command)) is
   split on tt(IFS) into an array of words.  In contrast, and unlike both
   bash and ksh, unquoted non-forking substitutions behave like parameter
   expansions with respect to the tt(SH_WORD_SPLIT) option.
 
-  When mytt(command) is myem(not) a builtin, mytt(${ command }) does fork, and
-  typically forks the same number of times as mytt($(command)), because in
-  the latter case zsh usually optimizes the final fork into an exec.
+  Both of the mytt(${|...}) formats retain any trailing newlines,
+  except as handled by the tt(SH_WORD_SPLIT) option, consistent with
+  mytt(${|...}) from mksh. mytt(${ command }) removes a single final
+  newline, but mytt("${ command }") retains it.  This differs from
+  bash and ksh, so in emulation modes, newlines are stripped even from
+  quoted command output.  In all cases, mytt($(command)) removes all
+  trailing newlines from the output of mytt(command).
+
+  When mytt(command) is myem(not) a builtin, mytt(${ command }) does
+  fork, and typically forks the same number of times as
+  mytt($(command)), because in the latter case zsh usually optimizes
+  the final fork into an exec.
 
   Redirecting input from files has subtle differences:
   itemization(
diff --git a/Src/subst.c b/Src/subst.c
index 49f7336bb..9d20a2d0e 100644
--- a/Src/subst.c
+++ b/Src/subst.c
@@ -1900,6 +1900,7 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int pf_flags,
 	/* The command string to be run by ${|...;} */
 	char *cmdarg = NULL;
 	size_t slen = 0;
+	int trim = (!EMULATION(EMULATE_ZSH)) ? 2 : !qt;
 	inbrace = 1;
 	s++;
 
@@ -2005,10 +2006,13 @@ paramsubst(LinkList l, LinkNode n, char **str, int qt, int pf_flags,
 		int onoerrs = noerrs, rplylen;
 		noerrs = 2;
 		rplylen = zstuff(&cmdarg, rplytmp);
-		if (! EMULATION(EMULATE_ZSH)) {
+		if (trim) {
 		    /* bash and ksh strip trailing newlines here */
-		    while (rplylen > 0 && cmdarg[rplylen-1] == '\n')
+		    while (rplylen > 0 && cmdarg[rplylen-1] == '\n') {
 			rplylen--;
+			if (trim == 1)
+			    break;
+		    }
 		    cmdarg[rplylen] = 0;
 		}
 		noerrs = onoerrs;
diff --git a/Test/D10nofork.ztst b/Test/D10nofork.ztst
index d6a5588df..fc6b84613 100644
--- a/Test/D10nofork.ztst
+++ b/Test/D10nofork.ztst
@@ -86,11 +86,41 @@ F:setting option inside is too late for that substitution
 ?(eval):8: no matches found: f?*
 
   purr ${| REPLY=$'trailing newlines remain\n\n' }
-0:newline removal should not occur
+0:newline removal should not occur, part 1
 >trailing newlines remain
 >
 >
 
+  purr ${ echo $'one trailing newline\nremoved\n\n\n' }
+0:newline removal in ${ ... }, zsh mode
+>one trailing newline
+>removed
+>
+>
+>
+
+  () {
+    emulate -L ksh
+    purl ${ echo $'all trailing newlines\nremoved\n\n\n' }
+    purr "${ echo $'all trailing newlines\nremoved\n\n\n' }"
+  }
+0:newline removal in ${ ... }, emulation mode, shwordsplit
+>all
+>trailing
+>newlines
+>removed
+>all trailing newlines
+>removed
+
+  purr "${ echo $'no trailing newlines\nremoved\n\n\n' }"
+0:newline removal should not occur, part 2
+>no trailing newlines
+>removed
+>
+>
+>
+>
+
   () {
    purr ${| REPLY=$* ; shift 2 }
    purr $*
@@ -159,7 +189,7 @@ F:Why not use this error in the previous case as well?
 1:unbalanced braces, part 4+
 ?(eval):1: closing brace expected
 
-  purr ${ purr STDOUT }
+  purr "${ purr STDOUT }"
 0:capture stdout
 >STDOUT
 >
@@ -322,7 +352,7 @@ F:Fiddly here to get EOF past the test syntax
 0:here-string behavior
 >in a here string
 
-  <<<${ purr $'stdout as a here string' }
+  <<<"${ purr $'stdout as a here string' }"
 0:another capture stdout
 >stdout as a here string
 >
@@ -331,7 +361,7 @@ F:Fiddly here to get EOF past the test syntax
   wrap=${ purr "capture in environment assignment" } typeset -p wrap
 0:assignment context
 >typeset -g wrap='REPLY in environment assignment'
->typeset -g wrap=$'capture in environment assignment\n'
+>typeset -g wrap='capture in environment assignment'
 
 # Repeat return and exit tests with stdout capture
 
@@ -410,7 +440,7 @@ F:must do this before evaluating the next test block
 0:ignored braces, part 1
 >buried}
 
-  purr ${ purr ${REPLY:-buried}}}
+  purr "${ purr ${REPLY:-buried}}}"
 0:ignored braces, part 2
 >buried
 >}
@@ -418,7 +448,6 @@ F:must do this before evaluating the next test block
   purr ${ { echo nested ;} }
 0:ignored braces, part 3
 >nested
->
 
   purr ${ { echo nested } } DONE
 1:ignored braces, part 4