I've written a feature I find useful for bash that escapes a single ampersand 
if it is surrounded on both sides by any non-blank or non-meta characters. This 
logic is toggled by a set option, "compressedand" that can be enabled with `set 
[-|+]c` or `set -o compressedand`.


As an example:

   $ echo this&echo that
   [1] 32324
   that
   this
   [1]+  Done                    echo this

   $ set -c
   $ echo this&echo that
   this&echo that

Handling of multi-character metas, such as &>, |&, &&, et al, is still parsed 
by BASH and their functions remain unaffected.

   $ echo this& echo that
   [1] 1373
   that
   this
   [1]+  Done                    echo this

   $ echo this &echo that
   [1] 1375
   that
   this
   [1]+  Done                    echo this

   $ echo this&&echo that
   this
   that

   $ ps ax&>/tmp/ps&echo
   $ ls /tmp/ps\&echo
   /tmp/ps&echo


The primary use case for the option is to ease using unquoted URLs as arguments 
that have multiple query parameters. Yes, quoting the URL precludes the need 
for this feature, but I forget sometimes (okay, too often):

   $ set +c
   $ echo https://example.com/inputs?v=22&time=now&cp=key
   [1] 32335
   [2] 32336
   https://example.com/inputs?v=22
   [1]-  Done                    echo https://example.com/inputs?v=22
   [2]+  Done                    time=now

   $ set -c
   $ echo https://example.com/inputs?v=22&time=now&cp=key
   https://example.com/inputs?v=22&time=now&cp=key


I've attached a patch that was written against bash-4.4 and tested to work 
against the most recent DEVEL commit (3235014e5b) as of 04/22. The patch 
provides the logic, set option control, and updates to the man and info pages. 
I'm still working on getting some tests worked up. I wanted to get this 
submitted to get a feel for general likeability and for input on things I need 
to do for a proper contribution.

And, for completeness, here are the bash VERSIONs I've tested this on:

   GNU bash, version 4.4.23(1)-release (x86_64-pc-linux-gnu)
   GNU bash, version 5.0.16(1)-maint (x86_64-pc-linux-gnu)

I haven't looked up to see what POSIX has to say about this, but I'm fairly 
confident that this flies in the face of it. If that's the case and this patch 
is dismissed accordingly, I'll accept that. This was a fun exercise anyway.  .)

- Eric.

-------------------------------
Fudd's First Law of Opposition:
"If you push something hard enough, it will fall over."
          - Firesign Theater

diff --git a/builtins/set.def b/builtins/set.def
index 8122361..2467e73 100644
--- a/builtins/set.def
+++ b/builtins/set.def
@@ -59,7 +59,7 @@ extern int no_line_editing;
 
 $BUILTIN set
 $FUNCTION set_builtin
-$SHORT_DOC set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]
+$SHORT_DOC set [-abcefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]
 Set or unset values of shell options and positional parameters.
 
 Change the value of shell attributes and positional parameters, or
@@ -68,6 +68,7 @@ display the names and values of shell variables.
 Options:
   -a  Mark variables which are modified or created for export.
   -b  Notify of job termination immediately.
+  -c  Escape a single ampersand surrounded by non-blank/non-meta text.
   -e  Exit immediately if a command exits with a non-zero status.
   -f  Disable file name generation (globbing).
   -h  Remember the location of commands as they are looked up.
@@ -77,50 +78,51 @@ Options:
   -n  Read commands but do not execute them.
   -o option-name
       Set the variable corresponding to option-name:
-          allexport    same as -a
-          braceexpand  same as -B
+          allexport      same as -a
+          braceexpand    same as -B
+          compressedand  same as -c
 #if defined (READLINE)
-          emacs        use an emacs-style line editing interface
+          emacs          use an emacs-style line editing interface
 #endif /* READLINE */
-          errexit      same as -e
-          errtrace     same as -E
-          functrace    same as -T
-          hashall      same as -h
+          errexit        same as -e
+          errtrace       same as -E
+          functrace      same as -T
+          hashall        same as -h
 #if defined (BANG_HISTORY)
-          histexpand   same as -H
+          histexpand     same as -H
 #endif /* BANG_HISTORY */
 #if defined (HISTORY)
-          history      enable command history
+          history        enable command history
 #endif
-          ignoreeof    the shell will not exit upon reading EOF
+          ignoreeof      the shell will not exit upon reading EOF
           interactive-comments
