Extend psql's \e and \ef commands so that a line number can be specified,

and the editor's cursor will be initially placed on that line.  In \e the
lines are counted with respect to the query buffer, while in \ef they are
counted with line 1 = first line of function body.  These choices are useful
for positioning the cursor on the line of a previously-reported error.

To avoid assumptions about what switch the user's editor takes for this
purpose, invent a new psql variable EDITOR_LINENUMBER_SWITCH with (at
present) no default value.

One incompatibility from previous behavior is that "\e 1234" will now
take "1234" as a line number not a file name.  There are at least two
ways to select a numerically-named file if you really want to.

Pavel Stehule, reviewed by Jan Urbanski, with further editing by Robert Haas
and Tom Lane
This commit is contained in:
Tom Lane 2010-08-12 00:40:59 +00:00
parent a4a3ef344e
commit 568e709372
4 changed files with 245 additions and 37 deletions

View File

@ -1,5 +1,5 @@
<!-- <!--
$PostgreSQL: pgsql/doc/src/sgml/ref/psql-ref.sgml,v 1.247 2010/08/03 18:33:09 tgl Exp $ $PostgreSQL: pgsql/doc/src/sgml/ref/psql-ref.sgml,v 1.248 2010/08/12 00:40:59 tgl Exp $
PostgreSQL documentation PostgreSQL documentation
--> -->
@ -1338,48 +1338,60 @@ testdb=&gt;
<varlistentry> <varlistentry>
<term><literal>\edit</literal> (or <literal>\e</literal>) <literal><optional> <replaceable class="parameter">filename</replaceable> </optional></literal></term> <term><literal>\edit</> (or <literal>\e</>) <literal> <optional> <replaceable class="parameter">filename</> </optional> <optional> <replaceable class="parameter">line_number</> </optional> </literal></term>
<listitem> <listitem>
<para> <para>
If <replaceable class="parameter">filename</replaceable> is If <replaceable class="parameter">filename</replaceable> is
specified, the file is edited; after the editor exits, its specified, the file is edited; after the editor exits, its
content is copied back to the query buffer. If no argument is content is copied back to the query buffer. If no <replaceable
given, the current query buffer is copied to a temporary file class="parameter">filename</replaceable> is given, the current query
which is then edited in the same fashion. buffer is copied to a temporary file which is then edited in the same
fashion.
</para> </para>
<para> <para>
The new query buffer is then re-parsed according to the normal The new query buffer is then re-parsed according to the normal
rules of <application>psql</application>, where the whole buffer rules of <application>psql</application>, where the whole buffer
is treated as a single line. (Thus you cannot make scripts this is treated as a single line. (Thus you cannot make scripts this
way. Use <command>\i</command> for that.) This means also that way. Use <command>\i</command> for that.) This means that
if the query ends with (or rather contains) a semicolon, it is if the query ends with (or contains) a semicolon, it is
immediately executed. In other cases it will merely wait in the immediately executed. Otherwise it will merely wait in the
query buffer. query buffer; type semicolon or <literal>\g</> to send it, or
<literal>\r</> to cancel.
</para> </para>
<tip> <tip>
<para> <para>
<application>psql</application> searches the environment <application>psql</application> checks the environment
variables <envar>PSQL_EDITOR</envar>, <envar>EDITOR</envar>, and variables <envar>PSQL_EDITOR</envar>, <envar>EDITOR</envar>, and
<envar>VISUAL</envar> (in that order) for an editor to use. If <envar>VISUAL</envar> (in that order) for an editor to use. If
all of them are unset, <filename>vi</filename> is used on Unix all of them are unset, <filename>vi</filename> is used on Unix
systems, <filename>notepad.exe</filename> on Windows systems. systems, <filename>notepad.exe</filename> on Windows systems.
</para> </para>
</tip> </tip>
<para>
If a line number is specified, <application>psql</application> will
position the cursor on the specified line of the file or query buffer.
This feature requires the <varname>EDITOR_LINENUMBER_SWITCH</varname>
variable to be set, so that <application>psql</application> knows how
to specify the line number to the editor. Note that if a single
all-digits argument is given, <application>psql</application> assumes
it is a line number not a file name.
</para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry> <varlistentry>
<term><literal>\ef <optional> <replaceable class="parameter">function_description</replaceable> </optional></literal></term> <term><literal>\ef <optional> <replaceable class="parameter">function_description</> <optional> <replaceable class="parameter">line_number</> </optional> </optional> </literal></term>
<listitem> <listitem>
<para> <para>
This command fetches and edits the definition of the named function, This command fetches and edits the definition of the named function,
in the form of a <command>CREATE OR REPLACE FUNCTION</> command. in the form of a <command>CREATE OR REPLACE FUNCTION</> command.
Editing is done in the same way as for <literal>\e</>. Editing is done in the same way as for <literal>\edit</>.
After the editor exits, the updated command waits in the query buffer; After the editor exits, the updated command waits in the query buffer;
type semicolon or <literal>\g</> to send it, or <literal>\r</> type semicolon or <literal>\g</> to send it, or <literal>\r</>
to cancel. to cancel.
@ -1396,6 +1408,16 @@ testdb=&gt;
If no function is specified, a blank <command>CREATE FUNCTION</> If no function is specified, a blank <command>CREATE FUNCTION</>
template is presented for editing. template is presented for editing.
</para> </para>
<para>
If a line number is specified, <application>psql</application> will
position the cursor on the specified line of the function body
(note that the function body typically does not begin on the
first line of the file).
This feature requires the <varname>EDITOR_LINENUMBER_SWITCH</varname>
variable to be set, so that <application>psql</application> knows how
to specify the line number to the editor.
</para>
</listitem> </listitem>
</varlistentry> </varlistentry>
@ -2457,6 +2479,27 @@ bar
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><varname>EDITOR_LINENUMBER_SWITCH</varname></term>
<listitem>
<para>
When <command>\edit</command> or <command>\ef</command> is used with a
line number argument, this variable specifies the command-line switch
used to pass the line number to the user's editor. For editors such
as <productname>emacs</> or <productname>vi</>, you can simply set
this variable to a plus sign. Include a trailing space in the value
of the variable if there needs to be space between the switch name and
the line number.
Examples:
<programlisting>
\set EDITOR_LINENUMBER_SWITCH +
\set EDITOR_LINENUMBER_SWITCH '--line '
</programlisting>
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><varname>ENCODING</varname></term> <term><varname>ENCODING</varname></term>
<listitem> <listitem>

