pspp-output: New command get-table-look.
authorBen Pfaff <blp@cs.stanford.edu>
Thu, 29 Oct 2020 05:30:43 +0000 (22:30 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Thu, 29 Oct 2020 05:39:39 +0000 (22:39 -0700)
NEWS
doc/pspp-output.texi
src/output/pivot-table.h
src/output/spv/spv-table-look.c
src/output/spv/spv-table-look.h
utilities/pspp-output.1
utilities/pspp-output.c

diff --git a/NEWS b/NEWS
index a2f3a864ebd003f0d459ca6d2c5a7e9ac4c38ff4..694ec94f62d38f69757ba1696d424afa20102830 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -17,7 +17,11 @@ Changes from 1.4.1 to 1.5.2:
    The new interface provides the user with a preview of the data to be imported
    and interactive methods to select the desired ranges.
 
- * The pspp-output utility has new --table-look and --nth-commands options.
+ * New features in pspp-output:
+
+   - New --table-look and --nth-commands options.
+
+   - New get-table-look command.
 
 Changes from 1.4.0 to 1.4.1:
 
index 35cc6004630e54a4139d74438ac2ddd972f79a72..bc68fa067fe1df65ad8759bb4ec7e0aca463751d 100644 (file)
@@ -29,6 +29,8 @@ SPSS 15 and earlier versions instead use @file{.spo} files.
 
 @t{pspp-output} [@var{options}] @t{convert} @var{source} @var{destination}
 
+@t{pspp-output} [@var{options}] @t{get-table-look} @var{source} @var{destination}
+
 @t{pspp-output -@w{-}help}
 
 @t{pspp-output -@w{-}version}
@@ -42,6 +44,7 @@ developers may find useful for debugging.
 * The pspp-output detect Command::
 * The pspp-output dir Command::
 * The pspp-output convert Command::
+* The pspp-output get-table-look Command::
 * Input Selection Options::
 @end menu
 
@@ -116,6 +119,23 @@ Reads a table style from @var{file} and applies it to all of the
 output tables.  The file should a TableLook @file{.stt} file.
 @end table
 
+@node The pspp-output get-table-look Command
+@section The @code{get-table-look} Command
+
+@display
+@t{pspp-output} [@var{options}] @t{get-table-look} @var{source} @var{destination}
+@end display
+
+Reads SPV file @var{source}, applies any selection options
+(@pxref{Input Selection Options}), picks the first table from the
+selected object, extracts the TableLook from that table, and writes it
+to @var{destination} (typically with an @file{.stt} extension) in the
+TableLook XML format.
+
+The user may use the TableLook file to change the style of tables in
+other files, by passing it to the @option{--table-look} option on the
+@code{convert} command.
+
 @node Input Selection Options
 @section Input Selection Options
 
index c4c9bb778f70626c37b1c03a7ed7db1fa474f346..ed8f9b7a2b33948cc961a3e1347a9d30d870f80f 100644 (file)
@@ -388,7 +388,7 @@ struct pivot_table
     bool show_caption;
     bool omit_empty;       /* Omit empty rows and columns? */
     size_t *current_layer; /* axis[PIVOT_AXIS_LAYER].n_dimensions elements. */
-    char *table_look;
+    char *table_look;      /* May be NULL. */
     enum settings_value_show show_values;
     enum settings_value_show show_variables;
     struct fmt_spec weight_format;
index d297c13981cbaa22bdb2ba4adf1d374a9ed00bb9..3a2fb4578c0734474b20599f1d8f0a4ef85e718f 100644 (file)
@@ -19,7 +19,9 @@
 #include "output/spv/spv-table-look.h"
 
 #include <errno.h>
+#include <inttypes.h>
 #include <libxml/xmlreader.h>
+#include <libxml/xmlwriter.h>
 #include <string.h>
 
 #include "output/spv/structure-xml-parser.h"
@@ -27,6 +29,9 @@
 #include "gl/read-file.h"
 #include "gl/xalloc.h"
 
+#include "gettext.h"
+#define _(msgid) gettext (msgid)
+
 static struct cell_color
 optional_color (int color, struct cell_color default_color)
 {
@@ -61,55 +66,55 @@ optional_pt (double inches, int default_pt)
   return inches != DBL_MAX ? inches * 72.0 + .5 : default_pt;
 }
 
+static const char *pivot_area_names[PIVOT_N_AREAS] = {
+  [PIVOT_AREA_TITLE] = "title",
+  [PIVOT_AREA_CAPTION] = "caption",
+  [PIVOT_AREA_FOOTER] = "footnotes",
+  [PIVOT_AREA_CORNER] = "cornerLabels",
+  [PIVOT_AREA_COLUMN_LABELS] = "columnLabels",
+  [PIVOT_AREA_ROW_LABELS] = "rowLabels",
+  [PIVOT_AREA_DATA] = "data",
+  [PIVOT_AREA_LAYERS] = "layers",
+};
+
 static enum pivot_area
 pivot_area_from_name (const char *name)
 {
-  static const char *area_names[PIVOT_N_AREAS] = {
-    [PIVOT_AREA_TITLE] = "title",
-    [PIVOT_AREA_CAPTION] = "caption",
-    [PIVOT_AREA_FOOTER] = "footnotes",
-    [PIVOT_AREA_CORNER] = "cornerLabels",
-    [PIVOT_AREA_COLUMN_LABELS] = "columnLabels",
-    [PIVOT_AREA_ROW_LABELS] = "rowLabels",
-    [PIVOT_AREA_DATA] = "data",
-    [PIVOT_AREA_LAYERS] = "layers",
-  };
-
   enum pivot_area area;
   for (area = 0; area < PIVOT_N_AREAS; area++)
-    if (!strcmp (name, area_names[area]))
+    if (!strcmp (name, pivot_area_names[area]))
       break;
   return area;
 }
 
+static const char *pivot_border_names[PIVOT_N_BORDERS] = {
+  [PIVOT_BORDER_TITLE] = "titleLayerSeparator",
+  [PIVOT_BORDER_OUTER_LEFT] = "leftOuterFrame",
+  [PIVOT_BORDER_OUTER_TOP] = "topOuterFrame",
+  [PIVOT_BORDER_OUTER_RIGHT] = "rightOuterFrame",
+  [PIVOT_BORDER_OUTER_BOTTOM] = "bottomOuterFrame",
+  [PIVOT_BORDER_INNER_LEFT] = "leftInnerFrame",
+  [PIVOT_BORDER_INNER_TOP] = "topInnerFrame",
+  [PIVOT_BORDER_INNER_RIGHT] = "rightInnerFrame",
+  [PIVOT_BORDER_INNER_BOTTOM] = "bottomInnerFrame",
+  [PIVOT_BORDER_DATA_LEFT] = "dataAreaLeft",
+  [PIVOT_BORDER_DATA_TOP] = "dataAreaTop",
+  [PIVOT_BORDER_DIM_ROW_HORZ] = "horizontalDimensionBorderRows",
+  [PIVOT_BORDER_DIM_ROW_VERT] = "verticalDimensionBorderRows",
+  [PIVOT_BORDER_DIM_COL_HORZ] = "horizontalDimensionBorderColumns",
+  [PIVOT_BORDER_DIM_COL_VERT] = "verticalDimensionBorderColumns",
+  [PIVOT_BORDER_CAT_ROW_HORZ] = "horizontalCategoryBorderRows",
+  [PIVOT_BORDER_CAT_ROW_VERT] = "verticalCategoryBorderRows",
+  [PIVOT_BORDER_CAT_COL_HORZ] = "horizontalCategoryBorderColumns",
+  [PIVOT_BORDER_CAT_COL_VERT] = "verticalCategoryBorderColumns",
+};
+
 static enum pivot_border
 pivot_border_from_name (const char *name)
 {
-  static const char *border_names[PIVOT_N_BORDERS] = {
-    [PIVOT_BORDER_TITLE] = "titleLayerSeparator",
-    [PIVOT_BORDER_OUTER_LEFT] = "leftOuterFrame",
-    [PIVOT_BORDER_OUTER_TOP] = "topOuterFrame",
-    [PIVOT_BORDER_OUTER_RIGHT] = "rightOuterFrame",
-    [PIVOT_BORDER_OUTER_BOTTOM] = "bottomOuterFrame",
-    [PIVOT_BORDER_INNER_LEFT] = "leftInnerFrame",
-    [PIVOT_BORDER_INNER_TOP] = "topInnerFrame",
-    [PIVOT_BORDER_INNER_RIGHT] = "rightInnerFrame",
-    [PIVOT_BORDER_INNER_BOTTOM] = "bottomInnerFrame",
-    [PIVOT_BORDER_DATA_LEFT] = "dataAreaLeft",
-    [PIVOT_BORDER_DATA_TOP] = "dataAreaTop",
-    [PIVOT_BORDER_DIM_ROW_HORZ] = "horizontalDimensionBorderRows",
-    [PIVOT_BORDER_DIM_ROW_VERT] = "verticalDimensionBorderRows",
-    [PIVOT_BORDER_DIM_COL_HORZ] = "horizontalDimensionBorderColumns",
-    [PIVOT_BORDER_DIM_COL_VERT] = "verticalDimensionBorderColumns",
-    [PIVOT_BORDER_CAT_ROW_HORZ] = "horizontalCategoryBorderRows",
-    [PIVOT_BORDER_CAT_ROW_VERT] = "verticalCategoryBorderRows",
-    [PIVOT_BORDER_CAT_COL_HORZ] = "horizontalCategoryBorderColumns",
-    [PIVOT_BORDER_CAT_COL_VERT] = "verticalCategoryBorderColumns",
-  };
-
   enum pivot_border border;
   for (border = 0; border < PIVOT_N_BORDERS; border++)
-    if (!strcmp (name, border_names[border]))
+    if (!strcmp (name, pivot_border_names[border]))
       break;
   return border;
 }
@@ -121,6 +126,8 @@ spv_table_look_decode (const struct spvsx_table_properties *in,
   struct spv_table_look *out = xzalloc (sizeof *out);
   char *error = NULL;
 
+  out->name = in->name ? xstrdup (in->name) : NULL;
+
   const struct spvsx_general_properties *g = in->general_properties;
   out->omit_empty = g->hide_empty_rows != 0;
   out->width_ranges[TABLE_HORZ][0] = optional_pt (g->minimum_column_width, -1);
@@ -299,11 +306,210 @@ spv_table_look_read (const char *filename, struct spv_table_look **outp)
   return error;
 }
 
+static void
+write_attr (xmlTextWriter *xml, const char *name, const char *value)
+{
+  xmlTextWriterWriteAttribute (xml,
+                               CHAR_CAST (xmlChar *, name),
+                               CHAR_CAST (xmlChar *, value));
+}
+
+static void PRINTF_FORMAT (3, 4)
+write_attr_format (xmlTextWriter *xml, const char *name,
+                   const char *format, ...)
+{
+  va_list args;
+  va_start (args, format);
+  char *value = xvasprintf (format, args);
+  va_end (args);
+
+  write_attr (xml, name, value);
+  free (value);
+}
+
+static void
+write_attr_color (xmlTextWriter *xml, const char *name,
+                  const struct cell_color *color)
+{
+  write_attr_format (xml, name, "#%02"PRIx8"%02"PRIx8"%02"PRIx8,
+                     color->r, color->g, color->b);
+}
+
+static void
+write_attr_dimension (xmlTextWriter *xml, const char *name, int px)
+{
+  int pt = px / 96.0 * 72.0;
+  write_attr_format (xml, name, "%dpt", pt);
+}
+
+static void
+write_attr_bool (xmlTextWriter *xml, const char *name, bool b)
+{
+  write_attr (xml, name, b ? "true" : "false");
+}
+
+static void
+start_elem (xmlTextWriter *xml, const char *name)
+{
+  xmlTextWriterStartElement (xml, CHAR_CAST (xmlChar *, name));
+}
+
+static void
+end_elem (xmlTextWriter *xml)
+{
+  xmlTextWriterEndElement (xml);
+}
+
+char * WARN_UNUSED_RESULT
+spv_table_look_write (const char *filename, const struct spv_table_look *look)
+{
+  FILE *file = fopen (filename, "w");
+  if (!file)
+    return xasprintf (_("%s: create failed (%s)"), filename, strerror (errno));
+
+  xmlTextWriter *xml = xmlNewTextWriter (xmlOutputBufferCreateFile (
+                                           file, NULL));
+  if (!xml)
+    {
+      fclose (file);
+      return xasprintf (_("%s: failed to start writing XML"), filename);
+    }
+
+  xmlTextWriterSetIndent (xml, 1);
+  xmlTextWriterSetIndentString (xml, CHAR_CAST (xmlChar *, "    "));
+
+  xmlTextWriterStartDocument (xml, NULL, "UTF-8", NULL);
+  start_elem (xml, "tableProperties");
+  if (look->name)
+    write_attr (xml, "name", look->name);
+  write_attr (xml, "xmlns", "http://www.ibm.com/software/analytics/spss/xml/table-looks");
+  write_attr (xml, "xmlns:vizml", "http://www.ibm.com/software/analytics/spss/xml/visualization");
+  write_attr (xml, "xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
+  write_attr (xml, "xsi:schemaLocation", "http://www.ibm.com/software/analytics/spss/xml/table-looks http://www.ibm.com/software/analytics/spss/xml/table-looks/table-looks-1.4.xsd");
+
+  start_elem (xml, "generalProperties");
+  write_attr_bool (xml, "hideEmptyRows", look->omit_empty);
+  const int (*wr)[2] = look->width_ranges;
+  write_attr_format (xml, "maximumColumnWidth", "%d", wr[TABLE_HORZ][1]);
+  write_attr_format (xml, "maximumRowWidth", "%d", wr[TABLE_VERT][1]);
+  write_attr_format (xml, "minimumColumnWidth", "%d", wr[TABLE_HORZ][0]);
+  write_attr_format (xml, "minimumRowWidth", "%d", wr[TABLE_VERT][0]);
+  write_attr (xml, "rowDimensionLabels",
+              look->row_labels_in_corner ? "inCorner" : "nested");
+  end_elem (xml);
+
+  start_elem (xml, "footnoteProperties");
+  write_attr (xml, "markerPosition",
+              look->footnote_marker_superscripts ? "superscript" : "subscript");
+  write_attr (xml, "numberFormat",
+              look->show_numeric_markers ? "numeric" : "alphabetic");
+  end_elem (xml);
+
+  start_elem (xml, "cellFormatProperties");
+  for (enum pivot_area a = 0; a < PIVOT_N_AREAS; a++)
+    {
+      const struct area_style *area = &look->areas[a];
+      const struct font_style *font = &area->font_style;
+      const struct cell_style *cell = &area->cell_style;
+
+      start_elem (xml, pivot_area_names[a]);
+      if (a == PIVOT_AREA_DATA
+          && (!cell_color_equal (&font->fg[0], &font->fg[1])
+              || !cell_color_equal (&font->bg[0], &font->bg[1])))
+        {
+          write_attr_color (xml, "alternatingColor", &font->bg[1]);
+          write_attr_color (xml, "alternatingTextColor", &font->fg[1]);
+        }
+
+      start_elem (xml, "vizml:style");
+      write_attr_color (xml, "color", &font->fg[0]);
+      write_attr_color (xml, "color2", &font->bg[0]);
+      write_attr (xml, "font-family", font->typeface);
+      write_attr_format (xml, "font-size", "%dpt", font->size);
+      write_attr (xml, "font-weight", font->bold ? "bold" : "regular");
+      write_attr (xml, "font-underline",
+                  font->underline ? "underline" : "none");
+      write_attr (xml, "labelLocationVertical",
+                  cell->valign == TABLE_VALIGN_BOTTOM ? "negative"
+                  : cell->valign == TABLE_VALIGN_TOP ? "positive"
+                  : "center");
+      write_attr_dimension (xml, "margin-bottom", cell->margin[TABLE_VERT][1]);
+      write_attr_dimension (xml, "margin-left", cell->margin[TABLE_HORZ][0]);
+      write_attr_dimension (xml, "margin-right", cell->margin[TABLE_HORZ][1]);
+      write_attr_dimension (xml, "margin-top", cell->margin[TABLE_VERT][0]);
+      write_attr (xml, "textAlignment",
+                  cell->halign == TABLE_HALIGN_LEFT ? "left"
+                  : cell->halign == TABLE_HALIGN_RIGHT ? "right"
+                  : cell->halign == TABLE_HALIGN_CENTER ? "center"
+                  : cell->halign == TABLE_HALIGN_DECIMAL ? "decimal"
+                  : "mixed");
+      if (cell->halign == TABLE_HALIGN_DECIMAL)
+        write_attr_dimension (xml, "decimal-offset", cell->decimal_offset);
+      end_elem (xml);
+
+      end_elem (xml);
+    }
+  end_elem (xml);
+
+  start_elem (xml, "borderProperties");
+  for (enum pivot_border b = 0; b < PIVOT_N_BORDERS; b++)
+    {
+      const struct table_border_style *border = &look->borders[b];
+
+      start_elem (xml, pivot_border_names[b]);
+
+      static const char *table_stroke_names[TABLE_N_STROKES] =
+        {
+          [TABLE_STROKE_NONE] = "none",
+          [TABLE_STROKE_SOLID] = "solid",
+          [TABLE_STROKE_DASHED] = "dashed",
+          [TABLE_STROKE_THICK] = "thick",
+          [TABLE_STROKE_THIN] = "thin",
+          [TABLE_STROKE_DOUBLE] = "double",
+        };
+      write_attr (xml, "borderStyleType", table_stroke_names[border->stroke]);
+      write_attr_color (xml, "color", &border->color);
+      end_elem (xml);
+    }
+  end_elem (xml);
+
+  start_elem (xml, "printingProperties");
+  write_attr_bool (xml, "printAllLayers", look->print_all_layers);
+  write_attr_bool (xml, "rescaleLongTableToFitPage", look->shrink_to_length);
+  write_attr_bool (xml, "rescaleWideTableToFitPage", look->shrink_to_width);
+  write_attr_format (xml, "windowOrphanLines", "%zu", look->n_orphan_lines);
+  if (look->continuation && look->continuation[0]
+      && (look->top_continuation || look->bottom_continuation))
+    {
+      write_attr_format (xml, "continuationText", look->continuation);
+      write_attr_bool (xml, "continuationTextAtTop", look->top_continuation);
+      write_attr_bool (xml, "continuationTextAtBottom",
+                       look->bottom_continuation);
+    }
+  end_elem (xml);
+
+  xmlTextWriterEndDocument (xml);
+
+  xmlFreeTextWriter (xml);
+
+  fflush (file);
+  bool ok = !ferror (file);
+  if (fclose (file) == EOF)
+    ok = false;
+
+  if (!ok)
+    return xasprintf (_("%s: error writing file (%s)"),
+                      filename, strerror (errno));
+
+  return NULL;
+}
+
 void
 spv_table_look_destroy (struct spv_table_look *look)
 {
   if (look)
     {
+      free (look->name);
       for (size_t i = 0; i < PIVOT_N_AREAS; i++)
         area_style_uninit (&look->areas[i]);
       free (look->continuation);
@@ -315,6 +521,10 @@ void
 spv_table_look_install (const struct spv_table_look *look,
                         struct pivot_table *table)
 {
+  free (table->table_look);
+  if (look->name)
+    table->table_look = xstrdup (look->name);
+
   table->omit_empty = look->omit_empty;
 
   for (enum table_axis axis = 0; axis < TABLE_N_AXES; axis++)
@@ -343,3 +553,37 @@ spv_table_look_install (const struct spv_table_look *look,
   table->continuation = xstrdup (look->continuation);
   table->n_orphan_lines = look->n_orphan_lines;
 }
+
+struct spv_table_look *
+spv_table_look_get (const struct pivot_table *table)
+{
+  struct spv_table_look *look = xzalloc (sizeof *look);
+
+  look->name = table->table_look ? xstrdup (table->table_look) : NULL;
+
+  look->omit_empty = table->omit_empty;
+
+  for (enum table_axis axis = 0; axis < TABLE_N_AXES; axis++)
+    for (int i = 0; i < 2; i++)
+      look->width_ranges[axis][i] = table->sizing[axis].range[i];
+  look->row_labels_in_corner = table->row_labels_in_corner;
+
+  look->footnote_marker_superscripts = table->footnote_marker_superscripts;
+  look->show_numeric_markers = table->show_numeric_markers;
+
+  for (size_t i = 0; i < PIVOT_N_AREAS; i++)
+    area_style_copy (NULL, &look->areas[i], &table->areas[i]);
+  for (size_t i = 0; i < PIVOT_N_BORDERS; i++)
+    look->borders[i] = table->borders[i];
+
+  look->print_all_layers = table->print_all_layers;
+  look->paginate_layers = table->paginate_layers;
+  look->shrink_to_width = table->shrink_to_fit[TABLE_HORZ];
+  look->shrink_to_length = table->shrink_to_fit[TABLE_VERT];
+  look->top_continuation = table->top_continuation;
+  look->bottom_continuation = table->bottom_continuation;
+  look->continuation = xstrdup (table->continuation);
+  look->n_orphan_lines = table->n_orphan_lines;
+
+  return look;
+}
index 3e817571ddb4d678c143ea800e010d78f4f1073e..98675950215d7d4374d21247dec712fa54ea0140 100644 (file)
@@ -33,6 +33,8 @@ struct spvsx_table_properties;
 
 struct spv_table_look
   {
+    char *name;                 /* May be null. */
+
     /* General properties. */
     bool omit_empty;
     int width_ranges[TABLE_N_AXES][2];      /* In 1/96" units. */
@@ -63,8 +65,11 @@ char *spv_table_look_decode (const struct spvsx_table_properties *,
   WARN_UNUSED_RESULT;
 char *spv_table_look_read (const char *, struct spv_table_look **)
   WARN_UNUSED_RESULT;
+char *spv_table_look_write (const char *, const struct spv_table_look *)
+  WARN_UNUSED_RESULT;
 
 void spv_table_look_install (const struct spv_table_look *,
                              struct pivot_table *);
+struct spv_table_look *spv_table_look_get (const struct pivot_table *);
 
 #endif /* output/spv/spv-table-look.h */
index 598b6f9ddc2b621c7e5e6ecfc861faa19b237764..252fec9a005a9edbf4634a7272088896a310b939 100644 (file)
@@ -16,6 +16,8 @@ pspp\-output \- convert and operate on SPSS viewer (SPV) files
 .br
 \fBpspp\-output \fR[\fIoptions\fR] \fBconvert\fR \fIsource destination\fR
 .br
+\fBpspp\-output \fR[\fIoptions\fR] \fBget\-table\-look\fR \fIsource destination\fR
+.br
 \fBpspp\-output \-\-help\fR | \fB\-h\fR
 .br
 \fBpspp\-output \-\-version\fR | \fB\-v\fR
@@ -83,6 +85,18 @@ write the output as best it can, even with errors.
 .IP \fB\-\-table\-look=\fIfile\fR
 Reads a table style from \fIfile\fR and applies it to all of the
 output tables.  The file should a TableLook \fB.stt\fR file.
+.SS The \fBget\-table\-look\fR command
+When invoked as \fBpspp\-output get\-table\-look \fIsource
+destination\fR, \fBpspp\-output\fR reads SPV file \fIsource\fR,
+applies any selection options (as described under \fBInput Selection
+Options\fR below), picks the first table from the selected object,
+extracts the TableLook from that table, and writes it to
+\fIdestination\fR (typically with an \fB.stt\fR extension) in the
+TableLook XML format.
+.PP
+The user may use the TableLook file to change the style of tables in
+other files, by passing it to the \fB\-\-table\-look\fR option on the
+\fBconvert\fR command.
 .SS "Input Selection Options"
 The \fBdir\fR and \fBconvert\fR commands, by default, operate on all
 of the objects in the source SPV file, except for objects that are not
index 8362ac57ff252ef78392c2c29fd33b809f52fbdd..4b9710a020bc1c3c24ddb7a08e22f8e60c511fb3 100644 (file)
@@ -325,6 +325,45 @@ run_convert (int argc UNUSED, char **argv)
     }
 }
 
+static const struct pivot_table *
+get_first_table (const struct spv_reader *spv)
+{
+  struct spv_item **items;
+  size_t n_items;
+  spv_select (spv, criteria, n_criteria, &items, &n_items);
+
+  for (size_t i = 0; i < n_items; i++)
+    if (spv_item_is_table (items[i]))
+      {
+        free (items);
+        return spv_item_get_table (items[i]);
+      }
+
+  free (items);
+  return NULL;
+}
+
+static void
+run_get_table_look (int argc UNUSED, char **argv)
+{
+  struct spv_reader *spv;
+  char *err = spv_open (argv[1], &spv);
+  if (err)
+    error (1, 0, "%s", err);
+
+  const struct pivot_table *table = get_first_table (spv);
+  if (!table)
+    error (1, 0, "%s: no tables found", argv[1]);
+
+  struct spv_table_look *look = spv_table_look_get (table);
+  err = spv_table_look_write (argv[2], look);
+  if (err)
+    error (1, 0, "%s", err);
+  spv_table_look_destroy (look);
+
+  spv_close (spv);
+}
+
 static void
 run_dump (int argc UNUSED, char **argv)
 {
@@ -672,6 +711,7 @@ static const struct command commands[] =
     { "detect", 1, 1, run_detect },
     { "dir", 1, 1, run_directory },
     { "convert", 2, 2, run_convert },
+    { "get-table-look", 2, 2, run_get_table_look },
 
     /* Undocumented commands. */
     { "dump", 1, 1, run_dump },
@@ -1053,6 +1093,7 @@ The following commands are available:\n\
   detect FILE            Detect whether FILE is an SPV file.\n\
   dir FILE               List tables and other items in FILE.\n\
   convert SOURCE DEST    Convert .spv SOURCE to DEST.\n\
+  get-table-look SOURCE DEST  Copies first selected TableLook into DEST\n\
 \n\
 Input selection options for \"dir\" and \"convert\":\n\
   --select=CLASS...   include only some kinds of objects\n\