-                       allow comments to appear in interactive commands
-          keyword      same as -k
+                         allow comments to appear in interactive commands
+          keyword        same as -k
 #if defined (JOB_CONTROL)
-          monitor      same as -m
+          monitor        same as -m
 #endif
-          noclobber    same as -C
-          noexec       same as -n
-          noglob       same as -f
-          nolog        currently accepted but ignored
+          noclobber      same as -C
+          noexec         same as -n
+          noglob         same as -f
+          nolog          currently accepted but ignored
 #if defined (JOB_CONTROL)
-          notify       same as -b
+          notify         same as -b
 #endif
-          nounset      same as -u
-          onecmd       same as -t
-          physical     same as -P
-          pipefail     the return value of a pipeline is the status of
-                       the last command to exit with a non-zero status,
-                       or zero if no command exited with a non-zero status
-          posix        change the behavior of bash where the default
-                       operation differs from the Posix standard to
-                       match the standard
-          privileged   same as -p
-          verbose      same as -v
+          nounset        same as -u
+          onecmd         same as -t
+          physical       same as -P
+          pipefail       the return value of a pipeline is the status of
+                         the last command to exit with a non-zero status,
+                         or zero if no command exited with a non-zero status
+          posix          change the behavior of bash where the default
+                         operation differs from the Posix standard to
+                         match the standard
+          privileged     same as -p
+          verbose        same as -v
 #if defined (READLINE)
-          vi           use a vi-style line editing interface
+          vi             use a vi-style line editing interface
 #endif /* READLINE */
-          xtrace       same as -x
+          xtrace         same as -x
   -p  Turned on whenever the real and effective user ids do not match.
       Disables processing of the $ENV file and importing of shell
       functions.  Turning this option off causes the effective uid and
