Re-implement psql's input scanning to use a flex-generated lexer, as per

recent discussion.  The lexer is used for both SQL command text and
backslash commands.  The purpose of this change is to make it easier to
track the behavior of the backend's SQL lexer --- essentially identical
flex rules are now used by psql.  Also, this cleans up a lot of very
squirrelly code in mainloop.c and command.c.  The flex code is somewhat
bulkier than the removed code, but should be lots easier to maintain.
This commit is contained in:
Tom Lane 2004-02-19 19:40:09 +00:00
parent 737f1cd44b
commit 4b39aa3a7c
8 changed files with 1805 additions and 838 deletions

View File

@ -1 +1,2 @@
psqlscan.c
sql_help.h

View File

@ -5,7 +5,7 @@
# Portions Copyright (c) 1996-2002, PostgreSQL Global Development Group
# Portions Copyright (c) 1994, Regents of the University of California
#
# $PostgreSQL: pgsql/src/bin/psql/Makefile,v 1.38 2003/11/29 19:52:06 pgsql Exp $
# $PostgreSQL: pgsql/src/bin/psql/Makefile,v 1.39 2004/02/19 19:40:08 tgl Exp $
#
#-------------------------------------------------------------------------
@ -19,7 +19,10 @@ override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) -DFRONTEND
OBJS= command.o common.o help.o input.o stringutils.o mainloop.o copy.o \
startup.o prompt.o variables.o large_obj.o print.o describe.o \
tab-complete.o mbprint.o
psqlscan.o tab-complete.o mbprint.o
FLEXFLAGS = -Cfe
all: submake-libpq submake-libpgport psql
@ -36,6 +39,13 @@ $(srcdir)/sql_help.h:
@echo "*** Perl is needed to build psql help."
endif
$(srcdir)/psqlscan.c: psqlscan.l
ifdef FLEX
$(FLEX) $(FLEXFLAGS) -o'$@' $<
else
@$(missing) flex $< $@
endif
distprep: $(srcdir)/sql_help.h
install: all installdirs
@ -47,8 +57,9 @@ installdirs:
uninstall:
rm -f $(DESTDIR)$(bindir)/psql$(X)
# psqlscan.c is in the distribution tarball, so is not cleaned here
clean distclean:
rm -f psql$(X) $(OBJS)
maintainer-clean: distclean
rm -f $(srcdir)/sql_help.h
rm -f $(srcdir)/sql_help.h $(srcdir)/psqlscan.c

View File

