diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index a1ca94057b..6d45f25aee 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -151,6 +151,21 @@ EOF
+
+
+
+
+
+ CSV
+ in psql
+
+ Switches to CSV (Comma Separated Values)
+ table output mode. This is equivalent to
+ \pset format csv.
+
+
+
+
@@ -2556,6 +2571,19 @@ lo_import 152801
+
+ fieldsep_csv
+
+
+ Specifies the field separator to be used in the
+ CSV format. When the separator appears in a field
+ value, that field is output inside double quotes according to
+ CSV rules. To set a tab as field separator, type
+ \pset fieldsep_csv '\t'. The default is a comma.
+
+
+
+
fieldsep_zero
@@ -2585,7 +2613,8 @@ lo_import 152801
Sets the output format to one of aligned,
- asciidoc, html,
+ asciidoc, csv,
+ html,
latex (uses tabular),
latex-longtable, troff-ms,
unaligned, or wrapped.
@@ -2604,6 +2633,21 @@ lo_import 152801
nicely formatted text output; this is the default.
+ csv format writes columns separated by
+ commas, applying the quoting rules described in
+ RFC 4180.
+ Alternative separators can be selected with
+ \pset fieldsep_csv.
+ The output is compatible with the CSV format of the
+ COPY command. The header with column names
+ is generated unless the tuples_only parameter is
+ on. Title and footers are not printed.
+ Each row is terminated by the system-dependent end-of-line character,
+ which is typically a single newline (\n) for
+ Unix-like systems or a carriage return and newline sequence
+ (\r\n) for Microsoft Windows.
+
+
wrapped format is like aligned but wraps
wide data values across lines to make the output fit in the target
column width. The target width is determined as described under
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 0dea54d3ce..7e3f577897 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -1941,8 +1941,8 @@ exec_command_pset(PsqlScanState scan_state, bool active_branch)
int i;
static const char *const my_list[] = {
- "border", "columns", "expanded", "fieldsep", "fieldsep_zero",
- "footer", "format", "linestyle", "null",
+ "border", "columns", "expanded", "fieldsep", "fieldsep_csv",
+ "fieldsep_zero", "footer", "format", "linestyle", "null",
"numericlocale", "pager", "pager_min_lines",
"recordsep", "recordsep_zero",
"tableattr", "title", "tuples_only",
@@ -3566,6 +3566,9 @@ _align2string(enum printFormat in)
case PRINT_ASCIIDOC:
return "asciidoc";
break;
+ case PRINT_CSV:
+ return "csv";
+ break;
case PRINT_HTML:
return "html";
break;
@@ -3643,6 +3646,8 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.format = PRINT_ALIGNED;
else if (pg_strncasecmp("asciidoc", value, vallen) == 0)
popt->topt.format = PRINT_ASCIIDOC;
+ else if (pg_strncasecmp("csv", value, vallen) == 0)
+ popt->topt.format = PRINT_CSV;
else if (pg_strncasecmp("html", value, vallen) == 0)
popt->topt.format = PRINT_HTML;
else if (pg_strncasecmp("latex", value, vallen) == 0)
@@ -3657,7 +3662,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
popt->topt.format = PRINT_WRAPPED;
else
{
- psql_error("\\pset: allowed formats are aligned, asciidoc, html, latex, latex-longtable, troff-ms, unaligned, wrapped\n");
+ psql_error("\\pset: allowed formats are aligned, asciidoc, csv, html, latex, latex-longtable, troff-ms, unaligned, wrapped\n");
return false;
}
}
@@ -3785,6 +3790,26 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet)
}
}
+ else if (strcmp(param, "fieldsep_csv") == 0)
+ {
+ if (value)
+ {
+ /* CSV separator has to be a one-byte character */
+ if (strlen(value) != 1)
+ {
+ psql_error("\\pset: the CSV field separator must be a single character\n");
+ return false;
+ }
+ if (value[0] == '"' || value[0] == '\n' || value[0] == '\r')
+ {
+ psql_error("\\pset: the CSV field separator cannot be a double quote, a newline, or a carriage return\n");
+ return false;
+ }
+ free(popt->topt.fieldSepCsv);
+ popt->topt.fieldSepCsv = pg_strdup(value);
+ }
+ }
+
else if (strcmp(param, "fieldsep_zero") == 0)
{
free(popt->topt.fieldSep.separator);
@@ -3940,6 +3965,13 @@ printPsetInfo(const char *param, struct printQueryOpt *popt)
printf(_("Field separator is zero byte.\n"));
}
+ /* show field separator for CSV format */
+ else if (strcmp(param, "fieldsep_csv") == 0)
+ {
+ printf(_("Field separator for CSV is \"%s\".\n"),
+ popt->topt.fieldSepCsv);
+ }
+
/* show disable "(x rows)" footer */
else if (strcmp(param, "footer") == 0)
{
@@ -4134,6 +4166,8 @@ pset_value_string(const char *param, struct printQueryOpt *popt)
return pset_quoted_string(popt->topt.fieldSep.separator
? popt->topt.fieldSep.separator
: "");
+ else if (strcmp(param, "fieldsep_csv") == 0)
+ return pset_quoted_string(popt->topt.fieldSepCsv);
else if (strcmp(param, "fieldsep_zero") == 0)
return pstrdup(pset_bool_string(popt->topt.fieldSep.separator_zero));
else if (strcmp(param, "footer") == 0)
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 586aebddd3..20b3dfc683 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -108,6 +108,7 @@ usage(unsigned short int pager)
fprintf(output, _("\nOutput format options:\n"));
fprintf(output, _(" -A, --no-align unaligned table output mode\n"));
+ fprintf(output, _(" --csv CSV (Comma Separated Values) table output mode\n"));
fprintf(output, _(" -F, --field-separator=STRING\n"
" field separator for unaligned output (default: \"%s\")\n"),
DEFAULT_FIELD_SEP);
@@ -272,10 +273,10 @@ slashUsage(unsigned short int pager)
fprintf(output, _(" \\H toggle HTML output mode (currently %s)\n"),
ON(pset.popt.topt.format == PRINT_HTML));
fprintf(output, _(" \\pset [NAME [VALUE]] set table output option\n"
- " (NAME := {border|columns|expanded|fieldsep|fieldsep_zero|\n"
- " footer|format|linestyle|null|numericlocale|pager|\n"
- " pager_min_lines|recordsep|recordsep_zero|tableattr|title|\n"
- " tuples_only|unicode_border_linestyle|\n"
+ " (NAME := {border|columns|expanded|fieldsep|fieldsep_csv|\n"
+ " fieldsep_zero|footer|format|linestyle|null|numericlocale|\n"
+ " pager|pager_min_lines|recordsep|recordsep_zero|tableattr|\n"
+ " title|tuples_only|unicode_border_linestyle|\n"
" unicode_column_linestyle|unicode_header_linestyle})\n"));
fprintf(output, _(" \\t [on|off] show only rows (currently %s)\n"),
ON(pset.popt.topt.tuples_only));
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 69e617e6b5..93d0b957f5 100644
--- a/src/bin/psql/settings.h
+++ b/src/bin/psql/settings.h
@@ -13,6 +13,7 @@
#include "fe_utils/print.h"
#define DEFAULT_FIELD_SEP "|"
+#define DEFAULT_FIELD_SEP_CSV ","
#define DEFAULT_RECORD_SEP "\n"
#if defined(WIN32) || defined(__CYGWIN__)
diff --git a/src/bin/psql/startup.c b/src/bin/psql/startup.c
index be57574cd3..40e14494cb 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -148,6 +148,8 @@ main(int argc, char *argv[])
pset.popt.topt.unicode_column_linestyle = UNICODE_LINESTYLE_SINGLE;
pset.popt.topt.unicode_header_linestyle = UNICODE_LINESTYLE_SINGLE;
+ pset.popt.topt.fieldSepCsv = pg_strdup(DEFAULT_FIELD_SEP_CSV);
+
refresh_utf8format(&(pset.popt.topt));
/* We must get COLUMNS here before readline() sets it */
@@ -468,6 +470,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
{"expanded", no_argument, NULL, 'x'},
{"no-psqlrc", no_argument, NULL, 'X'},
{"help", optional_argument, NULL, 1},
+ {"csv", no_argument, NULL, 2},
{NULL, 0, NULL, 0}
};
@@ -658,6 +661,9 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options)
exit(EXIT_SUCCESS);
}
break;
+ case 2:
+ pset.popt.topt.format = PRINT_CSV;
+ break;
default:
unknown_option:
fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
diff --git a/src/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c
index 7294824948..eb1c54e47a 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3527,9 +3527,9 @@ psql_completion(const char *text, int start, int end)
else if (TailMatchesCS("\\password"))
COMPLETE_WITH_QUERY(Query_for_list_of_roles);
else if (TailMatchesCS("\\pset"))
- COMPLETE_WITH_CS("border", "columns", "expanded",
- "fieldsep", "fieldsep_zero", "footer", "format",
- "linestyle", "null", "numericlocale",
+ COMPLETE_WITH_CS("border", "columns", "expanded", "fieldsep",
+ "fieldsep_csv", "fieldsep_zero", "footer",
+ "format", "linestyle", "null", "numericlocale",
"pager", "pager_min_lines",
"recordsep", "recordsep_zero",
"tableattr", "title", "tuples_only",
@@ -3539,7 +3539,7 @@ psql_completion(const char *text, int start, int end)
else if (TailMatchesCS("\\pset", MatchAny))
{
if (TailMatchesCS("format"))
- COMPLETE_WITH_CS("aligned", "asciidoc", "html", "latex",
+ COMPLETE_WITH_CS("aligned", "asciidoc", "csv", "html", "latex",
"latex-longtable", "troff-ms", "unaligned",
"wrapped");
else if (TailMatchesCS("linestyle"))
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index cb9a9a0613..a56c5f3762 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -2783,6 +2783,113 @@ print_troff_ms_vertical(const printTableContent *cont, FILE *fout)
}
}
+/*************************/
+/* CSV */
+/*************************/
+static void
+csv_escaped_print(const char *text, FILE *fout)
+{
+ const char *p;
+
+ fputc('"', fout);
+ for (p = text; *p; p++)
+ {
+ if (*p == '"')
+ fputc('"', fout); /* double quotes are doubled */
+ fputc(*p, fout);
+ }
+ fputc('"', fout);
+}
+
+static void
+csv_print_field(const char *text, FILE *fout, const char *sep)
+{
+ /*----------------
+ * Enclose and escape field contents when one of these conditions is met:
+ * - the field separator is found in the contents.
+ * - the field contains a CR or LF.
+ * - the field contains a double quote.
+ *----------------
+ */
+ if ((sep != NULL && *sep != '\0' && strstr(text, sep) != NULL) ||
+ strcspn(text, "\r\n\"") != strlen(text))
+ {
+ csv_escaped_print(text, fout);
+ }
+ else
+ fputs(text, fout);
+}
+
+static void
+print_csv_text(const printTableContent *cont, FILE *fout)
+{
+ const char *const *ptr;
+ int i;
+
+ if (cancel_pressed)
+ return;
+
+ /*
+ * The title and footer are never printed in csv format. The header is
+ * printed if opt_tuples_only is false.
+ *
+ * Despite RFC 4180 saying that end of lines are CRLF, terminate lines
+ * with '\n', which represent system-dependent end of lines in text mode
+ * (typically LF on Unix and CRLF on Windows).
+ */
+
+ if (cont->opt->start_table && !cont->opt->tuples_only)
+ {
+ /* print headers */
+ for (ptr = cont->headers; *ptr; ptr++)
+ {
+ if (ptr != cont->headers)
+ fputs(cont->opt->fieldSepCsv, fout);
+ csv_print_field(*ptr, fout, cont->opt->fieldSepCsv);
+ }
+ fputc('\n', fout);
+ }
+
+ /* print cells */
+ for (i = 0, ptr = cont->cells; *ptr; i++, ptr++)
+ {
+ csv_print_field(*ptr, fout, cont->opt->fieldSepCsv);
+
+ if ((i + 1) % cont->ncolumns)
+ fputs(cont->opt->fieldSepCsv, fout);
+ else
+ {
+ fputc('\n', fout);
+ }
+ }
+}
+
+static void
+print_csv_vertical(const printTableContent *cont, FILE *fout)
+{
+ unsigned int i;
+ const char *const *ptr;
+
+ /* print records */
+ for (i = 0, ptr = cont->cells; *ptr; i++, ptr++)
+ {
+ if (cancel_pressed)
+ return;
+
+ /* print name of column */
+ csv_print_field(cont->headers[i % cont->ncolumns], fout,
+ cont->opt->fieldSepCsv);
+
+ /* print field separator */
+ fputs(cont->opt->fieldSepCsv, fout);
+
+ /* print field value */
+ csv_print_field(*ptr, fout, cont->opt->fieldSepCsv);
+
+ fputc('\n', fout);
+ }
+}
+
/********************************/
/* Public functions */
@@ -3234,6 +3341,12 @@ printTable(const printTableContent *cont,
else
print_aligned_text(cont, fout, is_pager);
break;
+ case PRINT_CSV:
+ if (cont->opt->expanded == 1)
+ print_csv_vertical(cont, fout);
+ else
+ print_csv_text(cont, fout);
+ break;
case PRINT_HTML:
if (cont->opt->expanded == 1)
print_html_vertical(cont, fout);
diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h
index b761349bc7..665a7827e2 100644
--- a/src/include/fe_utils/print.h
+++ b/src/include/fe_utils/print.h
@@ -28,6 +28,7 @@ enum printFormat
PRINT_NOTHING = 0, /* to make sure someone initializes this */
PRINT_ALIGNED,
PRINT_ASCIIDOC,
+ PRINT_CSV,
PRINT_HTML,
PRINT_LATEX,
PRINT_LATEX_LONGTABLE,
@@ -112,6 +113,7 @@ typedef struct printTableOpt
const printTextFormat *line_style; /* line style (NULL for default) */
struct separator fieldSep; /* field separator for unaligned text mode */
struct separator recordSep; /* record separator for unaligned text mode */
+ char *fieldSepCsv; /* field separator for csv format */
bool numericLocale; /* locale-aware numeric units separator and
* decimal marker */
char *tableAttr; /* attributes for HTML
*/
diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out
index 3818cfea7e..ceea28f027 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -262,6 +262,7 @@ border 1
columns 0
expanded off
fieldsep '|'
+fieldsep_csv ','
fieldsep_zero off
footer on
format aligned
@@ -3243,3 +3244,146 @@ last error message: division by zero
\echo 'last error code:' :LAST_ERROR_SQLSTATE
last error code: 22012
\unset FETCH_COUNT
+-- test csv format
+-- test multi-line headers, wrapping, quoting rules and newline indicators
+prepare q as select 'ab,cd' as col1,
+ 'ab' as "col,2",
+ E'a\tb' as col3,
+ '"' as col4,
+ '""' as col5,
+ 'a"b' as "col""6",
+ E'a\nb' as col7,
+ NULL as col8,
+ 'ab' as "col
+9",
+ 'cd' as "col
+
+10",
+ array['ab', E'cd\nef'] as col11,
+ '{"a":"a,b", "a,b":null, "c":"a,\"b"}'::json as col12
+ from generate_series(1,2);
+\pset format csv
+\pset fieldsep_csv ','
+\pset expanded off
+\t off
+execute q;
+col1,"col,2",col3,col4,col5,"col""6",col7,col8,"col
+9","col
+
+10",col11,col12
+"ab,cd",ab,a b,"""","""""","a""b","a
+b",,ab,cd,"{ab,""cd
+ef""}","{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+"ab,cd",ab,a b,"""","""""","a""b","a
+b",,ab,cd,"{ab,""cd
+ef""}","{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\pset fieldsep_csv '\t'
+execute q;
+col1 col,2 col3 col4 col5 "col""6" col7 col8 "col
+9" "col
+
+10" col11 col12
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab cd "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab cd "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\t on
+execute q;
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab cd "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+ab,cd ab "a b" """" """""" "a""b" "a
+b" ab cd "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\t off
+\pset expanded on
+execute q;
+col1 ab,cd
+col,2 ab
+col3 "a b"
+col4 """"
+col5 """"""
+"col""6" "a""b"
+col7 "a
+b"
+col8
+"col
+9" ab
+"col
+
+10" cd
+col11 "{ab,""cd
+ef""}"
+col12 "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+col1 ab,cd
+col,2 ab
+col3 "a b"
+col4 """"
+col5 """"""
+"col""6" "a""b"
+col7 "a
+b"
+col8
+"col
+9" ab
+"col
+
+10" cd
+col11 "{ab,""cd
+ef""}"
+col12 "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\pset fieldsep_csv ','
+execute q;
+col1,"ab,cd"
+"col,2",ab
+col3,a b
+col4,""""
+col5,""""""
+"col""6","a""b"
+col7,"a
+b"
+col8,
+"col
+9",ab
+"col
+
+10",cd
+col11,"{ab,""cd
+ef""}"
+col12,"{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+col1,"ab,cd"
+"col,2",ab
+col3,a b
+col4,""""
+col5,""""""
+"col""6","a""b"
+col7,"a
+b"
+col8,
+"col
+9",ab
+"col
+
+10",cd
+col11,"{ab,""cd
+ef""}"
+col12,"{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+-- illegal csv separators
+\pset fieldsep_csv ''
+\pset: the CSV field separator must be a single character
+\pset fieldsep_csv ',,'
+\pset: the CSV field separator must be a single character
+\pset fieldsep_csv '\0'
+\pset: the CSV field separator must be a single character
+\pset fieldsep_csv '\n'
+\pset: the CSV field separator cannot be a double quote, a newline, or a carriage return
+\pset fieldsep_csv '\r'
+\pset: the CSV field separator cannot be a double quote, a newline, or a carriage return
+\pset fieldsep_csv '"'
+\pset: the CSV field separator cannot be a double quote, a newline, or a carriage return
+deallocate q;
+\pset format aligned
+\pset expanded off
+\t off
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index b45da9bb8d..fd2d0df0c3 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -688,3 +688,50 @@ select 1/(15-unique2) from tenk1 order by unique2 limit 19;
\echo 'last error code:' :LAST_ERROR_SQLSTATE
\unset FETCH_COUNT
+
+-- test csv format
+-- test multi-line headers, wrapping, quoting rules and newline indicators
+prepare q as select 'ab,cd' as col1,
+ 'ab' as "col,2",
+ E'a\tb' as col3,
+ '"' as col4,
+ '""' as col5,
+ 'a"b' as "col""6",
+ E'a\nb' as col7,
+ NULL as col8,
+ 'ab' as "col
+9",
+ 'cd' as "col
+
+10",
+ array['ab', E'cd\nef'] as col11,
+ '{"a":"a,b", "a,b":null, "c":"a,\"b"}'::json as col12
+ from generate_series(1,2);
+
+\pset format csv
+\pset fieldsep_csv ','
+\pset expanded off
+\t off
+execute q;
+\pset fieldsep_csv '\t'
+execute q;
+\t on
+execute q;
+\t off
+\pset expanded on
+execute q;
+\pset fieldsep_csv ','
+execute q;
+
+-- illegal csv separators
+\pset fieldsep_csv ''
+\pset fieldsep_csv ',,'
+\pset fieldsep_csv '\0'
+\pset fieldsep_csv '\n'
+\pset fieldsep_csv '\r'
+\pset fieldsep_csv '"'
+
+deallocate q;
+\pset format aligned
+\pset expanded off
+\t off