diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index d232ef895ac24ee83d7f46d652b8839b72e4051a..6e3525d76871021355bae70c5caf614a08ccb66d 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1,5 +1,5 @@
 <!--
-$Header: /cvsroot/pgsql/doc/src/sgml/ref/psql-ref.sgml,v 1.78 2002/10/11 23:03:48 petere Exp $
+$Header: /cvsroot/pgsql/doc/src/sgml/ref/psql-ref.sgml,v 1.79 2002/10/19 00:22:14 tgl Exp $
 PostgreSQL documentation
 -->
 
@@ -692,6 +692,7 @@ testdb=>
 
       <varlistentry>
         <term><literal>\copy <replaceable class="parameter">table</replaceable>
+	[ ( <replaceable class="parameter">column_list</replaceable> ) ]
         { <literal>from</literal> | <literal>to</literal> }
 	<replaceable class="parameter">filename</replaceable> | stdin | stdout
         [ <literal>with</literal> ] 
@@ -705,11 +706,12 @@ testdb=>
         Performs a frontend (client) copy. This is an operation that
         runs an <acronym>SQL</acronym> <xref linkend="SQL-COPY"
         endterm="SQL-COPY-title"> command, but instead of the backend's
-        reading or writing the specified file, and consequently
-        requiring backend access and special user privilege, as well as
-        being bound to the file system accessible by the backend,
+        reading or writing the specified file,
         <application>psql</application> reads or writes the file and
         routes the data between the backend and the local file system.
+	This means that file accessibility and privileges are those
+	of the local user, not the server, and no SQL superuser
+	privileges are required.
 	</para>
 
 	<para>
diff --git a/src/bin/psql/copy.c b/src/bin/psql/copy.c
index 7e1b05d5fe8ea841ce07ddef0fe0ef848c73127a..b70519be1590f8a805dcaee5b22862b8935e3cb1 100644
--- a/src/bin/psql/copy.c
+++ b/src/bin/psql/copy.c
@@ -3,7 +3,7 @@
  *
  * Copyright 2000 by PostgreSQL Global Development Group
  *
- * $Header: /cvsroot/pgsql/src/bin/psql/copy.c,v 1.27 2002/10/15 02:24:16 tgl Exp $
+ * $Header: /cvsroot/pgsql/src/bin/psql/copy.c,v 1.28 2002/10/19 00:22:14 tgl Exp $
  */
 #include "postgres_fe.h"
 #include "copy.h"
@@ -38,11 +38,15 @@ bool		copy_in_state;
  * parse_slash_copy
  * -- parses \copy command line
  *
- * Accepted syntax: \copy table|"table" [with oids] from|to filename|'filename' [with ] [ oids ] [ delimiter '<char>'] [ null as 'string' ]
+ * Accepted syntax: \copy table [(columnlist)] [with oids] from|to filename [with ] [ oids ] [ delimiter char] [ null as string ]
  * (binary is not here yet)
  *
  * Old syntax for backward compatibility: (2002-06-19):
- * \copy table|"table" [with oids] from|to filename|'filename' [ using delimiters '<char>'] [ with null as 'string' ]
+ * \copy table [(columnlist)] [with oids] from|to filename [ using delimiters char] [ with null as string ]
+ *
+ * table name can be double-quoted and can have a schema part.
+ * column names can be double-quoted.
+ * filename, char, and string can be single-quoted like SQL literals.
  *
  * returns a malloc'ed structure with the options, or NULL on parsing error
  */
@@ -50,6 +54,7 @@ bool		copy_in_state;
 struct copy_options
 {
 	char	   *table;
+	char	   *column_list;
 	char	   *file;			/* NULL = stdin/stdout */
 	bool		from;
 	bool		binary;
@@ -65,6 +70,7 @@ free_copy_options(struct copy_options * ptr)
 	if (!ptr)
 		return;
 	free(ptr->table);
+	free(ptr->column_list);
 	free(ptr->file);
 	free(ptr->delim);
 	free(ptr->null);
@@ -72,14 +78,32 @@ free_copy_options(struct copy_options * ptr)
 }
 
 