View File

@ -9,7 +9,7 @@
* *
* *
* IDENTIFICATION * IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.330 2010/08/03 19:24:04 tgl Exp $ * $PostgreSQL: pgsql/src/backend/utils/adt/ruleutils.c,v 1.331 2010/08/12 00:40:59 tgl Exp $
* *
*------------------------------------------------------------------------- *-------------------------------------------------------------------------
*/ */
@ -1620,6 +1620,10 @@ pg_get_serial_sequence(PG_FUNCTION_ARGS)
* pg_get_functiondef * pg_get_functiondef
* Returns the complete "CREATE OR REPLACE FUNCTION ..." statement for * Returns the complete "CREATE OR REPLACE FUNCTION ..." statement for
* the specified function. * the specified function.
*
* Note: if you change the output format of this function, be careful not
* to break psql's rules (in \ef) for identifying the start of the function
* body.
*/ */
Datum Datum
pg_get_functiondef(PG_FUNCTION_ARGS) pg_get_functiondef(PG_FUNCTION_ARGS)

View File

@ -3,7 +3,7 @@
* *
* Copyright (c) 2000-2010, PostgreSQL Global Development Group * Copyright (c) 2000-2010, PostgreSQL Global Development Group
* *
* $PostgreSQL: pgsql/src/bin/psql/command.c,v 1.225 2010/08/03 18:33:09 tgl Exp $ * $PostgreSQL: pgsql/src/bin/psql/command.c,v 1.226 2010/08/12 00:40:59 tgl Exp $
*/ */
#include "postgres_fe.h" #include "postgres_fe.h"
#include "command.h" #include "command.h"
@ -57,11 +57,12 @@ static backslashResult exec_command(const char *cmd,
PsqlScanState scan_state, PsqlScanState scan_state,
PQExpBuffer query_buf); PQExpBuffer query_buf);
static bool do_edit(const char *filename_arg, PQExpBuffer query_buf, static bool do_edit(const char *filename_arg, PQExpBuffer query_buf,
bool *edited); int lineno, bool *edited);
static bool do_connect(char *dbname, char *user, char *host, char *port); static bool do_connect(char *dbname, char *user, char *host, char *port);
static bool do_shell(const char *command); static bool do_shell(const char *command);
static bool lookup_function_oid(PGconn *conn, const char *desc, Oid *foid); static bool lookup_function_oid(PGconn *conn, const char *desc, Oid *foid);
static bool get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf); static bool get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf);
static int strip_lineno_from_funcdesc(char *func);
static void minimal_error_message(PGresult *res); static void minimal_error_message(PGresult *res);
static void printSSLInfo(void); static void printSSLInfo(void);
@ -497,8 +498,8 @@ exec_command(const char *cmd,
/* /*
* \e or \edit -- edit the current query buffer (or a file and make it the * \e or \edit -- edit the current query buffer, or edit a file and make
* query buffer * it the query buffer
*/ */
else if (strcmp(cmd, "e") == 0 || strcmp(cmd, "edit") == 0) else if (strcmp(cmd, "e") == 0 || strcmp(cmd, "edit") == 0)
{ {
@ -510,17 +511,51 @@ exec_command(const char *cmd,
else else
{ {
char *fname; char *fname;
char *ln = NULL;
int lineno = -1;
fname = psql_scan_slash_option(scan_state, fname = psql_scan_slash_option(scan_state,
OT_NORMAL, NULL, true); OT_NORMAL, NULL, true);
expand_tilde(&fname);
if (fname) if (fname)
canonicalize_path(fname); {
if (do_edit(fname, query_buf, NULL)) /* try to get separate lineno arg */
status = PSQL_CMD_NEWEDIT; ln = psql_scan_slash_option(scan_state,
else OT_NORMAL, NULL, true);
status = PSQL_CMD_ERROR; if (ln == NULL)
free(fname); {
/* only one arg; maybe it is lineno not fname */
if (fname[0] &&
strspn(fname, "0123456789") == strlen(fname))
{
/* all digits, so assume it is lineno */
ln = fname;
fname = NULL;
}
}
}
if (ln)
{
lineno = atoi(ln);
if (lineno < 1)
{
psql_error("invalid line number: %s\n", ln);
status = PSQL_CMD_ERROR;
}
}
if (status != PSQL_CMD_ERROR)
{
expand_tilde(&fname);
if (fname)
canonicalize_path(fname);
if (do_edit(fname, query_buf, lineno, NULL))
status = PSQL_CMD_NEWEDIT;
else
status = PSQL_CMD_ERROR;
}
if (fname)
free(fname);
if (ln)
free(ln);
} }
} }
@ -530,6 +565,8 @@ exec_command(const char *cmd,
*/ */
else if (strcmp(cmd, "ef") == 0) else if (strcmp(cmd, "ef") == 0)
{ {
int lineno = -1;
if (!query_buf) if (!query_buf)
{ {
psql_error("no query buffer\n"); psql_error("no query buffer\n");
@ -542,7 +579,13 @@ exec_command(const char *cmd,
func = psql_scan_slash_option(scan_state, func = psql_scan_slash_option(scan_state,
OT_WHOLE_LINE, NULL, true); OT_WHOLE_LINE, NULL, true);
if (!func) lineno = strip_lineno_from_funcdesc(func);
if (lineno == 0)
{
/* error already reported */
status = PSQL_CMD_ERROR;
}
else if (!func)
{ {
/* set up an empty command to fill in */ /* set up an empty command to fill in */
printfPQExpBuffer(query_buf, printfPQExpBuffer(query_buf,
@ -563,6 +606,32 @@ exec_command(const char *cmd,
/* error already reported */ /* error already reported */
status = PSQL_CMD_ERROR; status = PSQL_CMD_ERROR;
} }
else if (lineno > 0)
{
/*
* lineno "1" should correspond to the first line of the
* function body. We expect that pg_get_functiondef() will
* emit that on a line beginning with "AS $function", and that
* there can be no such line before the real start of the
* function body. Increment lineno by the number of lines
* before that line, so that it becomes relative to the first
* line of the function definition.
*/
const char *lines = query_buf->data;
while (*lines != '\0')
{
if (strncmp(lines, "AS $function", 12) == 0)
break;
lineno++;
/* find start of next line */
lines = strchr(lines, '\n');
if (!lines)
break;
lines++;
}
}
if (func) if (func)
free(func); free(func);
} }
@ -571,7 +640,7 @@ exec_command(const char *cmd,
{ {
bool edited = false; bool edited = false;
if (!do_edit(0, query_buf, &edited)) if (!do_edit(NULL, query_buf, lineno, &edited))
status = PSQL_CMD_ERROR; status = PSQL_CMD_ERROR;
else if (!edited) else if (!edited)
puts(_("No changes")); puts(_("No changes"));
@ -1543,11 +1612,11 @@ UnsyncVariables(void)
* If you do not specify a filename, the current query buffer will be copied * If you do not specify a filename, the current query buffer will be copied
* into a temporary one. * into a temporary one.
*/ */
static bool static bool
editFile(const char *fname) editFile(const char *fname, int lineno)
{ {
const char *editorName; const char *editorName;
const char *editor_lineno_switch = NULL;
char *sys; char *sys;
int result; int result;
@ -1562,6 +1631,26 @@ editFile(const char *fname)
if (!editorName) if (!editorName)
editorName = DEFAULT_EDITOR; editorName = DEFAULT_EDITOR;
/* Get line number switch, if we need it. */
if (lineno > 0)
{
editor_lineno_switch = GetVariable(pset.vars,
"EDITOR_LINENUMBER_SWITCH");
if (editor_lineno_switch == NULL)
{
psql_error("EDITOR_LINENUMBER_SWITCH variable must be set to specify a line number\n");
return false;
}
}
/* Allocate sufficient memory for command line. */
if (lineno > 0)
sys = pg_malloc(strlen(editorName)
+ strlen(editor_lineno_switch) + 10 /* for integer */
+ 1 + strlen(fname) + 10 + 1);
else
sys = pg_malloc(strlen(editorName) + strlen(fname) + 10 + 1);
/* /*
* On Unix the EDITOR value should *not* be quoted, since it might include * On Unix the EDITOR value should *not* be quoted, since it might include
* switches, eg, EDITOR="pico -t"; it's up to the user to put quotes in it * switches, eg, EDITOR="pico -t"; it's up to the user to put quotes in it
@ -1569,11 +1658,20 @@ editFile(const char *fname)
* severe brain damage in their command shell plus the fact that standard * severe brain damage in their command shell plus the fact that standard
* program paths include spaces. * program paths include spaces.
*/ */
sys = pg_malloc(strlen(editorName) + strlen(fname) + 10 + 1);
#ifndef WIN32 #ifndef WIN32
sprintf(sys, "exec %s '%s'", editorName, fname); if (lineno > 0)
sprintf(sys, "exec %s %s%d '%s'",
editorName, editor_lineno_switch, lineno, fname);
else
sprintf(sys, "exec %s '%s'",
editorName, fname);
#else #else
sprintf(sys, SYSTEMQUOTE "\"%s\" \"%s\"" SYSTEMQUOTE, editorName, fname); if (lineno > 0)
sprintf(sys, SYSTEMQUOTE "\"%s\" %s%d \"%s\"" SYSTEMQUOTE,
editorName, editor_lineno_switch, lineno, fname);
else
sprintf(sys, SYSTEMQUOTE "\"%s\" \"%s\"" SYSTEMQUOTE,
editorName, fname);
#endif #endif
result = system(sys); result = system(sys);
if (result == -1) if (result == -1)
@ -1588,7 +1686,8 @@ editFile(const char *fname)
/* call this one */ /* call this one */
static bool static bool
do_edit(const char *filename_arg, PQExpBuffer query_buf, bool *edited) do_edit(const char *filename_arg, PQExpBuffer query_buf,
int lineno, bool *edited)
{ {
char fnametmp[MAXPGPATH]; char fnametmp[MAXPGPATH];
FILE *stream = NULL; FILE *stream = NULL;
@ -1680,7 +1779,7 @@ do_edit(const char *filename_arg, PQExpBuffer query_buf, bool *edited)
/* call editor */ /* call editor */
if (!error) if (!error)
error = !editFile(fname); error = !editFile(fname, lineno);
if (!error && stat(fname, &after) != 0) if (!error && stat(fname, &after) != 0)
{ {
@ -2208,6 +2307,68 @@ get_create_function_cmd(PGconn *conn, Oid oid, PQExpBuffer buf)
return result; return result;
} }
/*
* If the given argument of \ef ends with a line number, delete the line
* number from the argument string and return it as an integer. (We need
* this kluge because we're too lazy to parse \ef's function name argument
* carefully --- we just slop it up in OT_WHOLE_LINE mode.)
*
* Returns -1 if no line number is present, 0 on error, or a positive value
* on success.
*/
static int
strip_lineno_from_funcdesc(char *func)
{
char *c;
int lineno;
if (!func || func[0] == '\0')
return -1;
c = func + strlen(func) - 1;
/*
* This business of parsing backwards is dangerous as can be in a
* multibyte environment: there is no reason to believe that we are
* looking at the first byte of a character, nor are we necessarily
* working in a "safe" encoding. Fortunately the bitpatterns we are
* looking for are unlikely to occur as non-first bytes, but beware
* of trying to expand the set of cases that can be recognized. We must
* guard the <ctype.h> macros by using isascii() first, too.
*/
/* skip trailing whitespace */
while (c > func && isascii(*c) && isspace(*c))
c--;
/* must have a digit as last non-space char */
if (c == func || !isascii(*c) || !isdigit(*c))
return -1;
/* find start of digit string */
while (c > func && isascii(*c) && isdigit(*c))
c--;
/* digits must be separated from func name by space or closing paren */
/* notice also that we are not allowing an empty func name ... */
if (c == func || !isascii(*c) || !(isspace(*c) || *c == ')'))
return -1;
/* parse digit string */
c++;
lineno = atoi(c);
if (lineno < 1)
{
psql_error("invalid line number: %s\n", c);
return 0;
}
/* strip digit string from func */
*c = '\0';
return lineno;
}
/* /*
* Report just the primary error; this is to avoid cluttering the output * Report just the primary error; this is to avoid cluttering the output
* with, for instance, a redisplay of the internally generated query * with, for instance, a redisplay of the internally generated query

View File

@ -3,7 +3,7 @@
* *
* Copyright (c) 2000-2010, PostgreSQL Global Development Group * Copyright (c) 2000-2010, PostgreSQL Global Development Group
* *
* $PostgreSQL: pgsql/src/bin/psql/help.c,v 1.160 2010/07/20 03:54:19 rhaas Exp $ * $PostgreSQL: pgsql/src/bin/psql/help.c,v 1.161 2010/08/12 00:40:59 tgl Exp $
*/ */
#include "postgres_fe.h" #include "postgres_fe.h"
@ -162,7 +162,7 @@ slashUsage(unsigned short int pager)
{ {
FILE *output; FILE *output;
output = PageOutput(87, pager); output = PageOutput(89, pager);
/* if you add/remove a line here, change the row count above */ /* if you add/remove a line here, change the row count above */
@ -174,8 +174,8 @@ slashUsage(unsigned short int pager)
fprintf(output, "\n"); fprintf(output, "\n");
fprintf(output, _("Query Buffer\n")); fprintf(output, _("Query Buffer\n"));
fprintf(output, _(" \\e [FILE] edit the query buffer (or file) with external editor\n")); fprintf(output, _(" \\e [FILE] [LINE] edit the query buffer (or file) with external editor\n"));
fprintf(output, _(" \\ef [FUNCNAME] edit function definition with external editor\n")); fprintf(output, _(" \\ef [FUNCNAME [LINE]] edit function definition with external editor\n"));
fprintf(output, _(" \\p show the contents of the query buffer\n")); fprintf(output, _(" \\p show the contents of the query buffer\n"));
fprintf(output, _(" \\r reset (clear) the query buffer\n")); fprintf(output, _(" \\r reset (clear) the query buffer\n"));
#ifdef USE_READLINE #ifdef USE_READLINE