SAVE TRANSLATE: Improve error messages and implementation of RENAME.
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 13 Sep 2022 02:19:38 +0000 (19:19 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 16 Sep 2022 20:34:28 +0000 (13:34 -0700)
src/language/data-io/combine-files.c
src/language/data-io/get.c
src/language/data-io/save-translate.c
src/language/data-io/save.c
src/language/data-io/trim.c
src/language/data-io/trim.h
src/language/lexer/variable-parser.h
tests/language/data-io/save-translate.at

index b95683ef8c752867cb26dd6bba6f1ac6e98f756f..dde261a68f5727d5d1b6c9ac95e6c0d64614e37d 100644 (file)
@@ -234,7 +234,7 @@ combine_files (enum comb_command_type command,
       while (lex_match (lexer, T_SLASH))
         if (lex_match_id (lexer, "RENAME"))
           {
-            if (!parse_dict_rename (lexer, file->dict, false))
+            if (!parse_dict_rename (lexer, file->dict))
               goto error;
           }
         else if (lex_match_id (lexer, "IN"))
index cb1789aa3704027dc53a3b09c0f3970324a45023..5650b4174c876da8d39ce9d08f0db7f74ba60de4 100644 (file)
@@ -139,7 +139,7 @@ parse_read_command (struct lexer *lexer, struct dataset *ds,
   while (lex_token (lexer) != T_ENDCMD)
     {
       lex_match (lexer, T_SLASH);
-      if (!parse_dict_trim (lexer, dict, false))
+      if (!parse_dict_trim (lexer, dict))
         goto error;
     }
   dict_compact_values (dict);
index c157701e6cbd21fbc36fda554cf5bde173d9d9e6..29ad78ac21b34152d92ddacc96a2750e8178b3d7 100644 (file)
 int
 cmd_save_translate (struct lexer *lexer, struct dataset *ds)
 {
-  enum { CSV_FILE = 1, TAB_FILE } type;
+  enum { CSV_FILE = 1, TAB_FILE } type = 0;
 
-  struct dictionary *dict;
-  struct case_map_stage *stage;
-  struct case_map *map;
-  struct casewriter *writer;
-  struct file_handle *handle;
-
-  bool replace;
-
-  bool retain_unselected;
-  bool recode_user_missing;
-  bool include_var_names;
-  bool use_value_labels;
-  bool use_print_formats;
-  char decimal;
-  char delimiter;
-  char qualifier;
-
-  bool ok;
+  struct dictionary *dict = dict_clone (dataset_dict (ds));
+  dict_set_names_must_be_ids (dict, false);
 
-  type = 0;
+  struct case_map_stage *stage = case_map_stage_create (dict);
+  dict_delete_scratch_vars (dict);
 
-  dict = dict_clone (dataset_dict (ds));
-  dict_set_names_must_be_ids (dict, false);
-  stage = NULL;
-  map = NULL;
+  struct file_handle *handle = NULL;
 
-  handle = NULL;
-  replace = false;
+  bool replace = false;
 
-  retain_unselected = true;
-  recode_user_missing = false;
-  include_var_names = false;
-  use_value_labels = false;
-  use_print_formats = false;
-  decimal = settings_get_fmt_settings ()->decimal;
-  delimiter = 0;
-  qualifier = '"';
+  bool retain_unselected = true;
+  bool recode_user_missing = false;
+  bool include_var_names = false;
+  bool use_value_labels = false;
+  bool use_print_formats = false;
+  char decimal = settings_get_fmt_settings ()->decimal;
+  char delimiter = 0;
+  char qualifier = '"';
 
-  stage = case_map_stage_create (dict);
-  dict_delete_scratch_vars (dict);
+  int outfile_start = 0;
+  int outfile_end = 0;
 
   while (lex_token (lexer) != T_ENDCMD)
     {
@@ -91,6 +72,7 @@ cmd_save_translate (struct lexer *lexer, struct dataset *ds)
 
       if (lex_match_id (lexer, "OUTFILE"))
        {
+          outfile_start = lex_ofs (lexer) - 1;
           if (handle != NULL)
             {
               lex_sbc_only_once (lexer, "OUTFILE");
@@ -102,6 +84,7 @@ cmd_save_translate (struct lexer *lexer, struct dataset *ds)
          handle = fh_parse (lexer, FH_REF_FILE, NULL);
          if (handle == NULL)
            goto error;
+          outfile_end = lex_ofs (lexer) - 1;
        }
       else if (lex_match_id (lexer, "TYPE"))
         {
@@ -230,7 +213,7 @@ cmd_save_translate (struct lexer *lexer, struct dataset *ds)
               goto error;
             }
         }
-      else if (!parse_dict_trim (lexer, dict, true))
+      else if (!parse_dict_trim (lexer, dict))
         goto error;
     }
 
@@ -246,8 +229,9 @@ cmd_save_translate (struct lexer *lexer, struct dataset *ds)
     }
   else if (!replace && fn_exists (handle))
     {
-      msg (SE, _("Output file `%s' exists but %s was not specified."),
-           fh_get_file_name (handle), "REPLACE");
+      lex_ofs_error (lexer, outfile_start, outfile_end,
+                     _("Output file `%s' exists but %s was not specified."),
+                     fh_get_file_name (handle), "REPLACE");
       goto error;
     }
 
@@ -266,19 +250,19 @@ cmd_save_translate (struct lexer *lexer, struct dataset *ds)
                   : ';'),
     .qualifier = qualifier,
   };
-  writer = csv_writer_open (handle, dict, &csv_opts);
+  struct casewriter *writer = csv_writer_open (handle, dict, &csv_opts);
   if (writer == NULL)
     goto error;
   fh_unref (handle);
 
-  map = case_map_stage_get_case_map (stage);
+  struct case_map *map = case_map_stage_get_case_map (stage);
   case_map_stage_destroy (stage);
   if (map != NULL)
     writer = case_map_create_output_translator (map, writer);
   dict_unref (dict);
 
   casereader_transfer (proc_open_filtering (ds, !retain_unselected), writer);
-  ok = casewriter_destroy (writer);
+  bool ok = casewriter_destroy (writer);
   ok = proc_commit (ds) && ok;
 
   return ok ? CMD_SUCCESS : CMD_CASCADING_FAILURE;
@@ -287,6 +271,5 @@ error:
   case_map_stage_destroy (stage);
   fh_unref (handle);
   dict_unref (dict);
-  case_map_destroy (map);
   return CMD_FAILURE;
 }
