David Steele wrote:

> Do you know when you'll have an updated patch that addresses the minor
> issues brought up in review and the concern above?

Here's an update addressing the issues discussed:

- fieldsep and recordsep are used, no more fieldsep_csv
- the command line option is --csv without short option and is equivalent
 to -P format=csv -P fieldsep=','
- pset output formats are reordered alphabetically on display
- a couple more cases in the regression tests


Best regards,
-- 
Daniel Vérité
PostgreSQL-powered mailer: http://www.manitou-mail.org
Twitter: @DanielVerite
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index 10b9795..c984a9c 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -152,6 +152,16 @@ EOF
     </varlistentry>
 
     <varlistentry>
+      <term><option>--csv</option></term>
+      <listitem>
+      <para>
+      Switches to csv output mode. This is equivalent to <command>\pset format
+      csv</command> followed by <command>\pset fieldsep ','</command>.
+      </para>
+      </listitem>
+    </varlistentry>
+
+    <varlistentry>
       <term><option>-d <replaceable 
class="parameter">dbname</replaceable></option></term>
       <term><option>--dbname=<replaceable 
class="parameter">dbname</replaceable></option></term>
       <listitem>
@@ -246,7 +256,7 @@ EOF
       <listitem>
       <para>
       Use <replaceable class="parameter">separator</replaceable> as the
-      field separator for unaligned output. This is equivalent to
+      field separator for unaligned and csv outputs. This is equivalent to
       <command>\pset fieldsep</command> or <command>\f</command>.
       </para>
       </listitem>
@@ -382,7 +392,7 @@ EOF
       <listitem>
       <para>
       Use <replaceable class="parameter">separator</replaceable> as the
-      record separator for unaligned output. This is equivalent to
+      record separator for unaligned and csv outputs. This is equivalent to
       <command>\pset recordsep</command>.
       </para>
       </listitem>
@@ -558,7 +568,7 @@ EOF
       <listitem>
       <para>
       Set the field separator for unaligned output to a zero byte.  This is
-      equvalent to <command>\pset fieldsep_zero</command>.
+      equivalent to <command>\pset fieldsep_zero</command>.
       </para>
       </listitem>
     </varlistentry>
@@ -1937,9 +1947,9 @@ Tue Oct 26 21:40:57 CEST 1999
 
         <listitem>
         <para>
-        Sets the field separator for unaligned query output. The default
-        is the vertical bar (<literal>|</literal>). It is equivalent to
-        <command>\pset fieldsep</command>.
+        Sets the field separator for unaligned and csv query outputs. The
+        default is the vertical bar (<literal>|</literal>). It is equivalent
+        to <command>\pset fieldsep</command>.
         </para>
         </listitem>
       </varlistentry>
@@ -2546,8 +2556,8 @@ lo_import 152801
           <term><literal>fieldsep</literal></term>
           <listitem>
           <para>
-          Specifies the field separator to be used in unaligned output
-          format. That way one can create, for example, tab- or
+          Specifies the field separator to be used in unaligned and csv output
+          formats. That way one can create, for example, tab- or
           comma-separated output, which other programs might prefer. To
           set a tab as field separator, type <literal>\pset fieldsep
           '\t'</literal>. The default field separator is
@@ -2584,9 +2594,13 @@ lo_import 152801
           <term><literal>format</literal></term>
           <listitem>
           <para>
-          Sets the output format to one of <literal>unaligned</literal>,
-          <literal>aligned</literal>, <literal>wrapped</literal>,
-          <literal>html</literal>, <literal>asciidoc</literal>,
+          Sets the output format to one of
+          <literal>unaligned</literal>,
+          <literal>aligned</literal>,
+          <literal>csv</literal>,
+          <literal>wrapped</literal>,
+          <literal>html</literal>,
+          <literal>asciidoc</literal>,
           <literal>latex</literal> (uses <literal>tabular</literal>),
           <literal>latex-longtable</literal>, or
           <literal>troff-ms</literal>.
@@ -2601,6 +2615,15 @@ lo_import 152801
           format).
           </para>
 
+          <para><literal>csv</literal> format writes columns separated
+          by <literal>fieldsep</literal>, applying the CSV quoting rules
+          described in RFC-4180 and compatible with the CSV format
+          of the <command>COPY</command> command.
+          The header with column names is output unless the
+          <literal>tuples_only</literal> parameter is <literal>on</literal>.
+          Title and footers are not printed.
+          </para>
+
           <para><literal>aligned</literal> format is the standard, 
