On Fri, 29 Aug 2025 at 01:00, Koichi Murase <myoga.mur...@gmail.com> wrote:

> If the feature is really desired by the community as an essential feature, it
> should be implemented as a new syntax.

I looked at adding alternate syntax for doing command substitution without
stripping trailing newlines, and it turned out to be easier than I expected.

This patch adds the syntax ${;cmd;}, which is handled identically to ${ cmd;}
except that trailing newlines are preserved.

It's only necessary to do a nofork version, because if someone deliberately
wants to run inside a subshell (ie. fork instead of no-fork) then they can just
do that explicitly with ${;(cmd;);}.

I don't care much about the choice of character, ie. if something else would be
better than ';'.  A single character is definitely easier to parse and handle
than some of the fancier previous proposals, such as
https://lists.gnu.org/archive/html/bug-bash/2021-01/msg00269.html which is very
extensible, but would be a lot more work.

It seems to work fine, but I might've missed something.  If there's interest in
adopting this, then I'd be happy to update the tests and docs.  Otherwise, I'm
curious to hear if there might be any potential problems with adding this, or
what else it would require to be accepted.

    bash-5.3$ echo "[$(echo foobar)]"
    [foobar]
    bash-5.3$ echo "[${ echo foobar;}]"
    [foobar]
    bash-5.3$ echo "[${;echo foobar;}]"
    [foobar
    ]
    bash-5.3$ echo "[${;echo -e 'foobar\n';}]"
    [foobar

    ]
    bash-5.3$ echo "[${;echo "(${;echo foobar;})";}]"
    [(foobar
    )
    ]
    bash-5.3$

With this, my original illustrative example script becomes:

    my_result=${;some | complex | pipeline;}
    while :; do
        if is_any_good "$my_result"; then
            break
        else
            my_result=${;echo -n "$my_result" | some | further | processing;}
        fi
    done
    echo -n "$my_result" | final_processing

which I think is about as good as possible.  It still can't use here-strings,
but using echo -n is maybe better anyway, since it more clearly shows the
left-to-right data flow.

