Module Name:    src
Committed By:   abhinav
Date:           Sun Mar 31 03:04:58 UTC 2019

Modified Files:
        src/lib/libedit: filecomplete.c
        src/lib/libedit/TEST: test_filecompletion.c

Log Message:
Perform quoting of filename completions when there are multiple matches as well

Quoting of special characters in filename completion was implemented for single 
match
case, this enables it for multiple matches as well. For example:

$ touch 'foo bar'
$ touch 'foo baz'
$ ls fo<TAB>
autocompletes to =>
$ ls foo\ ba
hitting <TAB> again shows:
foo bar foo baz

This required unescaping escape sequences generated during last completion
in order to find the word to complete.

While there, also update the test to include cases for multiple matches.

Reviewed by christos


To generate a diff of this commit:
cvs rdiff -u -r1.52 -r1.53 src/lib/libedit/filecomplete.c
cvs rdiff -u -r1.3 -r1.4 src/lib/libedit/TEST/test_filecompletion.c

Please note that diffs are not public domain; they are subject to the
copyright notices on the relevant files.

Modified files:

Index: src/lib/libedit/filecomplete.c
diff -u src/lib/libedit/filecomplete.c:1.52 src/lib/libedit/filecomplete.c:1.53
--- src/lib/libedit/filecomplete.c:1.52	Sun Mar 24 16:42:49 2019
+++ src/lib/libedit/filecomplete.c	Sun Mar 31 03:04:57 2019
@@ -1,4 +1,4 @@
-/*	$NetBSD: filecomplete.c,v 1.52 2019/03/24 16:42:49 abhinav Exp $	*/
+/*	$NetBSD: filecomplete.c,v 1.53 2019/03/31 03:04:57 abhinav Exp $	*/
 
 /*-
  * Copyright (c) 1997 The NetBSD Foundation, Inc.
@@ -31,7 +31,7 @@
 
 #include "config.h"
 #if !defined(lint) && !defined(SCCSID)
-__RCSID("$NetBSD: filecomplete.c,v 1.52 2019/03/24 16:42:49 abhinav Exp $");
+__RCSID("$NetBSD: filecomplete.c,v 1.53 2019/03/31 03:04:57 abhinav Exp $");
 #endif /* not lint && not SCCSID */
 
 #include <sys/types.h>
@@ -159,6 +159,23 @@ needs_escaping(char c)
 	}
 }
 