+/* catenate "more" onto "var", freeing the original value of *var */
+static void
+xstrcat(char **var, const char *more)
+{
+	char	   *newvar;
+
+	newvar = (char *) malloc(strlen(*var) + strlen(more) + 1);
+	if (!newvar)
+	{
+		psql_error("out of memory\n");
+		exit(EXIT_FAILURE);
+	}
+	strcpy(newvar, *var);
+	strcat(newvar, more);
+	free(*var);
+	*var = newvar;
+}
+
+
 static struct copy_options *
 parse_slash_copy(const char *args)
 {
 	struct copy_options *result;
 	char	   *line;
 	char	   *token;
-	bool		error = false;
-	char		quote;
+	const char *whitespace = " \t\n\r";
 
 	if (args)
 		line = xstrdup(args);
@@ -95,152 +119,183 @@ parse_slash_copy(const char *args)
 		exit(EXIT_FAILURE);
 	}
 
-	token = strtokx(line, " \t\n\r", "\"", '\\', &quote, NULL, pset.encoding);
+	token = strtokx(line, whitespace, ".,()", "\"",
+					0, false, pset.encoding);
 	if (!token)
-		error = true;
-	else
-	{
+		goto error;
+
 #ifdef NOT_USED
-		/* this is not implemented yet */
-		if (!quote && strcasecmp(token, "binary") == 0)
+	/* this is not implemented yet */
+	if (strcasecmp(token, "binary") == 0)
+	{
+		result->binary = true;
+		token = strtokx(NULL, whitespace, ".,()", "\"",
+						0, false, pset.encoding);
+		if (!token)
+			goto error;
+	}
+#endif
+
+	result->table = xstrdup(token);
+
+	token = strtokx(NULL, whitespace, ".,()", "\"",
+					0, false, pset.encoding);
+	if (!token)
+		goto error;
+
+	/*
+	 * strtokx() will not have returned a multi-character token starting with
+	 * '.', so we don't need strcmp() here.  Likewise for '(', etc, below.
+	 */
+	if (token[0] == '.')
+	{
+		/* handle schema . table */
+		xstrcat(&result->table, token);
+		token = strtokx(NULL, whitespace, ".,()", "\"",
+						0, false, pset.encoding);
+		if (!token)
+			goto error;
+		xstrcat(&result->table, token);
+		token = strtokx(NULL, whitespace, ".,()", "\"",
+						0, false, pset.encoding);
+		if (!token)
+			goto error;
+	}
+
+	if (token[0] == '(')
+	{
+		/* handle parenthesized column list */
+		result->column_list = xstrdup(token);
+		for (;;)
 		{
-			result->binary = true;
-			token = strtokx(NULL, " \t\n\r", "\"", '\\', &quote, NULL, pset.encoding);
+			token = strtokx(NULL, whitespace, ".,()", "\"",
+							0, false, pset.encoding);
+			if (!token || strchr(".,()", token[0]))
+				goto error;
+			xstrcat(&result->column_list, token);
+			token = strtokx(NULL, whitespace, ".,()", "\"",
+							0, false, pset.encoding);
 			if (!token)
-				error = true;
+				goto error;
+			xstrcat(&result->column_list, token);
+			if (token[0] == ')')
+				break;
+			if (token[0] != ',')
+				goto error;
 		}
-		if (token)
-#endif
-			result->table = xstrdup(token);
+		token = strtokx(NULL, whitespace, ".,()", "\"",
+						0, false, pset.encoding);
+		if (!token)
+			goto error;
 	}
 
-#ifdef USE_ASSERT_CHECKING
-	assert(error || result->table);
-#endif
-
-	if (!error)
+	/*
+	 * Allows old COPY syntax for backward compatibility
+	 * 2002-06-19
+	 */
+	if (strcasecmp(token, "with") == 0)
 	{
-		token = strtokx(NULL, " \t\n\r", NULL, '\\', NULL, NULL, pset.encoding);
+		token = strtokx(NULL, whitespace, NULL, NULL,
+						0, false, pset.encoding);
+		if (!token || strcasecmp(token, "oids") != 0)
+			goto error;
+		result->oids = true;
+
+		token = strtokx(NULL, whitespace, NULL, NULL,
+						0, false, pset.encoding);
 		if (!token)
-			error = true;
-		else
-		{
-			/*
-			 * Allows old COPY syntax for backward compatibility
-			 * 2002-06-19
-			 */
-			if (strcasecmp(token, "with") == 0)
-			{
-				token = strtokx(NULL, " \t\n\r", NULL, '\\', NULL, NULL, pset.encoding);
-				if (!token || strcasecmp(token, "oids") != 0)
-					error = true;
-				else
-					result->oids = true;
+			goto error;
+	}
 
-				if (!error)
-				{
-					token = strtokx(NULL, " \t\n\r", NULL, '\\', NULL, NULL, pset.encoding);
-					if (!token)
-						error = true;
-				}
-			}
+	if (strcasecmp(token, "from") == 0)
+		result->from = true;
+	else if (strcasecmp(token, "to") == 0)
+		result->from = false;
+	else
+		goto error;
 
-			if (!error && strcasecmp(token, "from") == 0)
-				result->from = true;
-			else if (!error && strcasecmp(token, "to") == 0)
-				result->from = false;
-			else
-				error = true;
-		}
-	}
+	token = strtokx(NULL, whitespace, NULL, "'",
+					'\\', true, pset.encoding);
+	if (!token)
+		goto error;
+
+	if (strcasecmp(token, "stdin") == 0 ||
+		strcasecmp(token, "stdout") == 0)
+		result->file = NULL;
+	else
+		result->file = xstrdup(token);
+
+	token = strtokx(NULL, whitespace, NULL, NULL,
+					0, false, pset.encoding);
 
