Le 2011-03-31 21:47, Chet Ramey a écrit:

On 3/15/11 4:44 PM, Chet Ramey wrote:

The difference is that bash-4.1 expanded $HOME and left the expansion
as part of the replacement text.  Bash-4.2 tries to leave what the
user typed alone, but that leads to an inherently ambiguous situation: when do you quote the `$' in a filename (or, in this case, a directory
name)?  It could be a shell variable, and it could be a character in
the filename.

I've attached a patch that applies a heuristic.  If the directory name
contains a variable, and bash expands it, it removes the appropriate
characters from the set that causes readline to ask bash to quote the
filename.  It's the 90% solution.

It is not perfect: if a character in the filename needs to be quoted,
bash will still quote the `$' in the directory name.  It should handle
most of the cases, however.

I may have a partial solution: not a silver bullet, but making the "90% solution" go to 95%.

=== Idea ===

The idea behind: the part that the user typed is considered well formed (sufficently to return matches),
so try to preserve it, and do quote only the completion's additions.

=== Implementation ===

The implementation (attached) of this idea is still imperfect,
because to identify the user-typed part, I simply compare the first match to the input;
this works in the example's simple case:
as bash completion knows NOT to direxpand, the $HOME from the input is included in the returned matches, and so is considered to be preserved; the quoting happens later, on spaces.
This happens to work on unclosed quotes too (ls "$HOME/sp<TAB>),
because readline starts matching AFTER the quote, so both input and results start with $HOME/sp (without quote).

=== Limitations ===

This DOES NOT work when 1. user-typed part is fully quoted (ls "$HOME/space cowboys"/<TAB>), because readline then passes the quote to the completion function (thus making input and matches diverge), nor (theorically) when 2. a space precedes a var (ls $HOME/space\ cowboys/$SUBDIR/f<TAB>), because the input backslash will be detected as first difference, where to start quoting.

Thus, due to 1., my proposed patch will not resolve Pedro's case in
https://lists.gnu.org/archive/html/bug-bash/2021-06/msg00131.html
However, a workaround (without direxpand) can be to not close the quote:
  i='a b'
  ls "$i/<TAB> # Instead of ls "$i"/
(which btw seems to work without my patch).

=== Going further ===

Maybe readline could go further to resolve 1. (and reach a "97% solution"?) by handling specially when, in a closed quote string to complete, the quote character appears at first and last chars (or forelast if the last one is a directory separator): then skip the closing char to handle the string as an unclosed quote.
This would make "$i"/<TAB> work.
To test: "$i/part"/unquoted/"requoted/hey"/<TAB>: how will the middle " be handled?

=== Optionality ===

The patch can easily be deactivated (shopt?):
it works by computing a common prefix length and passing it to make_quoted_replacement,
to tell it where to start replacing.
So by forcing a common prefix length of 0, we're back on the current behavior. (by the way, this is how the patch itself falls back when it is confused by the quote_char having changed)

=== History ===

Yes, you read 2011, not 2021!

Sorry for digging up this 10-years-old subject (archeology fun!), from:
  https://lists.gnu.org/archive/html/bug-bash/2011-03/msg00235.html
but it seems it has never been resolved,
only circumvented by direxpanding which breaks the beauty of bash knowing how to preserve a $.
  https://lists.gnu.org/archive/html/bug-bash/2015-08/msg00181.html
https://askubuntu.com/questions/70750/how-to-get-bash-to-stop-escaping-during-tab-completion
(and I happen to get the problem every day)

I'm sending this to bug-readline, because it seems to be solvable by targeting readline only, but bash would be the first "client" for it (reports are found in bug-bash), and in fact I tested it and built the patch on bash's integrated lib/readline.

=== Tests ===

t=/tmp/btest ; i="$t/space cowboys" ; mkdir -p $t/solid "$i/con" "$i/contents" "$i/other" "$i/con/solid" "$i/con/space cowboys"

# Each test block lists:
# Typed sequence
Actual result with current bash + readline
Result with current bash + patched readline

# echo $t/<tab>
echo $t/s
echo $t/s

# echo $t/so<tab>
echo $t/solid/
echo $t/solid/

#echo $t/sp<tab>
echo \$t/space cowboys/ # \$ incorrect
echo $t/space cowboys/

# echo $t/space cowboys/<tab>
echo \$t/space cowboys/ # \$ incorrect
echo $t/space cowboys/

#echo "$t/sp<tab>
echo "\$t/space cowboys/" # \$ incorrect, \  unnecessary.
echo "$t/space cowboys"/

# /!\ Note there's still a problem when quotes are closed:
# echo "$t/space cowboys"/<tab>
echo \$t/space cowboys/ # \$ incorrect
echo \$t/space cowboys/ # Incorrect too
# In forelast case the unclosed " made insert_match pass only the unquoted subpart (starting at $t), which matched with the results. # In the current case insert_match passes the whole string including quotes (starting at "$t), thus match ("$t) and replacement ($t) did not match. # This is the same as https://lists.gnu.org/archive/html/bug-bash/2021-06/msg00131.html # This could perhaps be handled by passing longest_prefix() the quote_char, so that it accepts comparing "$t" with $t when quote_char is "
# (like compare_match does).
# Or forcing callers *NOT* to dequote their matches (by temporarily inhibiting rl_filename_dequoting_function, mapping it to a function that
# removes quotes but not backslashes?).

# /!\ Note that I did *NOT* regress-test against a litteral $ in a filename.

--
Guillaume
diff -ruw complete.c complete.c
--- complete.c	2020-08-27 17:28:29.000000000 +0200
+++ complete.c	2021-12-08 06:20:13.883142000 +0100
@@ -141,17 +141,18 @@
 static char **gen_completion_matches PARAMS((char *, int, int, rl_compentry_func_t *, int, int));
 
 static char **remove_duplicate_matches PARAMS((char **));
-static void insert_match PARAMS((char *, int, int, char *));
+static void insert_match PARAMS((char *, int, int, char *, int));
 static int append_to_match PARAMS((char *, int, int, int));
-static void insert_all_matches PARAMS((char **, int, char *));
+static void insert_all_matches PARAMS((char **, int, char *, int));
 static int complete_fncmp PARAMS((const char *, int, const char *, int));
 static void display_matches PARAMS((char **));
 static int compute_lcd_of_matches PARAMS((char **, int, const char *));
 static int postprocess_matches PARAMS((char ***, int));
+static int longest_prefix PARAMS((const char *, const char *));
 static int compare_match PARAMS((char *, const char *));
 static int complete_get_screenwidth PARAMS((void));
 
-static char *make_quoted_replacement PARAMS((char *, int, char *));
+static char *make_quoted_replacement PARAMS((char *, int, char *, int));
 
 /* **************************************************************** */
 /*								    */
@@ -1751,10 +1752,11 @@
 
 /* qc == pointer to quoting character, if any */
 static char *
-make_quoted_replacement (char *match, int mtype, char *qc)
+make_quoted_replacement (char *match, int mtype, char *qc, int replace_from)
 {
-  int should_quote, do_replace;
-  char *replacement;
+  int should_quote, do_replace, repl_offset;
+  char *original, *replacement, *full;
+  char oqc;
 
   /* If we are doing completion on quoted substrings, and any matches
      contain any of the completer_word_break_characters, then auto-
@@ -1764,7 +1766,8 @@
      inserted quote character when it no longer is necessary, such as
      if we change the string we are completing on and the new set of
      matches don't require a quoted substring. */
-  replacement = match;
+  original = replacement = match;
+  oqc = qc ? *qc : '\0';
 
   should_quote = match && rl_completer_quote_characters &&
 			rl_filename_completion_desired &&
@@ -1776,6 +1779,8 @@
 
   if (should_quote)
     {
+      if (replace_from > 0)
+        match = &match[replace_from];
       /* If there is a single match, see if we need to quote it.
          This also checks whether the common prefix of several
 	 matches needs to be quoted. */
@@ -1788,19 +1793,46 @@
 	 word break character in a potential match. */
       if (do_replace != NO_MATCH && rl_filename_quoting_function)
 	replacement = (*rl_filename_quoting_function) (match, do_replace, qc);
+      /* If we fed the quoting function only a partial copy of original,
+	 reconstitute the full string. */
+      if (replacement && replacement != original)
+	{
+	  /* If qc changed, it may have quoted at the start of the subpart;
+	     this potentially leaves the original quote opened, which can be
+	     a mess to handle. So fall back to the original behaviour, to let it
+	     quote-replace everything, without preserving replace_from chars. */
+	  if (oqc && *qc != oqc)
+	    {
+	      xfree (replacement);
+	      *qc = oqc;
+	      return make_quoted_replacement (original, mtype, qc, 0);
+	    }
+	  full = (char *)xmalloc (replace_from + strlen (replacement) + 1);
+      strncpy (full, original, replace_from);
+      repl_offset = 0;
+      /* If replacement starts with the quote character, and we were already in
+	 a quote, skip the replacement's one.
+	 Only do this in case of partial replace, because full replace gets
+	 handled by caller skipping the original quote. */
+      if (replace_from && qc && *qc && replacement[0] == *qc)
+        repl_offset = 1;
+      strcpy (&full[replace_from], &replacement[repl_offset]);
+	  xfree (replacement);
+	  replacement = full;
+	}
     }
   return (replacement);
 }
 
 static void
-insert_match (char *match, int start, int mtype, char *qc)
+insert_match (char *match, int start, int mtype, char *qc, int replace_from)
 {
   char *replacement, *r;
   char oqc;
   int end, rlen;
 
   oqc = qc ? *qc : '\0';
-  replacement = make_quoted_replacement (match, mtype, qc);
+  replacement = make_quoted_replacement (match, mtype, qc, replace_from);
 
   /* Now insert the match. */
   if (replacement)
@@ -1917,7 +1949,7 @@
 }
 
 static void
-insert_all_matches (char **matches, int point, char *qc)
+insert_all_matches (char **matches, int point, char *qc, int replace_from)
 {
   int i;
   char *rp;
@@ -1934,7 +1966,7 @@
     {
       for (i = 1; matches[i]; i++)
 	{
-	  rp = make_quoted_replacement (matches[i], SINGLE_MATCH, qc);
+	  rp = make_quoted_replacement (matches[i], SINGLE_MATCH, qc, replace_from);
 	  rl_insert_text (rp);
 	  rl_insert_text (" ");
 	  if (rp != matches[i])
@@ -1943,7 +1975,7 @@
     }
   else
     {
-      rp = make_quoted_replacement (matches[0], SINGLE_MATCH, qc);
+      rp = make_quoted_replacement (matches[0], SINGLE_MATCH, qc, replace_from);
       rl_insert_text (rp);
       rl_insert_text (" ");
       if (rp != matches[0])
@@ -1965,6 +1997,17 @@
   xfree (matches);
 }
 
+/* Return the length of the common prefix between two strings. */
+static int
+longest_prefix (const char *a, const char *b)
+{
+  int i;
+
+  for (i = -1; a[++i] && a[i] == b[i]; ) {}
+
+  return i;
+}
+
 /* Compare a possibly-quoted filename TEXT from the line buffer and a possible
    MATCH that is the product of filename completion, which acts on the dequoted
    text. */
@@ -1999,7 +2042,7 @@
 {
   char **matches;
   rl_compentry_func_t *our_func;
-  int start, end, delimiter, found_quote, i, nontrivial_lcd;
+  int start, end, delimiter, found_quote, i, nontrivial_lcd, preserved_prefix_length;
   char *text, *saved_line_buffer;
   char quote_char;
   int tlen, mlen, saved_last_completion_failed;
@@ -2032,6 +2075,7 @@
   /* nontrivial_lcd is set if the common prefix adds something to the word
      being completed. */
   nontrivial_lcd = matches && compare_match (text, matches[0]) != 0;
+  preserved_prefix_length = matches ? longest_prefix(text, matches[0]) : 0;
   if (what_to_do == '!' || what_to_do == '@')
     tlen = strlen (text);
   xfree (text);
@@ -2075,16 +2119,16 @@
       if (what_to_do == TAB)
         {
           if (*matches[0])
-	    insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char);
+	    insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char, preserved_prefix_length);
         }
       else if (*matches[0] && matches[1] == 0)
 	/* should we perform the check only if there are multiple matches? */
-	insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char);
+	insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char, preserved_prefix_length);
       else if (*matches[0])	/* what_to_do != TAB && multiple matches */
 	{
 	  mlen = *matches[0] ? strlen (matches[0]) : 0;
 	  if (mlen >= tlen)
-	    insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char);
+	    insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char, preserved_prefix_length);
 	}
 
       /* If there are more matches, ring the bell to indicate.
@@ -2117,7 +2161,7 @@
       break;
 
     case '*':
-      insert_all_matches (matches, start, &quote_char);
+      insert_all_matches (matches, start, &quote_char, preserved_prefix_length);
       break;
 
     case '?':
@@ -2125,7 +2169,7 @@
 	 but this attempt returned a single match. */
       if (saved_last_completion_failed && matches[0] && *matches[0] && matches[1] == 0)
 	{
-	  insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char);
+	  insert_match (matches[0], start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char, preserved_prefix_length);
 	  append_to_match (matches[0], delimiter, quote_char, nontrivial_lcd);
 	  break;
 	}