@@ -197,6 +199,7 @@ const struct {
 #if defined (BRACE_EXPANSION)
   { "braceexpand",'B', (int *)NULL, (setopt_set_func_t *)NULL, (setopt_get_func_t *)NULL  },
 #endif
+  { "compressedand", 'c', (int *)NULL, (setopt_set_func_t *)NULL, (setopt_get_func_t *)NULL  },
 #if defined (READLINE)
   { "emacs",     '\0', (int *)NULL, set_edit_mode, get_edit_mode },
 #endif
diff --git a/doc/bash.1 b/doc/bash.1
index 9a7a384..f2d33d9 100644
--- a/doc/bash.1
+++ b/doc/bash.1
@@ -9019,10 +9019,10 @@ function and not during execution of a script by \fB.\fP\^ or \fBsource\fP.
 Any command associated with the \fBRETURN\fP trap is executed
 before execution resumes after the function or script.
 .TP
-\fBset\fP [\fB\-\-abefhkmnptuvxBCEHPT\fP] [\fB\-o\fP \fIoption\-name\fP] [\fIarg\fP ...]
+\fBset\fP [\fB\-\-abcefhkmnptuvxBCEHPT\fP] [\fB\-o\fP \fIoption\-name\fP] [\fIarg\fP ...]
 .PD 0
 .TP
-\fBset\fP [\fB+abefhkmnptuvxBCEHPT\fP] [\fB+o\fP \fIoption\-name\fP] [\fIarg\fP ...]
+\fBset\fP [\fB+abcefhkmnptuvxBCEHPT\fP] [\fB+o\fP \fIoption\-name\fP] [\fIarg\fP ...]
 .PD
 Without options, the name and value of each shell variable are displayed
 in a format that can be reused as input
@@ -9051,6 +9051,10 @@ Report the status of terminated background jobs
 immediately, rather than before the next primary prompt.  This is
 effective only when job control is enabled.
 .TP 8
+.B \-c
+Escape a single ampersand if surrounded by non-blank or non-meta
+characters.
+.TP 8
 .B \-e
 Exit immediately if a
 \fIpipeline\fP (which may consist of a single \fIsimple command\fP),
@@ -9141,6 +9145,10 @@ Same as
 Same as
 .BR \-B .
 .TP 8
+.B compressedand
+Same as
+.BR \-c .
+.TP 8
 .B emacs
 Use an emacs-style command line editing interface.  This is enabled
 by default when the shell is interactive, unless the shell is started
diff --git a/doc/bash.info b/doc/bash.info
index fac6786..d79e19d 100644
--- a/doc/bash.info
+++ b/doc/bash.info
@@ -3950,8 +3950,8 @@ allows you to change the values of shell options and set the positional
 parameters, or to display the names and values of shell variables.
 
 'set'
-          set [--abefhkmnptuvxBCEHPT] [-o OPTION-NAME] [ARGUMENT ...]
-          set [+abefhkmnptuvxBCEHPT] [+o OPTION-NAME] [ARGUMENT ...]
+          set [--abcefhkmnptuvxBCEHPT] [-o OPTION-NAME] [ARGUMENT ...]
+          set [+abcefhkmnptuvxBCEHPT] [+o OPTION-NAME] [ARGUMENT ...]
 
      If no options or arguments are supplied, 'set' displays the names
      and values of all shell variables and functions, sorted according
@@ -3973,6 +3973,10 @@ parameters, or to display the names and values of shell variables.
           immediately, rather than before printing the next primary
           prompt.
 
+     '-c'
+          Escape a single ampersand when surrounded by non-blank or
+          non-meta characters.
+
      '-e'
           Exit immediately if a pipeline (*note Pipelines::), which may
           consist of a single simple command (*note Simple Commands::),
@@ -4035,6 +4039,9 @@ parameters, or to display the names and values of shell variables.
           'braceexpand'
                Same as '-B'.
 
+          'compressedand'
+               Same as '-c'.
+
           'emacs'
                Use an 'emacs'-style line editing interface (*note
                Command Line Editing::).  This also affects the editing
diff --git a/flags.c b/flags.c
index 4b94fb0..d41eb1d 100644
--- a/flags.c
+++ b/flags.c
@@ -92,6 +92,10 @@ int unbound_vars_is_error = 0;
 int echo_input_at_read = 0;
 int verbose_flag = 0;
 
+/* Non-zero means to escape a single ampersand surrounded by non-blank or
+   non-meta charcters; `echo this&that` -> "this&that" */
+int compressedand = 0;
+
 /* Non-zero means type out the command definition after reading, but
    before executing. */
 int echo_command_at_execute = 0;
@@ -183,6 +187,7 @@ const struct flags_alist shell_flags[] = {
 #if defined (JOB_CONTROL)
   { 'b', &asynchronous_notification },
 #endif /* JOB_CONTROL */
+  { 'c', &compressedand },
   { 'e', &errexit_flag },
   { 'f', &disallow_filename_globbing },
   { 'h', &hashing_enabled },
@@ -368,6 +373,8 @@ reset_shell_flags ()
   exit_immediately_on_error = errexit_flag = 0;
   echo_input_at_read = verbose_flag = 0;
 
+  compressedand = 0;
+
   hashing_enabled = interactive_comments = 1;
 
 #if defined (JOB_CONTROL)
diff --git a/flags.h b/flags.h
index d5ed334..ff44a45 100644
--- a/flags.h
+++ b/flags.h
@@ -42,7 +42,7 @@ extern char optflags[];
 
 extern int
   mark_modified_vars, errexit_flag, exit_immediately_on_error,
-  disallow_filename_globbing,
+  disallow_filename_globbing, compressedand,
   place_keywords_in_env, read_but_dont_execute,
   just_one_command, unbound_vars_is_error, echo_input_at_read, verbose_flag,
   echo_command_at_execute, no_invisible_vars, noclobber,
diff --git a/parse.y b/parse.y
index f415d2e..e2adce4 100644
--- a/parse.y
+++ b/parse.y
@@ -4732,6 +4732,23 @@ read_token_word (character)
 
       cd = current_delimiter (dstack);
 
+	/* 2020-04-21 EMP: Added a "compressed ampersand" logic that would automatically
+           escape the ampersand if it was surrounded by something that wasn't a space or
+           another meta character. the latter preserves special function of various dbl-
+           meta specials like redirects and AND_AND regardless of bounding.
+           this is all to allow unquoted URLs that have multiple query parameters. */
+        if (compressedand) {
+	    if MBTEST(character == '&') {
+		peek_char = shell_getc(0);
+		shell_ungetc(peek_char);
+
+		if MBTEST(!shellblank(peek_char) && !shellmeta(peek_char)) {
+		    pass_next_character++;
+		    goto got_character;
+		}
+	    }
+	}
+
       /* Handle backslashes.  Quote lots of things when not inside of
 	 double-quotes, quote some things inside of double-quotes. */
       if MBTEST(character == '\\')

Reply via email to