-	if (!error)
+	/*
+	 * Allows old COPY syntax for backward compatibility
+	 * 2002-06-19
+	 */
+	if (token && strcasecmp(token, "using") == 0)
 	{
-		token = strtokx(NULL, " \t\n\r", "'", '\\', &quote, NULL, pset.encoding);
+		token = strtokx(NULL, whitespace, NULL, NULL,
+						0, false, pset.encoding);
+		if (!(token && strcasecmp(token, "delimiters") == 0))
+			goto error;
+		token = strtokx(NULL, whitespace, NULL, "'",
+						'\\', false, pset.encoding);
 		if (!token)
-			error = true;
-		else if (!quote && (strcasecmp(token, "stdin") == 0 || strcasecmp(token, "stdout") == 0))
-			result->file = NULL;
-		else
-			result->file = xstrdup(token);
+			goto error;
+		result->delim = xstrdup(token);
+		token = strtokx(NULL, whitespace, NULL, NULL,
+						0, false, pset.encoding);
 	}
 
-	if (!error)
+	if (token)
 	{
-		token = strtokx(NULL, " \t\n\r", NULL, '\\', NULL, NULL, pset.encoding);
-		if (token)
+		if (strcasecmp(token, "with") != 0)
+			goto error;
+		while ((token = strtokx(NULL, whitespace, NULL, NULL,
+								0, false, pset.encoding)) != NULL)
 		{
-			/*
-			 * Allows old COPY syntax for backward compatibility
-			 * 2002-06-19
-			 */
-			if (strcasecmp(token, "using") == 0)
+			if (strcasecmp(token, "delimiter") == 0)
 			{
-				token = strtokx(NULL, " \t\n\r", NULL, '\\', NULL, NULL, pset.encoding);
-				if (token && strcasecmp(token, "delimiters") == 0)
-				{
-					token = strtokx(NULL, " \t\n\r", "'", '\\', NULL, NULL, pset.encoding);
-					if (token)
-					{
-						result->delim = xstrdup(token);
-						token = strtokx(NULL, " \t\n\r", NULL, '\\', NULL, NULL, pset.encoding);
-					}
-					else
-						error = true;
-				}
+				token = strtokx(NULL, whitespace, NULL, "'",
+								'\\', false, pset.encoding);
+				if (token && strcasecmp(token, "as") == 0)
+					token = strtokx(NULL, whitespace, NULL, "'",
+									'\\', false, pset.encoding);
+				if (token)
+					result->delim = xstrdup(token);
 				else
-					error = true;
+					goto error;
 			}
-		}
-	}
-
-	if (!error && token)
-	{
-		if (strcasecmp(token, "with") == 0)
-		{
-			while (!error && (token = strtokx(NULL, " \t\n\r", NULL, '\\', NULL, NULL, pset.encoding)))
+			else if (strcasecmp(token, "null") == 0)
 			{
-				if (strcasecmp(token, "delimiter") == 0)
-				{
-					token = strtokx(NULL, " \t\n\r", "'", '\\', NULL, NULL, pset.encoding);
-					if (token && strcasecmp(token, "as") == 0)
-						token = strtokx(NULL, " \t\n\r", "'", '\\', NULL, NULL, pset.encoding);
-					if (token)
-						result->delim = xstrdup(token);
-					else
-						error = true;
-				}
-				else if (strcasecmp(token, "null") == 0)
-				{
-					token = strtokx(NULL, " \t\n\r", "'", '\\', NULL, NULL, pset.encoding);
-					if (token && strcasecmp(token, "as") == 0)
-						token = strtokx(NULL, " \t\n\r", "'", '\\', NULL, NULL, pset.encoding);
-					if (token)
-						result->null = xstrdup(token);
-					else
-						error = true;
-				}
+				token = strtokx(NULL, whitespace, NULL, "'",
+								'\\', false, pset.encoding);
+				if (token && strcasecmp(token, "as") == 0)
+					token = strtokx(NULL, whitespace, NULL, "'",
+									'\\', false, pset.encoding);
+				if (token)
+					result->null = xstrdup(token);
 				else
-					error = true;
+					goto error;
 			}
+			else
+				goto error;
 		}
-		else
-			error = true;
 	}
 
 	free(line);
 
