2018-03-24 10:06 GMT+01:00 Fabien COELHO <coe...@cri.ensmp.fr>: > > Hello Pavel, > > I'm suggesting to add \csv which would behave like \H to toggle CSV >>>> mode so as to improve this situation, with a caveat which is that >>>> toggling back \csv would have forgotted the previous settings (just >>>> like \H does, though, so would for instance reset to aligned with >>>> |), >>>> so it would not be perfect. >>>> >>> >>> this doesn't solve usual format settings by \pset format csv >>> >> > Indeed, it does not. Alas, "format" is not strictly about format but more > about string escaping. > > (2) your proposal as I understand it: >>>> >>>> "\pset format csv" may or may not use the fieldsep, depending on >>>> whether it was explicitely set, an information which is not shown, >>>> i.e.: >>>> >>>> \pset fieldsep # fieldsep separator is "|" >>>> \pset format csv # would output a,b,c or a|b|c... >>>> >>>> Because it depends on whether fieldsep was set explicitely to '|' or >>>> whether it has this value but it was due to the default. >>>> >>>> This kind of unclear behavioral determinism does not seem desirable. >>>> >>>> >>> please, check and test attached patch. It is very simply for usage - and >>> there is not any unclear behave. Just you should to accept so formats can >>> have own defaults for separators. >>> >> > I checked, and the behavior is intrinsically strange. > > Your patch shows an empty '' fieldsep at startup, which is debatable > because it is not its actual value. >
it is correct. Default format is aligned, that doesn't use fieldsep. > > psql> \pset # fieldsep '' -- doubtful > > ISTM that at the minimum it should show a special "<format-default>" or > whatever value, which creates some kind of exception because this special > value cannot be set and special values are a bad thing. > > When a format is set, a current default fielsep is shown. > > psql> \pset format unaligned > psql> \pset # fieldsep '|' -- new default shown > psql> SELECT 1 AS one, 2 AS two; > one|two > 1|2 > > psql> \pset format csv > psql> \pset # fieldsep ',' -- new default shown > > psql> SELECT 1 AS one, 2 AS two; > one,two > 1,2 > > However, if fieldsep is set once, the behaviors changes indefinitely: > > psql> \pset fieldsep '|' > psql> \pset format unaligned > # unaligned with '|' > > psql> \pset format csv > # csv with '|' > > There should be similar mechanism like fieldsep_zero, that reset settings. Some like fieldsep_default. > That is once the fieldsep has been used, you are back to the v4 behavior > that you are disagreeing with in the first place. > > The patch creates an incomplete state automaton which some unreachable > states, because once a value is set there is no way to switch back to the > previous "use default" behavior. > It is not implemented, but it is small problem > > Also, if I do "\pset fielsep ''" then the \pset output does not allow to > distinguish between the unset state and set to empty state. > This is question - how to fieldsep be related to current format. Aligned format doesn't use fieldsep. The alternative can be words "not used" -- and then you can see difference between "" and other. But if you see the code, the empty string is used like info about empty fieldsep now. > I would not expect a committer to accept such a strange no-possible-return > to previous state because of a hidden state (whether fieldsep has been set > or not in the past) behavior. > This design is very similar to already implemented fieldsep_zero - look to code. It is nothing new. > > So I do not think that this proposed version is really satisfactory. > So I can do better? 1. use special default string for formats that doesn't field sep - like "not used" or some 2. I can implemet the option fieldsep_default - very similary to fieldsep_zero to reset fieldsep to default state. please, check updated patch Regards Pavel > > -- > Fabien. >
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 10b97950ec..c984a9cbaa 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -151,6 +151,16 @@ EOF </listitem> </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> @@ -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 3560318749..f2751b3f26 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -1961,6 +1961,7 @@ exec_command_pset(PsqlScanState scan_state, bool active_branch) int i; static const char *const my_list[] = { "border", "columns", "expanded", "fieldsep", "fieldsep_zero", + "fieldsep_default", "footer", "format", "linestyle", "null", "numericlocale", "pager", "pager_min_lines", "recordsep", "recordsep_zero", @@ -3603,6 +3604,9 @@ _align2string(enum printFormat in) case PRINT_TROFF_MS: return "troff-ms"; break; + case PRINT_CSV: + return "csv"; + break; } return "unknown"; } @@ -3658,27 +3662,37 @@ 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; } + + if (!popt->topt.fieldSep.is_custom) + { + if (popt->topt.fieldSep.separator) + free(popt->topt.fieldSep.separator); + popt->topt.fieldSep.separator = + pg_strdup(get_format_fieldsep(popt->topt.format)); + } } /* set table line style */ @@ -3801,6 +3815,7 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) free(popt->topt.fieldSep.separator); popt->topt.fieldSep.separator = pg_strdup(value); popt->topt.fieldSep.separator_zero = false; + popt->topt.fieldSep.is_custom = true; } } @@ -3811,6 +3826,15 @@ do_pset(const char *param, const char *value, printQueryOpt *popt, bool quiet) popt->topt.fieldSep.separator_zero = true; } + else if (strcmp(param, "fieldsep_default") == 0) + { + if (popt->topt.fieldSep.separator) + free(popt->topt.fieldSep.separator); + popt->topt.fieldSep.separator = + pg_strdup(get_format_fieldsep(popt->topt.format)); + popt->topt.fieldSep.is_custom = false; + } + /* record separator for unaligned text */ else if (strcmp(param, "recordsep") == 0) { @@ -3959,6 +3983,11 @@ printPsetInfo(const char *param, struct printQueryOpt *popt) printf(_("Field separator is zero byte.\n")); } + else if (strcmp(param, "fieldsep_default") == 0) + { + printf(_("Field separator is format default.\n")); + } + /* show disable "(x rows)" footer */ else if (strcmp(param, "footer") == 0) { @@ -4150,11 +4179,20 @@ pset_value_string(const char *param, struct printQueryOpt *popt) ? "auto" : pset_bool_string(popt->topt.expanded)); else if (strcmp(param, "fieldsep") == 0) - return pset_quoted_string(popt->topt.fieldSep.separator + { + if (!popt->topt.fieldSep.is_custom && + popt->topt.fieldSep.separator && + strcmp(popt->topt.fieldSep.separator, "not used") == 0) + return pstrdup("not used"); + else + return pset_quoted_string(popt->topt.fieldSep.separator ? popt->topt.fieldSep.separator : ""); + } else if (strcmp(param, "fieldsep_zero") == 0) return pstrdup(pset_bool_string(popt->topt.fieldSep.separator_zero)); + else if (strcmp(param, "fieldsep_default") == 0) + return pstrdup(pset_bool_string(!popt->topt.fieldSep.is_custom)); else if (strcmp(param, "footer") == 0) return pstrdup(pset_bool_string(popt->topt.default_footer)); else if (strcmp(param, "format") == 0) diff --git a/src/bin/psql/help.c b/src/bin/psql/help.c index 702e742af4..4cb1dc3ebe 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"), - DEFAULT_FIELD_SEP); + " field separator for unaligned (default: \"%s\") or csv (default \"%s\") output\n"), + DEFAULT_FIELD_SEP, DEFAULT_FIELD_SEP_CSV); 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")); @@ -268,11 +269,11 @@ slashUsage(unsigned short int pager) fprintf(output, _("Formatting\n")); fprintf(output, _(" \\a toggle between unaligned and aligned output mode\n")); fprintf(output, _(" \\C [STRING] set table title, or unset if none\n")); - fprintf(output, _(" \\f [STRING] show or set field separator for unaligned query output\n")); + fprintf(output, _(" \\f [STRING] show or set field separator for unaligned or csv output\n")); 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" + " (NAME := {border|columns|expanded|fieldsep|fieldsep_zero|fieldsep_default\n" " footer|format|linestyle|null|numericlocale|pager|\n" " pager_min_lines|recordsep|recordsep_zero|tableattr|title|\n" " tuples_only|unicode_border_linestyle|\n" @@ -426,8 +427,10 @@ 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"), - DEFAULT_FIELD_SEP); + " field separator for unaligned (default: \"%s\") and csv (default \"%s\") output\n"), + DEFAULT_FIELD_SEP, DEFAULT_FIELD_SEP_CSV); + fprintf(output, _(" fieldsep_default\n" + " set field separator to format default\n")); fprintf(output, _(" fieldsep_zero\n" " set field separator for unaligned output to a zero byte\n")); fprintf(output, _(" footer\n" @@ -443,7 +446,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 69e617e6b5..73314dbca1 100644 --- a/src/bin/psql/settings.h +++ b/src/bin/psql/settings.h @@ -12,7 +12,6 @@ #include "variables.h" #include "fe_utils/print.h" -#define DEFAULT_FIELD_SEP "|" #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..4dd8d8f45e 100644 --- a/src/bin/psql/startup.c +++ b/src/bin/psql/startup.c @@ -194,7 +194,8 @@ main(int argc, char *argv[]) if (!pset.popt.topt.fieldSep.separator && !pset.popt.topt.fieldSep.separator_zero) { - pset.popt.topt.fieldSep.separator = pg_strdup(DEFAULT_FIELD_SEP); + pset.popt.topt.fieldSep.separator = pg_strdup(get_format_fieldsep(pset.popt.topt.format)); + pset.popt.topt.fieldSep.is_custom = false; pset.popt.topt.fieldSep.separator_zero = false; } if (!pset.popt.topt.recordSep.separator && @@ -436,6 +437,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'}, @@ -485,7 +487,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options) SetVariable(pset.vars, "ECHO", "all"); break; case 'A': - pset.popt.topt.format = PRINT_UNALIGNED; + do_pset("format", "unaligned", &pset.popt, true); break; case 'b': SetVariable(pset.vars, "ECHO", "errors"); @@ -515,14 +517,13 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options) optarg); break; case 'F': - pset.popt.topt.fieldSep.separator = pg_strdup(optarg); - pset.popt.topt.fieldSep.separator_zero = false; + do_pset("fieldsep", optarg, &pset.popt, true); break; case 'h': options->host = pg_strdup(optarg); break; case 'H': - pset.popt.topt.format = PRINT_HTML; + do_pset("format", "html", &pset.popt, true); break; case 'l': options->list_dbs = true; @@ -569,8 +570,7 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options) SetVariableBool(pset.vars, "QUIET"); break; case 'R': - pset.popt.topt.recordSep.separator = pg_strdup(optarg); - pset.popt.topt.recordSep.separator_zero = false; + do_pset("recordsep", optarg, &pset.popt, true); break; case 's': SetVariableBool(pset.vars, "SINGLESTEP"); @@ -625,10 +625,10 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options) options->no_psqlrc = true; break; case 'z': - pset.popt.topt.fieldSep.separator_zero = true; + do_pset("fieldsep_zero", NULL, &pset.popt, true); break; case '0': - pset.popt.topt.recordSep.separator_zero = true; + do_pset("recordsep_zero", NULL, &pset.popt, true); break; case '1': options->single_txn = true; @@ -658,6 +658,10 @@ parse_psql_options(int argc, char *argv[], struct adhoc_opts *options) exit(EXIT_SUCCESS); } break; + case 2: + /* --csv: set both format and field separator */ + do_pset("format", "csv", &pset.popt, true); + 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 08d8ef09a4..d0e6011b7f 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -3741,6 +3741,7 @@ psql_completion(const char *text, int start, int end) { static const char *const my_list[] = {"border", "columns", "expanded", "fieldsep", "fieldsep_zero", + "fieldsep_default", "footer", "format", "linestyle", "null", "numericlocale", "pager", "pager_min_lines", "recordsep", "recordsep_zero", "tableattr", "title", "tuples_only", "unicode_border_linestyle", @@ -3753,8 +3754,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 ec5ad45a30..954f448400 100644 --- a/src/fe_utils/print.c +++ b/src/fe_utils/print.c @@ -35,7 +35,6 @@ #include "catalog/pg_type.h" #include "fe_utils/mbprint.h" - /* * If the calling program doesn't have any mechanism for setting * cancel_pressed, it will have no effect. @@ -2783,6 +2782,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* 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 +3336,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); @@ -3424,6 +3532,21 @@ get_line_style(const printTableOpt *opt) return &pg_asciiformat; } +/* returns default fieldSep used by formats */ +const char * +get_format_fieldsep(enum printFormat format) +{ + switch (format) + { + case PRINT_UNALIGNED: + return DEFAULT_FIELD_SEP; + case PRINT_CSV: + return DEFAULT_FIELD_SEP_CSV; + default: + return "not used"; + } +} + void refresh_utf8format(const printTableOpt *opt) { diff --git a/src/include/fe_utils/print.h b/src/include/fe_utils/print.h index 83320d06bd..6ca04e7ea6 100644 --- a/src/include/fe_utils/print.h +++ b/src/include/fe_utils/print.h @@ -23,6 +23,9 @@ #define DEFAULT_PAGER "less" #endif +#define DEFAULT_FIELD_SEP "|" +#define DEFAULT_FIELD_SEP_CSV "," + enum printFormat { PRINT_NOTHING = 0, /* to make sure someone initializes this */ @@ -33,7 +36,8 @@ enum printFormat PRINT_ASCIIDOC, PRINT_LATEX, PRINT_LATEX_LONGTABLE, - PRINT_TROFF_MS + PRINT_TROFF_MS, + PRINT_CSV /* add your favourite output format here ... */ }; @@ -91,6 +95,7 @@ struct separator { char *separator; bool separator_zero; + bool is_custom; /* true, when user set this option */ }; typedef struct printTableOpt @@ -158,8 +163,8 @@ typedef struct printTableContent char *aligns; /* Array of alignment specifiers; 'l' or 'r', * one per column */ char *align; /* Pointer to the last added alignment */ -} printTableContent; +} printTableContent; typedef struct printQueryOpt { printTableOpt topt; /* the options above */ @@ -210,6 +215,7 @@ extern char column_type_alignment(Oid); extern void setDecimalLocale(void); extern const printTextFormat *get_line_style(const printTableOpt *opt); +extern const char *get_format_fieldsep(enum printFormat format); extern void refresh_utf8format(const printTableOpt *opt); #endif /* PRINT_H */ diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index 3818cfea7e..04f7ee11f7 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -261,7 +261,7 @@ select '2000-01-01'::date as party_over border 1 columns 0 expanded off -fieldsep '|' +fieldsep '' fieldsep_zero off footer on format aligned @@ -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 b45da9bb8d..6891a9b54c 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