Adopt use of gnulib for portability.
[pspp-builds.git] / src / get.c
index c33680fd5a4c11f939c03d9274f68cc84af7cfb5..a13277a6bf4676ccc0125d1bf846e5f50b9a25a4 100644 (file)
--- a/src/get.c
+++ b/src/get.c
 
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
-   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
-   02111-1307, USA. */
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+   02110-1301, USA. */
 
 #include <config.h>
-#include <assert.h>
+#include "error.h"
 #include <stdlib.h>
 #include "alloc.h"
+#include "case.h"
 #include "command.h"
+#include "dictionary.h"
 #include "error.h"
 #include "file-handle.h"
 #include "hash.h"
 #include "lexer.h"
 #include "misc.h"
-#include "pfm.h"
+#include "pfm-read.h"
+#include "pfm-write.h"
 #include "settings.h"
-#include "sfm.h"
+#include "sfm-read.h"
+#include "sfm-write.h"
 #include "str.h"
 #include "value-labels.h"
 #include "var.h"
 #include "vfm.h"
 #include "vfmP.h"
 
+#include "gettext.h"
+#define _(msgid) gettext (msgid)
+
 #include "debug-print.h"
 
-/* GET or IMPORT input program. */
-struct get_pgm 
+/* Rearranging and reducing a dictionary. */
+static void start_case_map (struct dictionary *);
+static struct case_map *finish_case_map (struct dictionary *);
+static void map_case (const struct case_map *,
+                      const struct ccase *, struct ccase *);
+static void destroy_case_map (struct case_map *);
+
+/* Operation type. */
+enum operation 
   {
-    struct file_handle *handle; /* File to GET or IMPORT from. */
-    size_t case_size;           /* Case size in bytes. */
+    OP_READ,    /* GET or IMPORT. */
+    OP_SAVE,    /* SAVE or XSAVE. */
+    OP_EXPORT   /* EXPORT. */
   };
 
-/* XSAVE transformation (and related SAVE, EXPORT procedures). */
-struct save_trns
+static bool trim_dictionary (struct dictionary *,
+                             enum operation, int *compress);
+\f
+/* GET input program. */
+struct get_pgm 
   {
-    struct trns_header h;
-    struct file_handle *f;     /* Associated system file. */
-    int nvar;                  /* Number of variables. */
-    struct variable **var;      /* Variables. */
-    flt64 *case_buf;           /* Case transfer buffer. */
+    struct sfm_reader *reader;  /* System file reader. */
+    struct case_map *map;       /* Map from system file to active file dict. */
+    struct ccase bounce;        /* Bounce buffer. */
   };
 