human-readable,
           nicely formatted text output;  this is the default.
           </para>
@@ -2747,8 +2770,8 @@ lo_import 152801
           <term><literal>recordsep</literal></term>
           <listitem>
           <para>
-          Specifies the record (line) separator to use in unaligned
-          output format. The default is a newline character.
+          Specifies the record (line) separator to use in unaligned or
+          csv output formats. The default is a newline character.
           </para>
           </listitem>
           </varlistentry>
diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c
index 3560318..1d8cc96 100644
--- a/src/bin/psql/command.c
+++ b/src/bin/psql/command.c
@@ -3603,6 +3603,9 @@ _align2string(enum printFormat in)
                case PRINT_TROFF_MS:
                        return "troff-ms";
                        break;
+               case PRINT_CSV:
+                       return "csv";
+                       break;
        }
        return "unknown";
 }
@@ -3658,25 +3661,27 @@ do_pset(const char *param, const char *value, 
printQueryOpt *popt, bool quiet)
        {
                if (!value)
                        ;
-               else if (pg_strncasecmp("unaligned", value, vallen) == 0)
-                       popt->topt.format = PRINT_UNALIGNED;
                else if (pg_strncasecmp("aligned", value, vallen) == 0)
                        popt->topt.format = PRINT_ALIGNED;
-               else if (pg_strncasecmp("wrapped", value, vallen) == 0)
-                       popt->topt.format = PRINT_WRAPPED;
-               else if (pg_strncasecmp("html", value, vallen) == 0)
-                       popt->topt.format = PRINT_HTML;
                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)
                        popt->topt.format = PRINT_LATEX;
                else if (pg_strncasecmp("latex-longtable", value, vallen) == 0)
                        popt->topt.format = PRINT_LATEX_LONGTABLE;
                else if (pg_strncasecmp("troff-ms", value, vallen) == 0)
                        popt->topt.format = PRINT_TROFF_MS;