index bf78276b84af29b234cf1f543b27b2768b668d1d..a4458707162ea8fcc1b561082ca0e2224449188c 100644 (file)
@@ -294,7 +294,7 @@ parse_write_command (struct lexer *lexer, struct dataset *ds,
           porfile_opts.digits = lex_integer (lexer);
           lex_get (lexer);
         }
-      else if (!parse_dict_trim (lexer, dict, false))
+      else if (!parse_dict_trim (lexer, dict))
         goto error;
 
       if (!lex_match (lexer, T_SLASH))
index e0150e04bceade6602b51d81011f44327723d473..fab97d5c9a444f74e4aca4ff93b03073fda986f7 100644 (file)
 #include "gettext.h"
 #define _(msgid) gettext (msgid)
 
-/* 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.  If RELAX is true, then the modified dictionary
-   need not conform to the usual variable name rules.  Returns
-   true on success, false on failure. */
+/* 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.  Returns true on
+   success, false on failure. */
 bool
-parse_dict_trim (struct lexer *lexer, struct dictionary *dict, bool relax)
+parse_dict_trim (struct lexer *lexer, struct dictionary *dict)
 {
   if (lex_match_id (lexer, "MAP"))
     {
@@ -52,7 +50,7 @@ parse_dict_trim (struct lexer *lexer, struct dictionary *dict, bool relax)
   else if (lex_match_id (lexer, "KEEP"))
     return parse_dict_keep (lexer, dict);
   else if (lex_match_id (lexer, "RENAME"))
-    return parse_dict_rename (lexer, dict, relax);
+    return parse_dict_rename (lexer, dict);
   else
     {
       lex_error_expecting (lexer, "MAP", "DROP", "KEEP", "RENAME");
@@ -60,230 +58,81 @@ parse_dict_trim (struct lexer *lexer, struct dictionary *dict, bool relax)
     }
 }
 
-/* Check that OLD_NAME can be renamed to NEW_NAME in DICT.  */
-static bool
-check_rename (struct lexer *lexer, int start_ofs, int end_ofs,
-              const struct dictionary *dict,
-              const char *old_name, const char *new_name)
-{
-  if (dict_lookup_var (dict, new_name) != NULL)
-    {
-      lex_ofs_error (lexer, start_ofs, end_ofs,
-                     _("Cannot rename %s as %s because a variable named %s "
-                       "already exists."),
-                     old_name, new_name, new_name);
-      msg (SN, _("To rename variables with overlapping names, use a single "
-                 "RENAME subcommand such as `/RENAME (A=B)(B=C)(C=A)', or "
-                 "equivalently, `/RENAME (A B C=B C A)'."));
-      return false;
-    }
-  return true;
-}
-
-/* Parse a  "VarX TO VarY" sequence where X and Y are integers
-   such that X >= Y.
-   If successfull, returns a string to the prefix Var and sets FIRST
-   to X and LAST to Y.  Returns NULL on failure.
-   The caller must free the return value.  */
-static char *
-try_to_sequence (struct lexer *lexer, const struct dictionary *dict,
-                 int *first, int *last)
-{
-  /* Check that the next 3 tokens are of the correct type.  */
-  if (lex_token (lexer) != T_ID
-      || lex_next_token (lexer, 1) != T_TO
-      || lex_next_token (lexer, 2) != T_ID)
-    return NULL;
-
-  /* Check that the first and last tokens are suitable as
-     variable names.  */
-  const char *s0 = lex_tokcstr (lexer);
-  char *error = id_is_valid__ (s0, dict_get_encoding (dict));
-  if (error)
-    {
-      lex_error (lexer, "%s", error);
-      free (error);
-      return NULL;
-    }
-
-  const char *s1 = lex_next_tokcstr (lexer, 2);
-  error = id_is_valid__ (s1, dict_get_encoding (dict));
-  if (error)
-    {
-      lex_next_error (lexer, 2, 2, "%s", error);
-      free (error);
-      return NULL;
-    }
-
-  int x0 = strcspn (s0, "0123456789");
-  int x1 = strcspn (s1, "0123456789");
-
-  /* The non-digit parts of s0 and s1 must be the same length.  */
-  if (x0 != x1)
-    return NULL;
-
-  /* Both s0 and s1 must have some digits.  */
-  if (strlen (s0) <= x0)
-    return NULL;
-
-  if (strlen (s1) <= x1)
-    return NULL;
-
-  /* The non-digit parts of s0 and s1 must be identical.  */
-  if (0 != strncmp (s0, s1, x0))
-    return NULL;
-
-  /* Both names must end with digits.  */
-  int len_s0_pfx = strspn (s0 + x0, "0123456789");
-  if (len_s0_pfx + x0 != strlen (s0))
-    return NULL;
-
-  int len_s1_pfx = strspn (s1 + x1, "0123456789");
-  if (len_s1_pfx + x1 != strlen (s1))
-    return NULL;
-
-  const char *n_start = s0 + x0;
-  const char *n_stop = s1 + x1;
-
-  /* The first may not be greater than the last.  */
-  if (atoi (n_start) > atoi (n_stop))
-    return NULL;
-
-  char *prefix = xstrndup (s0, x1);
-
-  *first = atoi (n_start);
-  *last = atoi (n_stop);
-
-  return prefix;
-}
-
-
 /* Parses and performs the RENAME subcommand of GET, SAVE, and
-   related commands.  If RELAX is true, then the new variable
-   names need  not conform to the normal dictionary rules.
-*/
+   related commands. */
 bool
-parse_dict_rename (struct lexer *lexer, struct dictionary *dict,
-                  bool relax)
+parse_dict_rename (struct lexer *lexer, struct dictionary *dict)
 {
-  struct variable **oldvars = NULL;
-  size_t n_newvars = 0;
-  int group = 0;
-  char **newnames = NULL;
   lex_match (lexer, T_EQUALS);
+  int start_ofs = lex_ofs (lexer);
+
+  struct variable **old_vars = NULL;
+  size_t n_old_vars = 0;
 
+  char **new_vars = NULL;
+  size_t n_new_vars = 0;
+
+  bool ok = false;
   while (lex_token (lexer) != T_SLASH && lex_token (lexer) != T_ENDCMD)
     {
-      size_t n_oldvars = 0;
-      oldvars = NULL;
-      n_newvars = 0;
-      n_oldvars = 0;
-      oldvars = NULL;
+      size_t prev_n_old = n_old_vars;
+      size_t prev_n_new = n_new_vars;
 
       bool paren = lex_match (lexer, T_LPAREN);
-      group++;
-      if (!parse_variables (lexer, dict, &oldvars, &n_oldvars, PV_NO_DUPLICATE))
-       goto fail;
+      int pv_opts = PV_NO_DUPLICATE | PV_APPEND | (paren ? 0 : PV_SINGLE);
+
+      int old_vars_start = lex_ofs (lexer);
+      if (!parse_variables (lexer, dict, &old_vars, &n_old_vars, pv_opts))
+        goto done;
+      int old_vars_end = lex_ofs (lexer) - 1;
 
       if (!lex_force_match (lexer, T_EQUALS))
-       goto fail;
-
-      newnames = xmalloc (sizeof *newnames * n_oldvars);
-
-      char *prefix = NULL;
-      int first, last;
-      /* First attempt to parse v1 TO v10 format.  */
-      if ((prefix = try_to_sequence (lexer, dict, &first, &last)))
-        {
-          /* These 3 tokens have already been checked in the
-             try_to_sequence function.  */
-          int start_ofs = lex_ofs (lexer);
-          lex_get (lexer);
-          lex_get (lexer);
-          lex_get (lexer);
-          int end_ofs = lex_ofs (lexer) - 1;
-
-          /* Make sure the new names are suitable.  */
-          for (int i = first; i <= last; ++i)
-            {
-              char *vn = xasprintf ("%s%d", prefix, i);
-
-              if (!check_rename (lexer, start_ofs, end_ofs,
-                                 dict, var_get_name (oldvars[n_newvars]), vn))
-                {
-                  free (vn);
-                  free (prefix);
-                  goto fail;
-                }
-
-              newnames[i - first] = vn;
-              n_newvars++;
-            }
-        }
-      else
-      while (lex_token (lexer) == T_ID || lex_token (lexer) == T_STRING)
-        {
-          if (n_newvars >= n_oldvars)
-            break;
-          const char *new_name = lex_tokcstr (lexer);
-          if (!relax)
-            {
-              char *error = id_is_plausible__ (new_name);
-              if (error)
-                {
-                  lex_error (lexer, "%s", error);
-                  free (error);
-                  goto fail;
-                }
-            }
-
-          int ofs = lex_ofs (lexer);
-          if (!check_rename (lexer, ofs, ofs,
-                             dict, var_get_name (oldvars[n_newvars]), new_name))
-            goto fail;
-          newnames[n_newvars] = xstrdup (new_name);
-          lex_get (lexer);
-          n_newvars++;
-        }
-      free (prefix);
-
-      if (n_newvars != n_oldvars)
-       {
-         msg (SE, _("Number of variables on left side of `=' (%zu) does not "
-                     "match number of variables on right side (%zu), in "
-                     "parenthesized group %d of RENAME subcommand."),
-              n_oldvars, n_newvars, group);
-         goto fail;
-       }
+        goto done;
+
+      int new_vars_start = lex_ofs (lexer);
+      if (!parse_DATA_LIST_vars (lexer, dict, &new_vars, &n_new_vars, pv_opts))
+        goto done;
+      int new_vars_end = lex_ofs (lexer) - 1;
 
-      if (paren)
-       if (!lex_force_match (lexer, T_RPAREN))
-         goto fail;
+      if (paren && !lex_force_match (lexer, T_RPAREN))
+        goto done;
 
-      char *errname = 0;
-      if (!dict_rename_vars (dict, oldvars, newnames, n_newvars, &errname))
+      if (n_new_vars != n_old_vars)
        {
-         msg (SE,
-              _("Requested renaming duplicates variable name %s."),
-              errname);
-         goto fail;
+          size_t added_old = n_old_vars - prev_n_old;
+          size_t added_new = n_new_vars - prev_n_new;
+
+          msg (SE, _("Old and new variable counts do not match."));
+          lex_ofs_msg (lexer, SN, old_vars_start, old_vars_end,
+                       ngettext ("There is %zu old variable.",
+                                 "There are %zu old variables.", added_old),
+                       added_old);
+          lex_ofs_msg (lexer, SN, new_vars_start, new_vars_end,
+                       ngettext ("There is %zu new variable name.",
+                                 "There are %zu new variable names.",
+                                 added_new),
+                       added_new);
+         goto done;
        }
-      free (oldvars);
-      for (int i = 0; i < n_newvars; ++i)
-       free (newnames[i]);
-      free (newnames);
-      newnames = NULL;
     }
+  int end_ofs = lex_ofs (lexer) - 1;
 
-  return true;
-
- fail:
-  free (oldvars);
-  for (int i = 0; i < n_newvars; ++i)
-    free (newnames[i]);
-  free (newnames);
-  newnames = NULL;
-  return false;
+  char *dup_name = NULL;
+  if (!dict_rename_vars (dict, old_vars, new_vars, n_new_vars, &dup_name))
+    {
+      lex_ofs_error (lexer, start_ofs, end_ofs,
+                     _("Requested renaming duplicates variable name %s."),
+                     dup_name);
+      goto done;
+    }
+  ok = true;
+
+done:
+  free (old_vars);
+  for (size_t i = 0; i < n_new_vars; ++i)
+    free (new_vars[i]);
+  free (new_vars);
+  return ok;
 }
 
 /* Parses and performs the DROP subcommand of GET, SAVE, and
index 188acb4781907295ea1969c2c8f4d733478f1766..106d1a8f32c0d800c8151f534b3255a97d114300 100644 (file)
@@ -21,8 +21,8 @@
 
 struct lexer;
 struct dictionary;
-bool parse_dict_trim (struct lexer *, struct dictionary *, bool);
-bool parse_dict_rename (struct lexer *, struct dictionary *, bool);
+bool parse_dict_trim (struct lexer *, struct dictionary *);
+bool parse_dict_rename (struct lexer *, struct dictionary *);
 bool parse_dict_drop (struct lexer *, struct dictionary *);
 bool parse_dict_keep (struct lexer *, struct dictionary *);
 
index 5815dc458b943fc1235fe52ef8fd0d345e9376a4..4419c9ebf61a1467805ae3782cbfd90c98d95951 100644 (file)
@@ -40,16 +40,16 @@ void var_set_destroy (struct var_set *vs);
 
 enum
   {
-    PV_NONE = 0,               /* No options. */
-    PV_SINGLE = 0001,          /* Restrict to a single name or TO use. */
-    PV_DUPLICATE = 0002,       /* Don't merge duplicates. */
-    PV_APPEND = 0004,          /* Append to existing list. */
-    PV_NO_DUPLICATE = 0010,    /* Error on duplicates. */
-    PV_NUMERIC = 0020,         /* Vars must be numeric. */
-    PV_STRING = 0040,          /* Vars must be string. */
-    PV_SAME_TYPE = 00100,      /* All vars must be the same type. */
-    PV_SAME_WIDTH = 00200,     /* All vars must be the same type and width. */
-    PV_NO_SCRATCH = 00400      /* Disallow scratch variables. */
+    PV_NONE = 0,                /* No options. */
+    PV_SINGLE = 1 << 0,         /* Restrict to a single name or TO use. */
+    PV_DUPLICATE = 1 << 1,      /* Don't merge duplicates. */
+    PV_APPEND = 1 << 2,         /* Append to existing list. */
+    PV_NO_DUPLICATE = 1 << 3,   /* Error on duplicates. */
+    PV_NUMERIC = 1 << 4,        /* Vars must be numeric. */
+    PV_STRING = 1 << 5,         /* Vars must be string. */
+    PV_SAME_TYPE = 1 << 6,      /* All vars must be the same type. */
+    PV_SAME_WIDTH = 1 << 7,     /* All vars must be the same type and width. */
+    PV_NO_SCRATCH = 1 << 8,     /* Disallow scratch variables. */
   };
 
 struct variable *parse_variable (struct lexer *, const struct dictionary *);