-/* Options bits set by trim_dictionary(). */
-#define GTSV_OPT_COMPRESSED    001     /* Compression; (X)SAVE only. */
-#define GTSV_OPT_SAVE          002     /* The SAVE/XSAVE/EXPORT procedures. */
-#define GTSV_OPT_MATCH_FILES   004     /* The MATCH FILES procedure. */
-#define GTSV_OPT_NONE          0
-
-static int trim_dictionary (struct dictionary * dict, int *options);
-static int save_write_case_func (struct ccase *, void *);
-static trns_proc_func save_trns_proc;
-static trns_free_func save_trns_free;
-
-#if DEBUGGING
-void dump_dict_variables (struct dictionary *);
-#endif
+static void get_pgm_free (struct get_pgm *);
 
 /* Parses the GET command. */
 int
 cmd_get (void)
 {
-  struct file_handle *handle;
-  struct dictionary *dict;
-  struct get_pgm *pgm;
-  int options = GTSV_OPT_NONE;
+  struct get_pgm *pgm = NULL;
+  struct file_handle *fh;
+  struct dictionary *dict = NULL;
+
+  pgm = xmalloc (sizeof *pgm);
+  pgm->reader = NULL;
+  pgm->map = NULL;
+  case_nullify (&pgm->bounce);
 
-  lex_match_id ("GET");
   discard_variables ();
 
   lex_match ('/');
   if (lex_match_id ("FILE"))
     lex_match ('=');
+  fh = fh_parse ();
+  if (fh == NULL)
+    goto error;
 
-  handle = fh_parse_file_handle ();
-  if (handle == NULL)
-    return CMD_FAILURE;
+  pgm->reader = sfm_open_reader (fh, &dict, NULL);
+  if (pgm->reader == NULL)
+    goto error;
+  case_create (&pgm->bounce, dict_get_next_value_idx (dict));
 
-  dict = sfm_read_dictionary (handle, NULL);
-  if (dict == NULL)
-    return CMD_FAILURE;
+  start_case_map (dict);
+  if (!trim_dictionary (dict, OP_READ, NULL))
+    goto error;
+  pgm->map = finish_case_map (dict);
 
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
-  if (0 == trim_dictionary (dict, &options))
-    {
-      fh_close_handle (handle);
-      return CMD_FAILURE;
-    }
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
+  dict_destroy (default_dict);
+  default_dict = dict;
 
-  dict_compact_values (dict);
+  vfm_source = create_case_source (&get_source_class, pgm);
 
-#if DEBUGGING
-  printf (_("GET translation table from file to memory:\n"));
-  for (i = 0; i < dict->nvar; i++)
-    {
-      struct variable *v = dict->var[i];
+  return CMD_SUCCESS;
 
-      printf (_("  %8s from %3d,%3d to %3d,%3d\n"), v->name,
-             v->get.fv, v->get.nv, v->fv, v->nv);
+ error:
+  get_pgm_free (pgm);
+  if (dict != NULL)
+    dict_destroy (dict);
+  return CMD_FAILURE;
+}
+
+/* Frees a struct get_pgm. */
+static void
+get_pgm_free (struct get_pgm *pgm) 
+{
+  if (pgm != NULL) 
+    {
+      sfm_close_reader (pgm->reader);
+      destroy_case_map (pgm->map);
+      case_destroy (&pgm->bounce);
+      free (pgm);
     }
-#endif
+}
 
-  dict_destroy (default_dict);
-  default_dict = dict;
+/* Clears internal state related to GET input procedure. */
+static void
+get_source_destroy (struct case_source *source)
+{
+  struct get_pgm *pgm = source->aux;
+  get_pgm_free (pgm);
+}
 
-  pgm = xmalloc (sizeof *pgm);
-  pgm->handle = handle;
-  pgm->case_size = dict_get_case_size (default_dict);
-  vfm_source = create_case_source (&get_source_class, default_dict, pgm);
+/* Reads all the cases from the data file into C and passes them
+   to WRITE_CASE one by one, passing WC_DATA. */
+static void
+get_source_read (struct case_source *source,
+                 struct ccase *c,
+                 write_case_func *write_case, write_case_data wc_data)
+{
+  struct get_pgm *pgm = source->aux;
+  int ok;
 
-  return CMD_SUCCESS;
+  do
+    {
+      if (pgm->map == NULL)
+        ok = sfm_read_case (pgm->reader, c);
+      else
+        {
+          ok = sfm_read_case (pgm->reader, &pgm->bounce);
+          if (ok)
+            map_case (pgm->map, &pgm->bounce, c);
+        }
+
+      if (ok)
+        ok = write_case (wc_data);
+    }
+  while (ok);
 }
 
-/* SAVE or XSAVE command? */
-enum save_cmd 
+const struct case_source_class get_source_class =
   {
-    CMD_SAVE,
-    CMD_XSAVE
+    "GET",
+    NULL,
+    get_source_read,
+    get_source_destroy,
+  };
+\f
+/* XSAVE transformation and SAVE procedure. */
+struct save_trns
+  {
+    struct trns_header h;
+    struct sfm_writer *writer;  /* System file writer. */
+    struct case_map *map;       /* Map from active file to system file dict. */
+    struct ccase bounce;        /* Bounce buffer. */
   };
 
-/* Parses the SAVE and XSAVE commands.  */
-static int
-cmd_save_internal (enum save_cmd save_cmd)
-{
-  struct file_handle *handle;
-  struct dictionary *dict;
-  int options = GTSV_OPT_SAVE;
+static int save_write_case_func (struct ccase *, void *);
+static trns_proc_func save_trns_proc;
+static trns_free_func save_trns_free;
 
-  struct save_trns *t;
-  struct sfm_write_info inf;
+/* Parses the SAVE or XSAVE command
+   and returns the parsed transformation. */
+static struct save_trns *
+cmd_save_internal (void)
+{
+  struct file_handle *fh = NULL;
+  struct dictionary *dict = NULL;
+  struct save_trns *t = NULL;
+  int compress = get_scompression ();
+  const int default_version = 3;
+  int version = default_version;
+  short no_name_table = 0;
 
-  int i;
+  t = xmalloc (sizeof *t);
+  t->h.proc = save_trns_proc;
+  t->h.free = save_trns_free;
+  t->writer = NULL;
+  t->map = NULL;
+  case_nullify (&t->bounce);
+  
 
-  lex_match_id ("SAVE");
+  /* Read most of the subcommands. */
+  for (;;)
+    {
+      if (lex_match_id ("VERSION"))
+       {
+         lex_match ('=');
+         if (lex_force_int ()) 
+           {
+             version = lex_integer ();
+              lex_get ();
+             
+             if (lex_match_id ("X")) 
+                no_name_table = 1;
+           }
+       }
+      else if (lex_match_id ("OUTFILE"))
+       {
+         lex_match ('=');
+      
+         fh = fh_parse ();
+         if (fh == NULL)
+           goto error;
 
-  lex_match ('/');
-  if (lex_match_id ("OUTFILE"))
-    lex_match ('=');
+       }
+      if ( ! lex_match('/')  ) 
+       break;
 
-  handle = fh_parse_file_handle ();
-  if (handle == NULL)
-    return CMD_FAILURE;
+    }
 
-  dict = dict_clone (default_dict);
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
-  for (i = 0; i < dict_get_var_cnt (dict); i++) 
-    dict_get_var (dict, i)->aux = dict_get_var (default_dict, i);
-  if (0 == trim_dictionary (dict, &options))
+  if (token != '.')
     {
-      fh_close_handle (handle);
-      return CMD_FAILURE;
+      lex_error (_("expecting end of command"));
+      goto error;
     }
 
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
+  if ( fh == NULL ) 
+    {
+      msg ( ME, _("The required %s subcommand was not present"), "OUTFILE");
+      goto error;
+    }
 
-  /* Write dictionary. */
-  inf.h = handle;
-  inf.dict = dict;
-  inf.compress = !!(options & GTSV_OPT_COMPRESSED);
-  if (!sfm_write_dictionary (&inf))
+  if ( version != default_version )
     {
-      dict_destroy (dict);
-      fh_close_handle (handle);
-      return CMD_FAILURE;
+      msg (MW, _("Unsupported sysfile version: %d. Using version %d instead."),
+          version, default_version);
+
+      version = default_version;
     }
 
-  /* Fill in transformation structure. */
-  t = xmalloc (sizeof *t);
-  t->h.proc = save_trns_proc;
-  t->h.free = save_trns_free;
-  t->f = handle;
-  t->nvar = dict_get_var_cnt (dict);
-  t->var = xmalloc (sizeof *t->var * t->nvar);
-  for (i = 0; i < t->nvar; i++)
-    t->var[i] = dict_get_var (dict, i)->aux;
-  t->case_buf = xmalloc (sizeof *t->case_buf * inf.case_size);
+  dict = dict_clone (default_dict);
+  start_case_map (dict);
+  if (!trim_dictionary (dict, OP_SAVE, &compress))
+    goto error;
+  t->map = finish_case_map (dict);
+  if (t->map != NULL)
+    case_create (&t->bounce, dict_get_next_value_idx (dict));
+
+  t->writer = sfm_open_writer (fh, dict, compress, no_name_table);
+  if (t->writer == NULL)
+    goto error;
+
   dict_destroy (dict);
 
-  if (save_cmd == CMD_SAVE)
-    {
-      procedure (save_write_case_func, t);
-      save_trns_free (&t->h);
-    }
-  else 
-    {
-      assert (save_cmd == CMD_XSAVE);
-      add_transformation (&t->h); 
-    }
+  return t;
 
-  return CMD_SUCCESS;
+ error:
+  assert (t != NULL);
+  dict_destroy (dict);
+  save_trns_free (&t->h);
+  return NULL;
 }
 
 /* Parses and performs the SAVE procedure. */
 int
 cmd_save (void)
 {
-  return cmd_save_internal (CMD_SAVE);
+  struct save_trns *t = cmd_save_internal ();
+  if (t != NULL) 
+    {
+      procedure (save_write_case_func, t);
+      save_trns_free (&t->h);
+      free(t);
+      return CMD_SUCCESS;
+    }
+  else
+    return CMD_FAILURE;
 }
 
 /* Parses the XSAVE transformation command. */
 int
 cmd_xsave (void)
 {
-  return cmd_save_internal (CMD_XSAVE);
+  struct save_trns *t = cmd_save_internal ();
+  if (t != NULL) 
+    {
+      add_transformation (&t->h);
+      return CMD_SUCCESS; 
+    }
+  else
+    return CMD_FAILURE;
 }
 
 /* Writes the given C to the file specified by T. */
 static void
 do_write_case (struct save_trns *t, struct ccase *c) 
 {
-  flt64 *p = t->case_buf;
-  int i;
-
-  for (i = 0; i < t->nvar; i++)
+  if (t->map == NULL)
+    sfm_write_case (t->writer, c);
+  else 
     {
-      struct variable *v = t->var[i];
-      if (v->type == NUMERIC)
-       {
-         double src = c->data[v->fv].f;
-         if (src == SYSMIS)
-           *p++ = -FLT64_MAX;
-         else
-           *p++ = src;
-       }
-      else
-       {
-         memcpy (p, c->data[v->fv].s, v->width);
-         memset (&((char *) p)[v->width], ' ',
-                 REM_RND_UP (v->width, sizeof *p));
-         p += DIV_RND_UP (v->width, sizeof *p);
-       }
+      map_case (t->map, c, &t->bounce);
+      sfm_write_case (t->writer, &t->bounce);
     }
-
-  sfm_write_case (t->f, t->case_buf, p - t->case_buf);
 }
 
 /* Writes case C to the system file specified on SAVE. */
@@ -276,33 +343,40 @@ save_trns_proc (struct trns_header *h, struct ccase *c, int case_num UNUSED)
 
 /* Frees a SAVE transformation. */
 static void
-save_trns_free (struct trns_header *pt)
+save_trns_free (struct trns_header *t_)
 {
-  struct save_trns *t = (struct save_trns *) pt;
+  struct save_trns *t = (struct save_trns *) t_;
 
-  fh_close_handle (t->f);
-  free (t->var);
-  free (t->case_buf);
-  free (t);
+  if (t != NULL) 
+    {
+      sfm_close_writer (t->writer);
+      destroy_case_map (t->map);
+      case_destroy (&t->bounce);
+    }
 }
 
-static int rename_variables (struct dictionary * dict);
-
-/* The GET and SAVE commands have a common structure after the
-   FILE/OUTFILE subcommand.  This function parses this structure and
-   returns nonzero on success, zero on failure.  It both reads
-   *OPTIONS, for the GTSV_OPT_SAVE bit, and writes it, for the
-   GTSV_OPT_COMPRESSED bit. */
-/* FIXME: IN, FIRST, LAST, MAP. */
-/* FIXME?  Should we call dict_compact_values() on dict as a
-   final step? */
-static int
-trim_dictionary (struct dictionary *dict, int *options)
+static bool rename_variables (struct dictionary *dict);
+static bool drop_variables (struct dictionary *dict);
+static bool keep_variables (struct dictionary *dict);
+
+/* Commands that read and write system files share a great deal
+   of common syntactic structure for rearranging and dropping
+   variables.  This function parses this syntax and modifies DICT
+   appropriately.
+
+   OP is the operation being performed.  For operations that
+   write a system file, *COMPRESS is set to 1 if the system file
+   should be compressed, 0 otherwise.
+   
+   Returns true on success, false on failure. */
+static bool
+trim_dictionary (struct dictionary *dict, enum operation op, int *compress)
 {
+  assert ((compress != NULL) == (op == OP_SAVE));
   if (get_scompression())
-    *options |= GTSV_OPT_COMPRESSED;
+    *compress = 1;
 
-  if (*options & GTSV_OPT_SAVE)
+  if (op == OP_SAVE || op == OP_EXPORT)
     {
       /* Delete all the scratch variables. */
       struct variable **v;
@@ -318,76 +392,40 @@ trim_dictionary (struct dictionary *dict, int *options)
       free (v);
     }
   
-  while ((*options & GTSV_OPT_MATCH_FILES) || lex_match ('/'))
+  while (lex_match ('/'))
     {
-      if (!(*options & GTSV_OPT_MATCH_FILES) && lex_match_id ("COMPRESSED"))
-       *options |= GTSV_OPT_COMPRESSED;
-      else if (!(*options & GTSV_OPT_MATCH_FILES) && lex_match_id ("UNCOMPRESSED"))
-       *options &= ~GTSV_OPT_COMPRESSED;
+      bool ok = true;
+      
+      if (op == OP_SAVE && lex_match_id ("COMPRESSED"))
+       *compress = 1;
+      else if (op == OP_SAVE && lex_match_id ("UNCOMPRESSED"))
+       *compress = 0;
       else if (lex_match_id ("DROP"))
-       {
-         struct variable **v;
-         int nv;
-
-         lex_match ('=');
-         if (!parse_variables (dict, &v, &nv, PV_NONE))
-           return 0;
-          dict_delete_vars (dict, v, nv);
-          free (v);
-       }
+        ok = drop_variables (dict);
       else if (lex_match_id ("KEEP"))
-       {
-         struct variable **v;
-         int nv;
-          int i;
-
-         lex_match ('=');
-         if (!parse_variables (dict, &v, &nv, PV_NONE))
-           return 0;
-
-          /* Move the specified variables to the beginning. */
-          dict_reorder_vars (dict, v, nv);
-          
-          /* Delete the remaining variables. */
-          v = xrealloc (v, (dict_get_var_cnt (dict) - nv) * sizeof *v);
-          for (i = nv; i < dict_get_var_cnt (dict); i++)
-            v[i - nv] = dict_get_var (dict, i);
-          dict_delete_vars (dict, v, dict_get_var_cnt (dict) - nv);
-          free (v);
-       }
+       ok = keep_variables (dict);
       else if (lex_match_id ("RENAME"))
-       {
-         if (!rename_variables (dict))
-           return 0;
-       }
+        ok = rename_variables (dict);
       else
        {
-         lex_error (_("while expecting a valid subcommand"));
-         return 0;
-       }
-
-      if (dict_get_var_cnt (dict) == 0)
-       {
-         msg (SE, _("All variables deleted from system file dictionary."));
-         return 0;
+         lex_error (_("expecting a valid subcommand"));
+         ok = false;
        }
 
-      if (*options & GTSV_OPT_MATCH_FILES)
-       return 1;
+      if (!ok)
+        return false;
     }
 
-  if (token != '.')
-    {
-      lex_error (_("expecting end of command"));
-      return 0;
-    }
-  
-  return 1;
+  if (!lex_end_of_command ())
+    return false;
+
+  dict_compact_values (dict);
+  return true;
 }
 
 /* Parses and performs the RENAME subcommand of GET and SAVE. */
-static int
-rename_variables (struct dictionary * dict)
+static bool
+rename_variables (struct dictionary *dict)
 {
   int i;
 
@@ -411,8 +449,6 @@ rename_variables (struct dictionary * dict)
       if (!lex_force_match ('=')
          || !lex_force_id ())
        return 0;
-      if (!strncmp (tokid, v->name, 8))
-       return 1;
       if (dict_lookup_var (dict, tokid) != NULL)
        {
          msg (SE, _("Cannot rename %s as %s because there already exists "
@@ -474,53 +510,138 @@ done:
   return success;
 }
 
-#if DEBUGGING
-void
-dump_dict_variables (struct dictionary * dict)
+/* Parses and performs the DROP subcommand of GET and SAVE.
+   Returns true if successful, false on failure.*/
+static bool
+drop_variables (struct dictionary *dict)
 {
-  int i;
+  struct variable **v;
+  int nv;
 
-  printf (_("\nVariables in dictionary:\n"));
-  for (i = 0; i < dict->nvar; i++)
-    printf ("%s, ", dict->var[i]->name);
-  printf ("\n");
+  lex_match ('=');
+  if (!parse_variables (dict, &v, &nv, PV_NONE))
+    return false;
+  dict_delete_vars (dict, v, nv);
+  free (v);
+
+  if (dict_get_var_cnt (dict) == 0)
+    {
+      msg (SE, _("Cannot DROP all variables from dictionary."));
+      return false;
+    }
+  return true;
 }
-#endif
-\f
-/* Clears internal state related to GET input procedure. */
-static void
-get_source_destroy (struct case_source *source)
+
+/* Parses and performs the KEEP subcommand of GET and SAVE.
+   Returns true if successful, false on failure.*/
+static bool
+keep_variables (struct dictionary *dict)
 {
-  struct get_pgm *pgm = source->aux;
+  struct variable **v;
+  int nv;
+  int i;
 
-  /* It is not necessary to destroy the dictionary because if we get
-     to this point then the dictionary is default_dict. */
-  fh_close_handle (pgm->handle);
-  free (pgm);
+  lex_match ('=');
+  if (!parse_variables (dict, &v, &nv, PV_NONE))
+    return false;
+
+  /* Move the specified variables to the beginning. */
+  dict_reorder_vars (dict, v, nv);
+          
+  /* Delete the remaining variables. */
+  v = xrealloc (v, (dict_get_var_cnt (dict) - nv) * sizeof *v);
+  for (i = nv; i < dict_get_var_cnt (dict); i++)
+    v[i - nv] = dict_get_var (dict, i);
+  dict_delete_vars (dict, v, dict_get_var_cnt (dict) - nv);
+  free (v);
+
+  return true;
 }
+\f
+/* EXPORT procedure. */
+struct export_proc 
+  {
+    struct pfm_writer *writer;  /* System file writer. */
+    struct case_map *map;       /* Map from active file to system file dict. */
+    struct ccase bounce;        /* Bounce buffer. */
+  };
 
-/* Reads all the cases from the data file into C and passes them
-   to WRITE_CASE one by one, passing WC_DATA. */
-static void
-get_source_read (struct case_source *source,
-                 struct ccase *c,
-                 write_case_func *write_case, write_case_data wc_data)
+static int export_write_case_func (struct ccase *, void *);
+static void export_proc_free (struct export_proc *);
+     
+/* Parses the EXPORT command.  */
+/* FIXME: same as cmd_save_internal(). */
+int
+cmd_export (void)
 {
-  struct get_pgm *pgm = source->aux;
+  struct file_handle *fh;
+  struct dictionary *dict;
+  struct export_proc *proc;
+
+  proc = xmalloc (sizeof *proc);
+  proc->writer = NULL;
+  proc->map = NULL;
+  case_nullify (&proc->bounce);
+
+  lex_match ('/');
+  if (lex_match_id ("OUTFILE"))
+    lex_match ('=');
+  fh = fh_parse ();
+  if (fh == NULL)
+    return CMD_FAILURE;
 
-  while (sfm_read_case (pgm->handle, c->data, default_dict)
-        && write_case (wc_data))
-    ;
+  dict = dict_clone (default_dict);
+  start_case_map (dict);
+  if (!trim_dictionary (dict, OP_EXPORT, NULL))
+    goto error;
+  proc->map = finish_case_map (dict);
+  if (proc->map != NULL)
+    case_create (&proc->bounce, dict_get_next_value_idx (dict));
+
+  proc->writer = pfm_open_writer (fh, dict);
+  if (proc->writer == NULL)
+    goto error;
+  
+  dict_destroy (dict);
+
+  procedure (export_write_case_func, proc);
+  export_proc_free (proc);
+  free (proc);
+
+  return CMD_SUCCESS;
+
+ error:
+  dict_destroy (dict);
+  export_proc_free (proc);
+  free (proc);
+  return CMD_FAILURE;
 }
 
-const struct case_source_class get_source_class =
-  {
-    "GET",
-    NULL,
-    get_source_read,
-    get_source_destroy,
-  };
+/* Writes case C to the EXPORT file. */
+static int
+export_write_case_func (struct ccase *c, void *aux) 
+{
+  struct export_proc *proc = aux;
+  if (proc->map == NULL)
+    pfm_write_case (proc->writer, c);
+  else 
+    {
+      map_case (proc->map, c, &proc->bounce);
+      pfm_write_case (proc->writer, &proc->bounce);
+    }
+  return 1;
+}
 
+static void
+export_proc_free (struct export_proc *proc) 
+{
+  if (proc != NULL) 
+    {
+      pfm_close_writer (proc->writer);
+      destroy_case_map (proc->map);
+      case_destroy (&proc->bounce);
+    }
+}
 \f
 /* MATCH FILES. */
 
@@ -542,11 +663,15 @@ struct mtf_file
     
     int type;                  /* One of MTF_*. */
     struct variable **by;      /* List of BY variables for this file. */
-    struct file_handle *handle;        /* File handle for the file. */
+    struct file_handle *handle; /* File handle. */
+    struct sfm_reader *reader;  /* System file reader. */
     struct dictionary *dict;   /* Dictionary from system file. */
-    char in[9];                        /* Name of the variable from IN=. */
-    char first[9], last[9];    /* Name of the variables from FIRST=, LAST=. */
-    union value *input;                /* Input record. */
+
+    /* IN subcommand. */
+    char *in_name;              /* Variable name. */
+    struct variable *in_var;    /* Variable (in master dictionary). */
+
+    struct ccase input;         /* Input record. */
   };
 
 /* MATCH FILES procedure. */
@@ -555,12 +680,14 @@ struct mtf_proc
     struct mtf_file *head;      /* First file mentioned on FILE or TABLE. */
     struct mtf_file *tail;      /* Last file mentioned on FILE or TABLE. */
     
-    struct variable **by;       /* Variables on the BY subcommand. */
     size_t by_cnt;              /* Number of variables on BY subcommand. */
 
+    /* Names of FIRST, LAST variables. */
+    char first[LONG_NAME_LEN + 1], last[LONG_NAME_LEN + 1];
+    
     struct dictionary *dict;    /* Dictionary of output file. */
     struct case_sink *sink;     /* Sink to receive output. */
-    struct ccase *mtf_case;     /* Case used for output. */
+    struct ccase mtf_case;      /* Case used for output. */
 
     unsigned seq_num;           /* Have we initialized this variable? */
     unsigned *seq_nums;         /* Sequence numbers for each var in dict. */
@@ -577,272 +704,295 @@ static int mtf_processing (struct ccase *, void *);
 
 static char *var_type_description (struct variable *);
 
+static void set_master (struct variable *, struct variable *master);
+static struct variable *get_master (struct variable *);
+
 /* Parse and execute the MATCH FILES command. */
 int
 cmd_match_files (void)
 {
   struct mtf_proc mtf;
   struct mtf_file *first_table = NULL;
+  struct mtf_file *iter;
   
-  int seen = 0;
+  bool used_active_file = false;
+  bool saw_table = false;
+  bool saw_in = false;
   
-  lex_match_id ("MATCH");
-  lex_match_id ("FILES");
-
   mtf.head = mtf.tail = NULL;
-  mtf.by = NULL;
   mtf.by_cnt = 0;
+  mtf.first[0] = '\0';
+  mtf.last[0] = '\0';
   mtf.dict = dict_create ();
   mtf.sink = NULL;
-  mtf.mtf_case = NULL;
+  case_nullify (&mtf.mtf_case);
   mtf.seq_num = 0;
   mtf.seq_nums = NULL;
   dict_set_case_limit (mtf.dict, dict_get_case_limit (default_dict));
-  
-  do
-    {
-      lex_match ('/');
 
-      if (lex_match (T_BY))
-       {
-         if (seen & 1)
-           {
-             msg (SE, _("The BY subcommand may be given once at most."));
-             goto lossage;
-           }
-         seen |= 1;
-             
-         lex_match ('=');
-         if (!parse_variables (mtf.dict, &mtf.by, &mtf.by_cnt,
-                               PV_NO_DUPLICATE | PV_NO_SCRATCH))
-           goto lossage;
-       }
-      else if (token != T_ID)
-       {
-         lex_error (NULL);
-         goto lossage;
-       }
-      else if (lex_id_match ("FILE", tokid) || lex_id_match ("TABLE", tokid))
-       {
-         struct mtf_file *file = xmalloc (sizeof *file);
-
-         file->in[0] = file->first[0] = file->last[0] = '\0';
-         file->dict = NULL;
-         file->by = NULL;
-         file->input = NULL;
-
-         if (lex_match_id ("FILE"))
-           file->type = MTF_FILE;
-         else if (lex_match_id ("TABLE"))
-           {
-             file->type = MTF_TABLE;
-             seen |= 4;
-           }
-         else
-           assert (0);
-
-         /* FILEs go first, then TABLEs. */
-         if (file->type == MTF_TABLE || first_table == NULL)
-           {
-             file->next = NULL;
-             file->prev = mtf.tail;
-             if (mtf.tail)
-               mtf.tail->next = file;
-             mtf.tail = file;
-             if (mtf.head == NULL)
-               mtf.head = file;
-             if (file->type == MTF_TABLE && first_table == NULL)
-               first_table = file;
-           }
-         else 
-           {
-             assert (file->type == MTF_FILE);
-             file->next = first_table;
-             file->prev = first_table->prev;
-             if (first_table->prev)
-               first_table->prev->next = file;
-             else
-               mtf.head = file;
-             first_table->prev = file;
-           }
-         
-         lex_match ('=');
-         
-         if (lex_match ('*'))
-           {
-             file->handle = NULL;
-
-             if (seen & 2)
-               {
-                 msg (SE, _("The active file may not be specified more "
-                            "than once."));
-                 goto lossage;
-               }
-             seen |= 2;
-
-             assert (pgm_state != STATE_INPUT);
-             if (pgm_state == STATE_INIT)
-               {
-                 msg (SE, _("Cannot specify the active file since no active "
-                            "file has been defined."));
-                 goto lossage;
-               }
-
-              if (temporary != 0)
-                {
-                  msg (SE,
-                       _("MATCH FILES may not be used after TEMPORARY when "
-                         "the active file is an input source.  "
-                         "Temporary transformations will be made permanent."));
-                  cancel_temporary (); 
-                }
-           }
-         else
-           {
-             file->handle = fh_parse_file_handle ();
-             if (!file->handle)
-               goto lossage;
-           }
+  lex_match ('/');
+  while (token == T_ID
+         && (lex_id_match ("FILE", tokid) || lex_id_match ("TABLE", tokid)))
+    {
+      struct mtf_file *file = xmalloc (sizeof *file);
 
-         if (file->handle)
-           {
-             file->dict = sfm_read_dictionary (file->handle, NULL);
-             if (!file->dict)
-               goto lossage;
-           }
-         else
-           file->dict = default_dict;
-         if (!mtf_merge_dictionary (mtf.dict, file))
-           goto lossage;
-       }
-      else if (lex_id_match ("IN", tokid)
-              || lex_id_match ("FIRST", tokid)
-              || lex_id_match ("LAST", tokid))
-       {
-         const char *sbc;
-         char *name;
-         
-         if (mtf.tail == NULL)
-           {
-             msg (SE, _("IN, FIRST, and LAST subcommands may not occur "
-                        "before the first FILE or TABLE."));
-             goto lossage;
-           }
+      if (lex_match_id ("FILE"))
+        file->type = MTF_FILE;
+      else if (lex_match_id ("TABLE"))
+        {
+          file->type = MTF_TABLE;
+          saw_table = true;
+        }
+      else
+        assert (0);
+      lex_match ('=');
+
+      file->by = NULL;
+      file->handle = NULL;
+      file->reader = NULL;
+      file->dict = NULL;
+      file->in_name = NULL;
+      file->in_var = NULL;
+      case_nullify (&file->input);
+
+      /* FILEs go first, then TABLEs. */
+      if (file->type == MTF_TABLE || first_table == NULL)
+        {
+          file->next = NULL;
+          file->prev = mtf.tail;
+          if (mtf.tail)
+            mtf.tail->next = file;
+          mtf.tail = file;
+          if (mtf.head == NULL)
+            mtf.head = file;
+          if (file->type == MTF_TABLE && first_table == NULL)
+            first_table = file;
+        }
+      else 
+        {
+          assert (file->type == MTF_FILE);
+          file->next = first_table;
+          file->prev = first_table->prev;
+          if (first_table->prev)
+            first_table->prev->next = file;
+          else
+            mtf.head = file;
+          first_table->prev = file;
+        }
 
-         if (lex_match_id ("IN"))
-           {
-             name = mtf.tail->in;
-             sbc = "IN";
-           }
-         else if (lex_match_id ("FIRST"))
-           {
-             name = mtf.tail->first;
-             sbc = "FIRST";
-           }
-         else if (lex_match_id ("LAST"))
-           {
-             name = mtf.tail->last;
-             sbc = "LAST";
-           }
-         else
-           assert (0);
+      if (lex_match ('*'))
+        {
+          file->handle = NULL;
+          file->reader = NULL;
+              
+          if (used_active_file)
+            {
+              msg (SE, _("The active file may not be specified more "
+                         "than once."));
+              goto error;
+            }
+          used_active_file = true;
+
+          assert (pgm_state != STATE_INPUT);
+          if (pgm_state == STATE_INIT)
+            {
+              msg (SE, _("Cannot specify the active file since no active "
+                         "file has been defined."));
+              goto error;
+            }
+
+          if (temporary != 0)
+            {
+              msg (SE,
+                   _("MATCH FILES may not be used after TEMPORARY when "
+                     "the active file is an input source.  "
+                     "Temporary transformations will be made permanent."));
+              cancel_temporary (); 
+            }
+
+          file->dict = default_dict;
+        }
+      else
+        {
+          file->handle = fh_parse ();
+          if (file->handle == NULL)
+            goto error;
 
-         lex_match ('=');
-         if (token != T_ID)
-           {
-             lex_error (NULL);
-             goto lossage;
-           }
+          file->reader = sfm_open_reader (file->handle, &file->dict, NULL);
+          if (file->reader == NULL)
+            goto error;
 
-         if (*name)
-           {
-             msg (SE, _("Multiple %s subcommands for a single FILE or "
-                        "TABLE."),
-                  sbc);
-             goto lossage;
-           }
-         strcpy (name, tokid);
-         lex_get ();
+          case_create (&file->input, dict_get_next_value_idx (file->dict));
+        }
 
-         if (!dict_create_var (mtf.dict, name, 0))
-           {
-             msg (SE, _("Duplicate variable name %s while creating %s "
-                        "variable."),
-                  name, sbc);
-             goto lossage;
-           }
-       }
-      else if (lex_id_match ("RENAME", tokid)
-              || lex_id_match ("KEEP", tokid)
-              || lex_id_match ("DROP", tokid))
+      while (lex_match ('/'))
+        if (lex_match_id ("RENAME")) 
+          {
+            if (!rename_variables (file->dict))
+              goto error; 
+          }
+        else if (lex_match_id ("IN"))
+          {
+            lex_match ('=');
+            if (token != T_ID)
+              {
+                lex_error (NULL);
+                goto error;
+              }
+
+            if (file->in_name != NULL)
+              {
+                msg (SE, _("Multiple IN subcommands for a single FILE or "
+                           "TABLE."));
+                goto error;
+              }
+            file->in_name = xstrdup (tokid);
+            lex_get ();
+            saw_in = true;
+          }
+
+      mtf_merge_dictionary (mtf.dict, file);
+    }
+  
+  while (token != '.')
+    {
+      if (lex_match (T_BY))
        {
-         int options = GTSV_OPT_MATCH_FILES;
-         
-         if (mtf.tail == NULL)
+          struct variable **by;
+          
+         if (mtf.by_cnt)
            {
-             msg (SE, _("RENAME, KEEP, and DROP subcommands may not occur "
-                        "before the first FILE or TABLE."));
-             goto lossage;
+             msg (SE, _("BY may appear at most once."));
+             goto error;
            }
+             
+         lex_match ('=');
+         if (!parse_variables (mtf.dict, &by, &mtf.by_cnt,
+                               PV_NO_DUPLICATE | PV_NO_SCRATCH))
+           goto error;
+
+          for (iter = mtf.head; iter != NULL; iter = iter->next)
+            {
+              int i;
+         
+              iter->by = xmalloc (sizeof *iter->by * mtf.by_cnt);
 
-         if (!trim_dictionary (mtf.tail->dict, &options))
-           goto lossage;
+              for (i = 0; i < mtf.by_cnt; i++)
+                {
+                  iter->by[i] = dict_lookup_var (iter->dict, by[i]->name);
+                  if (iter->by[i] == NULL)
+                    {
+                      msg (SE, _("File %s lacks BY variable %s."),
+                           iter->handle ? handle_get_name (iter->handle) : "*",
+                           by[i]->name);
+                      free (by);
+                      goto error;
+                    }
+                }
+            }
+          free (by);
        }
+      else if (lex_match_id ("FIRST")) 
+        {
+          if (mtf.first[0] != '\0')
+            {
+              msg (SE, _("FIRST may appear at most once."));
+              goto error;
+            }
+             
+         lex_match ('=');
+          if (!lex_force_id ())
+            goto error;
+          strcpy (mtf.first, tokid);
+          lex_get ();
+        }
+      else if (lex_match_id ("LAST")) 
+        {
+          if (mtf.last[0] != '\0')
+            {
+              msg (SE, _("LAST may appear at most once."));
+              goto error;
+            }
+             
+         lex_match ('=');
+          if (!lex_force_id ())
+            goto error;
+          strcpy (mtf.last, tokid);
+          lex_get ();
+        }
       else if (lex_match_id ("MAP"))
        {
          /* FIXME. */
        }
+      else if (lex_match_id ("DROP")) 
+        {
+          if (!drop_variables (mtf.dict))
+            goto error;
+        }
+      else if (lex_match_id ("KEEP")) 
+        {
+          if (!keep_variables (mtf.dict))
+            goto error;
+        }
       else
        {
          lex_error (NULL);
-         goto lossage;
+         goto error;
        }
+
+      if (!lex_match ('/') && token != '.') 
+        {
+          lex_end_of_command ();
+          goto error;
+        }
     }
-  while (token != '.');
 
-  if (seen & 4)
+  if (mtf.by_cnt == 0)
     {
-      if (!(seen & 1))
-       {
-         msg (SE, _("The BY subcommand is required when a TABLE subcommand "
-                    "is given."));
-         goto lossage;
-       }
+      if (saw_table)
+        {
+          msg (SE, _("BY is required when TABLE is specified."));
+          goto error;
+        }
+      if (saw_in)
+        {
+          msg (SE, _("BY is required when IN is specified."));
+          goto error;
+        }
     }
 
-  if (seen & 1)
+  /* Set up mapping from each file's variables to master
+     variables. */
+  for (iter = mtf.head; iter != NULL; iter = iter->next)
     {
-      struct mtf_file *iter;
-
-      for (iter = mtf.head; iter; iter = iter->next)
-       {
-         int i;
-         
-         iter->by = xmalloc (sizeof *iter->by * mtf.by_cnt);
+      struct dictionary *d = iter->dict;
+      int i;
 
-         for (i = 0; i < mtf.by_cnt; i++)
-           {
-             iter->by[i] = dict_lookup_var (iter->dict, mtf.by[i]->name);
-             if (iter->by[i] == NULL)
-               {
-                 msg (SE, _("File %s lacks BY variable %s."),
-                      iter->handle ? fh_handle_name (iter->handle) : "*",
-                      mtf.by[i]->name);
-                 goto lossage;
-               }
-           }
-       }
+      for (i = 0; i < dict_get_var_cnt (d); i++)
+        {
+          struct variable *v = dict_get_var (d, i);
+          struct variable *mv = dict_lookup_var (mtf.dict, v->name);
+          if (mv != NULL)
+            set_master (v, mv);
+        }
     }
 
-#if DEBUGGING
-  {
-    /* From sfm-read.c. */
-    extern void dump_dictionary (struct dictionary *);
-
-    dump_dictionary (mtf.dict);
-  }
-#endif
-
+  /* Add IN variables to master dictionary. */
+  for (iter = mtf.head; iter != NULL; iter = iter->next) 
+    if (iter->in_name != NULL)
+      {
+        iter->in_var = dict_create_var (mtf.dict, iter->in_name, 0);
+        if (iter->in_var == NULL)
+          {
+            msg (SE, _("IN variable name %s duplicates an "
+                       "existing variable name."),
+                 iter->in_var->name);
+            goto error;
+          }
+        iter->in_var->print = iter->in_var->write
+          = make_output_format (FMT_F, 1, 0);
+      }
+    
   /* MATCH FILES performs an n-way merge on all its input files.
      Abstract algorithm:
 
@@ -850,49 +1000,50 @@ cmd_match_files (void)
 
      2. If no FILEs are left, stop.  Otherwise, proceed to step 3.
 
-     3. Find the FILE input record with minimum BY values.  Store all
-     the values from this input record into the output record.
-
-     4. Find all the FILE input records with BY values identical to
-     the minimums.  Store all the values from these input records into
+     3. Find the FILE input record(s) that have minimum BY
+     values.  Store all the values from these input records into
      the output record.
 
-     5. For every TABLE, read another record as long as the BY values
+     4. For every TABLE, read another record as long as the BY values
      on the TABLE's input record are less than the FILEs' BY values.
      If an exact match is found, store all the values from the TABLE
      input record into the output record.
 
-     6. Write the output record.
+     5. Write the output record.
 
-     7. Read another record from each input file FILE and TABLE that
+     6. Read another record from each input file FILE and TABLE that
      we stored values from above.  If we come to the end of one of the
      input files, remove it from the list of input files.
 
-     8. Repeat from step 2.
+     7. Repeat from step 2.
 
-     Unfortunately, this algorithm can't be directly implemented
-     because there's no function to read a record from the active
-     file; instead, it has to be done using callbacks.
+     Unfortunately, this algorithm can't be implemented in a
+     straightforward way because there's no function to read a
+     record from the active file.  Instead, it has to be written
+     as a state machine.
 
      FIXME: For merging large numbers of files (more than 10?) a
      better algorithm would use a heap for finding minimum
      values. */
 
-  if (!(seen & 2))
+  if (!used_active_file)
     discard_variables ();
 
+  dict_compact_values (mtf.dict);
   mtf.sink = create_case_sink (&storage_sink_class, mtf.dict, NULL);
+  if (mtf.sink->class->open != NULL)
+    mtf.sink->class->open (mtf.sink);
+
+  mtf.seq_nums = xcalloc (dict_get_var_cnt (mtf.dict), sizeof *mtf.seq_nums);
+  case_create (&mtf.mtf_case, dict_get_next_value_idx (mtf.dict));
 
-  mtf.seq_nums = xmalloc (dict_get_var_cnt (mtf.dict)
-                          * sizeof *mtf.seq_nums);
-  memset (mtf.seq_nums, 0,
-          dict_get_var_cnt (mtf.dict) * sizeof *mtf.seq_nums);
-  mtf.mtf_case = xmalloc (dict_get_case_size (mtf.dict));
+  mtf_read_nonactive_records (&mtf);
+  if (used_active_file)
+    procedure (mtf_processing, &mtf);
+  mtf_processing_finish (&mtf);
 
-  mtf_read_nonactive_records (NULL);
-  if (seen & 2)
-    procedure (mtf_processing, NULL);
-  mtf_processing_finish (NULL);
+  free_case_source (vfm_source);
+  vfm_source = NULL;
 
   dict_destroy (default_dict);
   default_dict = mtf.dict;
@@ -903,12 +1054,12 @@ cmd_match_files (void)
   mtf_free (&mtf);
   return CMD_SUCCESS;
   
-lossage:
+error:
   mtf_free (&mtf);
   return CMD_FAILURE;
 }
 
-/* Repeats 2...8 an arbitrary number of times. */
+/* Repeats 2...7 an arbitrary number of times. */
 static void
 mtf_processing_finish (void *mtf_)
 {
@@ -954,12 +1105,12 @@ var_type_description (struct variable *v)
 static void
 mtf_free_file (struct mtf_file *file)
 {
-  fh_close_handle (file->handle);
-  if (file->dict != NULL && file->dict != default_dict)
-    dict_destroy (file->dict);
   free (file->by);
-  if (file->handle)
-    free (file->input);
+  sfm_close_reader (file->reader);
+  if (file->dict != default_dict)
+    dict_destroy (file->dict);
+  case_destroy (&file->input);
+  free (file->in_name);
   free (file);
 }
 
@@ -972,13 +1123,12 @@ mtf_free (struct mtf_proc *mtf)
   for (iter = mtf->head; iter; iter = next)
     {
       next = iter->next;
-
       mtf_free_file (iter);
     }
   
-  free (mtf->by);
   if (mtf->dict)
     dict_destroy (mtf->dict);
+  case_destroy (&mtf->mtf_case);
   free (mtf->seq_nums);
 }
 
@@ -988,6 +1138,7 @@ static void
 mtf_delete_file_in_place (struct mtf_proc *mtf, struct mtf_file **file)
 {
   struct mtf_file *f = *file;
+  int i;
 
   if (f->prev)
     f->prev->next = f->next;
@@ -999,44 +1150,38 @@ mtf_delete_file_in_place (struct mtf_proc *mtf, struct mtf_file **file)
     mtf->tail = f->prev;
   *file = f->next;
 
-  {
-    int i;
-
-    for (i = 0; i < dict_get_var_cnt (f->dict); i++)
-      {
-       struct variable *v = dict_get_var (f->dict, i);
+  if (f->in_var != NULL)
+    case_data_rw (&mtf->mtf_case, f->in_var->fv)->f = 0.;
+  for (i = 0; i < dict_get_var_cnt (f->dict); i++)
+    {
+      struct variable *v = dict_get_var (f->dict, i);
+      struct variable *mv = get_master (v);
+      if (mv != NULL) 
+        {
+          union value *out = case_data_rw (&mtf->mtf_case, mv->fv);
          
-       if (v->type == NUMERIC)
-         mtf->mtf_case->data[v->p.mtf.master->fv].f = SYSMIS;
-       else
-         memset (mtf->mtf_case->data[v->p.mtf.master->fv].s, ' ', v->width);
-      }
-  }
+          if (v->type == NUMERIC)
+            out->f = SYSMIS;
+          else
+            memset (out->s, ' ', v->width);
+        } 
+    }
 
   mtf_free_file (f);
 }
 
 /* Read a record from every input file except the active file. */
 static void
-mtf_read_nonactive_records (void *mtf_ UNUSED)
+mtf_read_nonactive_records (void *mtf_)
 {
   struct mtf_proc *mtf = mtf_;
-  struct mtf_file *iter;
+  struct mtf_file *iter, *next;
 
-  for (iter = mtf->head; iter)
+  for (iter = mtf->head; iter != NULL; iter = next)
     {
-      if (iter->handle)
-       {
-         assert (iter->input == NULL);
-         iter->input = xmalloc (dict_get_case_size (iter->dict));
-         
-         if (!sfm_read_case (iter->handle, iter->input, iter->dict))
-           mtf_delete_file_in_place (mtf, &iter);
-         else
-           iter = iter->next;
-       }
-      else
-        iter = iter->next;
+      next = iter->next;
+      if (iter->handle && !sfm_read_case (iter->reader, &iter->input))
+        mtf_delete_file_in_place (mtf, &iter);
     }
 }
 
@@ -1047,148 +1192,103 @@ mtf_compare_BY_values (struct mtf_proc *mtf,
                        struct mtf_file *a, struct mtf_file *b,
                        struct ccase *c)
 {
-  union value *a_input, *b_input;
-  int i;
-
+  struct ccase *ca = case_is_null (&a->input) ? c : &a->input;
+  struct ccase *cb = case_is_null (&b->input) ? c : &b->input;
   assert ((a == NULL) + (b == NULL) + (c == NULL) <= 1);
-  a_input = a->input != NULL ? a->input : c->data;
-  b_input = b->input != NULL ? b->input : c->data;
-  for (i = 0; i < mtf->by_cnt; i++)
-    {
-      assert (a->by[i]->type == b->by[i]->type);
-      assert (a->by[i]->width == b->by[i]->width);
-      
-      if (a->by[i]->type == NUMERIC)
-       {
-         double af = a_input[a->by[i]->fv].f;
-         double bf = b_input[b->by[i]->fv].f;
-
-         if (af < bf)
-           return -1;
-         else if (af > bf)
-           return 1;
-       }
-      else 
-       {
-         int result;
-         
-         assert (a->by[i]->type == ALPHA);
-         result = memcmp (a_input[a->by[i]->fv].s,
-                          b_input[b->by[i]->fv].s,
-                          a->by[i]->width);
-         if (result < 0)
-           return -1;
-         else if (result > 0)
-           return 1;
-       }
-    }
-  return 0;
+  return case_compare_2dict (ca, cb, a->by, b->by, mtf->by_cnt);
 }
 
 /* Perform one iteration of steps 3...7 above. */
 static int
-mtf_processing (struct ccase *c, void *mtf_ UNUSED)
+mtf_processing (struct ccase *c, void *mtf_)
 {
   struct mtf_proc *mtf = mtf_;
-  struct mtf_file *min_head, *min_tail; /* Files with minimum BY values. */
-  struct mtf_file *max_head, *max_tail; /* Files with non-minimum BY values. */
-  struct mtf_file *iter;                /* Iterator. */
 
-  for (;;)
+  /* Do we need another record from the active file? */
+  bool read_active_file;
+
+  assert (mtf->head != NULL);
+  if (mtf->head->type == MTF_TABLE)
+    return 1;
+  
+  do
     {
-      /* If the active file doesn't have the minimum BY values, don't
-        return because that would cause a record to be skipped. */
-      int advance = 1;
+      struct mtf_file *min_head, *min_tail; /* Files with minimum BY values. */
+      struct mtf_file *max_head, *max_tail; /* Files with non-minimum BYs. */
+      struct mtf_file *iter, *next;
 
-      if (mtf->head->type == MTF_TABLE)
-       return 0;
+      read_active_file = false;
       
-      /* 3. Find the FILE input record with minimum BY values.  Store
-        all the values from this input record into the output record.
-
-        4. Find all the FILE input records with BY values identical
-        to the minimums.  Store all the values from these input
-        records into the output record. */
+      /* 3. Find the FILE input record(s) that have minimum BY
+         values.  Store all the values from these input records into
+         the output record. */
       min_head = min_tail = mtf->head;
       max_head = max_tail = NULL;
       for (iter = mtf->head->next; iter && iter->type == MTF_FILE;
-          iter = iter->next)
-       switch (mtf_compare_BY_values (mtf, min_head, iter, c))
-         {
-         case -1:
-           if (max_head)
-             max_tail = max_tail->next_min = iter;
-           else
-             max_head = max_tail = iter;
-           break;
-
-         case 0:
+          iter = iter->next) 
+        {
+          int cmp = mtf_compare_BY_values (mtf, min_head, iter, c);
+          if (cmp < 0) 
+            {
+              if (max_head)
+                max_tail = max_tail->next_min = iter;
+              else
+                max_head = max_tail = iter;
+            }
+          else if (cmp == 0) 
            min_tail = min_tail->next_min = iter;
-           break;
-
-         case 1:
-           if (max_head)
-             {
-               max_tail->next_min = min_head;
-               max_tail = min_tail;
-             }
-           else
-             {
-               max_head = min_head;
-               max_tail = min_tail;
-             }
-           min_head = min_tail = iter;
-           break;
-
-         default:
-           assert (0);
-         }
-
-      /* 5. For every TABLE, read another record as long as the BY
+          else /* cmp > 0 */
+            {
+              if (max_head)
+                {
+                  max_tail->next_min = min_head;
+                  max_tail = min_tail;
+                }
+              else
+                {
+                  max_head = min_head;
+                  max_tail = min_tail;
+                }
+              min_head = min_tail = iter;
+            }
+        }
+      
+      /* 4. For every TABLE, read another record as long as the BY
         values on the TABLE's input record are less than the FILEs'
         BY values.  If an exact match is found, store all the values
         from the TABLE input record into the output record. */
-      while (iter)
+      for (; iter != NULL; iter = next)
        {
-         struct mtf_file *next = iter->next;
-         
          assert (iter->type == MTF_TABLE);
       
-         if (iter->handle == NULL)
-           advance = 0;
-
-       again:
-         switch (mtf_compare_BY_values (mtf, min_head, iter, c))
-           {
-           case -1:
-             if (max_head)
-               max_tail = max_tail->next_min = iter;
-             else
-               max_head = max_tail = iter;
-             break;
-
-           case 0:
-             min_tail = min_tail->next_min = iter;
-             break;
-
-           case 1:
-             if (iter->handle == NULL)
-               return 1;
-             if (sfm_read_case (iter->handle, iter->input, iter->dict))
-               goto again;
-             mtf_delete_file_in_place (mtf, &iter);
-             break;
-
-           default:
-             assert (0);
-           }
-
-         iter = next;
+         next = iter->next;
+          for (;;) 
+            {
+              int cmp = mtf_compare_BY_values (mtf, min_head, iter, c);
+              if (cmp < 0) 
+                {
+                  if (max_head)
+                    max_tail = max_tail->next_min = iter;
+                  else
+                    max_head = max_tail = iter;
+                }
+              else if (cmp == 0)
+                min_tail = min_tail->next_min = iter;
+              else /* cmp > 0 */
+                {
+                  if (iter->handle == NULL)
+                    return 1;
+                  if (sfm_read_case (iter->reader, &iter->input))
+                    continue;
+                  mtf_delete_file_in_place (mtf, &iter);
+                }
+              break;
+            }
        }
 
       /* Next sequence number. */
       mtf->seq_num++;
-  
+
       /* Store data to all the records we are using. */
       if (min_tail)
        min_tail->next_min = NULL;
@@ -1199,24 +1299,30 @@ mtf_processing (struct ccase *c, void *mtf_ UNUSED)
          for (i = 0; i < dict_get_var_cnt (iter->dict); i++)
            {
              struct variable *v = dict_get_var (iter->dict, i);
-              union value *record;
+              struct variable *mv = get_master (v);
          
-             if (mtf->seq_nums[v->p.mtf.master->index] == mtf->seq_num)
-               continue;
-              mtf->seq_nums[v->p.mtf.master->index] = mtf->seq_num;
-
-              record = iter->input != NULL ? iter->input : c->data;
-
-              assert (v->type == NUMERIC || v->type == ALPHA);
-             if (v->type == NUMERIC)
-               mtf->mtf_case->data[v->p.mtf.master->fv].f = record[v->fv].f;
-             else
-                memcpy (mtf->mtf_case->data[v->p.mtf.master->fv].s,
-                        record[v->fv].s, v->width);
-           }
+             if (mv != NULL && mtf->seq_nums[mv->index] != mtf->seq_num) 
+                {
+                  struct ccase *record
+                    = case_is_null (&iter->input) ? c : &iter->input;
+                  union value *out = case_data_rw (&mtf->mtf_case, mv->fv);
+
+                  mtf->seq_nums[mv->index] = mtf->seq_num;
+                  if (v->type == NUMERIC)
+                    out->f = case_num (record, v->fv);
+                  else
+                    memcpy (out->s, case_str (record, v->fv), v->width);
+                } 
+            }
+          if (iter->in_var != NULL)
+            case_data_rw (&mtf->mtf_case, iter->in_var->fv)->f = 1.;
+
+          if (iter->type == MTF_FILE && iter->handle == NULL)
+            read_active_file = true;
        }
 
-      /* Store missing values to all the records we're not using. */
+      /* Store missing values to all the records we're not
+         using. */
       if (max_tail)
        max_tail->next_min = NULL;
       for (iter = max_head; iter; iter = iter->next_min)
@@ -1226,64 +1332,51 @@ mtf_processing (struct ccase *c, void *mtf_ UNUSED)
          for (i = 0; i < dict_get_var_cnt (iter->dict); i++)
            {
              struct variable *v = dict_get_var (iter->dict, i);
-         
-             if (mtf->seq_nums[v->p.mtf.master->index] == mtf->seq_num)
-               continue;
-              mtf->seq_nums[v->p.mtf.master->index] = mtf->seq_num;
-
-#if 0
-             printf ("%s/%s: dest-fv=%d\n",
-                     fh_handle_name (iter->handle),
-                     v->name,
-                     v->p.mtf.master->fv);
-#endif
-             if (v->type == NUMERIC)
-               mtf->mtf_case->data[v->p.mtf.master->fv].f = SYSMIS;
-             else
-                memset (mtf->mtf_case->data[v->p.mtf.master->fv].s, ' ',
-                        v->width);
-           }
+              struct variable *mv = get_master (v);
+
+             if (mv != NULL && mtf->seq_nums[mv->index] != mtf->seq_num) 
+                {
+                  union value *out = case_data_rw (&mtf->mtf_case, mv->fv);
+                  mtf->seq_nums[mv->index] = mtf->seq_num;
 
-         if (iter->handle == NULL)
-           advance = 0;
+                  if (v->type == NUMERIC)
+                    out->f = SYSMIS;
+                  else
+                    memset (out->s, ' ', v->width);
+                }
+            }
+          if (iter->in_var != NULL)
+            case_data_rw (&mtf->mtf_case, iter->in_var->fv)->f = 0.;
        }
 
-      /* 6. Write the output record. */
-      mtf->sink->class->write (mtf->sink, mtf->mtf_case);
+      /* 5. Write the output record. */
+      mtf->sink->class->write (mtf->sink, &mtf->mtf_case);
 
-      /* 7. Read another record from each input file FILE and TABLE
+      /* 6. Read another record from each input file FILE and TABLE
         that we stored values from above.  If we come to the end of
         one of the input files, remove it from the list of input
         files. */
-      for (iter = min_head; iter && iter->type == MTF_FILE; )
+      for (iter = min_head; iter && iter->type == MTF_FILE; iter = next)
        {
-         struct mtf_file *next = iter->next_min;
-         
-         if (iter->handle)
-           {
-             assert (iter->input != NULL);
-
-             if (!sfm_read_case (iter->handle, iter->input, iter->dict))
-               mtf_delete_file_in_place (mtf, &iter);
-           }
-
-         iter = next;
+         next = iter->next_min;
+         if (iter->reader != NULL
+              && !sfm_read_case (iter->reader, &iter->input))
+            mtf_delete_file_in_place (mtf, &iter);
        }
-      
-      if (advance)
-       break;
     }
+  while (!read_active_file
+         && mtf->head != NULL && mtf->head->type == MTF_FILE);
 
-  return (mtf->head && mtf->head->type != MTF_TABLE);
+  return mtf->head != NULL && mtf->head->type == MTF_FILE;
 }
 