Kev
diff --git a/parse.y b/parse.y
index b698f86b..b3e070fd 100644
--- a/parse.y
+++ b/parse.y
@@ -4678,7 +4678,7 @@ INTERNAL_DEBUG(("current_token (%d) != shell_eof_token (%c)", current_token, she
       lastc = tcmd[retlen - 1];
       retlen++;
       ret = xmalloc (retlen + 4);
-      ret[0] = (dolbrace_spec == '|') ? '|' : ' ';
+      ret[0] = (dolbrace_spec == '|' || dolbrace_spec == ';') ? dolbrace_spec : ' ';
       strcpy (ret + 1, tcmd);		/* ( */
       if (was_newline)
 	ret[retlen++] = '\n';
@@ -4762,7 +4762,7 @@ xparse_dolparen (const char *base, char *string, size_t *indp, int flags)
   local_extglob = extended_glob;
 #endif
 
-  if (funsub && FUNSUB_CHAR (*string) && *string == '|')
+  if (funsub && FUNSUB_CHAR (*string) && (*string == '|' || *string == ';'))
     string++;
 
   token_to_read = funsub ? DOLBRACE : DOLPAREN;			/* let's trick the parser */
diff --git a/parser.h b/parser.h
index ccfc8e16..2c75a96f 100644
--- a/parser.h
+++ b/parser.h
@@ -80,9 +80,9 @@ struct dstack {
 /* characters that can appear following ${ to introduce a nofork command
    substitution. */
 #if 0
-#define FUNSUB_CHAR(n) ((n) == ' ' || (n) == '\t' || (n) == '\n' || (n) == '|' || (n) == '(')	/* ) */
+#define FUNSUB_CHAR(n) ((n) == ' ' || (n) == '\t' || (n) == '\n' || (n) == '|' || (n) == ';' || (n) == '(')	/* ) */
 #else
-#define FUNSUB_CHAR(n) ((n) == ' ' || (n) == '\t' || (n) == '\n' || (n) == '|')
+#define FUNSUB_CHAR(n) ((n) == ' ' || (n) == '\t' || (n) == '\n' || (n) == '|' || (n) == ';')
 #endif
 
 /* variable declarations from parse.y */
diff --git a/subst.c b/subst.c
index fd7262a4..310645db 100644
--- a/subst.c
+++ b/subst.c
@@ -323,7 +323,7 @@ static int valid_parameter_transform (const char *);
 static char *process_substitute (char *, int);
 
 static char *optimize_cat_file (REDIRECT *, int, int, int *);
-static char *read_comsub (int, int, int, int *);
+static char *read_comsub (int, int, int, int *, int);
 
 #ifdef ARRAY_VARS
 static arrayind_t array_length_reference (const char *);
@@ -6694,14 +6694,14 @@ optimize_cat_file (REDIRECT *r, int quoted, int flags, int *flagp)
   if (fd < 0)
     return &expand_param_error;
 
-  ret = read_comsub (fd, quoted, flags, flagp);
+  ret = read_comsub (fd, quoted, flags, flagp, 0);
   close (fd);
 
   return ret;
 }
 
 static char *
-read_comsub (int fd, int quoted, int flags, int *rflag)
+read_comsub (int fd, int quoted, int flags, int *rflag, int keep_nls)
 {
   char *istring, buf[COMSUB_PIPEBUF], *bufp;
   int c, tflag, skip_ctlesc, skip_ctlnul;
@@ -6794,37 +6794,40 @@ read_comsub (int fd, int quoted, int flags, int *rflag)
       return (char *)NULL;
     }
 
-  /* Strip trailing newlines from the output of the command. */
-  if (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES))
+  if (keep_nls == 0)
     {
-      while (istring_index > 0)
+      /* Strip trailing newlines from the output of the command. */
+      if (quoted & (Q_HERE_DOCUMENT|Q_DOUBLE_QUOTES))
 	{
-	  if (istring[istring_index - 1] == '\n')
+	  while (istring_index > 0)
 	    {
-	      --istring_index;
-
-	      /* If the newline was quoted, remove the quoting char. */
-	      if (istring[istring_index - 1] == CTLESC)
-		--istring_index;
-
-#ifdef __MSYS__
-	      if (istring_index > 0 && istring[istring_index - 1] == '\r')
+	      if (istring[istring_index - 1] == '\n')
 		{
 		  --istring_index;
 
-		  /* If the carriage return was quoted, remove the quoting char. */
+		  /* If the newline was quoted, remove the quoting char. */
 		  if (istring[istring_index - 1] == CTLESC)
 		    --istring_index;
-		}
+
+#ifdef __MSYS__
+		  if (istring_index > 0 && istring[istring_index - 1] == '\r')
+		    {
+		      --istring_index;
+
+		      /* If the carriage return was quoted, remove the quoting char. */
+		      if (istring[istring_index - 1] == CTLESC)
+			--istring_index;
+		    }
 #endif
+		}
+	      else
+		break;
 	    }
-	  else
-	    break;
+	  istring[istring_index] = '\0';
 	}
-      istring[istring_index] = '\0';
+      else
+	strip_trailing (istring, istring_index - 1, 1);
     }
-  else
-    strip_trailing (istring, istring_index - 1, 1);
 
   if (rflag)
     *rflag = tflag;
@@ -6948,7 +6951,7 @@ function_substitute (char *string, int quoted, int flags)
 {
   volatile int function_code;
   int valsub, stdout_valid, saveout, old_frozen;
-  int result, pflags, tflag, was_trap;
+  int result, pflags, tflag, was_trap, keep_nls;
   char *istring, *s;
   WORD_DESC *ret;
   SHELL_VAR *v;
@@ -6963,8 +6966,11 @@ function_substitute (char *string, int quoted, int flags)
   ARRAY *psa;
 #endif
 
+  keep_nls = 0;
   if (valsub = (string && *string == '|'))
     string++;
+  else if (keep_nls = (string && *string == ';'))
+    string++;
 
   /* In the case of no command to run, just return NULL. */
   for (s = string; s && *s && (shellblank (*s) || *s == '\n'); s++)
@@ -7130,7 +7136,7 @@ function_substitute (char *string, int quoted, int flags)
       /* We call anonclose as part of the outer nofork unwind-protects */
       BLOCK_SIGNAL (SIGINT, set, oset);
       lseek (afd, 0, SEEK_SET);
-      istring = read_comsub (afd, quoted, flags, &tflag);
+      istring = read_comsub (afd, quoted, flags, &tflag, keep_nls);
       UNBLOCK_SIGNAL (oset);
     }
   else
@@ -7462,7 +7468,7 @@ command_substitute (char *string, int quoted, int flags)
 	 read will return. */
       BLOCK_SIGNAL (SIGINT, set, oset);
       tflag = 0;
-      istring = read_comsub (fildes[0], quoted, flags, &tflag);
+      istring = read_comsub (fildes[0], quoted, flags, &tflag, 0);
 
       close (fildes[0]);
       discard_unwind_frame ("read-comsub");

Reply via email to