+
+static wchar_t *
+unescape_string(const wchar_t *string, size_t length)
+{
+	wchar_t *unescaped = el_malloc(sizeof(*string) * (length + 1));
+	if (unescaped == NULL)
+		return NULL;
+	size_t j = 0;
+	for (size_t i = 0; i < length ; i++) {
+		if (string[i] == '\\')
+			continue;
+		unescaped[j++] = string[i];
+	}
+	unescaped[j] = 0;
+	return unescaped;
+}
+
 static char *
 escape_filename(EditLine * el, const char *filename)
 {
@@ -172,6 +189,8 @@ escape_filename(EditLine * el, const cha
 	size_t d_quoted = 0;	/* does the input contain a double quote */
 	char *escaped_str;
 	wchar_t *temp = el->el_line.buffer;
+	if (filename == NULL)
+		return NULL;
 
 	while (temp != el->el_line.cursor) {
 		/*
@@ -534,9 +553,7 @@ find_word_to_complete(const wchar_t * cu
 {
 	/* We now look backwards for the start of a filename/variable word */
 	const wchar_t *ctemp = cursor;
-	int cursor_at_quote;
 	size_t len;
-	wchar_t *temp;
 
 	/* if the cursor is placed at a slash or a quote, we need to find the
 	 * word before it
@@ -546,30 +563,34 @@ find_word_to_complete(const wchar_t * cu
 		case '\\':
 		case '\'':
 		case '"':
-			cursor_at_quote = 1;
 			ctemp--;
 			break;
 		default:
-			cursor_at_quote = 0;
+			break;
 		}
-	} else
-		cursor_at_quote = 0;
+	}
 
-	while (ctemp > buffer
-	    && !wcschr(word_break, ctemp[-1])
-	    && (!special_prefixes || !wcschr(special_prefixes, ctemp[-1])))
+	for (;;) {
+		if (ctemp <= buffer)
+			break;
+		if (wcschr(word_break, ctemp[-1])) {
+			if (ctemp - buffer >= 2 && ctemp[-2] == '\\') {
+				ctemp -= 2;
+				continue;
+			} else
+				break;
+		}
+		if (special_prefixes && wcschr(special_prefixes, ctemp[-1]))
+			break;
 		ctemp--;
+	}
 
-	len = (size_t) (cursor - ctemp - cursor_at_quote);
-	temp = el_malloc((len + 1) * sizeof(*temp));
-	if (temp == NULL)
-		return NULL;
-	(void) wcsncpy(temp, ctemp, len);
-	temp[len] = '\0';
-	if (cursor_at_quote)
-		len++;
+	len = (size_t) (cursor - ctemp);
 	*length = len;
-	return temp;
+	wchar_t *unescaped_word = unescape_string(ctemp, len);
+	if (unescaped_word == NULL)
+		return NULL;
+	return unescaped_word;
 }
 
 /*
@@ -595,6 +616,7 @@ fn_complete(EditLine *el,
 	const LineInfoW *li;
 	wchar_t *temp;
 	char **matches;
+	char *completion;
 	size_t len;
 	int what_to_do = '\t';
 	int retval = CC_NORM;
@@ -649,33 +671,32 @@ fn_complete(EditLine *el,
 
 		if (matches[0][0] != '\0') {
 			el_deletestr(el, (int) len);
+			if (!attempted_completion_function)
+				completion = escape_filename(el, matches[0]);
+			else
+				completion = strdup(matches[0]);
+			if (completion == NULL)
+				goto out;
 			if (single_match) {
 				/*
 				 * We found exact match. Add a space after
 				 * it, unless we do filename completion and the
 				 * object is a directory. Also do necessary escape quoting
 				 */
-				char *completion;
-				if (!attempted_completion_function)
-					completion = escape_filename(el, matches[0]);
-				else
-					completion = strdup(matches[0]);
-				if (completion == NULL)
-					goto out;
 				el_winsertstr(el,
 					ct_decode_string(completion, &el->el_scratch));
 				el_winsertstr(el,
 						ct_decode_string((*app_func)(completion),
 							&el->el_scratch));
-				free(completion);
 			} else {
 				/*
 				 * Only replace the completed string with common part of
 				 * possible matches if there is possible completion.
 				 */
 				el_winsertstr(el,
-					ct_decode_string(matches[0], &el->el_scratch));
+					ct_decode_string(completion, &el->el_scratch));
 			}
+			free(completion);
 		}
 
 

Index: src/lib/libedit/TEST/test_filecompletion.c
diff -u src/lib/libedit/TEST/test_filecompletion.c:1.3 src/lib/libedit/TEST/test_filecompletion.c:1.4
--- src/lib/libedit/TEST/test_filecompletion.c:1.3	Fri May  4 16:39:15 2018
+++ src/lib/libedit/TEST/test_filecompletion.c	Sun Mar 31 03:04:57 2019
@@ -1,4 +1,4 @@
-/*	$NetBSD: test_filecompletion.c,v 1.3 2018/05/04 16:39:15 abhinav Exp $	*/
+/*	$NetBSD: test_filecompletion.c,v 1.4 2019/03/31 03:04:57 abhinav Exp $	*/
 
 /*-
  * Copyright (c) 2017 Abhinav Upadhyay <abhi...@netbsd.org>
@@ -45,7 +45,7 @@
 typedef struct {
 	const wchar_t *user_typed_text; /* The actual text typed by the user on the terminal */
 	const char *completion_function_input ; /*the text received by fn_filename_completion_function */