-/* Merge the dictionary for file F into the master dictionary
-   mtf_dict. */
+/* Merge the dictionary for file F into master dictionary M. */
 static int
 mtf_merge_dictionary (struct dictionary *const m, struct mtf_file *f)
 {
   struct dictionary *d = f->dict;
   const char *d_docs, *m_docs;
+  int i;
 
   if (dict_get_label (m) == NULL)
     dict_set_label (m, dict_get_label (d));
@@ -1308,63 +1401,86 @@ mtf_merge_dictionary (struct dictionary *const m, struct mtf_file *f)
         }
     }
   
-  dict_compact_values (d);
+  for (i = 0; i < dict_get_var_cnt (d); i++)
+    {
+      struct variable *dv = dict_get_var (d, i);
+      struct variable *mv = dict_lookup_var (m, dv->name);
 
-  {
-    int i;
+      if (dict_class_from_id (dv->name) == DC_SCRATCH)
+        continue;
 
-    for (i = 0; i < dict_get_var_cnt (d); i++)
-      {
-       struct variable *dv = dict_get_var (d, i);
-       struct variable *mv = dict_lookup_var (m, dv->name);
-
-       assert (dv->type == ALPHA || dv->width == 0);
-       assert (!mv || mv->type == ALPHA || mv->width == 0);
-       if (mv && dv->width == mv->width)
-         {
-           if (val_labs_count (dv->val_labs)
-                && !val_labs_count (mv->val_labs))
-             mv->val_labs = val_labs_copy (dv->val_labs);
-           if (dv->miss_type != MISSING_NONE
-                && mv->miss_type == MISSING_NONE)
-             copy_missing_values (mv, dv);
-         }
-       if (mv && dv->label && !mv->label)
-         mv->label = xstrdup (dv->label);
-       if (!mv) 
-          {
-            mv = dict_clone_var (m, dv, dv->name);
-            assert (mv != NULL);
-          }
-       else if (mv->width != dv->width)
-         {
-           msg (SE, _("Variable %s in file %s (%s) has different "
-                      "type or width from the same variable in "
-                      "earlier file (%s)."),
-                dv->name, fh_handle_name (f->handle),
-                var_type_description (dv), var_type_description (mv));
-           return 0;
-         }
-       dv->p.mtf.master = mv;
-      }
-  }
+      if (mv != NULL)
+        {
+          if (mv->width != dv->width) 
+            {
+              msg (SE, _("Variable %s in file %s (%s) has different "
+                         "type or width from the same variable in "
+                         "earlier file (%s)."),
+                   dv->name, handle_get_name (f->handle),
+                   var_type_description (dv), var_type_description (mv));
+              return 0;
+            }
+        
+          if (dv->width == mv->width)
+            {
+              if (val_labs_count (dv->val_labs)
+                  && !val_labs_count (mv->val_labs))
+                mv->val_labs = val_labs_copy (dv->val_labs);
+              if (dv->miss_type != MISSING_NONE
+                  && mv->miss_type == MISSING_NONE)
+                copy_missing_values (mv, dv);
+            }
+
+          if (dv->label && !mv->label)
+            mv->label = xstrdup (dv->label);
+        }
+      else
+        mv = dict_clone_var_assert (m, dv, dv->name);
+    }
 
   return 1;
 }
