1fc1fed014272d6781d8f98845efae537e808e58
markd
  Sat May 16 20:07:30 2026 -0700
added option to get attribute output from gtfToGenePred in TSV format

diff --git src/hg/utils/gtfToGenePred/gtfToGenePred.c src/hg/utils/gtfToGenePred/gtfToGenePred.c
index d6659382ffb..4b21b1f3593 100644
--- src/hg/utils/gtfToGenePred/gtfToGenePred.c
+++ src/hg/utils/gtfToGenePred/gtfToGenePred.c
@@ -13,46 +13,48 @@
 /* Explain usage and exit. */
 {
 errAbort(
   "gtfToGenePred - convert a GTF file to a genePred\n"
   "usage:\n"
   "   gtfToGenePred gtf genePred\n"
   "\n"
   "options:\n"
   "     -genePredExt - create a extended genePred, including frame\n"
   "      information and gene name\n"
   "     -allErrors - skip groups with errors rather than aborting.\n"
   "      Useful for getting information about as many errors as possible.\n"
   "     -ignoreGroupsWithoutExons - skip groups contain no exons rather than\n"
   "      generate an error.\n"
   "     -infoOut=file - write a file with information on each transcript\n"
+  "     -infoTsv=file - write a TSV with information on each transcript\n"
   "     -sourcePrefix=pre - only process entries where the source name has the\n"
   "      specified prefix.  May be repeated.\n"
   "     -impliedStopAfterCds - implied stop codon in after CDS\n"
   "     -simple    - just check column validity, not hierarchy, resulting genePred may be damaged\n"
   "     -geneNameAsName2 - if specified, use gene_name for the name2 field\n"
   "      instead of gene_id.\n"
   "     -includeVersion - it gene_version and/or transcript_version attributes exist, include the version\n"
   "      in the corresponding identifiers.\n");
 }
 
 static struct optionSpec options[] = {
     {"simple", OPTION_BOOLEAN},
     {"genePredExt", OPTION_BOOLEAN},
     {"allErrors", OPTION_BOOLEAN},
     {"ignoreGroupsWithoutExons", OPTION_BOOLEAN},
     {"infoOut", OPTION_STRING},
+    {"infoTsv", OPTION_STRING},
     {"sourcePrefix", OPTION_STRING|OPTION_MULTI},
     {"impliedStopAfterCds", OPTION_BOOLEAN},
     {"geneNameAsName2", OPTION_BOOLEAN},
     {"includeVersion", OPTION_BOOLEAN},
     {NULL, 0},
 };
 boolean clGenePredExt = FALSE;  /* include frame and geneName */
 boolean clAllErrors = FALSE;    /* report as many errors as possible */
 boolean clIgnoreGroupsWithoutExons = FALSE;  /* ignore groups without exons */
 struct slName *clSourcePrefixes; /* list of source prefixes to match */
 boolean clIncludeVersion = FALSE; /* add version numbers to identifiers if available */
 unsigned clGxfOptions = 0;       /* options for converting GTF/GFF */
 boolean doSimple = FALSE;      /* only check column validity */
 int badGroupCount = 0;  /* count of inconsistent groups found */
 