-	const char *expanded_text; /* the value to which completion_function_input should be expanded */
+	const char *expanded_text[2]; /* the value to which completion_function_input should be expanded */
 	const wchar_t *escaped_output; /* expected escaped value of expanded_text */
 } test_input;
 
@@ -54,446 +54,463 @@ static test_input inputs[] = {
 		/* simple test for escaping angular brackets */
 		L"ls ang",
 		"ang",
-		"ang<ular>test",
+		{"ang<ular>test", NULL},
 		L"ls ang\\<ular\\>test "
 	},
 	{
 		/* test angular bracket inside double quotes: ls "dq_ang */
 		L"ls \"dq_ang",
 		"dq_ang",
-		"dq_ang<ular>test",
+		{"dq_ang<ular>test", NULL},
 		L"ls \"dq_ang<ular>test\" "
 	},
 	{
 		/* test angular bracket inside singlq quotes: ls "sq_ang */
 		L"ls 'sq_ang",
 		"sq_ang",
-		"sq_ang<ular>test",
+		{"sq_ang<ular>test", NULL},
 		L"ls 'sq_ang<ular>test' "
 	},
 	{
 		/* simple test for backslash */
 		L"ls back",
 		"back",
-		"backslash\\test",
+		{"backslash\\test", NULL},
 		L"ls backslash\\\\test "
 	},
 	{
 		/* backslash inside single quotes */
 		L"ls 'sback",
 		"sback",
-		"sbackslash\\test",
+		{"sbackslash\\test", NULL},
 		L"ls 'sbackslash\\test' "
 	},
 	{
 		/* backslash inside double quotes */
 		L"ls \"dback",
 		"dback",
-		"dbackslash\\test",
+		{"dbackslash\\test", NULL},
 		L"ls \"dbackslash\\\\test\" "
 	},
 	{
 		/* test braces */
 		L"ls br",
 		"br",
-		"braces{test}",
+		{"braces{test}", NULL},
 		L"ls braces\\{test\\} "
 	},
 	{
 		/* test braces inside single quotes */
 		L"ls 'sbr",
 		"sbr",
-		"sbraces{test}",
+		{"sbraces{test}", NULL},
 		L"ls 'sbraces{test}' "
 	},
 	{
 		/* test braces inside double quotes */
 		L"ls \"dbr",
 		"dbr",
-		"dbraces{test}",
+		{"dbraces{test}", NULL},
 		L"ls \"dbraces{test}\" "
 	},
 	{
 		/* test dollar */
 		L"ls doll",
 		"doll",
-		"doll$artest",
+		{"doll$artest", NULL},
 		L"ls doll\\$artest "
 	},
 	{
 		/* test dollar inside single quotes */
 		L"ls 'sdoll",
 		"sdoll",
-		"sdoll$artest",
+		{"sdoll$artest", NULL},
 		L"ls 'sdoll$artest' "
 	},
 	{
 		/* test dollar inside double quotes */
 		L"ls \"ddoll",
 		"ddoll",
-		"ddoll$artest",
+		{"ddoll$artest", NULL},
 		L"ls \"ddoll\\$artest\" "
 	},
 	{
 		/* test equals */
 		L"ls eq",
 		"eq",
-		"equals==test",
+		{"equals==test", NULL},
 		L"ls equals\\=\\=test "
 	},
 	{
 		/* test equals inside sinqle quotes */
 		L"ls 'seq",
 		"seq",
-		"sequals==test",
+		{"sequals==test", NULL},
 		L"ls 'sequals==test' "
 	},
 	{
 		/* test equals inside double quotes */
 		L"ls \"deq",
 		"deq",
-		"dequals==test",
+		{"dequals==test", NULL},
 		L"ls \"dequals==test\" "
 	},
 	{
 		/* test \n */
 		L"ls new",
 		"new",
-		"new\\nline",
+		{"new\\nline", NULL},
 		L"ls new\\\\nline "
 	},
 	{
 		/* test \n inside single quotes */
 		L"ls 'snew",
 		"snew",
-		"snew\nline",
+		{"snew\nline", NULL},
 		L"ls 'snew\nline' "
 	},
 	{
 		/* test \n inside double quotes */
 		L"ls \"dnew",
 		"dnew",
-		"dnew\nline",
+		{"dnew\nline", NULL},
 		L"ls \"dnew\nline\" "
 	},
 	{
 		/* test single space */
 		L"ls spac",
 		"spac",
-		"space test",
+		{"space test", NULL},
 		L"ls space\\ test "
 	},
 	{
 		/* test single space inside singlq quotes */
 		L"ls 's_spac",
 		"s_spac",
-		"s_space test",
+		{"s_space test", NULL},
 		L"ls 's_space test' "
 	},
 	{
 		/* test single space inside double quotes */
 		L"ls \"d_spac",
 		"d_spac",
-		"d_space test",
+		{"d_space test", NULL},
 		L"ls \"d_space test\" "
 	},
 	{
 		/* test multiple spaces */
 		L"ls multi",
 		"multi",
-		"multi space  test",
+		{"multi space  test", NULL},
 		L"ls multi\\ space\\ \\ test "
 	},
 	{
 		/* test multiple spaces inside single quotes */
 		L"ls 's_multi",
 		"s_multi",
-		"s_multi space  test",
+		{"s_multi space  test", NULL},
 		L"ls 's_multi space  test' "
 	},
 	{
 		/* test multiple spaces inside double quotes */
 		L"ls \"d_multi",
 		"d_multi",
-		"d_multi space  test",
+		{"d_multi space  test", NULL},
 		L"ls \"d_multi space  test\" "
 	},
 	{
 		/* test double quotes */
 		L"ls doub",
 		"doub",
-		"doub\"quotes",
+		{"doub\"quotes", NULL},
 		L"ls doub\\\"quotes "
 	},
 	{
 		/* test double quotes inside single quotes */
 		L"ls 's_doub",
 		"s_doub",
-		"s_doub\"quotes",
+		{"s_doub\"quotes", NULL},
 		L"ls 's_doub\"quotes' "
 	},
 	{
 		/* test double quotes inside double quotes */
 		L"ls \"d_doub",
 		"d_doub",
-		"d_doub\"quotes",
+		{"d_doub\"quotes", NULL},
 		L"ls \"d_doub\\\"quotes\" "
 	},
 	{
 		/* test multiple double quotes */
 		L"ls mud",
 		"mud",
-		"mud\"qu\"otes\"",
+		{"mud\"qu\"otes\"", NULL},
 		L"ls mud\\\"qu\\\"otes\\\" "
 	},
 	{
 		/* test multiple double quotes inside single quotes */
 		L"ls 'smud",
 		"smud",
-		"smud\"qu\"otes\"",
+		{"smud\"qu\"otes\"", NULL},
 		L"ls 'smud\"qu\"otes\"' "
 	},
 	{
 		/* test multiple double quotes inside double quotes */
 		L"ls \"dmud",
 		"dmud",
-		"dmud\"qu\"otes\"",
+		{"dmud\"qu\"otes\"", NULL},
 		L"ls \"dmud\\\"qu\\\"otes\\\"\" "
 	},
 	{
 		/* test one single quote */
 		L"ls sing",
 		"sing",
-		"single'quote",
+		{"single'quote", NULL},
 		L"ls single\\'quote "
 	},
 	{
 		/* test one single quote inside single quote */
 		L"ls 'ssing",
 		"ssing",
-		"ssingle'quote",
+		{"ssingle'quote", NULL},
 		L"ls 'ssingle'\\''quote' "
 	},
 	{
 		/* test one single quote inside double quote */
 		L"ls \"dsing",
 		"dsing",
-		"dsingle'quote",
+		{"dsingle'quote", NULL},
 		L"ls \"dsingle'quote\" "
 	},
 	{
 		/* test multiple single quotes */
 		L"ls mu_sing",
 		"mu_sing",
-		"mu_single''quotes''",
+		{"mu_single''quotes''", NULL},
 		L"ls mu_single\\'\\'quotes\\'\\' "
 	},
 	{
 		/* test multiple single quotes inside single quote */
 		L"ls 'smu_sing",
 		"smu_sing",
-		"smu_single''quotes''",
+		{"smu_single''quotes''", NULL},
 		L"ls 'smu_single'\\'''\\''quotes'\\\'''\\''' "
 	},
 	{
 		/* test multiple single quotes inside double quote */
 		L"ls \"dmu_sing",
 		"dmu_sing",
-		"dmu_single''quotes''",
+		{"dmu_single''quotes''", NULL},
 		L"ls \"dmu_single''quotes''\" "
 	},
 	{
 		/* test parenthesis */
 		L"ls paren",
 		"paren",
-		"paren(test)",
+		{"paren(test)", NULL},
 		L"ls paren\\(test\\) "
 	},
 	{
 		/* test parenthesis inside single quote */
 		L"ls 'sparen",
 		"sparen",
-		"sparen(test)",
+		{"sparen(test)", NULL},
 		L"ls 'sparen(test)' "
 	},
 	{
 		/* test parenthesis inside double quote */
 		L"ls \"dparen",
 		"dparen",
-		"dparen(test)",
+		{"dparen(test)", NULL},
 		L"ls \"dparen(test)\" "
 	},
 	{
 		/* test pipe */
 		L"ls pip",
 		"pip",
-		"pipe|test",
+		{"pipe|test", NULL},
 		L"ls pipe\\|test "
 	},
 	{
 		/* test pipe inside single quote */
 		L"ls 'spip",
 		"spip",
-		"spipe|test",
+		{"spipe|test", NULL},
 		L"ls 'spipe|test' ",
 	},
 	{
 		/* test pipe inside double quote */
 		L"ls \"dpip",
 		"dpip",
-		"dpipe|test",
+		{"dpipe|test", NULL},
 		L"ls \"dpipe|test\" "
 	},
 	{
 		/* test tab */
 		L"ls ta",
 		"ta",
-		"tab\ttest",
+		{"tab\ttest", NULL},
 		L"ls tab\\\ttest "
 	},
 	{
 		/* test tab inside single quote */
 		L"ls 'sta",
 		"sta",
-		"stab\ttest",
+		{"stab\ttest", NULL},
 		L"ls 'stab\ttest' "
 	},
 	{
 		/* test tab inside double quote */
 		L"ls \"dta",
 		"dta",
-		"dtab\ttest",
+		{"dtab\ttest", NULL},
 		L"ls \"dtab\ttest\" "
 	},
 	{
 		/* test back tick */
 		L"ls tic",
 		"tic",
-		"tick`test`",
+		{"tick`test`", NULL},
 		L"ls tick\\`test\\` "
 	},
 	{
 		/* test back tick inside single quote */
 		L"ls 'stic",
 		"stic",
-		"stick`test`",
+		{"stick`test`", NULL},
 		L"ls 'stick`test`' "
 	},
 	{
 		/* test back tick inside double quote */
 		L"ls \"dtic",
 		"dtic",
-		"dtick`test`",
+		{"dtick`test`", NULL},
 		L"ls \"dtick\\`test\\`\" "
 	},
 	{
 		/* test for @ */
 		L"ls at",
 		"at",
-		"atthe@rate",
+		{"atthe@rate", NULL},
 		L"ls atthe\\@rate "
 	},
 	{
 		/* test for @ inside single quote */
 		L"ls 'sat",
 		"sat",
-		"satthe@rate",
+		{"satthe@rate", NULL},
 		L"ls 'satthe@rate' "
 	},
 	{
 		/* test for @ inside double quote */
 		L"ls \"dat",
 		"dat",
-		"datthe@rate",
+		{"datthe@rate", NULL},
 		L"ls \"datthe@rate\" "
 	},
 	{
 		/* test ; */
 		L"ls semi",
 		"semi",
-		"semi;colon;test",
+		{"semi;colon;test", NULL},
 		L"ls semi\\;colon\\;test "
 	},
 	{
 		/* test ; inside single quote */
 		L"ls 'ssemi",
 		"ssemi",
-		"ssemi;colon;test",
+		{"ssemi;colon;test", NULL},
 		L"ls 'ssemi;colon;test' "
 	},
 	{
 		/* test ; inside double quote */
 		L"ls \"dsemi",
 		"dsemi",
-		"dsemi;colon;test",
+		{"dsemi;colon;test", NULL},
 		L"ls \"dsemi;colon;test\" "
 	},
 	{
 		/* test & */
 		L"ls amp",
 		"amp",
-		"ampers&and",
+		{"ampers&and", NULL},
 		L"ls ampers\\&and "
 	},
 	{
 		/* test & inside single quote */
 		L"ls 'samp",
 		"samp",
-		"sampers&and",
+		{"sampers&and", NULL},
 		L"ls 'sampers&and' "
 	},
 	{
 		/* test & inside double quote */
 		L"ls \"damp",
 		"damp",
-		"dampers&and",
+		{"dampers&and", NULL},
 		L"ls \"dampers&and\" "
 	},
 	{
 		/* test completion when cursor at \ */
 		L"ls foo\\",
 		"foo",
-		"foo bar",
+		{"foo bar", NULL},
 		L"ls foo\\ bar "
 	},
 	{
 		/* test completion when cursor at single quote */
 		L"ls foo'",
-		"foo",
-		"foo bar",
+		"foo'",
+		{"foo bar", NULL},
 		L"ls foo\\ bar "
 	},
 	{
 		/* test completion when cursor at double quote */
 		L"ls foo\"",
-		"foo",
-		"foo bar",
+		"foo\"",
+		{"foo bar", NULL},
 		L"ls foo\\ bar "
+	},
+	{
+		/* test multiple completion matches */
+		L"ls fo",
+		"fo",
+		{"foo bar", "foo baz"},
+		L"ls foo\\ ba"
+	},
+	{
+		L"ls ba",
+		"ba",
+		{"bar <bar>", "bar <baz>"},
+		L"ls bar\\ \\<ba"
 	}
 };
 
 static const wchar_t break_chars[] = L" \t\n\"\\'`@$><=;|&{(";
 
 /*
- * Custom completion function passed to fn_complete.
+ * Custom completion function passed to fn_complet, NULLe.
  * The function returns hardcoded completion matches
  * based on the test cases present in inputs[] (above)
  */
 static char *
 mycomplet_func(const char *text, int index)
 {
-	static char *last_input = NULL;
+	static int last_index = 0;
 	size_t i = 0;
-	if (last_input && strcmp(last_input, text) == 0) {
-		free(last_input);
-		last_input = NULL;
+	if (last_index == 2) {
+		last_index = 0;
 		return NULL;
 	}
-	last_input = strdup(text);
 
 	for (i = 0; i < sizeof(inputs)/sizeof(inputs[0]); i++) {
-		if (strcmp(text, inputs[i].completion_function_input) == 0)
-			return strdup(inputs[i].expanded_text);
+		if (strcmp(text, inputs[i].completion_function_input) == 0) {
+			if (inputs[i].expanded_text[last_index] != NULL)
+				return strdup(inputs[i].expanded_text[last_index++]);
+			else {
+				last_index = 0;
+				return NULL;
+			}
+		}
 	}
 
 	return NULL;

Reply via email to