5e8e0ed53c05f2ef9b51ce897440bae308367f47
chmalee
  Wed Feb 9 14:45:54 2022 -0800
Add csv output option to table browser to support opening files in
Excel, refs #27623. Also change the output filename message to hint
that Excel can auto-recognize .csv extensions and behave appropriately.
Lastly, fix output dropdown selection to properly warn on update
when using GTF output option or a snp table.

diff --git src/hg/hgTables/hgTables.c src/hg/hgTables/hgTables.c
index 76ec309..3a0df73 100644
--- src/hg/hgTables/hgTables.c
+++ src/hg/hgTables/hgTables.c
@@ -986,31 +986,31 @@
 	hOrFPrintf(f, "%s\t", field);
     field = sqlFieldName(sr);
     }
 if (sameWord("reserved",field))
     {
     hOrFPrintf(f, "itemRgb\n");
     ret = lastCol;
     }
 else
     hOrFPrintf(f, "%s\n", field);
 
 return(ret);
 }
 
 void doTabOutDb( char *db, char *dbVarName, char *table, char *tableVarName,
-	FILE *f, struct sqlConnection *conn, char *fields)
+        FILE *f, struct sqlConnection *conn, char *fields, char outSep)
 /* Do tab-separated output on fields of a single table. */
 {
 struct region *regionList = getRegions();
 struct region *region;
 struct hTableInfo *hti = NULL;
 struct dyString *fieldSpec = newDyString(256);
 struct hash *idHash = NULL;
 int outCount = 0;
 boolean isPositional;
 int fieldCount;
 char *idField;
 boolean showItemRgb = FALSE;
 int itemRgbCol = -1;        /*        -1 means not found        */
 boolean printedColumns = FALSE;
 struct trackDb *tdb = findTdbForTable(db, curTrack, table, ctLookupName);
@@ -1070,83 +1070,99 @@
         // Show only the SQL filter built from filter page options, not identifierFilter,
         // because identifierFilter can get enormous (like 126kB for 12,500 rsIDs).
         char *filterNoIds = filterClause(dbVarName, tableVarName, region->chrom, NULL);
         if (filterNoIds != NULL)
             hOrFPrintf(f, "#filter: %s\n", filterNoIds);
         hOrFPrintf(f, "#");
         if (showItemRgb)
             {
             itemRgbCol = itemRgbHeader(f, sr, lastCol);
             if (itemRgbCol == -1)
                 showItemRgb = FALSE;        /*  did not find "reserved" */
             }
         else
             {
             for (colIx = 0; colIx < lastCol; ++colIx)
-		hOrFPrintf(f, "%s\t", sqlFieldName(sr));
-	    hOrFPrintf(f, "%s\n", sqlFieldName(sr));
+                {
+                if (outSep == ',') hOrFPrintf(f, "\"");
+                hOrFPrintf(f, "%s", sqlFieldName(sr));
+                if (outSep == ',') hOrFPrintf(f, "\"");
+                hOrFPrintf(f, "%c", outSep);
+                }
+            if (outSep == ',') hOrFPrintf(f, "\"");
+            hOrFPrintf(f, "%s", sqlFieldName(sr));
+            if (outSep == ',') hOrFPrintf(f, "\"");
+            hOrFPrintf(f, "\n");
             }
         printedColumns = TRUE;
         }
     while ((row = sqlNextRow(sr)) != NULL)
         {
         if (idHash == NULL || isNotEmpty(identifierFilter) || hashLookup(idHash, row[fieldCount]))
             {
             if (showItemRgb)
                 itemRgbDataOut(f, row, lastCol, itemRgbCol);
             else
                 {
                 for (colIx = 0; colIx < lastCol; ++colIx)
-		    hOrFPrintf(f, "%s\t", row[colIx]);
-		hOrFPrintf(f, "%s\n", row[lastCol]);
+                    {
+                    if (outSep == ',') hOrFPrintf(f, "\"");
+                    hOrFPrintf(f, "%s", row[colIx]);
+                    if (outSep == ',') hOrFPrintf(f, "\"");
+                    hOrFPrintf(f, "%c", outSep);
+                    }
+                if (outSep == ',') hOrFPrintf(f, "\"");
+                hOrFPrintf(f, "%s", row[lastCol]);
+                if (outSep == ',') hOrFPrintf(f, "\"");
+                hOrFPrintf(f, "\n");
                 }
             ++outCount;
             }
         }
     sqlFreeResult(&sr);
     if (!isPositional)
         break;        /* No need to iterate across regions in this case. */
     freez(&filter);
     }
 
 /* Do some error diagnostics for user. */
 if (outCount == 0)
     explainWhyNoResults(f);
 hashFree(&idHash);
 }
 
 