@@ -98,31 +100,31 @@
 else
     safecpy(transcriptIdToUse, sizeof(transcriptIdToUse), group->name);
 if (clIncludeVersion && (proteinId != NULL) && (proteinVersion != NULL))
     safef(proteinIdToUse, sizeof(proteinIdToUse), "%s.%s", proteinId, proteinVersion);
 else
     safecpy(proteinIdToUse, sizeof(proteinIdToUse), emptyForNull(proteinId));
 
 fprintf(infoFh, "%s\t%s\t%s\t%s\t%ld\t%ld\t%c\t%s\t%s\t%s\t%s\t%s\n",
         transcriptIdToUse, geneIdToUse, group->source,
         group->seq, group->start, group->end, group->strand,
         proteinIdToUse, emptyForNull(geneName), emptyForNull(transcriptName),
         emptyForNull(geneType), emptyForNull(transcriptType));
 }
 
 static void gtfGroupToGenePred(struct gffFile *gtf, struct gffGroup *group, FILE *gpFh,
-                               FILE *infoFh)
+                               FILE *infoFh, FILE *infoTsvFh)
 /* convert one gtf group to a genePred */
 {
 unsigned optFields = (clGenePredExt ? genePredAllFlds : 0);
 struct errCatch *errCatch = errCatchNew();
 
 if (errCatchStart(errCatch))
     {
     struct genePred *gp = genePredFromGroupedGtf(gtf, group, group->name, optFields, clGxfOptions);
     if (gp == NULL)
         {
         if (!clIgnoreGroupsWithoutExons)
             {
             char *msg = "no exons defined for group %s, feature %s (perhaps try -ignoreGroupsWithoutExons)";
             if (clAllErrors)
                 {
@@ -146,90 +148,98 @@
     // drop trailing newline in caught message
     if (endsWith(errCatch->message->string, "\n"))
         dyStringResize(errCatch->message, dyStringLen(errCatch->message)-1);
     if (clAllErrors)
         {
         fprintf(stderr, "%s\n", errCatch->message->string);
         badGroupCount++;
         }
     else
         errAbort("%s", errCatch->message->string);
     }
 else
     {
     if (infoFh != NULL)
         writeInfo(infoFh, group);
+    if (infoTsvFh != NULL)
+        writeInfo(infoTsvFh, group);
     }
 errCatchFree(&errCatch); 
 }
 
 static bool sourceMatches(struct gffGroup *group)
 /* see if the source matches on on the list */
 {
 struct slName *pre = NULL;
 for (pre = clSourcePrefixes; pre != NULL; pre = pre->next)
     if (startsWith(pre->name, group->source))
         return TRUE;
 return FALSE;
 }
         
 
 static bool inclGroup(struct gffGroup *group)
 /* check if a group should be included in the output */
 {
 if (clSourcePrefixes != NULL)
     {
     if (!sourceMatches(group))
         return FALSE;
     }
 return TRUE;
 }
 
-static void gtfToGenePred(char *gtfFile, char *gpFile, char *infoFile)
+static void gtfToGenePred(char *gtfFile, char *gpFile, char *infoFile, char *infoTsv)
 /* gtfToGenePred -  convert a GTF file to a genePred.. */
 {
 struct gffFile *gtf = gffRead(gtfFile);
-FILE *gpFh, *infoFh = NULL;
+FILE *gpFh, *infoFh = NULL, *infoTsvFh = NULL;
 struct gffGroup *group;
 
 if (!gtf->isGtf)
     errAbort("%s doesn't appear to be a GTF file (GFF not supported by this program)", gtfFile);
 gffGroupLines(gtf);
 gpFh = mustOpen(gpFile, "w");
 if (infoFile != NULL)
     {
     infoFh = mustOpen(infoFile, "w");
     fputs(infoHeader, infoFh);
     }
+if (infoTsv != NULL)
+    {
+    infoTsvFh = mustOpen(infoTsv, "w");
+    fputs(infoHeader + 1, infoTsvFh);
+    }
 
 if (!doSimple)
     for (group = gtf->groupList; group != NULL; group = group->next)
 	if (inclGroup(group))
-	    gtfGroupToGenePred(gtf, group, gpFh, infoFh);
+	    gtfGroupToGenePred(gtf, group, gpFh, infoFh, infoTsvFh);
 
 carefulClose(&gpFh);
 gffFileFree(&gtf);
 }
 
 int main(int argc, char *argv[])
 /* Process command line. */
 {
 optionInit(&argc, argv, options);
 if (argc != 3)
     usage();
 clGenePredExt = optionExists("genePredExt");
 doSimple = optionExists("simple");
 clIgnoreGroupsWithoutExons = optionExists("ignoreGroupsWithoutExons");
 clAllErrors = optionExists("allErrors");
 clIncludeVersion = optionExists("includeVersion");
 clSourcePrefixes = optionMultiVal("sourcePrefix", NULL);
 if (optionExists("impliedStopAfterCds"))
     clGxfOptions |= genePredGxfImpliedStopAfterCds;
 if (optionExists("geneNameAsName2"))
     clGxfOptions |= genePredGxfGeneNameAsName2;
 if (optionExists("includeVersion"))
     clGxfOptions |= genePredGxfIncludeVersion;
-gtfToGenePred(argv[1], argv[2], optionVal("infoOut", NULL));
+gtfToGenePred(argv[1], argv[2], optionVal("infoOut", NULL),
+              optionVal("infoTsv", NULL));
 if (badGroupCount > 0)
     errAbort("%d errors", badGroupCount);
 return 0;
 }