-	if (error)
-	{
-		if (token)
-			psql_error("\\copy: parse error at '%s'\n", token);
-		else
-			psql_error("\\copy: parse error at end of line\n");
-		free_copy_options(result);
-		return NULL;
-	}
+	return result;
+
+error:
+	if (token)
+		psql_error("\\copy: parse error at '%s'\n", token);
 	else
-		return result;
+		psql_error("\\copy: parse error at end of line\n");
+	free_copy_options(result);
+	free(line);
+
+	return NULL;
 }
 
 
@@ -272,7 +327,11 @@ do_copy(const char *args)
 	if (options->binary)
 		appendPQExpBuffer(&query, "BINARY ");
 
-	appendPQExpBuffer(&query, "\"%s\" ", options->table);
+	appendPQExpBuffer(&query, "%s ", options->table);
+
+	if (options->column_list)
+		appendPQExpBuffer(&query, "%s ", options->column_list);
+
 	/* Uses old COPY syntax for backward compatibility 2002-06-19 */
 	if (options->oids)
 		appendPQExpBuffer(&query, "WITH OIDS ");
@@ -285,10 +344,22 @@ do_copy(const char *args)
 
 	/* Uses old COPY syntax for backward compatibility 2002-06-19 */
 	if (options->delim)
-		appendPQExpBuffer(&query, " USING DELIMITERS '%s'", options->delim);
+	{
+		if (options->delim[0] == '\'')
+			appendPQExpBuffer(&query, " USING DELIMITERS %s",
+							  options->delim);
+		else
+			appendPQExpBuffer(&query, " USING DELIMITERS '%s'",
+							  options->delim);
+	}
 
 	if (options->null)