+               else if (pg_strncasecmp("unaligned", value, vallen) == 0)
+                       popt->topt.format = PRINT_UNALIGNED;
+               else if (pg_strncasecmp("wrapped", value, vallen) == 0)
+                       popt->topt.format = PRINT_WRAPPED;
                else
                {
-                       psql_error("\\pset: allowed formats are unaligned, 
aligned, wrapped, html, asciidoc, latex, latex-longtable, troff-ms\n");
+                       psql_error("\\pset: allowed formats are aligned, 
asciidoc, csv, html, latex, latex-longtable, troff-ms, unaligned, wrapped\n");
                        return false;
                }
        }
diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c
index 702e742..338b275 100644
--- a/src/bin/psql/help.c
+++ b/src/bin/psql/help.c
@@ -108,13 +108,14 @@ usage(unsigned short int pager)
 
        fprintf(output, _("\nOutput format options:\n"));
        fprintf(output, _("  -A, --no-align           unaligned table output 
mode\n"));
+       fprintf(output, _("      --csv                Comma-Separated-Values 
output mode\n"));
        fprintf(output, _("  -F, --field-separator=STRING\n"
-                                         "                           field 
separator for unaligned output (default: \"%s\")\n"),
+                                         "                           field 
separator for unaligned or csv output (default: \"%s\")\n"),
                        DEFAULT_FIELD_SEP);
        fprintf(output, _("  -H, --html               HTML table output 
mode\n"));
        fprintf(output, _("  -P, --pset=VAR[=ARG]     set printing option VAR 
to ARG (see \\pset command)\n"));
        fprintf(output, _("  -R, --record-separator=STRING\n"
-                                         "                           record 
separator for unaligned output (default: newline)\n"));
+                                         "                           record 
separator for unaligned or csv output (default: newline)\n"));
        fprintf(output, _("  -t, --tuples-only        print rows only\n"));
        fprintf(output, _("  -T, --table-attr=TEXT    set HTML table tag 
attributes (e.g., width, border)\n"));
        fprintf(output, _("  -x, --expanded           turn on expanded table 
output\n"));
@@ -426,7 +427,7 @@ helpVariables(unsigned short int pager)
        fprintf(output, _("  expanded (or x)\n"
                                          "    expanded output [on, off, 
auto]\n"));
        fprintf(output, _("  fieldsep\n"
-                                         "    field separator for unaligned 
output (default \"%s\")\n"),
+                                         "    field separator for unaligned 
and csv output (default \"%s\")\n"),
                        DEFAULT_FIELD_SEP);
        fprintf(output, _("  fieldsep_zero\n"
                                          "    set field separator for 
unaligned output to a zero byte\n"));
@@ -443,7 +444,7 @@ helpVariables(unsigned short int pager)
        fprintf(output, _("  pager\n"
                                          "    control when an external pager 
is used [yes, no, always]\n"));
        fprintf(output, _("  recordsep\n"
-                                         "    record (line) separator for 
unaligned output\n"));
+                                         "    record (line) separator for 
unaligned and csv output\n"));
        fprintf(output, _("  recordsep_zero\n"
                                          "    set record separator for 
unaligned output to a zero byte\n"));
        fprintf(output, _("  tableattr (or T)\n"
diff --git a/src/bin/psql/settings.h b/src/bin/psql/settings.h
index 69e617e..93d0b95 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 be57574..a46880e 100644
--- a/src/bin/psql/startup.c
+++ b/src/bin/psql/startup.c
@@ -436,6 +436,7 @@ parse_psql_options(int argc, char *argv[], struct 
adhoc_opts *options)
                {"echo-all", no_argument, NULL, 'a'},
                {"no-align", no_argument, NULL, 'A'},
                {"command", required_argument, NULL, 'c'},
+               {"csv", no_argument, NULL, 2}, /* no single-letter (leave -C 
for future use) */
                {"dbname", required_argument, NULL, 'd'},
                {"echo-queries", no_argument, NULL, 'e'},
                {"echo-errors", no_argument, NULL, 'b'},
@@ -658,6 +659,12 @@ parse_psql_options(int argc, char *argv[], struct 
adhoc_opts *options)
                                        exit(EXIT_SUCCESS);
                                }
                                break;
+                       case 2:
+                               /*  --csv: set both format and field separator 
*/
+                               pset.popt.topt.format = PRINT_CSV;
+                               pset.popt.topt.fieldSep.separator = 
pg_strdup(DEFAULT_FIELD_SEP_CSV);
+                               pset.popt.topt.fieldSep.separator_zero = false;
+                               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 08d8ef0..b9fc423 100644
--- a/src/bin/psql/tab-complete.c
+++ b/src/bin/psql/tab-complete.c
@@ -3753,8 +3753,8 @@ psql_completion(const char *text, int start, int end)
                if (TailMatchesCS1("format"))
                {
                        static const char *const my_list[] =
-                       {"unaligned", "aligned", "wrapped", "html", "asciidoc",
-                       "latex", "latex-longtable", "troff-ms", NULL};
+                       {"unaligned", "aligned", "csv", "wrapped", "html", 
"asciidoc",
+                        "latex", "latex-longtable", "troff-ms", NULL};
 
                        COMPLETE_WITH_LIST_CS(my_list);
                }
diff --git a/src/fe_utils/print.c b/src/fe_utils/print.c
index ec5ad45..fcd8a24 100644
--- a/src/fe_utils/print.c
+++ b/src/fe_utils/print.c
@@ -2783,6 +2783,109 @@ 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;
+       const char* const fieldsep = cont->opt->fieldSep.separator;
+       const char* const recordsep = cont->opt->recordSep.separator;
+       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.
+        */
+
+       if (cont->opt->start_table && !cont->opt->tuples_only)
+       {
+               /* print headers */
+               for (ptr = cont->headers; *ptr; ptr++)
+               {
+                       if (ptr != cont->headers)
+                               fputs(fieldsep, fout);
+                       csv_print_field(*ptr, fout, fieldsep);
+               }
+               fputs(recordsep, fout);
+       }
+
+       /* print cells */
+       for (i = 0, ptr = cont->cells; *ptr; i++, ptr++)
+       {
+               if (cancel_pressed)
+                       break;
+
+               csv_print_field(*ptr, fout, fieldsep);
+
+               if ((i + 1) % cont->ncolumns)
+                       fputs(fieldsep, fout);
+               else
+                       fputs(recordsep, 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)
+                       break;
+
+               /* Field name */
+               csv_print_field(cont->headers[i % cont->ncolumns], fout,
+                                               cont->opt->fieldSep.separator);
+               fputs(cont->opt->fieldSep.separator, fout);
+
+               /* Field value followed by record separator */
+               csv_print_field(*ptr, fout, cont->opt->fieldSep.separator);
+               fputs(cont->opt->recordSep.separator, fout);
+       }
+}
+
 
 /********************************/
 /* Public functions                            */
@@ -3234,6 +3337,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 83320d0..82e50fb 100644
--- a/src/include/fe_utils/print.h
+++ b/src/include/fe_utils/print.h
@@ -33,7 +33,8 @@ enum printFormat
        PRINT_ASCIIDOC,
        PRINT_LATEX,
        PRINT_LATEX_LONGTABLE,
-       PRINT_TROFF_MS
+       PRINT_TROFF_MS,
+       PRINT_CSV
        /* add your favourite output format here ... */
 };
 
diff --git a/src/test/regress/expected/psql.out 
b/src/test/regress/expected/psql.out
index 3818cfe..0bdfa4d 100644
--- a/src/test/regress/expected/psql.out
+++ b/src/test/regress/expected/psql.out
@@ -3243,3 +3243,106 @@ last error message: division by zero
 \echo 'last error code:' :LAST_ERROR_SQLSTATE
 last error code: 22012
 \unset FETCH_COUNT
+-- test csv format
+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",  array['ab', E'cd\nef'] as col10,
+  '{"a":"a,b", "a,b":null, "c":"a,\"b"}'::json as col11
+   from generate_series(1,2);
+\pset format csv
+\pset fieldsep ','
+\pset expanded off
+\t off
+execute q;
+col1,"col,2",col3,col4,col5,"col""6",col7,col8,"col
+  9",col10,col11
+"ab,cd",ab,a   b,"""","""""","a""b","a
+b",,ab,"{ab,""cd
+ef""}","{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+"ab,cd",ab,a   b,"""","""""","a""b","a
+b",,ab,"{ab,""cd
+ef""}","{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\pset fieldsep '\t'
+execute q;
+col1   col,2   col3    col4    col5    "col""6"        col7    col8    "col
+  9"   col10   col11
+ab,cd  ab      "a      b"      """"    """"""  "a""b"  "a
+b"             ab      "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+ab,cd  ab      "a      b"      """"    """"""  "a""b"  "a
+b"             ab      "{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      "{ab,""cd
+ef""}" "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+ab,cd  ab      "a      b"      """"    """"""  "a""b"  "a
+b"             ab      "{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
+col10  "{ab,""cd
+ef""}"
+col11  "{""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
+col10  "{ab,""cd
+ef""}"
+col11  "{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+\pset fieldsep ','
+execute q;
+col1,"ab,cd"
+"col,2",ab
+col3,a b
+col4,""""
+col5,""""""
+"col""6","a""b"
+col7,"a
+b"
+col8,
+"col
+  9",ab
+col10,"{ab,""cd
+ef""}"
+col11,"{""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
+col10,"{ab,""cd
+ef""}"
+col11,"{""a"":""a,b"", ""a,b"":null, ""c"":""a,\""b""}"
+deallocate q;
+\pset format aligned
+\pset expanded off
+\pset fieldsep '|'
+\t off
diff --git a/src/test/regress/sql/psql.sql b/src/test/regress/sql/psql.sql
index b45da9b..6891a9b 100644
--- a/src/test/regress/sql/psql.sql
+++ b/src/test/regress/sql/psql.sql
@@ -688,3 +688,31 @@ select 1/(15-unique2) from tenk1 order by unique2 limit 19;
 \echo 'last error code:' :LAST_ERROR_SQLSTATE
 
 \unset FETCH_COUNT
+
+-- test csv format
+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",  array['ab', E'cd\nef'] as col10,
+  '{"a":"a,b", "a,b":null, "c":"a,\"b"}'::json as col11
+   from generate_series(1,2);
+
+\pset format csv
+\pset fieldsep ','
+\pset expanded off
+\t off
+execute q;
+\pset fieldsep '\t'
+execute q;
+\t on
+execute q;
+\t off
+\pset expanded on
+execute q;
+\pset fieldsep ','
+execute q;
+
+deallocate q;
+\pset format aligned
+\pset expanded off
+\pset fieldsep '|'
+\t off

Reply via email to