From 25182cc2e69ab1cfeeb3f0faa1d28d774393043b Mon Sep 17 00:00:00 2001 From: Bart Schaefer Date: Sun, 17 Mar 2024 14:28:28 -0700 Subject: [PATCH] 52759: ${ ... } trims one trailing newline; "${ ... }" preserves that newline. --- ChangeLog | 5 +++++ Doc/Zsh/expn.yo | 3 +++ Etc/FAQ.yo | 21 ++++++++++++--------- Src/subst.c | 8 ++++++-- Test/D10nofork.ztst | 41 +++++++++++++++++++++++++++++++++++------ 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 + * 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,9 +86,39 @@ 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 +> +> +> > () { @@ -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