index b1f860e8e74466e29144a57d31144d83452df07f..ebfca92d4c4d5c277dfb2f2aa47efd166de2fb40 100644 (file)
@@ -115,6 +115,7 @@ AT_CLEANUP
 
 
 AT_SETUP([CSV output -- KEEP, RENAME bad name ])
+AT_KEYWORDS([SAVE TRANSLATE])
 AT_DATA([bad.sps], [
 data list notable list /Var1 Var2 Var3 Var4 Var5 *.
 begin data
@@ -129,7 +130,7 @@ SAVE TRANSLATE
   /FIELDNAMES
   /Unselected=DELETE
    /RENAME =
-        Var4 = foobar
+        Var4 = Var5
         (Var1 Var2 = one Var3 )
         (Var3 = "The second")
   /CELLS=VALUES
@@ -137,11 +138,13 @@ SAVE TRANSLATE
 ])
 
 AT_CHECK([pspp -O format=csv bad.sps], [1], [dnl
-"bad.sps:16.26-16.29: error: SAVE TRANSLATE: Cannot rename Var2 as Var3 because a variable named Var3 already exists.
+"bad.sps:15.9-17.29: error: SAVE TRANSLATE: Requested renaming duplicates variable name Var5.
+   15 |         Var4 = Var5
+      |         ^~~~~~~~~~~
    16 |         (Var1 Var2 = one Var3 )
-      |                          ^~~~"
-
-"bad.sps:16: note: SAVE TRANSLATE: To rename variables with overlapping names, use a single RENAME subcommand such as `/RENAME (A=B)(B=C)(C=A)', or equivalently, `/RENAME (A B C=B C A)'."
+      | -------------------------------
+   17 |         (Var3 = ""The second"")
+      | -----------------------------"
 ])
 
 