@@ -2787,11 +2831,11 @@
   if (match_list_index == 0 && match_list_size > 1)
     {
       rl_ding ();
-      insert_match (orig_text, orig_start, MULT_MATCH, &quote_char);
+      insert_match (orig_text, orig_start, MULT_MATCH, &quote_char, strlen (orig_text));
     }
   else
     {
-      insert_match (matches[match_list_index], orig_start, SINGLE_MATCH, &quote_char);
+      insert_match (matches[match_list_index], orig_start, SINGLE_MATCH, &quote_char, longest_prefix (matches[match_list_index], orig_text));
       append_to_match (matches[match_list_index], delimiter, quote_char,
 		       compare_match (orig_text, matches[match_list_index]));
     }
@@ -2905,7 +2949,7 @@
 	 code below should take care of it. */
       if (*matches[0])
 	{
-	  insert_match (matches[0], orig_start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char);
+	  insert_match (matches[0], orig_start, matches[1] ? MULT_MATCH : SINGLE_MATCH, &quote_char, longest_prefix (matches[0], orig_text));
 	  orig_end = orig_start + strlen (matches[0]);
 	  completion_changed_buffer = STREQ (orig_text, matches[0]) == 0;
 	}
@@ -2968,11 +3012,11 @@
   if (match_list_index == 0 && match_list_size > 1)
     {
       rl_ding ();
-      insert_match (matches[0], orig_start, MULT_MATCH, &quote_char);
+      insert_match (matches[0], orig_start, MULT_MATCH, &quote_char, longest_prefix (matches[0], orig_text));
     }
   else
     {
-      insert_match (matches[match_list_index], orig_start, SINGLE_MATCH, &quote_char);
+      insert_match (matches[match_list_index], orig_start, SINGLE_MATCH, &quote_char, longest_prefix (matches[match_list_index], orig_text));
       append_to_match (matches[match_list_index], delimiter, quote_char,
 		       compare_match (orig_text, matches[match_list_index]));
     }

Reply via email to