psql-file-completion-v1.patch

text/plain

Filename: psql-file-completion-v1.patch
Type: text/plain
Part: 0
Message: psql filename completion: quoting

Patch

Same data as JSON: GET /api/v1/attachments/:id/patch the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes. API reference →
Format: unified
Series: patch v1
File+
src/bin/psql/stringutils.c 0 0
diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c
index 3b5ce1b..77387dc 100644
*** a/src/bin/psql/stringutils.c
--- b/src/bin/psql/stringutils.c
***************
*** 272,274 **** strip_quotes(char *source, char quote, char escape, int encoding)
--- 272,343 ----
  
  	*dst = '\0';
  }
+ 
+ 
+ /*
+  * quote_if_needed
+  *
+  * Opposite of strip_quotes().  If "source" denotes itself literally without
+  * quoting or escaping, returns NULL.  Otherwise, returns a malloc'd copy with
+  * quoting and escaping applied:
+  *
+  * source -			string to parse
+  * entails_quote -	any of these present?  need outer quotes
+  * quote -			doubled within string, affixed to both ends
+  * escape -			doubled within string
+  * encoding -		the active character-set encoding
+  *
+  * Do not use this as a substitute for PQescapeStringConn().  Use it for
+  * strings to be parsed by strtokx() or psql_scan_slash_option().
+  */
+ char *
+ quote_if_needed(const char *source, const char *entails_quote,
+ 				char quote, char escape, int encoding)
+ {
+ 	const char *src;
+ 	char	   *ret;
+ 	char	   *dst;
+ 	bool		need_quotes = false;
+ 
+ 	psql_assert(source);
+ 	psql_assert(quote);
+ 
+ 	src = source;
+ 	dst = ret = pg_malloc(2 * strlen(src) + 3);	/* excess */
+ 
+ 	*dst++ = quote;
+ 
+ 	while (*src)
+ 	{
+ 		char		c = *src;
+ 		int			i;
+ 
+ 		if (c == quote)
+ 		{
+ 			need_quotes = true;
+ 			*dst++ = quote;
+ 		}
+ 		else if (c == escape)
+ 		{
+ 			need_quotes = true;
+ 			*dst++ = escape;
+ 		}
+ 		else if (strchr(entails_quote, c))
+ 			need_quotes = true;
+ 
+ 		i = PQmblen(src, encoding);
+ 		while (i--)
+ 			*dst++ = *src++;
+ 	}
+ 
+ 	*dst++ = quote;
+ 	*dst = '\0';
+ 
+ 	if (!need_quotes)
+ 	{
+ 		free(ret);
+ 		ret = NULL;
+ 	}
+ 
+ 	return ret;
+ }
diff --git a/src/bin/psql/stringuindex c7c5f38..c64fc58 100644
*** a/src/bin/psql/stringutils.h
--- b/src/bin/psql/stringutils.h
***************
*** 19,22 **** extern char *strtokx(const char *s,
--- 19,25 ----
  		bool del_quotes,
  		int encoding);
  
+ extern char *quote_if_needed(const char *source, const char *entails_quote,
+ 				char quote, char escape, int encoding);
+ 
  #endif   /* STRINGUTILS_H */
diff --git a/src/bin/psql/tab-comindex a27ef69..d226106 100644
*** a/src/bin/psql/tab-complete.c
--- b/src/bin/psql/tab-complete.c
***************
*** 670,675 **** static char *complete_from_list(const char *text, int state);
--- 670,676 ----
  static char *complete_from_const(const char *text, int state);
  static char **complete_from_variables(char *text,
  						const char *prefix, const char *suffix);
+ static char *complete_from_files(const char *text, int state);
  
  static PGresult *exec_query(const char *query);
  
***************
*** 1619,1625 **** psql_completion(char *text, int start, int end)
  			  pg_strcasecmp(prev3_wd, "BINARY") == 0) &&
  			 (pg_strcasecmp(prev_wd, "FROM") == 0 ||
  			  pg_strcasecmp(prev_wd, "TO") == 0))
! 		matches = completion_matches(text, filename_completion_function);
  
  	/* Handle COPY|BINARY <sth> FROM|TO filename */
  	else if ((pg_strcasecmp(prev4_wd, "COPY") == 0 ||
--- 1620,1629 ----
  			  pg_strcasecmp(prev3_wd, "BINARY") == 0) &&
  			 (pg_strcasecmp(prev_wd, "FROM") == 0 ||
  			  pg_strcasecmp(prev_wd, "TO") == 0))
! 	{
! 		completion_charp = "";
! 		matches = completion_matches(text, complete_from_files);
! 	}
  
  	/* Handle COPY|BINARY <sth> FROM|TO filename */
  	else if ((pg_strcasecmp(prev4_wd, "COPY") == 0 ||
***************
*** 2899,2905 **** psql_completion(char *text, int start, int end)
  			 strcmp(prev_wd, "\\s") == 0 ||
  			 strcmp(prev_wd, "\\w") == 0 || strcmp(prev_wd, "\\write") == 0
  		)
! 		matches = completion_matches(text, filename_completion_function);
  
  	/*
  	 * Finally, we look through the list of "things", such as TABLE, INDEX and
--- 2903,2912 ----
  			 strcmp(prev_wd, "\\s") == 0 ||
  			 strcmp(prev_wd, "\\w") == 0 || strcmp(prev_wd, "\\write") == 0
  		)
! 	{
! 		completion_charp = "\\";
! 		matches = completion_matches(text, complete_from_files);
! 	}
  
  	/*
  	 * Finally, we look through the list of "things", such as TABLE, INDEX and
***************
*** 3361,3366 **** complete_from_variables(char *text, const char *prefix, const char *suffix)
--- 3368,3420 ----
  }
  
  
+ /*
+  * This function wraps rl_filename_completion_function() to strip quotes from
+  * the input before searching for matches and to quote any matches for which
+  * the consuming command will require it.
+  */
+ static char *
+ complete_from_files(const char *text, int state)
+ {
+ 	static const char *unquoted_text;
+ 	char	   *unquoted_match;
+ 	char	   *ret = NULL;
+ 
+ 	if (state == 0)
+ 	{
+ 		/* Initialization: stash the unquoted input. */
+ 		unquoted_text = strtokx(text, "", NULL, "'", *completion_charp,
+ 								false, true, pset.encoding);
+ 		/* expect a NULL return for the empty string only */
+ 		if (!unquoted_text)
+ 		{
+ 			psql_assert(!*text);
+ 			unquoted_text = text;
+ 		}
+ 	}
+ 
+ 	unquoted_match = filename_completion_function(unquoted_text, state);
+ 	if (unquoted_match)
+ 	{
+ 		/*
+ 		 * Caller sets completion_charp to a zero- or one-character string
+ 		 * containing the escape character.  This is necessary since \copy has
+ 		 * no escape character, but every other backslash command recognizes
+ 		 * "\" as an escape character.  Since we have only two callers, don't
+ 		 * bother providing a macro to simplify this.
+ 		 */
+ 		ret = quote_if_needed(unquoted_match, " \t\r\n\"`",
+ 							  '\'', *completion_charp, pset.encoding);
+ 		if (ret)
+ 			free(unquoted_match);
+ 		else
+ 			ret = unquoted_match;
+ 	}
+ 
+ 	return ret;
+ }
+ 
+ 
  /* HELPER FUNCTIONS */