@@ -161,3 +164,198 @@ number    time    date    datetime        string  filter
 ])
 AT_CLEANUP
 
+AT_SETUP([SAVE TRANSLATE syntax errors])
+: > xyzzy.csv
+AT_DATA([save-translate.sps], [dnl
+DATA LIST LIST NOTABLE /v1 to v10.
+SAVE TRANSLATE **.
+SAVE TRANSLATE/OUTFILE=**.
+SAVE TRANSLATE/OUTFILE='xyzzy.txt'/OUTFILE='xyzzy.txt'.
+SAVE TRANSLATE/TYPE=CSV/TYPE=**.
+SAVE TRANSLATE/TYPE=**.
+SAVE TRANSLATE/MISSING=**.
+SAVE TRANSLATE/CELLS=**.
+SAVE TRANSLATE/TEXTOPTIONS DELIMITER=**.
+SAVE TRANSLATE/TEXTOPTIONS DELIMITER='ab'.
+SAVE TRANSLATE/TEXTOPTIONS QUALIFIER=**.
+SAVE TRANSLATE/TEXTOPTIONS QUALIFIER='ab'.
+SAVE TRANSLATE/TEXTOPTIONS DECIMAL=**.
+SAVE TRANSLATE/UNSELECTED=**.
+SAVE TRANSLATE/ **.
+SAVE TRANSLATE/OUTFILE='xyzzy.csv'.
+SAVE TRANSLATE/TYPE=CSV.
+SAVE TRANSLATE/OUTFILE='xyzzy.csv'/TYPE=CSV.
+SAVE TRANSLATE/RENAME **.
+SAVE TRANSLATE/RENAME v1**.
+SAVE TRANSLATE/RENAME(v1**).
+SAVE TRANSLATE/RENAME v1=.
+SAVE TRANSLATE/RENAME v1=**.
+SAVE TRANSLATE/RENAME v1 to v5=v6.
+SAVE TRANSLATE/RENAME (v1=v2 v3).
+SAVE TRANSLATE/RENAME (v1 v2=v3).
+SAVE TRANSLATE/RENAME (v1=v3**.
+SAVE TRANSLATE/RENAME v1=v5.
+SAVE TRANSLATE/RENAME v1 v5=v5 v1.
+SAVE TRANSLATE/RENAME(v1 v5=v5 v1).
+SAVE TRANSLATE/RENAME(v1 to v10=v01 to v10).
+SAVE TRANSLATE/RENAME=v1=v1.
+SAVE TRANSLATE/DROP=ALL.
+SAVE TRANSLATE/DROP=**.
+SAVE TRANSLATE/KEEP=**.
+])
+AT_CHECK([pspp -O format=csv save-translate.sps], [1], [dnl
+"save-translate.sps:2.16-2.17: error: SAVE TRANSLATE: Syntax error expecting `/'.
+    2 | SAVE TRANSLATE **.
+      |                ^~"
+
+"save-translate.sps:3.24-3.25: error: SAVE TRANSLATE: Syntax error expecting a file name or handle name.
+    3 | SAVE TRANSLATE/OUTFILE=**.
+      |                        ^~"
+
+"save-translate.sps:4.36-4.42: error: SAVE TRANSLATE: Subcommand OUTFILE may only be specified once.
+    4 | SAVE TRANSLATE/OUTFILE='xyzzy.txt'/OUTFILE='xyzzy.txt'.
+      |                                    ^~~~~~~"
+
+"save-translate.sps:5.25-5.28: error: SAVE TRANSLATE: Subcommand TYPE may only be specified once.
+    5 | SAVE TRANSLATE/TYPE=CSV/TYPE=**.
+      |                         ^~~~"
+
+"save-translate.sps:6.21-6.22: error: SAVE TRANSLATE: Syntax error expecting CSV or TAB.
+    6 | SAVE TRANSLATE/TYPE=**.
+      |                     ^~"
+
+"save-translate.sps:7.24-7.25: error: SAVE TRANSLATE: Syntax error expecting IGNORE or RECODE.
+    7 | SAVE TRANSLATE/MISSING=**.
+      |                        ^~"
+
+"save-translate.sps:8.22-8.23: error: SAVE TRANSLATE: Syntax error expecting VALUES or LABELS.
+    8 | SAVE TRANSLATE/CELLS=**.
+      |                      ^~"
+
+"save-translate.sps:9.38-9.39: error: SAVE TRANSLATE: Syntax error expecting string.
+    9 | SAVE TRANSLATE/TEXTOPTIONS DELIMITER=**.
+      |                                      ^~"
+
+"save-translate.sps:10.38-10.41: error: SAVE TRANSLATE: The DELIMITER string must contain exactly one character.
+   10 | SAVE TRANSLATE/TEXTOPTIONS DELIMITER='ab'.
+      |                                      ^~~~"
+
+"save-translate.sps:11.38-11.39: error: SAVE TRANSLATE: Syntax error expecting string.
+   11 | SAVE TRANSLATE/TEXTOPTIONS QUALIFIER=**.
+      |                                      ^~"
+
+"save-translate.sps:12.38-12.41: error: SAVE TRANSLATE: The QUALIFIER string must contain exactly one character.
+   12 | SAVE TRANSLATE/TEXTOPTIONS QUALIFIER='ab'.
+      |                                      ^~~~"
+
+"save-translate.sps:13.36-13.37: error: SAVE TRANSLATE: Syntax error expecting DOT or COMMA.
+   13 | SAVE TRANSLATE/TEXTOPTIONS DECIMAL=**.
+      |                                    ^~"
+
+"save-translate.sps:14.27-14.28: error: SAVE TRANSLATE: Syntax error expecting RETAIN or DELETE.
+   14 | SAVE TRANSLATE/UNSELECTED=**.
+      |                           ^~"
+
+"save-translate.sps:15.17-15.18: error: SAVE TRANSLATE: Syntax error expecting MAP, DROP, KEEP, or RENAME.
+   15 | SAVE TRANSLATE/ **.
+      |                 ^~"
+
+"save-translate.sps:16.1-16.35: error: SAVE TRANSLATE: Required subcommand TYPE was not specified.
+   16 | SAVE TRANSLATE/OUTFILE='xyzzy.csv'.
+      | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+
+"save-translate.sps:17.1-17.24: error: SAVE TRANSLATE: Required subcommand OUTFILE was not specified.
+   17 | SAVE TRANSLATE/TYPE=CSV.
+      | ^~~~~~~~~~~~~~~~~~~~~~~~"
+
+"save-translate.sps:18.16-18.34: error: SAVE TRANSLATE: Output file `xyzzy.csv' exists but REPLACE was not specified.
+   18 | SAVE TRANSLATE/OUTFILE='xyzzy.csv'/TYPE=CSV.
+      |                ^~~~~~~~~~~~~~~~~~~"
+
+"save-translate.sps:19.23-19.24: error: SAVE TRANSLATE: Syntax error expecting variable name.
+   19 | SAVE TRANSLATE/RENAME **.
+      |                       ^~"
+
+"save-translate.sps:20.25-20.26: error: SAVE TRANSLATE: Syntax error expecting `='.
+   20 | SAVE TRANSLATE/RENAME v1**.
+      |                         ^~"
+
+"save-translate.sps:21.25-21.26: error: SAVE TRANSLATE: Syntax error expecting `='.
+   21 | SAVE TRANSLATE/RENAME(v1**).
+      |                         ^~"
+
+"save-translate.sps:22.26: error: SAVE TRANSLATE: Syntax error expecting variable name.
+   22 | SAVE TRANSLATE/RENAME v1=.
+      |                          ^"
+
+"save-translate.sps:23.26-23.27: error: SAVE TRANSLATE: Syntax error expecting variable name.
+   23 | SAVE TRANSLATE/RENAME v1=**.
+      |                          ^~"
+
+save-translate.sps:24: error: SAVE TRANSLATE: Old and new variable counts do not match.
+
+"save-translate.sps:24.23-24.30: note: SAVE TRANSLATE: There are 5 old variables.
+   24 | SAVE TRANSLATE/RENAME v1 to v5=v6.
+      |                       ^~~~~~~~"
+
+"save-translate.sps:24.32-24.33: note: SAVE TRANSLATE: There is 1 new variable name.
+   24 | SAVE TRANSLATE/RENAME v1 to v5=v6.
+      |                                ^~"
+
+save-translate.sps:25: error: SAVE TRANSLATE: Old and new variable counts do not match.
+
+"save-translate.sps:25.24-25.25: note: SAVE TRANSLATE: There is 1 old variable.
+   25 | SAVE TRANSLATE/RENAME (v1=v2 v3).
+      |                        ^~"
+
+"save-translate.sps:25.27-25.31: note: SAVE TRANSLATE: There are 2 new variable names.
+   25 | SAVE TRANSLATE/RENAME (v1=v2 v3).
+      |                           ^~~~~"
+
+save-translate.sps:26: error: SAVE TRANSLATE: Old and new variable counts do not match.
+
+"save-translate.sps:26.24-26.28: note: SAVE TRANSLATE: There are 2 old variables.
+   26 | SAVE TRANSLATE/RENAME (v1 v2=v3).
+      |                        ^~~~~"
+
+"save-translate.sps:26.30-26.31: note: SAVE TRANSLATE: There is 1 new variable name.
+   26 | SAVE TRANSLATE/RENAME (v1 v2=v3).
+      |                              ^~"
+
+"save-translate.sps:27.29-27.30: error: SAVE TRANSLATE: Syntax error expecting `)'.
+   27 | SAVE TRANSLATE/RENAME (v1=v3**.
+      |                             ^~"
+
+"save-translate.sps:28.23-28.27: error: SAVE TRANSLATE: Requested renaming duplicates variable name v5.
+   28 | SAVE TRANSLATE/RENAME v1=v5.
+      |                       ^~~~~"
+
+"save-translate.sps:29.26-29.27: error: SAVE TRANSLATE: Syntax error expecting `='.
+   29 | SAVE TRANSLATE/RENAME v1 v5=v5 v1.
+      |                          ^~"
+
+"save-translate.sps:30.1-30.35: error: SAVE TRANSLATE: Required subcommand TYPE was not specified.
+   30 | SAVE TRANSLATE/RENAME(v1 v5=v5 v1).
+      | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+
+"save-translate.sps:31.1-31.44: error: SAVE TRANSLATE: Required subcommand TYPE was not specified.
+   31 | SAVE TRANSLATE/RENAME(v1 to v10=v01 to v10).
+      | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+
+"save-translate.sps:32.1-32.28: error: SAVE TRANSLATE: Required subcommand TYPE was not specified.
+   32 | SAVE TRANSLATE/RENAME=v1=v1.
+      | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~"
+
+"save-translate.sps:33.16-33.23: error: SAVE TRANSLATE: Cannot DROP all variables from dictionary.
+   33 | SAVE TRANSLATE/DROP=ALL.
+      |                ^~~~~~~~"
+
+"save-translate.sps:34.21-34.22: error: SAVE TRANSLATE: Syntax error expecting variable name.
+   34 | SAVE TRANSLATE/DROP=**.
+      |                     ^~"
+
+"save-translate.sps:35.21-35.22: error: SAVE TRANSLATE: Syntax error expecting variable name.
+   35 | SAVE TRANSLATE/KEEP=**.
+      |                     ^~"
+])
+AT_CLEANUP