+
+/* Marks V's master variable as MASTER. */
+static void
+set_master (struct variable *v, struct variable *master) 
+{
+  var_attach_aux (v, master, NULL);
+}
+
+/* Returns the master variable corresponding to V,
+   as set with set_master(). */
+static struct variable *
+get_master (struct variable *v) 
+{
+  return v->aux;
+}
 \f
 /* IMPORT command. */
 
+/* IMPORT input program. */
+struct import_pgm 
+  {
+    struct pfm_reader *reader;  /* Portable file reader. */
+    struct case_map *map;       /* Map from system file to active file dict. */
+    struct ccase bounce;        /* Bounce buffer. */
+  };
+
+static void import_pgm_free (struct import_pgm *);
+
 /* Parses the IMPORT command. */
 int
 cmd_import (void)
 {
-  struct file_handle *handle = NULL;
-  struct dictionary *dict;
-  struct get_pgm *pgm;
-  int options = GTSV_OPT_NONE;
+  struct import_pgm *pgm = NULL;
+  struct file_handle *fh = NULL;
+  struct dictionary *dict = NULL;
   int type;
 
-  lex_match_id ("IMPORT");
+  pgm = xmalloc (sizeof *pgm);
+  pgm->reader = NULL;
+  pgm->map = NULL;
+  case_nullify (&pgm->bounce);
 
   for (;;)
     {
@@ -1374,8 +1490,8 @@ cmd_import (void)
        {
          lex_match ('=');
 
-         handle = fh_parse_file_handle ();
-         if (handle == NULL)
+         fh = fh_parse ();
+         if (fh == NULL)
            return CMD_FAILURE;
        }
       else if (lex_match_id ("TYPE"))
@@ -1402,58 +1518,76 @@ cmd_import (void)
 
   discard_variables ();
 
-  dict = pfm_read_dictionary (handle, NULL);
-  if (dict == NULL)
+  pgm->reader = pfm_open_reader (fh, &dict, NULL);
+  if (pgm->reader == NULL)
     return CMD_FAILURE;
+  case_create (&pgm->bounce, dict_get_next_value_idx (dict));
+  
+  start_case_map (dict);
+  if (!trim_dictionary (dict, OP_READ, NULL))
+    goto error;
+  pgm->map = finish_case_map (dict);
+  
+  dict_destroy (default_dict);
+  default_dict = dict;
 
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
-  if (0 == trim_dictionary (dict, &options))
-    {
-      fh_close_handle (handle);
-      return CMD_FAILURE;
-    }
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
+  vfm_source = create_case_source (&import_source_class, pgm);
 
-  dict_compact_values (dict);
+  return CMD_SUCCESS;
 
-#if DEBUGGING
-  printf (_("IMPORT translation table from file to memory:\n"));
-  for (i = 0; i < dict->nvar; i++)
-    {
-      struct variable *v = dict->var[i];
+ error:
+  import_pgm_free (pgm);
+  if (dict != NULL)
+    dict_destroy (dict);
+  return CMD_FAILURE;
+}
 
-      printf (_("  %8s from %3d,%3d to %3d,%3d\n"), v->name,
-             v->get.fv, v->get.nv, v->fv, v->nv);
+/* Frees a struct import_pgm. */
+static void
+import_pgm_free (struct import_pgm *pgm) 
+{
+  if (pgm != NULL) 
+    {
+      pfm_close_reader (pgm->reader);
+      destroy_case_map (pgm->map);
+      case_destroy (&pgm->bounce);
+      free (pgm);
     }
-#endif
-
-  dict_destroy (default_dict);
-  default_dict = dict;
-
-  pgm = xmalloc (sizeof *pgm);
-  pgm->handle = handle;
-  pgm->case_size = dict_get_case_size (default_dict);
-  vfm_source = create_case_source (&import_source_class, default_dict, pgm);
+}
 
-  return CMD_SUCCESS;
+/* Clears internal state related to IMPORT input procedure. */
+static void
+import_source_destroy (struct case_source *source)
+{
+  struct import_pgm *pgm = source->aux;
+  import_pgm_free (pgm);
 }
 
-/* Reads all the cases from the data file and passes them to
-   write_case(). */
+/* Reads all the cases from the data file into C and passes them
+   to WRITE_CASE one by one, passing WC_DATA. */
 static void
 import_source_read (struct case_source *source,
-                    struct ccase *c,
-                    write_case_func *write_case, write_case_data wc_data)
+                 struct ccase *c,
+                 write_case_func *write_case, write_case_data wc_data)
 {
-  struct get_pgm *pgm = source->aux;
-  
-  while (pfm_read_case (pgm->handle, c->data, default_dict))
-    if (!write_case (wc_data))
-      break;
+  struct import_pgm *pgm = source->aux;
+  int ok;
+
+  do
+    {
+      if (pgm->map == NULL)
+        ok = pfm_read_case (pgm->reader, c);
+      else
+        {
+          ok = pfm_read_case (pgm->reader, &pgm->bounce);
+          if (ok)
+            map_case (pgm->map, &pgm->bounce, c);
+        }
+
+      if (ok)
+        ok = write_case (wc_data);
+    }
+  while (ok);
 }
 
 const struct case_source_class import_source_class =
@@ -1461,94 +1595,129 @@ const struct case_source_class import_source_class =
     "IMPORT",
     NULL,
     import_source_read,
-    get_source_destroy,
+    import_source_destroy,
   };
