]> git.sesse.net Git - pgn-extract/blob - argsfile.c
Add support for outputting positions in my own bit-packed FEN format.
[pgn-extract] / argsfile.c
1 /*
2  *  Program: pgn-extract: a Portable Game Notation (PGN) extractor.
3  *  Copyright (C) 1994-2014 David Barnes
4  *  This program is free software; you can redistribute it and/or modify
5  *  it under the terms of the GNU General Public License as published by
6  *  the Free Software Foundation; either version 1, or (at your option)
7  *  any later version.
8  *
9  *  This program is distributed in the hope that it will be useful,
10  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  *  GNU General Public License for more details.
13  *
14  *  You should have received a copy of the GNU General Public License
15  *  along with this program; if not, write to the Free Software
16  *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17  *
18  *  David Barnes may be contacted as D.J.Barnes@kent.ac.uk
19  *  http://www.cs.kent.ac.uk/people/staff/djb/
20  *
21  */
22
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <ctype.h>
27 #include "bool.h"
28 #include "defs.h"
29 #include "typedef.h"
30 #include "lines.h"
31 #include "taglist.h"
32 #include "tokens.h"
33 #include "lex.h"
34 #include "taglines.h"
35 #include "moves.h"
36 #include "end.h"
37 #include "eco.h"
38 #include "argsfile.h"
39 #include "apply.h"
40 #include "output.h"
41 #include "lists.h"
42 #include "mymalloc.h"
43
44 #define CURRENT_VERSION "v17-19"
45 #define URL "http://www.cs.kent.ac.uk/people/staff/djb/pgn-extract/"
46
47 /* The prefix of the arguments allowed in an argsfile.
48  * The full format is:
49  *         :-?
50  * where ? is an argument letter.
51  *
52  * A line of the form:
53  *         :filename
54  * means use filename as a NORMALFILE source of games.
55  *
56  * A line with no leading colon character is taken to apply to the
57  * move-reason argument line. Currently, this only applies to the
58  *        -t -v -x -z
59  * arguments.
60  */
61 static const char argument_prefix[] = ":-";
62 static const int argument_prefix_len = sizeof(argument_prefix)-1;
63 static ArgType classify_arg(const char *line);
64
65         /* Select the correct function according to operating system. */
66 static int
67 stringcompare(const char *s1, const char *s2)
68 {
69 #if defined(__unix__) || defined(__linux__) || defined(__APPLE__)
70     return strcasecmp(s1,s2);
71 #else
72     return _stricmp(s1,s2);
73 #endif
74 }
75
76 #if 0
77         /* Return TRUE if str contains prefix as a prefix, FALSE otherwise. */
78 static Boolean
79 prefixMatch(const char *prefix, const char *str)
80 {
81     size_t prefixLen = strlen(prefix);
82     if(strlen(str) >= prefixLen) {
83 #if defined(__unix__) || defined(__linux__)
84         return strncasecmp(prefix, str, prefixLen) == 0;
85 #else
86         return _strnicmp(prefix, str, prefixLen) == 0;
87 #endif
88     }
89     else {
90         return FALSE;
91     }
92 }
93 #endif
94
95     /* Skip over leading spaces from the string. */
96 static const char *skip_leading_spaces(const char *str)
97 {
98     while(*str == ' ') {
99         str++;
100     }
101     return str;
102 }
103
104         /* Print a usage message, and exit. */
105 static void
106 usage_and_exit(void)
107 {
108     const char *help_data[] = {
109       "-7 -- output only the seven tag roster for each game. Other tags (apart",
110       "      from FEN and possibly ECO) are discarded (See -e).",
111       "-#num -- output num games per file, to files named 1.pgn, 2.pgn, etc.",
112
113       "",
114
115       "-aoutputfile -- append extracted games to outputfile. (See -o).",
116       "-Aargsfile -- read the program's arguments from argsfile.",
117       "-b[elu]num -- restricted bounds on the number of moves in a game.",
118       "       lnum set a lower bound of `num' moves,",
119       "       unum set an upper bound of `num' moves,",
120       "       otherwise num (or enum) means equal-to `num' moves.",
121       "-cfile[.pgn] -- Use file.pgn as a check-file for duplicates or",
122       "      contents of file (no pgn suffix) as a list of check-file names.",
123       "-C -- don't include comments in the output. Ordinarily these are retained.",
124       "-dduplicates -- write duplicate games to the file duplicates.",
125       "-D -- don't output duplicate games.",
126       "-eECO_file -- perform ECO classification of games. The optional",
127       "      ECO_file should contain a PGN format list of ECO lines",
128       "      Default is to use eco.pgn from the current directory.",
129       "-E[123 etc.] -- split output into separate files according to ECO.",
130       "      E1 : Produce files from ECO letter, A.pgn, B.pgn, ...",
131       "      E2 : Produce files from ECO letter and first digit, A0.pgn, ...",
132       "      E3 : Produce files from full ECO code, A00.pgn, A01.pgn, ...",
133       "      Further digits may be used to produce non-standard further",
134       "      refined division of games.",
135       "      All files are opened in append mode.",
136       "-F -- output a FEN string comment of the final game position.",
137       "-ffile_list  -- file_list contains the list of PGN source files, one per line.",
138       "-h -- print details of the arguments.",
139       "-llogfile  -- Save the diagnostics in logfile rather than using stderr.",
140       "-Llogfile  -- Append all diagnostics to logfile, rather than overwriting.",
141       "-M -- Match only games which end in checkmate.",
142       "-noutputfile -- Write all valid games not otherwise output to outputfile.",
143       "-N -- don't include NAGs in the output. Ordinarily these are retained.",
144       "-ooutputfile -- write extracted games to outputfile (existing contents lost).",
145       "-P -- don't match permutations of the textual variations (-v).",
146       "-Rtagorder -- Use the tag ordering specified in the file tagorder.",
147       "-r -- report any errors but don't extract.",
148       "-S -- Use a simple soundex algorithm for some tag matches. If used",
149       "      this option must precede the -t or -T options.",
150       "-s -- silent mode: don't report each game as it is extracted.",
151       "-ttagfile -- file of player, date, result or FEN extraction criteria.",
152       "-Tcriterion -- player, date, or result extraction criterion.",
153       "-U -- don't output games that only occur once. (See -d).",
154       "-vvariations -- the file variations contains the textual lines of interest.",
155       "-V -- don't include variations in the output. Ordinarily these are retained.",
156       "-wwidth -- set width as an approximate line width for output.",
157       "-W[cm|epd|halg|lalg|elalg|san] -- specify the output format to use.",
158       "      Default is SAN.",
159       "      -W means use the input format.",
160       "      -Wcm is (a possibly obsolete) ChessMaster format.",
161       "      -Wepd is EPD format.",
162       "      -Wsan[PNBRQK] for language specific output.",
163       "      -Whalg is hyphenated long algebraic.",
164       "      -Wlalg is long algebraic.",
165       "      -Welalg is enhanced long algebraic.",
166       "      -Wuci is output compatible with the UCI protocol.",
167       "-xvariations -- the file variations contains the lines resulting in",
168       "                positions of interest.",
169       "-zendings -- the file endings contains the end positions of interest.",
170       "-Z -- use the file virtual.tmp as an external hash table for duplicates.",
171       "      Use when MallocOrDie messages occur with big datasets.",
172
173       "",
174
175       "--addhashcode - output a HashCode tag",
176       "--append - see -a",
177       "--checkfile - see -c",
178       "--checkmate - see -M",
179       "--duplicates - see -d",
180       "--evaluation - include a position evaluation after each move",
181       "--fencomments - include a FEN string after each move",
182       "--fuzzydepth plies - positional duplicates match",
183       "--help - see -h",
184       "--keepbroken - retain games with errors",
185       "--linelength - see -w",
186       "--markmatches - mark positional and material matches with a comment; see -t, -v, and -z",
187       "--nochecks - don't output + and # after moves.",
188       "--nocomments - see -C",
189       "--noduplicates - see -D",
190       "--nomovenumbers - don't output move numbers.",
191       "--nonags - see -N",
192       "--noresults - don't output results.",
193       "--notags - don't output any tags.",
194       "--nounique - see -U",
195       "--novars - see -V",
196       "--output - see -o",
197       "--plylimit - limit the number of plies output.",
198       "--selectonly N - only output the Nth matched game (N > 0)",
199       "--seven - see -7",
200       "--stalemate - only output games that end in stalemate.",
201       "--totalplycount - include a tag with the total number of plies in a game.",
202       "--version - print the current version number and exit.",
203
204       /* Must be NULL terminated. */
205       (char *)NULL,
206   };
207
208   const char **data = help_data;
209
210   fprintf(GlobalState.logfile,
211           "pgn-extract %s (%s): a Portable Game Notation (PGN) manipulator.\n",
212           CURRENT_VERSION,__DATE__);
213   fprintf(GlobalState.logfile,
214           "Copyright (C) 1994-2014 David J. Barnes (d.j.barnes@kent.ac.uk)\n");
215   fprintf(GlobalState.logfile,"%s\n\n",URL);
216   fprintf(GlobalState.logfile,"Usage: pgn-extract [arguments] [file.pgn ...]\n");
217   
218   for(; *data != NULL; data++){
219       fprintf(GlobalState.logfile,"%s\n",*data);
220   }
221   exit(1);
222 }
223
224 void
225 read_args_file(const char *infile)
226 {   char *line;
227     FILE *fp = fopen(infile,"r");
228
229     if(fp == NULL){
230         fprintf(GlobalState.logfile,"Cannot open %s for reading.\n",infile);
231         exit(1);
232     }
233     else{
234         ArgType linetype = NO_ARGUMENT_MATCH;
235         ArgType nexttype;
236         while((line = read_line(fp)) != NULL){
237             if(blank_line(line)){
238                 (void) free(line);
239                 continue;
240             }
241             nexttype = classify_arg(line);
242             if(nexttype == NO_ARGUMENT_MATCH){
243                 if(*line == argument_prefix[0]){
244                     /* Treat the line as a source file name. */
245                     add_filename_to_source_list(&line[1],NORMALFILE);
246                 }
247                 else if(linetype != NO_ARGUMENT_MATCH){
248                     /* Handle the line. */
249                     switch(linetype){
250                         case MOVES_ARGUMENT:
251                             add_textual_variation_from_line(line);
252                             break;
253                         case POSITIONS_ARGUMENT:
254                             add_positional_variation_from_line(line);
255                             break;
256                         case TAGS_ARGUMENT:
257                             process_tag_line(infile,line);
258                             break;
259                         case TAG_ROSTER_ARGUMENT:
260                             process_roster_line(line);
261                             break;
262                         case ENDINGS_ARGUMENT:
263                             process_ending_line(line);
264                             (void) free(line);
265                             break;
266                         default:
267                             fprintf(GlobalState.logfile,
268                                     "Internal error: unknown linetype %d in read_args_file\n",
269                                     linetype);
270                             (void) free(line);
271                             exit(-1);
272                     }
273                 }
274                 else{
275                     /* It should have been a line applying to the
276                      * current linetype.
277                      */
278                     fprintf(GlobalState.logfile,
279                         "Missing argument type for line %s in the argument file.\n",
280                         line);
281                     exit(1);
282                 }
283             }
284             else{
285                 switch(nexttype){
286                         /* Arguments with a possible additional
287                          * argument value.
288                          * All of these apply only to the current
289                          * line in the argument file.
290                          */
291                     case WRITE_TO_OUTPUT_FILE_ARGUMENT:
292                     case APPEND_TO_OUTPUT_FILE_ARGUMENT:
293                     case WRITE_TO_LOG_FILE_ARGUMENT:
294                     case APPEND_TO_LOG_FILE_ARGUMENT:
295                     case DUPLICATES_FILE_ARGUMENT:
296                     case USE_ECO_FILE_ARGUMENT:
297                     case CHECK_FILE_ARGUMENT:
298                     case FILE_OF_FILES_ARGUMENT:
299                     case BOUNDS_ARGUMENT:
300                     case GAMES_PER_FILE_ARGUMENT:
301                     case ECO_OUTPUT_LEVEL_ARGUMENT:
302                     case FILE_OF_ARGUMENTS_ARGUMENT:
303                     case NON_MATCHING_GAMES_ARGUMENT:
304                     case TAG_EXTRACTION_ARGUMENT:
305                     case LINE_WIDTH_ARGUMENT:
306                     case OUTPUT_FORMAT_ARGUMENT:
307                         process_argument(line[argument_prefix_len],
308                                          &line[argument_prefix_len+1]);
309                         linetype = NO_ARGUMENT_MATCH;
310                         break;
311                     case LONG_FORM_ARGUMENT:
312                         {
313                             char *arg = &line[argument_prefix_len+1];
314                             char *space = strchr(arg, ' ');
315                             if(space != NULL) {
316                                 /* We need to drop an associated value from arg. */
317                                 int arglen = space - arg;
318                                 char *just_arg = (char *) MallocOrDie(arglen + 1);
319                                 strncpy(just_arg, arg, arglen);
320                                 just_arg[arglen] = '\0';
321                                 process_long_form_argument(just_arg,
322                                                            skip_leading_spaces(space));
323                             }
324                             else {
325                                 process_long_form_argument(arg, "");
326                                 linetype = NO_ARGUMENT_MATCH;
327                             }
328                         }
329                         break;
330
331                         /* Arguments with no additional
332                          * argument value.
333                          * All of these apply only to the current
334                          * line in the argument file.
335                          */
336                     case SEVEN_TAG_ROSTER_ARGUMENT:
337                     case HELP_ARGUMENT:
338                     case ALTERNATIVE_HELP_ARGUMENT:
339                     case DONT_KEEP_COMMENTS_ARGUMENT:
340                     case DONT_KEEP_DUPLICATES_ARGUMENT:
341                     case DONT_MATCH_PERMUTATIONS_ARGUMENT:
342                     case DONT_KEEP_NAGS_ARGUMENT:
343                     case OUTPUT_FEN_STRING_ARGUMENT:
344                     case CHECK_ONLY_ARGUMENT:
345                     case KEEP_SILENT_ARGUMENT:
346                     case USE_SOUNDEX_ARGUMENT:
347                     case MATCH_CHECKMATE_ARGUMENT:
348                     case SUPPRESS_ORIGINALS_ARGUMENT:
349                     case DONT_KEEP_VARIATIONS_ARGUMENT:
350                     case USE_VIRTUAL_HASH_TABLE_ARGUMENT:
351                         process_argument(line[argument_prefix_len],"");
352                         linetype = NO_ARGUMENT_MATCH;
353                         break;
354
355                         /* Arguments whose values persist beyond
356                          * the current line.
357                          */
358                     case MOVES_ARGUMENT:
359                     case POSITIONS_ARGUMENT:
360                     case ENDINGS_ARGUMENT:
361                     case TAGS_ARGUMENT:
362                     case TAG_ROSTER_ARGUMENT:
363                         process_argument(line[argument_prefix_len],
364                                          &line[argument_prefix_len+1]);
365                         /* Apply this type to subsequent lines. */
366                         linetype = nexttype;
367                         break;
368                     default:
369                         linetype = nexttype;
370                         break;
371                 }
372                 (void) free(line);
373             }
374         }
375         (void) fclose(fp);
376     }
377 }
378
379         /* Determine which (if any) type of argument is
380          * indicated by the contents of the current line.
381          * Arguments are assumed to be surrounded by
382          * square brackets.
383          */
384 static ArgType
385 classify_arg(const char *line)
386 {
387     /* Valid arguments must have at least one character beyond
388      * the prefix.
389      */
390     static const size_t min_argument_length = 1+sizeof(argument_prefix)-1;
391     size_t line_length = strlen(line);
392
393     /* Check for a line of the form:
394      *            :-argument
395      */
396     if((strncmp(line,argument_prefix,argument_prefix_len) == 0) &&
397                    (line_length >= min_argument_length)){
398         char argument_letter = line[argument_prefix_len];
399         switch(argument_letter){
400             case TAGS_ARGUMENT:
401             case MOVES_ARGUMENT:
402             case POSITIONS_ARGUMENT:
403             case ENDINGS_ARGUMENT:
404             case TAG_EXTRACTION_ARGUMENT:
405             case LINE_WIDTH_ARGUMENT:
406             case OUTPUT_FORMAT_ARGUMENT:
407             case SEVEN_TAG_ROSTER_ARGUMENT:
408             case FILE_OF_ARGUMENTS_ARGUMENT:
409             case NON_MATCHING_GAMES_ARGUMENT:
410             case DONT_KEEP_COMMENTS_ARGUMENT:
411             case DONT_KEEP_DUPLICATES_ARGUMENT:
412             case DONT_KEEP_NAGS_ARGUMENT:
413             case DONT_MATCH_PERMUTATIONS_ARGUMENT:
414             case OUTPUT_FEN_STRING_ARGUMENT:
415             case CHECK_ONLY_ARGUMENT:
416             case KEEP_SILENT_ARGUMENT:
417             case USE_SOUNDEX_ARGUMENT:
418             case MATCH_CHECKMATE_ARGUMENT:
419             case SUPPRESS_ORIGINALS_ARGUMENT:
420             case DONT_KEEP_VARIATIONS_ARGUMENT:
421             case WRITE_TO_OUTPUT_FILE_ARGUMENT:
422             case WRITE_TO_LOG_FILE_ARGUMENT:
423             case APPEND_TO_LOG_FILE_ARGUMENT:
424             case APPEND_TO_OUTPUT_FILE_ARGUMENT:
425             case DUPLICATES_FILE_ARGUMENT:
426             case USE_ECO_FILE_ARGUMENT:
427             case CHECK_FILE_ARGUMENT:
428             case FILE_OF_FILES_ARGUMENT:
429             case BOUNDS_ARGUMENT:
430             case GAMES_PER_FILE_ARGUMENT:
431             case ECO_OUTPUT_LEVEL_ARGUMENT:
432             case HELP_ARGUMENT:
433             case ALTERNATIVE_HELP_ARGUMENT:
434             case TAG_ROSTER_ARGUMENT:
435             case LONG_FORM_ARGUMENT:
436                 return (ArgType) argument_letter;
437             default:
438                 fprintf(GlobalState.logfile,
439                         "Unrecognized argument: %s in the argument file.\n",
440                         line);
441                 exit(1);
442                 return NO_ARGUMENT_MATCH;
443         }
444     }
445     else{
446         return NO_ARGUMENT_MATCH;
447     }
448 }
449
450     /* Process the argument character and its associated value.
451      * This function processes arguments from the command line and
452      * from an argument file associated with the -A argument.
453      *
454      * An argument -ofile.pgn would be passed in as:
455      *                'o' and "file.pgn".
456      * A zero-length string for associated_value is not necessarily
457      * an error, e.g. -e has an optional following filename.
458      * NB: If the associated_value is to be used beyond this function,
459      * it must be copied.
460      */
461 void
462 process_argument(char arg_letter,const char *associated_value)
463 {   /* Provide an alias for associated_value because it will
464      * often represent a file name.
465      */
466     const char *filename = skip_leading_spaces(associated_value);
467
468     switch(arg_letter){
469         case WRITE_TO_OUTPUT_FILE_ARGUMENT:
470         case APPEND_TO_OUTPUT_FILE_ARGUMENT:
471           if(GlobalState.ECO_level > 0){
472               fprintf(GlobalState.logfile,"-%c conflicts with -E\n",
473                       arg_letter);
474           }
475           else if(GlobalState.games_per_file > 0){
476               fprintf(GlobalState.logfile,"-%c conflicts with -#\n",
477                       arg_letter);
478           }
479           else if(GlobalState.output_filename != NULL){
480               fprintf(GlobalState.logfile,
481                       "-%c: File %s has already been selected for output.\n",
482                       arg_letter,GlobalState.output_filename);
483               exit(1);
484           }
485           else if(*filename == '\0'){
486               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",arg_letter);
487               exit(1);
488           }
489           else{
490               if(GlobalState.outputfile != NULL){
491                   (void) fclose(GlobalState.outputfile);
492               }
493               if(arg_letter == WRITE_TO_OUTPUT_FILE_ARGUMENT){
494                   GlobalState.outputfile = must_open_file(filename,"w");
495               }
496               else{
497                   GlobalState.outputfile = must_open_file(filename,"a");
498               }
499               GlobalState.output_filename = filename;
500           }
501           break;
502         case WRITE_TO_LOG_FILE_ARGUMENT:
503         case APPEND_TO_LOG_FILE_ARGUMENT:
504           /* Take precautions against multiple log files. */
505           if((GlobalState.logfile != stderr) && (GlobalState.logfile != NULL)){
506                 (void) fclose(GlobalState.logfile);
507           }
508           if(arg_letter == WRITE_TO_LOG_FILE_ARGUMENT){
509                 GlobalState.logfile = fopen(filename,"w");
510           }
511           else{
512                 GlobalState.logfile = fopen(filename,"a");
513           }
514           if(GlobalState.logfile == NULL){
515                 fprintf(stderr,"Unable to open %s for writing.\n",filename);
516                 GlobalState.logfile = stderr;
517           }
518           break;
519         case DUPLICATES_FILE_ARGUMENT:
520           if(*filename == '\0'){
521               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",arg_letter);
522               exit(1);
523           }
524           else if(GlobalState.suppress_duplicates){
525               fprintf(GlobalState.logfile,
526                       "-%c clashes with the -%c flag.\n",arg_letter,
527                       DONT_KEEP_DUPLICATES_ARGUMENT);
528               exit(1);
529           }
530           else{
531               GlobalState.duplicate_file = must_open_file(filename,"w");
532           }
533           break;
534         case USE_ECO_FILE_ARGUMENT:
535           GlobalState.add_ECO = TRUE;
536           if(*filename != '\0'){
537               GlobalState.eco_file = copy_string(filename);
538           }
539           else if((filename = getenv("ECO_FILE")) != NULL){
540               GlobalState.eco_file = filename;
541           }
542           else{
543               /* Use the default which is already set up. */
544           }
545           initEcoTable();
546           break;
547         case ECO_OUTPUT_LEVEL_ARGUMENT:
548           {   unsigned level;
549
550               if(GlobalState.output_filename != NULL){
551                   fprintf(GlobalState.logfile,
552                           "-%c: File %s has already been selected for output.\n",
553                           arg_letter,
554                           GlobalState.output_filename);
555                   exit(1);
556               }
557               else if(GlobalState.games_per_file > 0){
558                   fprintf(GlobalState.logfile,
559                             "-%c conflicts with -#.\n",
560                             arg_letter);
561                   exit(1);
562               }
563               else if(sscanf(associated_value,"%u",&level) != 1){
564                     fprintf(GlobalState.logfile,
565                             "-%c requires a number attached, e.g., -%c1.\n",
566                             arg_letter,arg_letter);
567                      exit(1);
568               }
569               else if((level < MIN_ECO_LEVEL) || (level > MAX_ECO_LEVEL)){
570                     fprintf(GlobalState.logfile,
571                             "-%c level should be between %u and %u.\n",
572                             MIN_ECO_LEVEL,MAX_ECO_LEVEL,arg_letter);
573                     exit(1);
574               }
575               else{
576                     GlobalState.ECO_level = level;
577               }
578           }
579           break;
580         case CHECK_FILE_ARGUMENT:
581           if(*filename != '\0'){
582               /* See if it is a single PGN file, or a list
583                * of files.
584                */
585               size_t len = strlen(filename);
586               /* Check for a .PGN suffix. */
587               const char *suffix = output_file_suffix(SAN);
588
589               if((len > strlen(suffix)) &&
590                     (stringcompare(&filename[len-strlen(suffix)],
591                                             suffix) == 0)){
592                   add_filename_to_source_list(filename,CHECKFILE);
593               }
594               else{
595                   FILE *fp = must_open_file(filename,"r");
596                   add_filename_list_from_file(fp,CHECKFILE);
597                   (void) fclose(fp);
598               }
599           }
600           break;
601         case FILE_OF_FILES_ARGUMENT:
602           if(*filename != '\0'){
603               FILE *fp = must_open_file(filename,"r");
604               add_filename_list_from_file(fp,NORMALFILE);
605               (void) fclose(fp);
606           }
607           else{
608               fprintf(GlobalState.logfile,"Filename expected with -%c\n",
609                       arg_letter);
610           }
611           break;
612         case BOUNDS_ARGUMENT:
613           {
614               /* Bounds on the number of moves are to be found.
615                * "l#" means less-than-or-equal-to.
616                * "g#" means greater-than-or-equal-to.
617                * Otherwise "#" (or "e#") means that number.
618                */
619               /* Equal by default. */
620               char which = 'e';
621               unsigned value;
622               Boolean Ok = TRUE;
623               const char *bound = associated_value;
624
625               switch(*bound){
626                 case 'l':
627                 case 'u':
628                 case 'e':
629                   which = *bound;
630                   bound++;
631                   break;
632                 default:
633                   if(!isdigit((int) *bound)){
634                       fprintf(GlobalState.logfile,
635                               "-%c must be followed by e, l, or u.\n",
636                               arg_letter);
637                       Ok = FALSE;
638                   }
639                   break;
640               }
641               if(Ok && (sscanf(bound,"%u",&value) == 1)){
642                 GlobalState.check_move_bounds = TRUE;
643                 switch(which){
644                   case 'e':
645                         GlobalState.lower_move_bound = value; 
646                         GlobalState.upper_move_bound = value; 
647                         break;
648                   case 'l':
649                     if(value <= GlobalState.upper_move_bound){
650                         GlobalState.lower_move_bound = value; 
651                     }
652                     else{
653                         fprintf(GlobalState.logfile,
654                            "Lower bound is greater than the upper bound; -%c ignored.\n",
655                            arg_letter);
656                         Ok = FALSE;
657                     }
658                     break;
659                   case 'u':
660                     if(value >= GlobalState.lower_move_bound){
661                         GlobalState.upper_move_bound = value; 
662                     }
663                     else{
664                         fprintf(GlobalState.logfile,
665                            "Upper bound is smaller than the lower bound; -%c ignored.\n",
666                            arg_letter);
667                         Ok = FALSE;
668                     }
669                     break;
670                 }
671              }
672              else{
673                fprintf(GlobalState.logfile,
674                        "-%c should be in the form -%c[elu]number.\n",
675                        arg_letter,arg_letter);
676                Ok = FALSE;
677              }
678              if(!Ok){
679                 exit(1);
680              }
681          }
682          break;
683         case GAMES_PER_FILE_ARGUMENT:
684           if(GlobalState.ECO_level > 0){
685               fprintf(GlobalState.logfile,
686                       "-%c conflicts with -E.\n",arg_letter);
687               exit(1);
688           }
689           else if(GlobalState.output_filename != NULL){
690               fprintf(GlobalState.logfile,
691                         "-%c: File %s has already been selected for output.\n",
692                         arg_letter,
693                         GlobalState.output_filename);
694               exit(1);
695           }
696           else if(sscanf(associated_value,"%u",
697                        &GlobalState.games_per_file) != 1){
698             fprintf(GlobalState.logfile,
699                     "-%c should be followed by an unsigned integer.\n",
700                     arg_letter);
701             exit(1);
702           }
703           else{
704             /* Value set. */
705           }
706           break;
707         case FILE_OF_ARGUMENTS_ARGUMENT:
708           if(*filename != '\0'){
709               /* @@@ Potentially recursive call. Is this safe? */
710               read_args_file(filename);
711           }
712           else{
713               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",
714                       arg_letter);
715           }
716           break;
717         case NON_MATCHING_GAMES_ARGUMENT:
718           if(*filename != '\0'){
719               if(GlobalState.non_matching_file != NULL){
720                   (void) fclose(GlobalState.non_matching_file);
721               }
722               GlobalState.non_matching_file = must_open_file(filename,"w");
723           }
724           else{
725               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",arg_letter);
726               exit(1);
727           }
728           break;
729         case TAG_EXTRACTION_ARGUMENT:
730             /* A single tag extraction criterion. */
731             extract_tag_argument(associated_value);
732             break;
733         case LINE_WIDTH_ARGUMENT:
734             { /* Specify an output line width. */
735               unsigned length;
736
737               if(sscanf(associated_value,"%u",&length) > 0){
738                   set_output_line_length(length);
739               }
740               else{
741                   fprintf(GlobalState.logfile,
742                           "-%c should be followed by an unsigned integer.\n",
743                           arg_letter);
744                   exit(1);
745               }
746             }
747             break;
748         case HELP_ARGUMENT:
749             usage_and_exit();
750             break;
751         case OUTPUT_FORMAT_ARGUMENT:
752             /* Whether to use the source form of moves or
753              * rewrite them into another format.
754              */
755             {
756                 OutputFormat format = which_output_format(associated_value);
757                 if(format == UCI) {
758                     /* Rewrite the game in a format suitable for input to
759                      * a UCI-compatible engine.
760                      * This is actually LALG but involves adjusting a lot of
761                      * the other statuses, too.
762                      */
763                     GlobalState.keep_NAGs = FALSE;
764                     GlobalState.keep_comments = FALSE;
765                     GlobalState.keep_move_numbers = FALSE;
766                     GlobalState.keep_checks = FALSE;
767                     GlobalState.keep_variations = FALSE;
768                     set_output_line_length(5000);
769                     format = LALG;
770                 }
771                 GlobalState.output_format = format;
772             }
773             break;
774         case SEVEN_TAG_ROSTER_ARGUMENT:
775             if(GlobalState.tag_output_format == ALL_TAGS ||
776                GlobalState.tag_output_format == SEVEN_TAG_ROSTER) {
777                 GlobalState.tag_output_format = SEVEN_TAG_ROSTER;
778             }
779             else {
780                 fprintf(GlobalState.logfile,
781                         "-%c clashes with another argument.\n",
782                         SEVEN_TAG_ROSTER_ARGUMENT);
783                 exit(1);
784             }
785             break;
786         case DONT_KEEP_COMMENTS_ARGUMENT:
787             GlobalState.keep_comments = FALSE;
788             break;
789         case DONT_KEEP_DUPLICATES_ARGUMENT:
790             /* Make sure that this doesn't clash with -d. */
791             if(GlobalState.duplicate_file == NULL){
792                 GlobalState.suppress_duplicates = TRUE;
793             }
794             else{
795                 fprintf(GlobalState.logfile,
796                             "-%c clashes with -%c flag.\n",
797                             DONT_KEEP_DUPLICATES_ARGUMENT,
798                             DUPLICATES_FILE_ARGUMENT);
799                 exit(1);
800             }
801             break;
802         case DONT_MATCH_PERMUTATIONS_ARGUMENT:
803             GlobalState.match_permutations = FALSE;
804             break;
805         case DONT_KEEP_NAGS_ARGUMENT:
806             GlobalState.keep_NAGs = FALSE;
807             break;
808         case OUTPUT_FEN_STRING_ARGUMENT:
809             /* Output a FEN string of the final position.
810              * This is displayed in a comment.
811              */
812             if(GlobalState.add_FEN_comments) {
813                 /* Already implied. */
814                 GlobalState.output_FEN_string = FALSE;
815             }
816             else {
817                 GlobalState.output_FEN_string = TRUE;
818             }
819             break;
820         case CHECK_ONLY_ARGUMENT:
821             /* Report errors, but don't convert. */
822             GlobalState.check_only = TRUE;
823             break;
824         case KEEP_SILENT_ARGUMENT:
825             /* Turn off progress reporting. */
826             GlobalState.verbose = FALSE;
827             break;
828         case USE_SOUNDEX_ARGUMENT:
829             /* Use soundex matches for player tags. */
830             GlobalState.use_soundex = TRUE;
831             break;
832         case MATCH_CHECKMATE_ARGUMENT:
833             /* Match only games that end in checkmate. */
834             GlobalState.match_only_checkmate = TRUE;
835             break;
836         case SUPPRESS_ORIGINALS_ARGUMENT:
837             GlobalState.suppress_originals = TRUE;
838             break;
839         case DONT_KEEP_VARIATIONS_ARGUMENT:
840             GlobalState.keep_variations = FALSE;
841             break;
842         case USE_VIRTUAL_HASH_TABLE_ARGUMENT:
843             GlobalState.use_virtual_hash_table = TRUE;
844             break;
845
846         case TAGS_ARGUMENT:
847             if(*filename != '\0'){
848                 read_tag_file(filename);
849             }
850             break;
851         case TAG_ROSTER_ARGUMENT:
852             if(*filename != '\0'){
853                 read_tag_roster_file(filename);
854             }
855             break;
856         case MOVES_ARGUMENT:
857             if(*filename != '\0'){
858                 /* Where the list of variations of interest are kept. */
859                 FILE *variation_file = must_open_file(filename,"r");
860                 /* We wish to search for particular variations. */
861                 add_textual_variations_from_file(variation_file);
862                 fclose(variation_file);
863             }
864             break;
865         case POSITIONS_ARGUMENT:
866             if(*filename != '\0'){
867                 FILE *variation_file = must_open_file(filename,"r");
868                 /* We wish to search for positional variations. */
869                 add_positional_variations_from_file(variation_file);
870                 fclose(variation_file);
871             }
872             break;
873         case ENDINGS_ARGUMENT:
874             if(*filename != '\0'){
875                 if(!build_endings(filename)){
876                     exit(1);
877                 }
878             }
879             break;
880         default:
881             fprintf(GlobalState.logfile,
882                     "Unrecognized argument -%c\n", arg_letter);
883     }
884 }
885
886     /* The argument has been expressed in a long-form, i.e. prefixed
887      * by --
888      * Decode and act on the argument.
889      * The associated_value will only be required by some arguments.
890      * Return whether one or both were required.
891      */
892 int
893 process_long_form_argument(const char *argument, const char *associated_value)
894 {
895     if(stringcompare(argument, "addhashcode") == 0) {
896         GlobalState.add_hashcode_tag = TRUE;
897         return 1;
898     }
899     else if(stringcompare(argument, "append") == 0) {
900         process_argument(APPEND_TO_OUTPUT_FILE_ARGUMENT, associated_value);
901         return 2;
902     }
903     else if(stringcompare(argument, "checkfile") == 0) {
904         process_argument(CHECK_FILE_ARGUMENT, associated_value);
905         return 2;
906     }
907     else if(stringcompare(argument, "checkmate") == 0) {
908         process_argument(MATCH_CHECKMATE_ARGUMENT, "");
909         return 1;
910     }
911     else if(stringcompare(argument, "duplicates") == 0) {
912         process_argument(DUPLICATES_FILE_ARGUMENT, associated_value);
913         return 2;
914     }
915     else if(stringcompare(argument, "evaluation") == 0) {
916         /* Output an evaluation is required with each move. */
917         GlobalState.output_evaluation = TRUE;
918         return 1;
919     }
920     else if(stringcompare(argument, "fencomments") == 0) {
921         /* Output an evaluation is required with each move. */
922         GlobalState.add_FEN_comments = TRUE;
923         /* Turn off any separate setting of output_FEN_comment. */
924         GlobalState.output_FEN_string = FALSE;
925         return 1;
926     }
927     else if(stringcompare(argument, "fuzzydepth") == 0) {
928         /* Extract the depth. */
929         int depth = 0;
930
931         if(sscanf(associated_value, "%d",&depth) == 1){
932             if(depth >= 0) {
933                 GlobalState.fuzzy_match_duplicates = TRUE;
934                 GlobalState.fuzzy_match_depth = depth;
935             }
936             else {
937                 fprintf(GlobalState.logfile,
938                         "--%s requires a number greater than or equal to zero.\n", argument);
939                 exit(1);
940             }
941         }
942         else {
943             fprintf(GlobalState.logfile,
944                     "--%s requires a number following it.\n", argument);
945             exit(1);
946         }
947         return 2;
948     }
949     else if(stringcompare(argument, "help") == 0) {
950         process_argument(HELP_ARGUMENT, "");
951         return 1;
952     }
953     else if(stringcompare(argument, "keepbroken") == 0) {
954         GlobalState.keep_broken_games = TRUE;
955         return 1;
956     }
957     else if(stringcompare(argument, "linelength") == 0) {
958         process_argument(LINE_WIDTH_ARGUMENT,
959                          associated_value);
960         return 2;
961     }
962     else if(stringcompare(argument, "markmatches") == 0) {
963         if(*associated_value != '\0') {
964             GlobalState.add_position_match_comments = TRUE;
965             GlobalState.position_match_comment = copy_string(associated_value);
966         }
967         else {
968               fprintf(GlobalState.logfile,
969                         "--markmatches requires a comment string following it.\n");
970               exit(1);
971         }
972         return 2;
973     }
974     else if(stringcompare(argument, "nochecks") == 0) {
975         GlobalState.keep_checks = FALSE;
976         return 1;
977     }
978     else if(stringcompare(argument, "nocomments") == 0) {
979         process_argument(DONT_KEEP_COMMENTS_ARGUMENT, "");
980         return 1;
981     }
982     else if(stringcompare(argument, "noduplicates") == 0) {
983         process_argument(DONT_KEEP_DUPLICATES_ARGUMENT, "");
984         return 1;
985     }
986     else if(stringcompare(argument, "nomovenumbers") == 0) {
987         GlobalState.keep_move_numbers = FALSE;
988         return 1;
989     }
990     else if(stringcompare(argument, "nonags") == 0) {
991         process_argument(DONT_KEEP_NAGS_ARGUMENT, "");
992         return 1;
993     }
994     else if(stringcompare(argument, "noresults") == 0) {
995         GlobalState.keep_results = FALSE;
996         return 1;
997     }
998     else if(stringcompare(argument, "notags") == 0) {
999         if(GlobalState.tag_output_format == ALL_TAGS ||
1000            GlobalState.tag_output_format == NO_TAGS) {
1001             GlobalState.tag_output_format = NO_TAGS;
1002         }
1003         else {
1004             fprintf(GlobalState.logfile,
1005                     "--notags clashes with another argument.\n");
1006             exit(1);
1007         }
1008         return 1;
1009     }
1010     else if(stringcompare(argument, "nounique") == 0) {
1011         process_argument(SUPPRESS_ORIGINALS_ARGUMENT, "");
1012         return 1;
1013     }
1014     else if(stringcompare(argument, "novars") == 0) {
1015         process_argument(DONT_KEEP_VARIATIONS_ARGUMENT, "");
1016         return 1;
1017     }
1018     else if(stringcompare(argument, "selectonly") == 0) {
1019           unsigned long selection = 0;
1020
1021           /* Extract the selected match number. */
1022           if(sscanf(associated_value, "%lu",&selection) == 1){
1023               if(selection > 0) {
1024                   GlobalState.matching_game_number = selection;
1025               }
1026               else {
1027                   fprintf(GlobalState.logfile,
1028                         "--%s requires a number greater than zero.\n", argument);
1029                   exit(1);
1030               }
1031           }
1032           else {
1033               fprintf(GlobalState.logfile,
1034                         "--%s requires a number greater than zero following it.\n", argument);
1035               exit(1);
1036           }
1037           return 2;
1038     }
1039     else if(stringcompare(argument, "output") == 0) {
1040         process_argument(WRITE_TO_OUTPUT_FILE_ARGUMENT, associated_value);
1041         return 2;
1042     }
1043     else if(stringcompare(argument, "plylimit") == 0) {
1044           int limit = 0;
1045
1046           /* Extract the limit. */
1047           if(sscanf(associated_value, "%d",&limit) == 1){
1048               if(limit >= 0) {
1049                   GlobalState.output_ply_limit = limit;
1050               }
1051               else {
1052                   fprintf(GlobalState.logfile,
1053                         "--%s requires a number greater than or equal to zero.\n", argument);
1054                   exit(1);
1055               }
1056           }
1057           else {
1058               fprintf(GlobalState.logfile,
1059                         "--%s requires a number following it.\n", argument);
1060               exit(1);
1061           }
1062           return 2;
1063     }
1064     else if(stringcompare(argument, "seven") == 0) {
1065         process_argument(SEVEN_TAG_ROSTER_ARGUMENT, "");
1066         return 1;
1067     }
1068     else if(stringcompare(argument, "stalemate") == 0) {
1069         GlobalState.match_only_stalemate = TRUE;
1070         return 1;
1071     }
1072     else if(stringcompare(argument, "totalplycount") == 0) {
1073         GlobalState.output_total_plycount = TRUE;
1074         return 1;
1075     }
1076     else if(stringcompare(argument, "version") == 0) {
1077         fprintf(GlobalState.logfile, "pgn-extract %s\n", CURRENT_VERSION);
1078         exit(0);
1079         return 1;
1080     }
1081     else {
1082         fprintf(GlobalState.logfile,
1083                 "Unrecognised long-form argument: --%s\n",
1084                 argument);
1085         exit(1);
1086         return 1;
1087     }
1088 }