@ -3,7 +3,7 @@
*
* Copyright (c) 2000-2003, PostgreSQL Global Development Group
*
* $PostgreSQL: pgsql/src/bin/psql/command.c,v 1.112 2004/01/26 22:35:32 tgl Exp $
* $PostgreSQL: pgsql/src/bin/psql/command.c,v 1.113 2004/02/19 19:40:08 tgl Exp $
*/
#include "postgres_fe.h"
#include "command.h"
@ -36,47 +36,33 @@
#include "large_obj.h"
#include "mainloop.h"
#include "print.h"
#include "psqlscan.h"
#include "settings.h"
#include "variables.h"
#include "mb/pg_wchar.h"
/* functions for use in this file */
static backslashResult exec_command(const char *cmd,
const char *options_string,
const char **continue_parse,
PQExpBuffer query_buf,
volatile int *paren_level);
/* different ways for scan_option to handle parameter words */
enum option_type
{
OT_NORMAL, /* normal case */
OT_SQLID, /* treat as SQL identifier */
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE /* it's a filename or pipe */
};
static char *scan_option(char **string, enum option_type type,
char *quote, bool semicolon);
static char *unescape(const unsigned char *source, size_t len);
PsqlScanState scan_state,
PQExpBuffer query_buf);
static bool do_edit(const char *filename_arg, PQExpBuffer query_buf);
static bool do_connect(const char *new_dbname, const char *new_user);
static bool do_shell(const char *command);
/*----------
* HandleSlashCmds:
*
* Handles all the different commands that start with '\',
* ordinarily called by MainLoop().
*
* 'line' is the current input line, which should not start with a '\'
* but with the actual command name
* (that is taken care of by MainLoop)
* scan_state is a lexer working state that is set to continue scanning
* just after the '\'. The lexer is advanced past the command and all
* arguments on return.
*
* 'query_buf' contains the query-so-far, which may be modified by
* execution of the backslash command (for example, \r clears it)
* execution of the backslash command (for example, \r clears it).
* query_buf can be NULL if there is no query so far.
*
* Returns a status code indicating what action is desired, see command.h.
@ -84,124 +70,88 @@ static bool do_shell(const char *command);
*/
backslashResult
HandleSlashCmds(const char *line,
PQExpBuffer query_buf,
const char **end_of_cmd,
volatile int *paren_level)
HandleSlashCmds(PsqlScanState scan_state,
PQExpBuffer query_buf)
{
backslashResult status = CMD_SKIP_LINE;
char *my_line;
char *options_string = NULL;
size_t blank_loc;
const char *continue_parse = NULL; /* tell the mainloop where the
* backslash command ended */
char *cmd;
char *arg;
psql_assert(line);
my_line = pg_strdup(line);
psql_assert(scan_state);
/*
* Find the first whitespace. line[blank_loc] will now be the
* whitespace character or the \0 at the end
*
* Also look for a backslash, so stuff like \p\g works.
*/
blank_loc = strcspn(my_line, " \t\n\r\\");
/* Parse off the command name */
cmd = psql_scan_slash_command(scan_state);
if (my_line[blank_loc] == '\\')
{
continue_parse = &my_line[blank_loc];
my_line[blank_loc] = '\0';
/* If it's a double backslash, we skip it. */
if (my_line[blank_loc + 1] == '\\')
continue_parse += 2;
}
/* do we have an option string? */
else if (my_line[blank_loc] != '\0')
{
options_string = &my_line[blank_loc + 1];
my_line[blank_loc] = '\0';
}
/* And try to execute it */
status = exec_command(cmd, scan_state, query_buf);
status = exec_command(my_line, options_string, &continue_parse, query_buf, paren_level);
if (status == CMD_UNKNOWN)
if (status == CMD_UNKNOWN && strlen(cmd) > 1)
{
/*
* If the command was not recognized, try to parse it as a
* one-letter command with immediately following argument (a
* still-supported, but no longer encouraged, syntax).
*/
char new_cmd[2];
char new_cmd[2];
new_cmd[0] = my_line[0];
/* don't change cmd until we know it's okay */
new_cmd[0] = cmd[0];
new_cmd[1] = '\0';
/* use line for options, because my_line was clobbered above */
status = exec_command(new_cmd, line + 1, &continue_parse, query_buf, paren_level);
psql_scan_slash_pushback(scan_state, cmd + 1);
/*
* continue_parse must be relative to my_line for calculation
* below
*/
continue_parse += my_line - line;
status = exec_command(new_cmd, scan_state, query_buf);
if (status != CMD_UNKNOWN)
{
/* adjust cmd for possible messages below */
cmd[1] = '\0';
#if 0 /* turned out to be too annoying */
if (status != CMD_UNKNOWN && isalpha((unsigned char) new_cmd[0]))
psql_error("Warning: This syntax is deprecated.\n");
if (isalpha((unsigned char) cmd[0]))
psql_error("Warning: This syntax is deprecated.\n");
#endif
}
}
if (status == CMD_UNKNOWN)
{
if (pset.cur_cmd_interactive)
fprintf(stderr, gettext("Invalid command \\%s. Try \\? for help.\n"), my_line);
fprintf(stderr, gettext("Invalid command \\%s. Try \\? for help.\n"), cmd);
else
psql_error("invalid command \\%s\n", my_line);
psql_error("invalid command \\%s\n", cmd);
status = CMD_ERROR;
}
if (continue_parse && *continue_parse && *(continue_parse + 1) == '\\')
continue_parse += 2;
if (end_of_cmd)
/* eat the rest of the options, if any */
while ((arg = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false)))
{
if (continue_parse)
*end_of_cmd = line + (continue_parse - my_line);
else
*end_of_cmd = line + strlen(line);
if (status != CMD_ERROR)
psql_error("\\%s: extra argument \"%s\" ignored\n", cmd, arg);
free(arg);
}
free(my_line);
/* if there is a trailing \\, swallow it */
psql_scan_slash_command_end(scan_state);
free(cmd);
return status;
}
/*
* Subroutine to actually try to execute a backslash command.
*/
static backslashResult
exec_command(const char *cmd,
const char *options_string,
const char **continue_parse,
PQExpBuffer query_buf,
volatile int *paren_level)
PsqlScanState scan_state,
PQExpBuffer query_buf)
{
bool success = true; /* indicate here if the command ran ok or
* failed */
bool quiet = QUIET();
backslashResult status = CMD_SKIP_LINE;
char *string,
*string_cpy,
*val;
/*
* The 'string' variable will be overwritten to point to the next
* token, hence we need an extra pointer so we can free this at the
* end.
*/
if (options_string)
string = string_cpy = pg_strdup(options_string);
else
string = string_cpy = NULL;
/*
* \a -- toggle field alignment This makes little sense but we keep it
@ -218,7 +168,8 @@ exec_command(const char *cmd,
/* \C -- override table title (formerly change HTML caption) */
else if (strcmp(cmd, "C") == 0)
{
char *opt = scan_option(&string, OT_NORMAL, NULL, true);
char *opt = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
success = do_pset("title", opt, &pset.popt, quiet);
free(opt);
@ -249,8 +200,10 @@ exec_command(const char *cmd,
* files can be expected to double-quote all mixed-case \connect
* arguments, and then we can get rid of OT_SQLIDHACK.
*/
opt1 = scan_option(&string, OT_SQLIDHACK, &opt1q, true);
opt2 = scan_option(&string, OT_SQLIDHACK, &opt2q, true);
opt1 = psql_scan_slash_option(scan_state,
OT_SQLIDHACK, &opt1q, true);
opt2 = psql_scan_slash_option(scan_state,
OT_SQLIDHACK, &opt2q, true);
if (opt2)
/* gave username */
@ -270,7 +223,8 @@ exec_command(const char *cmd,
/* \cd */
else if (strcmp(cmd, "cd") == 0)
{
char *opt = scan_option(&string, OT_NORMAL, NULL, true);
char *opt = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
char *dir;
if (opt)
@ -311,9 +265,11 @@ exec_command(const char *cmd,
/* \copy */
else if (strcasecmp(cmd, "copy") == 0)
{
success = do_copy(options_string);
if (options_string)
string += strlen(string);
char *opt = psql_scan_slash_option(scan_state,
OT_WHOLE_LINE, NULL, false);
success = do_copy(opt);
free(opt);
}
/* \copyright */
@ -327,7 +283,8 @@ exec_command(const char *cmd,
bool show_verbose;
/* We don't do SQLID reduction on the pattern yet */
pattern = scan_option(&string, OT_NORMAL, NULL, true);
pattern = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
show_verbose = strchr(cmd, '+') ? true : false;
@ -412,7 +369,8 @@ exec_command(const char *cmd,
}
else
{
fname = scan_option(&string, OT_NORMAL, NULL, true);
fname = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
expand_tilde(&fname);
status = do_edit(fname, query_buf) ? CMD_NEWEDIT : CMD_ERROR;
free(fname);
@ -433,7 +391,8 @@ exec_command(const char *cmd,
else
fout = stdout;
while ((value = scan_option(&string, OT_NORMAL, &quoted, false)))
while ((value = psql_scan_slash_option(scan_state,
OT_NORMAL, &quoted, false)))
{
if (!quoted && strcmp(value, "-n") == 0)
no_newline = true;
@ -454,7 +413,8 @@ exec_command(const char *cmd,
/* \encoding -- set/show client side encoding */
else if (strcmp(cmd, "encoding") == 0)
{
char *encoding = scan_option(&string, OT_NORMAL, NULL, false);
char *encoding = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
if (!encoding)
{
@ -481,7 +441,8 @@ exec_command(const char *cmd,
/* \f -- change field separator */
else if (strcmp(cmd, "f") == 0)
{
char *fname = scan_option(&string, OT_NORMAL, NULL, false);
char *fname = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
success = do_pset("fieldsep", fname, &pset.popt, quiet);
free(fname);
@ -490,7 +451,8 @@ exec_command(const char *cmd,
/* \g means send query */
else if (strcmp(cmd, "g") == 0)
{
char *fname = scan_option(&string, OT_FILEPIPE, NULL, false);
char *fname = psql_scan_slash_option(scan_state,
OT_FILEPIPE, NULL, false);
if (!fname)
pset.gfname = NULL;
@ -506,11 +468,11 @@ exec_command(const char *cmd,
/* help */
else if (strcmp(cmd, "h") == 0 || strcmp(cmd, "help") == 0)
{
helpSQL(options_string ? &options_string[strspn(options_string, " \t\n\r")] : NULL,
pset.popt.topt.pager);
/* set pointer to end of line */
if (string)
string += strlen(string);
char *opt = psql_scan_slash_option(scan_state,
OT_WHOLE_LINE, NULL, false);
helpSQL(opt, pset.popt.topt.pager);
free(opt);
}
/* HTML mode */
@ -526,7 +488,8 @@ exec_command(const char *cmd,
/* \i is include file */
else if (strcmp(cmd, "i") == 0 || strcmp(cmd, "include") == 0)
{
char *fname = scan_option(&string, OT_NORMAL, NULL, true);
char *fname = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
if (!fname)
{
@ -555,8 +518,10 @@ exec_command(const char *cmd,
char *opt1,
*opt2;
opt1 = scan_option(&string, OT_NORMAL, NULL, true);
opt2 = scan_option(&string, OT_NORMAL, NULL, true);
opt1 = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
opt2 = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
if (strcmp(cmd + 3, "export") == 0)
{
@ -611,7 +576,8 @@ exec_command(const char *cmd,
/* \o -- set query output */
else if (strcmp(cmd, "o") == 0 || strcmp(cmd, "out") == 0)
{
char *fname = scan_option(&string, OT_FILEPIPE, NULL, true);
char *fname = psql_scan_slash_option(scan_state,
OT_FILEPIPE, NULL, true);
expand_tilde(&fname);
success = setQFout(fname);
@ -631,8 +597,10 @@ exec_command(const char *cmd,
/* \pset -- set printing parameters */
else if (strcmp(cmd, "pset") == 0)
{
char *opt0 = scan_option(&string, OT_NORMAL, NULL, false);
char *opt1 = scan_option(&string, OT_NORMAL, NULL, false);
char *opt0 = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
char *opt1 = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
if (!opt0)
{
@ -654,8 +622,7 @@ exec_command(const char *cmd,
else if (strcmp(cmd, "r") == 0 || strcmp(cmd, "reset") == 0)
{
resetPQExpBuffer(query_buf);
if (paren_level)
*paren_level = 0;
psql_scan_reset(scan_state);
if (!quiet)
puts(gettext("Query buffer reset (cleared)."));
}
@ -663,7 +630,8 @@ exec_command(const char *cmd,
/* \s save history in a file or show it on the screen */
else if (strcmp(cmd, "s") == 0)
{
char *fname = scan_option(&string, OT_NORMAL, NULL, true);
char *fname = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
expand_tilde(&fname);
success = saveHistory(fname ? fname : "/dev/tty");
@ -676,7 +644,8 @@ exec_command(const char *cmd,
/* \set -- generalized set variable/option command */
else if (strcmp(cmd, "set") == 0)
{
char *opt0 = scan_option(&string, OT_NORMAL, NULL, false);
char *opt0 = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
if (!opt0)
{
@ -689,14 +658,16 @@ exec_command(const char *cmd,
/*
* Set variable to the concatenation of the arguments.
*/
char *newval = NULL;
char *newval;
char *opt;
opt = scan_option(&string, OT_NORMAL, NULL, false);
opt = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
newval = pg_strdup(opt ? opt : "");
free(opt);
while ((opt = scan_option(&string, OT_NORMAL, NULL, false)))
while ((opt = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false)))
{
newval = realloc(newval, strlen(newval) + strlen(opt) + 1);
if (!newval)
@ -732,7 +703,8 @@ exec_command(const char *cmd,
/* \T -- define html <table ...> attributes */
else if (strcmp(cmd, "T") == 0)
{
char *value = scan_option(&string, OT_NORMAL, NULL, false);
char *value = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
success = do_pset("tableattr", value, &pset.popt, quiet);
free(value);
@ -754,7 +726,8 @@ exec_command(const char *cmd,
/* \unset */
else if (strcmp(cmd, "unset") == 0)
{
char *opt = scan_option(&string, OT_NORMAL, NULL, false);
char *opt = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, false);
if (!opt)
{
@ -783,7 +756,8 @@ exec_command(const char *cmd,
}
else
{
fname = scan_option(&string, OT_FILEPIPE, NULL, true);
fname = psql_scan_slash_option(scan_state,
OT_FILEPIPE, NULL, true);
expand_tilde(&fname);
if (!fname)
@ -839,7 +813,8 @@ exec_command(const char *cmd,
/* \z -- list table rights (equivalent to \dp) */
else if (strcmp(cmd, "z") == 0)
{
char *pattern = scan_option(&string, OT_NORMAL, NULL, true);
char *pattern = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true);
success = permissionsList(pattern);
if (pattern)
@ -849,10 +824,11 @@ exec_command(const char *cmd,
/* \! -- shell escape */
else if (strcmp(cmd, "!") == 0)
{
success = do_shell(options_string);
/* wind pointer to end of line */
if (string)
string += strlen(string);
char *opt = psql_scan_slash_option(scan_state,
OT_WHOLE_LINE, NULL, false);
success = do_shell(opt);
free(opt);
}
/* \? -- slash command help */
@ -870,8 +846,8 @@ exec_command(const char *cmd,
int i = 0;
char *value;
fprintf(stderr, "+ optstr = |%s|\n", options_string);
while ((value = scan_option(&string, OT_NORMAL, NULL, true)))
while ((value = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true)))
{
fprintf(stderr, "+ opt(%d) = |%s|\n", i++, value);
free(value);
@ -885,431 +861,11 @@ exec_command(const char *cmd,
if (!success)
status = CMD_ERROR;
/* eat the rest of the options string */
while ((val = scan_option(&string, OT_NORMAL, NULL, false)))
{
if (status != CMD_UNKNOWN)
psql_error("\\%s: extra argument \"%s\" ignored\n", cmd, val);
if (val)
free(val);
}
if (options_string && continue_parse)
*continue_parse = options_string + (string - string_cpy);
free(string_cpy);
return status;
}
/*
* scan_option()
*
* *string points to possible option string on entry; on exit, it's updated
* to point past the option string (if any).
*
* type tells what processing, if any, to perform on the option string;
* for example, if it's a SQL identifier, we want to downcase any unquoted
* letters.
*
* if quote is not NULL, *quote is set to 0 if no quoting was found, else
* the quote symbol.
*
* if semicolon is true, trailing semicolon(s) that would otherwise be taken
* as part of the option string will be stripped.
*
* Return value is NULL if no option found, else a malloc'd copy of the
* processed option value.
*/
static char *
scan_option(char **string, enum option_type type, char *quote, bool semicolon)
{
unsigned int pos;
char *options_string;
char *return_val;
if (quote)
*quote = 0;
if (!string || !(*string))
return NULL;
options_string = *string;
/* skip leading whitespace */
pos = strspn(options_string, " \t\n\r");
switch (options_string[pos])
{
/*
* End of line: no option present
*/
case '\0':
*string = &options_string[pos];
return NULL;
/*
* Next command: treat like end of line
*
* XXX this means we can't conveniently accept options that start
* with a backslash; therefore, option processing that
* encourages use of backslashes is rather broken.
*/
case '\\':
*string = &options_string[pos];
return NULL;
/*
* A single quote has a psql internal meaning, such as for
* delimiting file names, and it also allows for such escape
* sequences as \t.
*/
case '\'':
{
unsigned int jj;
unsigned short int bslash_count = 0;
for (jj = pos + 1; options_string[jj]; jj += PQmblen(&options_string[jj], pset.encoding))
{
if (options_string[jj] == '\'' && bslash_count % 2 == 0)
break;
if (options_string[jj] == '\\')
bslash_count++;
else
bslash_count = 0;
}
if (options_string[jj] == 0)
{
psql_error("parse error at the end of line\n");
*string = &options_string[jj];
return NULL;
}
return_val = unescape(&options_string[pos + 1], jj - pos - 1);
*string = &options_string[jj + 1];
if (quote)
*quote = '\'';
return return_val;
}
/*
* Backticks are for command substitution, like in shells
*/
case '`':
{
bool error = false;
FILE *fd;
char *file;
PQExpBufferData output;
char buf[512];
size_t result,
len;
len = strcspn(options_string + pos + 1, "`");
if (options_string[pos + 1 + len] == 0)
{
psql_error("parse error at the end of line\n");
*string = &options_string[pos + 1 + len];
return NULL;
}
options_string[pos + 1 + len] = '\0';
file = options_string + pos + 1;
fd = popen(file, "r");
if (!fd)
{
psql_error("%s: %s\n", file, strerror(errno));
error = true;
}
initPQExpBuffer(&output);
if (!error)
{
do
{
result = fread(buf, 1, 512, fd);
if (ferror(fd))
{
psql_error("%s: %s\n", file, strerror(errno));
error = true;
break;
}
appendBinaryPQExpBuffer(&output, buf, result);
} while (!feof(fd));
appendPQExpBufferChar(&output, '\0');
}
if (fd && pclose(fd) == -1)
{
psql_error("%s: %s\n", file, strerror(errno));
error = true;
}
if (!error)
{
if (output.data[strlen(output.data) - 1] == '\n')
output.data[strlen(output.data) - 1] = '\0';
return_val = output.data;
}
else
{
return_val = pg_strdup("");
termPQExpBuffer(&output);
}
options_string[pos + 1 + len] = '`';
*string = options_string + pos + len + 2;
if (quote)
*quote = '`';
return return_val;
}
/*
* Variable substitution
*/
case ':':
{
size_t token_end;
const char *value;
char save_char;
token_end = strcspn(&options_string[pos + 1], " \t\n\r");
save_char = options_string[pos + token_end + 1];
options_string[pos + token_end + 1] = '\0';
value = GetVariable(pset.vars, options_string + pos + 1);
return_val = pg_strdup(value ? value : "");
options_string[pos + token_end + 1] = save_char;
*string = &options_string[pos + token_end + 1];
/* XXX should we set *quote to ':' here? */
return return_val;
}
/*
* | could be the beginning of a pipe if so, take rest of line
* as command
*/
case '|':
if (type == OT_FILEPIPE)
{
*string += strlen(*string);
return pg_strdup(options_string + pos);
}
/* fallthrough for other option types */
/*
* Default case: token extends to next whitespace, except that
* whitespace within double quotes doesn't end the token.
*
* If we are processing the option as a SQL identifier, then
* downcase unquoted letters and remove double-quotes --- but
* doubled double-quotes become output double-quotes, per
* spec.
*
* Note that a string like FOO"BAR"BAZ will be converted to
* fooBARbaz; this is somewhat inconsistent with the SQL spec,
* which would have us parse it as several identifiers. But
* for psql's purposes, we want a string like "foo"."bar" to
* be treated as one option, so there's little choice.
*/
default:
{
bool inquotes = false;
size_t token_len;
char *cp;
/* Find end of option */
cp = &options_string[pos];
for (;;)
{
/* Find next quote, whitespace, or end of string */
cp += strcspn(cp, "\" \t\n\r");
if (inquotes)
{
if (*cp == '\0')
{
psql_error("parse error at the end of line\n");
*string = cp;
return NULL;
}
if (*cp == '"')
inquotes = false;
cp++;
}
else
{
if (*cp != '"')
break; /* whitespace or end of string */
if (quote)
*quote = '"';
inquotes = true;
cp++;
}
}
*string = cp;
/* Copy the option */
token_len = cp - &options_string[pos];
return_val = pg_malloc(token_len + 1);
memcpy(return_val, &options_string[pos], token_len);
return_val[token_len] = '\0';
/* Strip any trailing semi-colons if requested */
if (semicolon)
{
int i;
for (i = token_len - 1;
i >= 0 && return_val[i] == ';';
i--)
/* skip */ ;
if (i < 0)
{
/* nothing left after stripping the semicolon... */
free(return_val);
return NULL;
}
if (i < (int) token_len - 1)
return_val[i + 1] = '\0';
}
/*
* If SQL identifier processing was requested, then we
* strip out excess double quotes and downcase unquoted
* letters.
*/
if (type == OT_SQLID || type == OT_SQLIDHACK)
{
inquotes = false;
cp = return_val;
while (*cp)
{
if (*cp == '"')
{
if (inquotes && cp[1] == '"')
{
/* Keep the first quote, remove the second */
cp++;
}
inquotes = !inquotes;
/* Collapse out quote at *cp */
memmove(cp, cp + 1, strlen(cp));
/* do not advance cp */
}
else
{
if (!inquotes && type == OT_SQLID)
{
if (isupper((unsigned char) *cp))
*cp = tolower((unsigned char) *cp);
}
cp += PQmblen(cp, pset.encoding);
}
}
}
return return_val;
}
}
}
/*
* unescape
*
* Replaces \n, \t, and the like.
*
* The return value is malloc'ed.
*/
static char *
unescape(const unsigned char *source, size_t len)
{
const unsigned char *p;
bool esc = false; /* Last character we saw was the escape
* character */
char *destination,
*tmp;
size_t length;
psql_assert(source);
length = Min(len, strlen(source)) + 1;
tmp = destination = pg_malloc(length);
for (p = source; p - source < (int) len && *p; p += PQmblen(p, pset.encoding))
{
if (esc)
{
char c;
switch (*p)
{
case 'n':
c = '\n';
break;
case 't':
c = '\t';
break;
case 'b':
c = '\b';
break;
case 'r':
c = '\r';
break;
case 'f':
c = '\f';
break;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
c = parse_char((char **) &p);
break;
default:
c = *p;
}
*tmp++ = c;
esc = false;
}
else if (*p == '\\')
esc = true;
else
{
int i;
const unsigned char *mp = p;
for (i = 0; i < PQmblen(p, pset.encoding); i++)
*tmp++ = *mp++;
esc = false;
}
}
*tmp = '\0';
return destination;
}
/* do_connect
* -- handler for \connect
*

View File

@ -3,15 +3,14 @@
*
* Copyright (c) 2000-2003, PostgreSQL Global Development Group
*
* $PostgreSQL: pgsql/src/bin/psql/command.h,v 1.18 2003/11/29 19:52:06 pgsql Exp $
* $PostgreSQL: pgsql/src/bin/psql/command.h,v 1.19 2004/02/19 19:40:09 tgl Exp $
*/
#ifndef COMMAND_H
#define COMMAND_H
#include "pqexpbuffer.h"
#include "settings.h"
#include "print.h"
#include "psqlscan.h"
typedef enum _backslashResult
@ -26,10 +25,8 @@ typedef enum _backslashResult
} backslashResult;
extern backslashResult HandleSlashCmds(const char *line,
PQExpBuffer query_buf,
const char **end_of_cmd,
volatile int *paren_level);
extern backslashResult HandleSlashCmds(PsqlScanState scan_state,
PQExpBuffer query_buf);
extern int process_file(char *filename);

View File

@ -3,18 +3,19 @@
*
* Copyright (c) 2000-2003, PostgreSQL Global Development Group
*
* $PostgreSQL: pgsql/src/bin/psql/mainloop.c,v 1.61 2004/01/25 03:07:22 neilc Exp $
* $PostgreSQL: pgsql/src/bin/psql/mainloop.c,v 1.62 2004/02/19 19:40:09 tgl Exp $
*/
#include "postgres_fe.h"
#include "mainloop.h"
#include "pqexpbuffer.h"
#include "settings.h"
#include "prompt.h"
#include "input.h"
#include "common.h"
#include "command.h"
#include "common.h"
#include "input.h"
#include "prompt.h"
#include "psqlscan.h"
#include "settings.h"
#ifndef WIN32
#include <setjmp.h>
@ -28,48 +29,39 @@ sigjmp_buf main_loop_jmp;
*
* This loop is re-entrant. May be called by \i command
* which reads input from a file.
*
* FIXME: rewrite this whole thing with flex
*/
int
MainLoop(FILE *source)
{
PsqlScanState scan_state; /* lexer working state */
PQExpBuffer query_buf; /* buffer for query being accumulated */
PQExpBuffer previous_buf; /* if there isn't anything in the new
* buffer yet, use this one for \e, etc. */
char *line; /* current line of input */
int len; /* length of the line */
int added_nl_pos;
bool success;
volatile int successResult = EXIT_SUCCESS;
volatile backslashResult slashCmdStatus = CMD_UNKNOWN;
bool success;
volatile char in_quote = 0; /* == 0 for no in_quote */
volatile int in_xcomment = 0; /* in extended comment */
volatile int paren_level = 0;
unsigned int query_start;
volatile promptStatus_t prompt_status = PROMPT_READY;
volatile int count_eof = 0;
volatile unsigned int bslash_count = 0;
int i,
prevlen,
thislen;
volatile bool die_on_error = false;
/* Save the prior command source */
FILE *prev_cmd_source;
bool prev_cmd_interactive;
unsigned int prev_lineno;
volatile bool die_on_error = false;
/* Save old settings */
prev_cmd_source = pset.cur_cmd_source;
prev_cmd_interactive = pset.cur_cmd_interactive;
prev_lineno = pset.lineno;
/* Establish new source */
pset.cur_cmd_source = source;
pset.cur_cmd_interactive = ((source == stdin) && !pset.notty);
pset.lineno = 0;
/* Create working state */
scan_state = psql_scan_create();
query_buf = createPQExpBuffer();
previous_buf = createPQExpBuffer();
@ -79,10 +71,6 @@ MainLoop(FILE *source)
exit(EXIT_FAILURE);
}
prev_lineno = pset.lineno;
pset.lineno = 0;
/* main loop to get queries and execute them */
while (successResult == EXIT_SUCCESS)
{
@ -110,17 +98,17 @@ MainLoop(FILE *source)
{
/* got here with longjmp */
/* reset parsing state */
resetPQExpBuffer(query_buf);
psql_scan_finish(scan_state);
psql_scan_reset(scan_state);
count_eof = 0;
slashCmdStatus = CMD_UNKNOWN;
prompt_status = PROMPT_READY;
if (pset.cur_cmd_interactive)
{
putc('\n', stdout);
resetPQExpBuffer(query_buf);
/* reset parsing state */
in_xcomment = 0;
in_quote = 0;
paren_level = 0;
count_eof = 0;
slashCmdStatus = CMD_UNKNOWN;
}
else
{
@ -145,48 +133,30 @@ MainLoop(FILE *source)
* input buffer
*/
line = pg_strdup(query_buf->data);
resetPQExpBuffer(query_buf);
/* reset parsing state since we are rescanning whole line */
in_xcomment = 0;
in_quote = 0;
paren_level = 0;
resetPQExpBuffer(query_buf);
psql_scan_reset(scan_state);
slashCmdStatus = CMD_UNKNOWN;
prompt_status = PROMPT_READY;
}
/*
* otherwise, set interactive prompt if necessary and get another
* line
* otherwise, get another line
*/
else if (pset.cur_cmd_interactive)
{
int prompt_status;
if (in_quote && in_quote == '\'')
prompt_status = PROMPT_SINGLEQUOTE;
else if (in_quote && in_quote == '"')
prompt_status = PROMPT_DOUBLEQUOTE;
else if (in_xcomment)
prompt_status = PROMPT_COMMENT;
else if (paren_level)
prompt_status = PROMPT_PAREN;
else if (query_buf->len > 0)
prompt_status = PROMPT_CONTINUE;
else
/* May need to reset prompt, eg after \r command */
if (query_buf->len == 0)
prompt_status = PROMPT_READY;
line = gets_interactive(get_prompt(prompt_status));
}
else
line = gets_fromFile(source);
/* Setting this will not have effect until next line. */
die_on_error = GetVariableBool(pset.vars, "ON_ERROR_STOP");
/*
* query_buf holds query already accumulated. line is the
* malloc'd new line of input (note it must be freed before
* looping around!) query_start is the next command start location
* within the line.
* looping around!)
*/
/* No more input. Time to quit, or \i done */
@ -214,165 +184,52 @@ MainLoop(FILE *source)
pset.lineno++;
/* nothing left on line? then ignore */
if (line[0] == '\0' && !in_quote)
if (line[0] == '\0' && !psql_scan_in_quote(scan_state))
{
free(line);
continue;
}
/* echo back if flag is set */
if (!pset.cur_cmd_interactive && VariableEquals(pset.vars, "ECHO", "all"))
if (!pset.cur_cmd_interactive &&
VariableEquals(pset.vars, "ECHO", "all"))
puts(line);
fflush(stdout);
len = strlen(line);
query_start = 0;
/* insert newlines into query buffer between source lines */
if (query_buf->len > 0)
{
appendPQExpBufferChar(query_buf, '\n');
added_nl_pos = query_buf->len;
}
else
added_nl_pos = -1; /* flag we didn't add one */
/* Setting this will not have effect until next line. */
die_on_error = GetVariableBool(pset.vars, "ON_ERROR_STOP");
/*
* Parse line, looking for command separators.
*
* The current character is at line[i], the prior character at line[i
* - prevlen], the next character at line[i + thislen].
*/
#define ADVANCE_1 (prevlen = thislen, i += thislen, thislen = PQmblen(line+i, pset.encoding))
psql_scan_setup(scan_state, line, strlen(line));
success = true;
prevlen = 0;
thislen = ((len > 0) ? PQmblen(line, pset.encoding) : 0);
for (i = 0; (i < len) && (success || !die_on_error); ADVANCE_1)
while (success || !die_on_error)
{
/* was the previous character a backslash? */
if (i > 0 && line[i - prevlen] == '\\')
bslash_count++;
else
bslash_count = 0;
PsqlScanResult scan_result;
promptStatus_t prompt_tmp = prompt_status;
rescan:
scan_result = psql_scan(scan_state, query_buf, &prompt_tmp);
prompt_status = prompt_tmp;
/*
* It is important to place the in_* test routines before the
* in_* detection routines. i.e. we have to test if we are in
* a quote before testing for comments. bjm 2000-06-30
* Send command if semicolon found, or if end of line and
* we're in single-line mode.
*/
/* in quote? */
if (in_quote)
if (scan_result == PSCAN_SEMICOLON ||
(scan_result == PSCAN_EOL &&
GetVariableBool(pset.vars, "SINGLELINE")))
{
/*
* end of quote if matching non-backslashed character.
* backslashes don't count for double quotes, though.
*/
if (line[i] == in_quote &&
(bslash_count % 2 == 0 || in_quote == '"'))
in_quote = 0;
}
/* start of extended comment? */
else if (line[i] == '/' && line[i + thislen] == '*')
{
in_xcomment++;
if (in_xcomment == 1)
ADVANCE_1;
}
/* in or end of extended comment? */
else if (in_xcomment)
{
if (line[i] == '*' && line[i + thislen] == '/' &&
!--in_xcomment)
ADVANCE_1;
}
/* start of quote? */
else if (line[i] == '\'' || line[i] == '"')
in_quote = line[i];
/* single-line comment? truncate line */
else if (line[i] == '-' && line[i + thislen] == '-')
{
line[i] = '\0'; /* remove comment */
break;
}
/* count nested parentheses */
else if (line[i] == '(')
paren_level++;
else if (line[i] == ')' && paren_level > 0)
paren_level--;
/* colon -> substitute variable */
/* we need to be on the watch for the '::' operator */
else if (line[i] == ':' && !bslash_count
&& strspn(line + i + thislen, VALID_VARIABLE_CHARS) > 0
&& !(prevlen > 0 && line[i - prevlen] == ':')
)
{
size_t in_length,
out_length;
const char *value;
char *new;
char after; /* the character after the
* variable name will be
* temporarily overwritten */
in_length = strspn(&line[i + thislen], VALID_VARIABLE_CHARS);
/* mark off the possible variable name */
after = line[i + thislen + in_length];
line[i + thislen + in_length] = '\0';
value = GetVariable(pset.vars, &line[i + thislen]);
/* restore overwritten character */
line[i + thislen + in_length] = after;
if (value)
{
/* It is a variable, perform substitution */
out_length = strlen(value);
new = pg_malloc(len + out_length - in_length + 1);
sprintf(new, "%.*s%s%s", i, line, value,
&line[i + thislen + in_length]);
free(line);
line = new;
len = strlen(new);
if (i < len)
{
thislen = PQmblen(line + i, pset.encoding);
goto rescan; /* reparse the just substituted */
}
}
else
{
/*
* if the variable doesn't exist we'll leave the
* string as is ... move on ...
*/
}
}
/* semicolon? then send query */
else if (line[i] == ';' && !bslash_count && !paren_level)
{
line[i] = '\0';
/* is there anything else on the line? */
if (line[query_start + strspn(line + query_start, " \t\n\r")] != '\0')
{
/*
* insert a cosmetic newline, if this is not the first
* line in the buffer
*/
if (query_buf->len > 0)
appendPQExpBufferChar(query_buf, '\n');
/* append the line to the query buffer */
appendPQExpBufferStr(query_buf, line + query_start);
appendPQExpBufferChar(query_buf, ';');
}
/* execute query */
success = SendQuery(query_buf->data);
slashCmdStatus = success ? CMD_SEND : CMD_ERROR;
@ -380,46 +237,26 @@ MainLoop(FILE *source)
resetPQExpBuffer(previous_buf);
appendPQExpBufferStr(previous_buf, query_buf->data);
resetPQExpBuffer(query_buf);
query_start = i + thislen;
added_nl_pos = -1;
/* we need not do psql_scan_reset() here */
}
/*
* if you have a burning need to send a semicolon or colon to
* the backend ...
*/
else if (bslash_count && (line[i] == ';' || line[i] == ':'))
else if (scan_result == PSCAN_BACKSLASH)
{
/* remove the backslash */
memmove(line + i - prevlen, line + i, len - i + 1);
len--;
i--;
}
/* backslash command */
else if (bslash_count)
{
const char *end_of_cmd = NULL;
line[i - prevlen] = '\0'; /* overwrites backslash */
/* is there anything else on the line for the command? */
if (line[query_start + strspn(line + query_start, " \t\n\r")] != '\0')
{
/*
* insert a cosmetic newline, if this is not the first
* line in the buffer
*/
if (query_buf->len > 0)
appendPQExpBufferChar(query_buf, '\n');
/* append the line to the query buffer */
appendPQExpBufferStr(query_buf, line + query_start);
}
/* handle backslash command */
slashCmdStatus = HandleSlashCmds(&line[i],
query_buf->len > 0 ? query_buf : previous_buf,
&end_of_cmd,
&paren_level);
/*
* If we added a newline to query_buf, and nothing else has
* been inserted in query_buf by the lexer, then strip off
* the newline again. This avoids any change to query_buf
* when a line contains only a backslash command.
*/
if (query_buf->len == added_nl_pos)
query_buf->data[--query_buf->len] = '\0';
added_nl_pos = -1;
slashCmdStatus = HandleSlashCmds(scan_state,
query_buf->len > 0 ?
query_buf : previous_buf);
success = slashCmdStatus != CMD_ERROR;
@ -433,22 +270,27 @@ MainLoop(FILE *source)
if (slashCmdStatus == CMD_SEND)
{
success = SendQuery(query_buf->data);
query_start = i + thislen;
resetPQExpBuffer(previous_buf);
appendPQExpBufferStr(previous_buf, query_buf->data);
resetPQExpBuffer(query_buf);
/* flush any paren nesting info after forced send */
psql_scan_reset(scan_state);
}
if (query_buf->len == 0 && previous_buf->len == 0)
paren_level = 0;
/* process anything left after the backslash command */
i = end_of_cmd - line;
query_start = i;
if (slashCmdStatus == CMD_TERMINATE)
break;
}
} /* for (line) */
/* fall out of loop if lexer reached EOL */
if (scan_result == PSCAN_INCOMPLETE ||
scan_result == PSCAN_EOL)
break;
}
psql_scan_finish(scan_state);
free(line);
if (slashCmdStatus == CMD_TERMINATE)
{
@ -456,28 +298,6 @@ MainLoop(FILE *source)
break;
}
/* Put the rest of the line in the query buffer. */
if (in_quote || line[query_start + strspn(line + query_start, " \t\n\r")] != '\0')
{
if (query_buf->len > 0)
appendPQExpBufferChar(query_buf, '\n');
appendPQExpBufferStr(query_buf, line + query_start);
}
free(line);
/* In single line mode, send off the query if any */
if (query_buf->data[0] != '\0' && GetVariableBool(pset.vars, "SINGLELINE"))
{
success = SendQuery(query_buf->data);
slashCmdStatus = (success ? CMD_SEND : CMD_ERROR);
resetPQExpBuffer(previous_buf);
appendPQExpBufferStr(previous_buf, query_buf->data);
resetPQExpBuffer(query_buf);
}
if (!pset.cur_cmd_interactive)
{
if (!success && die_on_error)
@ -515,6 +335,8 @@ MainLoop(FILE *source)
destroyPQExpBuffer(query_buf);
destroyPQExpBuffer(previous_buf);
psql_scan_destroy(scan_state);
pset.cur_cmd_source = prev_cmd_source;
pset.cur_cmd_interactive = prev_cmd_interactive;
pset.lineno = prev_lineno;

65
src/bin/psql/psqlscan.h Normal file
View File

@ -0,0 +1,65 @@
/*
* psql - the PostgreSQL interactive terminal
*
* Copyright (c) 2000-2003, PostgreSQL Global Development Group
*
* $PostgreSQL: pgsql/src/bin/psql/psqlscan.h,v 1.1 2004/02/19 19:40:09 tgl Exp $
*/
#ifndef PSQLSCAN_H
#define PSQLSCAN_H
#include "pqexpbuffer.h"
#include "prompt.h"
/* Abstract type for lexer's internal state */
typedef struct PsqlScanStateData *PsqlScanState;
/* Termination states for psql_scan() */
typedef enum
{
PSCAN_SEMICOLON, /* found command-ending semicolon */
PSCAN_BACKSLASH, /* found backslash command */
PSCAN_INCOMPLETE, /* end of line, SQL statement incomplete */
PSCAN_EOL /* end of line, SQL possibly complete */
} PsqlScanResult;
/* Different ways for scan_slash_option to handle parameter words */
enum slash_option_type
{
OT_NORMAL, /* normal case */
OT_SQLID, /* treat as SQL identifier */
OT_SQLIDHACK, /* SQL identifier, but don't downcase */
OT_FILEPIPE, /* it's a filename or pipe */
OT_WHOLE_LINE /* just snarf the rest of the line */
};
extern PsqlScanState psql_scan_create(void);
extern void psql_scan_destroy(PsqlScanState state);
extern void psql_scan_setup(PsqlScanState state,
const char *line, int line_len);
extern void psql_scan_finish(PsqlScanState state);
extern PsqlScanResult psql_scan(PsqlScanState state,
PQExpBuffer query_buf,
promptStatus_t *prompt);
extern void psql_scan_reset(PsqlScanState state);
extern bool psql_scan_in_quote(PsqlScanState state);
extern char *psql_scan_slash_command(PsqlScanState state);
extern char *psql_scan_slash_option(PsqlScanState state,
enum slash_option_type type,
char *quote,
bool semicolon);
extern void psql_scan_slash_command_end(PsqlScanState state);
extern void psql_scan_slash_pushback(PsqlScanState state, const char *str);
#endif /* PSQLSCAN_H */

1506
src/bin/psql/psqlscan.l Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
*
* Copyright (c) 2000-2003, PostgreSQL Global Development Group
*
* $PostgreSQL: pgsql/src/bin/psql/startup.c,v 1.84 2004/02/12 19:58:16 momjian Exp $
* $PostgreSQL: pgsql/src/bin/psql/startup.c,v 1.85 2004/02/19 19:40:09 tgl Exp $
*/
#include "postgres_fe.h"
@ -238,11 +238,20 @@ main(int argc, char *argv[])
*/
else if (options.action == ACT_SINGLE_SLASH)
{
PsqlScanState scan_state;
if (VariableEquals(pset.vars, "ECHO", "all"))
puts(options.action_string);
successResult = HandleSlashCmds(options.action_string, NULL, NULL, NULL) != CMD_ERROR
scan_state = psql_scan_create();
psql_scan_setup(scan_state,
options.action_string,
strlen(options.action_string));
successResult = HandleSlashCmds(scan_state, NULL) != CMD_ERROR
? EXIT_SUCCESS : EXIT_FAILURE;
psql_scan_destroy(scan_state);
}
/*