-\f
-static int export_write_case_func (struct ccase *c, void *);
-     
-/* Parses the EXPORT command.  */
-/* FIXME: same as cmd_save_internal(). */
-int
-cmd_export (void)
-{
-  struct file_handle *handle;
-  struct dictionary *dict;
-  int options = GTSV_OPT_SAVE;
 
-  struct save_trns *t;
-
-  int i;
+\f
+/* Case map.
 
-  lex_match_id ("EXPORT");
+   A case map copies data from a case that corresponds for one
+   dictionary to a case that corresponds to a second dictionary
+   derived from the first by, optionally, deleting, reordering,
+   or renaming variables.  (No new variables may be created.)
+   */
 
-  lex_match ('/');
-  if (lex_match_id ("OUTFILE"))
-    lex_match ('=');
+/* A case map. */
+struct case_map
+  {
+    size_t value_cnt;   /* Number of values in map. */
+    int *map;           /* For each destination index, the
+                           corresponding source index. */
+  };
 
-  handle = fh_parse_file_handle ();
-  if (handle == NULL)
-    return CMD_FAILURE;
+/* Prepares dictionary D for producing a case map.  Afterward,
+   the caller may delete, reorder, or rename variables within D
+   at will before using finish_case_map() to produce the case
+   map.
 
-  dict = dict_clone (default_dict);
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
-  for (i = 0; i < dict_get_var_cnt (dict); i++)
-    dict_get_var (dict, i)->aux = dict_get_var (default_dict, i);
-  if (0 == trim_dictionary (dict, &options))
+   Uses D's aux members, which may not otherwise be in use. */
+static void
+start_case_map (struct dictionary *d) 
+{
+  size_t var_cnt = dict_get_var_cnt (d);
+  size_t i;
+  
+  for (i = 0; i < var_cnt; i++)
     {
-      fh_close_handle (handle);
-      return CMD_FAILURE;
+      struct variable *v = dict_get_var (d, i);
+      int *src_fv = xmalloc (sizeof *src_fv);
+      *src_fv = v->fv;
+      var_attach_aux (v, src_fv, var_dtor_free);
     }
+}
+
+/* Produces a case map from dictionary D, which must have been
+   previously prepared with start_case_map().
 
-#if DEBUGGING
-  dump_dict_variables (dict);
-#endif
+   Does not retain any reference to D, and clears the aux members
+   set up by start_case_map().
 
-  /* Write dictionary. */
-  if (!pfm_write_dictionary (handle, dict))
+   Returns the new case map, or a null pointer if no mapping is
+   required (that is, no data has changed position). */
+static struct case_map *
+finish_case_map (struct dictionary *d) 
+{
+  struct case_map *map;
+  size_t var_cnt = dict_get_var_cnt (d);
+  size_t i;
+  int identity_map;
+
+  map = xmalloc (sizeof *map);
+  map->value_cnt = dict_get_next_value_idx (d);
+  map->map = xmalloc (sizeof *map->map * map->value_cnt);
+  for (i = 0; i < map->value_cnt; i++)
+    map->map[i] = -1;
+
+  identity_map = 1;
+  for (i = 0; i < var_cnt; i++) 
     {
-      dict_destroy (dict);
-      fh_close_handle (handle);
-      return CMD_FAILURE;
+      struct variable *v = dict_get_var (d, i);
+      int *src_fv = (int *) var_detach_aux (v);
+      size_t idx;
+
+      if (v->fv != *src_fv)
+        identity_map = 0;
+      
+      for (idx = 0; idx < v->nv; idx++)
+        {
+          int src_idx = *src_fv + idx;
+          int dst_idx = v->fv + idx;
+          
+          assert (map->map[dst_idx] == -1);
+          map->map[dst_idx] = src_idx;
+        }
+      free (src_fv);
     }
 
-  /* Fill in transformation structure. */
-  t = xmalloc (sizeof *t);
-  t->h.proc = save_trns_proc;
-  t->h.free = save_trns_free;
-  t->f = handle;
-  t->nvar = dict_get_var_cnt (dict);
-  t->var = xmalloc (sizeof *t->var * t->nvar);
-  for (i = 0; i < t->nvar; i++)
-    t->var[i] = dict_get_var (dict, i)->aux;
-  t->case_buf = xmalloc (sizeof *t->case_buf * t->nvar);
-  dict_destroy (dict);
+  if (identity_map) 
+    {
+      destroy_case_map (map);
+      return NULL;
+    }
 
-  procedure (export_write_case_func, t);
-  save_trns_free (&t->h);
+  while (map->value_cnt > 0 && map->map[map->value_cnt - 1] == -1)
+    map->value_cnt--;
 
-  return CMD_SUCCESS;
+  return map;
 }
 
-/* Writes case C to the EXPORT file. */
-static int
-export_write_case_func (struct ccase *c, void *aux)
+/* Maps from SRC to DST, applying case map MAP. */
+static void
+map_case (const struct case_map *map,
+          const struct ccase *src, struct ccase *dst) 
 {
-  struct save_trns *t = aux;
-  union value *p = (union value *) t->case_buf;
-  int i;
+  size_t dst_idx;
 
-  for (i = 0; i < t->nvar; i++)
-    {
-      struct variable *v = t->var[i];
+  assert (map != NULL);
+  assert (src != NULL);
+  assert (dst != NULL);
+  assert (src != dst);
 
-      if (v->type == NUMERIC)
-       *p++ = c->data[v->fv];
-      else
-       (*p++).c = c->data[v->fv].s;
+  for (dst_idx = 0; dst_idx < map->value_cnt; dst_idx++)
+    {
+      int src_idx = map->map[dst_idx];
+      if (src_idx != -1)
+        *case_data_rw (dst, dst_idx) = *case_data (src, src_idx);
     }
+}
 
-  pfm_write_case (t->f, (union value *) t->case_buf);
-  return 1;
+/* Destroys case map MAP. */
+static void
+destroy_case_map (struct case_map *map) 
+{
+  if (map != NULL) 
+    {
+      free (map->map);
+      free (map);
+    }
 }