-void doTabOutTable( char *db, char *table, FILE *f, struct sqlConnection *conn, char *fields)
+void doTabOutTable( char *db, char *table, FILE *f, struct sqlConnection *conn, char *fields, char outSep)
 /* Do tab-separated output on fields of a single table. */
 {
 boolean isTabix = FALSE;
 if (isBigBed(database, table, curTrack, ctLookupName))
-    bigBedTabOut(db, table, conn, fields, f);
+    bigBedTabOut(db, table, conn, fields, f, outSep);
 else if (isLongTabixTable(table))
-    longTabixTabOut(db, table, conn, fields, f);
+    longTabixTabOut(db, table, conn, fields, f, outSep);
 else if (isBamTable(table))
-    bamTabOut(db, table, conn, fields, f);
+    bamTabOut(db, table, conn, fields, f, outSep);
 else if (isVcfTable(table, &isTabix))
     vcfTabOut(db, table, conn, fields, f, isTabix);
 else if (isHicTable(table))
-    hicTabOut(db, table, conn, fields, f);
+    hicTabOut(db, table, conn, fields, f, outSep);
 else if (isCustomTrack(table))
     {
-    doTabOutCustomTracks(db, table, conn, fields, f);
+    doTabOutCustomTracks(db, table, conn, fields, f, outSep);
     }
 else
-    doTabOutDb(db, db, table, table, f, conn, fields);
+    doTabOutDb(db, db, table, table, f, conn, fields, outSep);
 }
 
 struct slName *fullTableFields(char *db, char *table)
 /* Return list of fields in db.table.field format. */
 {
 char dtBuf[256];
 struct sqlConnection *conn=NULL;
 struct slName *fieldList = NULL, *dtfList = NULL, *field, *dtf;
 if (isBigBed(database, table, curTrack, ctLookupName))
     {
     if (!trackHubDatabase(database))
 	conn = hAllocConn(db);
     fieldList = bigBedGetFields(table, conn);
     hFreeConn(&conn);
     }
@@ -1199,34 +1215,35 @@
 slReverse(&dtfList);
 slFreeList(&fieldList);
 return dtfList;
 }
 
 void doOutPrimaryTable(char *table, struct sqlConnection *conn)
 /* Dump out primary table. */
 {
 if (anySubtrackMerge(database, table))
     errAbort("Can't do all fields output when subtrack merge is on. "
     "Please go back and select another output type (BED or custom track is good), or clear the subtrack merge.");
 if (anyIntersection())
     errAbort("Can't do all fields output when intersection is on. "
     "Please go back and select another output type (BED or custom track is good), or clear the intersection.");
 textOpen();
+char sep = sameString(cartUsualString(cart, hgtaOutSep, outTab), outTab) ? '\t' : ',';
 if (sameWord(table, WIKI_TRACK_TABLE))
-    tabOutSelectedFields(wikiDbName(), table, NULL, fullTableFields(wikiDbName(), table));
+    sepOutSelectedFields(wikiDbName(), table, NULL, fullTableFields(wikiDbName(), table), sep);
 else
-    tabOutSelectedFields(database, table, NULL, fullTableFields(database, table));
+    sepOutSelectedFields(database, table, NULL, fullTableFields(database, table), sep);
 }
 
 void ensureVisibility(char *db, char *table, struct trackDb *tdb)
 /* Check track visibility; if hide, print out CGI to set it to pack or full. */
 {
 enum trackVisibility vis = tvHide;
 char *cartVis = cartOptionalString(cart, table);
 if (isNotEmpty(cartVis))
     vis = hTvFromString(cartVis);
 else
     {
     if (tdb == NULL)
 	tdb = hTrackDbForTrackAndAncestors(db, table);
     if (tdb != NULL)
 	{