-		appendPQExpBuffer(&query, " WITH NULL AS '%s'", options->null);
+	{
+		if (options->null[0] == '\'')
+			appendPQExpBuffer(&query, " WITH NULL AS %s", options->null);
+		else
+			appendPQExpBuffer(&query, " WITH NULL AS '%s'", options->null);
+	}
 
 	if (options->from)
 	{
diff --git a/src/bin/psql/stringutils.c b/src/bin/psql/stringutils.c
index 8ff58c464301cc63700b3552fc3ca56254dfdb24..0401c92718c109da2d7f33e6048bba89d13f7fce 100644
--- a/src/bin/psql/stringutils.c
+++ b/src/bin/psql/stringutils.c
@@ -1,45 +1,61 @@
 /*
  * psql - the PostgreSQL interactive terminal
  *
- * Copyright 2000 by PostgreSQL Global Development Group
+ * Copyright 2000-2002 by PostgreSQL Global Development Group
  *
- * $Header: /cvsroot/pgsql/src/bin/psql/stringutils.c,v 1.30 2002/08/27 20:16:49 petere Exp $
+ * $Header: /cvsroot/pgsql/src/bin/psql/stringutils.c,v 1.31 2002/10/19 00:22:14 tgl Exp $
  */
 #include "postgres_fe.h"
-#include "stringutils.h"
-#include "settings.h"
 
-#include <ctype.h>
 #include <assert.h>
+#include <ctype.h>
 
 #include "libpq-fe.h"
+#include "settings.h"
+#include "stringutils.h"
 
 
-
-static void unescape_quotes(char *source, int quote, int escape);
+static void strip_quotes(char *source, char quote, char escape, int encoding);
 
 
 /*
  * Replacement for strtok() (a.k.a. poor man's flex)
  *
- * The calling convention is similar to that of strtok.
+ * Splits a string into tokens, returning one token per call, then NULL
+ * when no more tokens exist in the given string.
+ *
+ * The calling convention is similar to that of strtok, but with more
+ * frammishes.
+ *
  * s -			string to parse, if NULL continue parsing the last string
- * delim -		set of characters that delimit tokens (usually whitespace)
- * quote -		set of characters that quote stuff, they're not part of the token
- * escape -		character than can quote quotes
- * was_quoted - if not NULL, stores the quoting character if any was encountered
- * token_pos -	if not NULL, receives a count to the start of the token in the
- *				parsed string
+ * whitespace -	set of whitespace characters that separate tokens
+ * delim -		set of non-whitespace separator characters (or NULL)
+ * quote -		set of characters that can quote a token (NULL if none)
+ * escape -		character that can quote quotes (0 if none)
+ * del_quotes -	if TRUE, strip quotes from the returned token, else return
+ *				it exactly as found in the string
+ * encoding -	the active character-set encoding
+ *
+ * Characters in 'delim', if any, will be returned as single-character
+ * tokens unless part of a quoted token.
+ *
+ * Double occurences of the quoting character are always taken to represent
+ * a single quote character in the data.  If escape isn't 0, then escape
+ * followed by anything (except \0) is a data character too.
  *
  * Note that the string s is _not_ overwritten in this implementation.
+ *
+ * NB: it's okay to vary delim, quote, and escape from one call to the
+ * next on a single source string, but changing whitespace is a bad idea
+ * since you might lose data.
  */
 char *
 strtokx(const char *s,
+		const char *whitespace,
 		const char *delim,
 		const char *quote,
-		int escape,
-		char *was_quoted,
-		unsigned int *token_pos,
+		char escape,
+		bool del_quotes,
 		int encoding)
 {
 	static char *storage = NULL;/* store the local copy of the users
@@ -50,23 +66,32 @@ strtokx(const char *s,
 	/* variously abused variables: */
 	unsigned int offset;
 	char	   *start;
-	char	   *cp = NULL;
+	char	   *p;
 
 	if (s)
 	{
 		free(storage);
-		storage = strdup(s);
+		/*
+		 * We may need extra space to insert delimiter nulls for adjacent
+		 * tokens.  2X the space is a gross overestimate, but it's
+		 * unlikely that this code will be used on huge strings anyway.
+		 */
+		storage = (char *) malloc(2 * strlen(s) + 1);
+		if (!storage)
+			return NULL;		/* really "out of memory" */
+		strcpy(storage, s);
 		string = storage;
 	}
 
 	if (!storage)
 		return NULL;
 
-	/* skip leading "whitespace" */
-	offset = strspn(string, delim);
+	/* skip leading whitespace */
+	offset = strspn(string, whitespace);
+	start = &string[offset];
 
-	/* end of string reached */
-	if (string[offset] == '\0')
+	/* end of string reached? */
+	if (*start == '\0')
 	{
 		/* technically we don't need to free here, but we're nice */
 		free(storage);
@@ -75,118 +100,165 @@ strtokx(const char *s,
 		return NULL;
 	}
 
-	/* test if quoting character */
-	if (quote)
-		cp = strchr(quote, string[offset]);
-
-	if (cp)
+	/* test if delimiter character */
+	if (delim && strchr(delim, *start))
 	{
-		/* okay, we have a quoting character, now scan for the closer */
-		char	   *p;
+		/*
+		 * If not at end of string, we need to insert a null to terminate
+		 * the returned token.  We can just overwrite the next character
+		 * if it happens to be in the whitespace set ... otherwise move over
+		 * the rest of the string to make room.  (This is why we allocated
+		 * extra space above).
+		 */
+		p = start + 1;
+		if (*p != '\0')
+		{
+			if (!strchr(whitespace, *p))
+				memmove(p + 1, p, strlen(p) + 1);
+			*p = '\0';
+			string = p + 1;
+		}
+		else
+		{
+			/* at end of string, so no extra work */
+			string = p;
+		}
 
-		start = &string[offset + 1];
+		return start;
+	}
 
-		if (token_pos)
-			*token_pos = start - storage;
+	/* test if quoting character */
+	if (quote && strchr(quote, *start))
+	{
+		/* okay, we have a quoted token, now scan for the closer */
+		char		thisquote = *start;
 
-		for (p = start;
-			 *p && (*p != *cp || *(p - 1) == escape);
-			 p += PQmblen(p, encoding)
-			);
+		for (p = start + 1; *p; p += PQmblen(p, encoding))
+		{
+			if (*p == escape && p[1] != '\0')
+				p++;			/* process escaped anything */
+			else if (*p == thisquote && p[1] == thisquote)
+				p++;			/* process doubled quote */
+			else if (*p == thisquote)
+			{
+				p++;			/* skip trailing quote */
+				break;
+			}
+		}
 
-		/* not yet end of string? */
+		/*
+		 * If not at end of string, we need to insert a null to terminate
+		 * the returned token.  See notes above.
+		 */
 		if (*p != '\0')
 		{
+			if (!strchr(whitespace, *p))
+				memmove(p + 1, p, strlen(p) + 1);
 			*p = '\0';
 			string = p + 1;
-			if (was_quoted)
-				*was_quoted = *cp;
-			unescape_quotes(start, *cp, escape);
-			return start;
 		}
 		else
 		{
-			if (was_quoted)
-				*was_quoted = *cp;
+			/* at end of string, so no extra work */
 			string = p;
-
-			unescape_quotes(start, *cp, escape);
-			return start;
 		}
+
+		/* Clean up the token if caller wants that */
+		if (del_quotes)
+			strip_quotes(start, thisquote, escape, encoding);
+
+		return start;
 	}
 
-	/* otherwise no quoting character. scan till next delimiter */
-	start = &string[offset];
+	/*
+	 * Otherwise no quoting character.  Scan till next whitespace,
+	 * delimiter or quote.  NB: at this point, *start is known not to be
+	 * '\0', whitespace, delim, or quote, so we will consume at least
+	 * one character.
+	 */
+	offset = strcspn(start, whitespace);
 
-	if (token_pos)
-		*token_pos = start - storage;
+	if (delim)
+	{
+		unsigned int offset2 = strcspn(start, delim);
 
-	offset = strcspn(start, delim);
-	if (was_quoted)
-		*was_quoted = 0;
+		if (offset > offset2)
+			offset = offset2;
+	}
 
-	if (start[offset] != '\0')
+	if (quote)
 	{
-		start[offset] = '\0';
-		string = &start[offset] + 1;
+		unsigned int offset2 = strcspn(start, quote);
 
-		return start;
+		if (offset > offset2)
+			offset = offset2;
+	}
+
+	p = start + offset;
+
+	/*
+	 * If not at end of string, we need to insert a null to terminate
+	 * the returned token.  See notes above.
+	 */
+	if (*p != '\0')
+	{
+		if (!strchr(whitespace, *p))
+			memmove(p + 1, p, strlen(p) + 1);
+		*p = '\0';
+		string = p + 1;
 	}
 	else
 	{
-		string = &start[offset];
-		return start;
+		/* at end of string, so no extra work */
+		string = p;
 	}
-}
-
 
+	return start;
+}
 
 
 /*
- * unescape_quotes
+ * strip_quotes
  *
- * Resolves escaped quotes. Used by strtokx above.
+ * Remove quotes from the string at *source.  Leading and trailing occurrences
+ * of 'quote' are removed; embedded double occurrences of 'quote' are reduced
+ * to single occurrences; if 'escape' is not 0 then 'escape' removes special
+ * significance of next character.
+ *
+ * Note that the source string is overwritten in-place.
  */
 static void
-unescape_quotes(char *source, int quote, int escape)
+strip_quotes(char *source, char quote, char escape, int encoding)
 {
-	char	   *p;
-	char	   *destination,
-			   *tmp;
+	char	   *src;
+	char	   *dst;
 
 #ifdef USE_ASSERT_CHECKING
 	assert(source);
+	assert(quote);
 #endif
 
-	destination = calloc(1, strlen(source) + 1);
-	if (!destination)
-	{
-		perror("calloc");
-		exit(EXIT_FAILURE);
-	}
+	src = dst = source;
 
-	tmp = destination;
+	if (*src && *src == quote)
+		src++;					/* skip leading quote */
 
-	for (p = source; *p; p++)
+	while (*src)
 	{
-		char		c;
-
-		if (*p == escape && *(p + 1) && quote == *(p + 1))
-		{
-			c = *(p + 1);
-			p++;
-		}
-		else
-			c = *p;
-
-		*tmp = c;
-		tmp++;
+		char		c = *src;
+		int			i;
+
+		if (c == quote && src[1] == '\0')
+			break;				/* skip trailing quote */
+		else if (c == quote && src[1] == quote)
+			src++;				/* process doubled quote */
+		else if (c == escape && src[1] != '\0')
+			src++;				/* process escaped character */
+
+		i = PQmblen(src, encoding);
+		while (i--)
+			*dst++ = *src++;
 	}
 
-	/* Terminating null character */
-	*tmp = '\0';
-
-	strcpy(source, destination);
-
-	free(destination);
+	*dst = '\0';
 }
diff --git a/src/bin/psql/stringutils.h b/src/bin/psql/stringutils.h
index faafc606d5e18813385caa7281a51ed6767142b7..62c8ba9e3af7acb577715016e0db57b513c7feb1 100644
--- a/src/bin/psql/stringutils.h
+++ b/src/bin/psql/stringutils.h
@@ -1,9 +1,9 @@
 /*
  * psql - the PostgreSQL interactive terminal
  *
- * Copyright 2000 by PostgreSQL Global Development Group
+ * Copyright 2000-2002 by PostgreSQL Global Development Group
  *
- * $Header: /cvsroot/pgsql/src/bin/psql/stringutils.h,v 1.17 2001/11/05 17:46:31 momjian Exp $
+ * $Id: stringutils.h,v 1.18 2002/10/19 00:22:14 tgl Exp $
  */
 #ifndef STRINGUTILS_H
 #define STRINGUTILS_H
@@ -11,11 +11,11 @@
 /* The cooler version of strtok() which knows about quotes and doesn't
  * overwrite your input */
 extern char *strtokx(const char *s,
+		const char *whitespace,
 		const char *delim,
 		const char *quote,
-		int escape,
-		char *was_quoted,
-		unsigned int *token_pos,
+		char escape,
+		bool del_quotes,
 		int encoding);
 
 #endif   /* STRINGUTILS_H */