]> git.sesse.net Git - pgn-extract/blob - argsfile.c
e2caa076645d738b20e8c7158f29d051e7822dd3
[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       "--dumpeco - dump a list of all ECO hashes with names, then quit",
180       "--duplicates - see -d",
181       "--evaluation - include a position evaluation after each move",
182       "--fencomments - include a FEN string after each move",
183       "--fuzzydepth plies - positional duplicates match",
184       "--help - see -h",
185       "--keepbroken - retain games with errors",
186       "--linelength - see -w",
187       "--markmatches - mark positional and material matches with a comment; see -t, -v, and -z",
188       "--nochecks - don't output + and # after moves.",
189       "--nocomments - see -C",
190       "--noduplicates - see -D",
191       "--nomovenumbers - don't output move numbers.",
192       "--nonags - see -N",
193       "--noresults - don't output results.",
194       "--notags - don't output any tags.",
195       "--nounique - see -U",
196       "--novars - see -V",
197       "--output - see -o",
198       "--plylimit - limit the number of plies output.",
199       "--selectonly N - only output the Nth matched game (N > 0)",
200       "--seven - see -7",
201       "--stalemate - only output games that end in stalemate.",
202       "--totalplycount - include a tag with the total number of plies in a game.",
203       "--version - print the current version number and exit.",
204
205       /* Must be NULL terminated. */
206       (char *)NULL,
207   };
208
209   const char **data = help_data;
210
211   fprintf(GlobalState.logfile,
212           "pgn-extract %s (%s): a Portable Game Notation (PGN) manipulator.\n",
213           CURRENT_VERSION,__DATE__);
214   fprintf(GlobalState.logfile,
215           "Copyright (C) 1994-2014 David J. Barnes (d.j.barnes@kent.ac.uk)\n");
216   fprintf(GlobalState.logfile,"%s\n\n",URL);
217   fprintf(GlobalState.logfile,"Usage: pgn-extract [arguments] [file.pgn ...]\n");
218   
219   for(; *data != NULL; data++){
220       fprintf(GlobalState.logfile,"%s\n",*data);
221   }
222   exit(1);
223 }
224
225 void
226 read_args_file(const char *infile)
227 {   char *line;
228     FILE *fp = fopen(infile,"r");
229
230     if(fp == NULL){
231         fprintf(GlobalState.logfile,"Cannot open %s for reading.\n",infile);
232         exit(1);
233     }
234     else{
235         ArgType linetype = NO_ARGUMENT_MATCH;
236         ArgType nexttype;
237         while((line = read_line(fp)) != NULL){
238             if(blank_line(line)){
239                 (void) free(line);
240                 continue;
241             }
242             nexttype = classify_arg(line);
243             if(nexttype == NO_ARGUMENT_MATCH){
244                 if(*line == argument_prefix[0]){
245                     /* Treat the line as a source file name. */
246                     add_filename_to_source_list(&line[1],NORMALFILE);
247                 }
248                 else if(linetype != NO_ARGUMENT_MATCH){
249                     /* Handle the line. */
250                     switch(linetype){
251                         case MOVES_ARGUMENT:
252                             add_textual_variation_from_line(line);
253                             break;
254                         case POSITIONS_ARGUMENT:
255                             add_positional_variation_from_line(line);
256                             break;
257                         case TAGS_ARGUMENT:
258                             process_tag_line(infile,line);
259                             break;
260                         case TAG_ROSTER_ARGUMENT:
261                             process_roster_line(line);
262                             break;
263                         case ENDINGS_ARGUMENT:
264                             process_ending_line(line);
265                             (void) free(line);
266                             break;
267                         default:
268                             fprintf(GlobalState.logfile,
269                                     "Internal error: unknown linetype %d in read_args_file\n",
270                                     linetype);
271                             (void) free(line);
272                             exit(-1);
273                     }
274                 }
275                 else{
276                     /* It should have been a line applying to the
277                      * current linetype.
278                      */
279                     fprintf(GlobalState.logfile,
280                         "Missing argument type for line %s in the argument file.\n",
281                         line);
282                     exit(1);
283                 }
284             }
285             else{
286                 switch(nexttype){
287                         /* Arguments with a possible additional
288                          * argument value.
289                          * All of these apply only to the current
290                          * line in the argument file.
291                          */
292                     case WRITE_TO_OUTPUT_FILE_ARGUMENT:
293                     case APPEND_TO_OUTPUT_FILE_ARGUMENT:
294                     case WRITE_TO_LOG_FILE_ARGUMENT:
295                     case APPEND_TO_LOG_FILE_ARGUMENT:
296                     case DUPLICATES_FILE_ARGUMENT:
297                     case USE_ECO_FILE_ARGUMENT:
298                     case CHECK_FILE_ARGUMENT:
299                     case FILE_OF_FILES_ARGUMENT:
300                     case BOUNDS_ARGUMENT:
301                     case GAMES_PER_FILE_ARGUMENT:
302                     case ECO_OUTPUT_LEVEL_ARGUMENT:
303                     case FILE_OF_ARGUMENTS_ARGUMENT:
304                     case NON_MATCHING_GAMES_ARGUMENT:
305                     case TAG_EXTRACTION_ARGUMENT:
306                     case LINE_WIDTH_ARGUMENT:
307                     case OUTPUT_FORMAT_ARGUMENT:
308                         process_argument(line[argument_prefix_len],
309                                          &line[argument_prefix_len+1]);
310                         linetype = NO_ARGUMENT_MATCH;
311                         break;
312                     case LONG_FORM_ARGUMENT:
313                         {
314                             char *arg = &line[argument_prefix_len+1];
315                             char *space = strchr(arg, ' ');
316                             if(space != NULL) {
317                                 /* We need to drop an associated value from arg. */
318                                 int arglen = space - arg;
319                                 char *just_arg = (char *) MallocOrDie(arglen + 1);
320                                 strncpy(just_arg, arg, arglen);
321                                 just_arg[arglen] = '\0';
322                                 process_long_form_argument(just_arg,
323                                                            skip_leading_spaces(space));
324                             }
325                             else {
326                                 process_long_form_argument(arg, "");
327                                 linetype = NO_ARGUMENT_MATCH;
328                             }
329                         }
330                         break;
331
332                         /* Arguments with no additional
333                          * argument value.
334                          * All of these apply only to the current
335                          * line in the argument file.
336                          */
337                     case SEVEN_TAG_ROSTER_ARGUMENT:
338                     case HELP_ARGUMENT:
339                     case ALTERNATIVE_HELP_ARGUMENT:
340                     case DONT_KEEP_COMMENTS_ARGUMENT:
341                     case DONT_KEEP_DUPLICATES_ARGUMENT:
342                     case DONT_MATCH_PERMUTATIONS_ARGUMENT:
343                     case DONT_KEEP_NAGS_ARGUMENT:
344                     case OUTPUT_FEN_STRING_ARGUMENT:
345                     case CHECK_ONLY_ARGUMENT:
346                     case KEEP_SILENT_ARGUMENT:
347                     case USE_SOUNDEX_ARGUMENT:
348                     case MATCH_CHECKMATE_ARGUMENT:
349                     case SUPPRESS_ORIGINALS_ARGUMENT:
350                     case DONT_KEEP_VARIATIONS_ARGUMENT:
351                     case USE_VIRTUAL_HASH_TABLE_ARGUMENT:
352                         process_argument(line[argument_prefix_len],"");
353                         linetype = NO_ARGUMENT_MATCH;
354                         break;
355
356                         /* Arguments whose values persist beyond
357                          * the current line.
358                          */
359                     case MOVES_ARGUMENT:
360                     case POSITIONS_ARGUMENT:
361                     case ENDINGS_ARGUMENT:
362                     case TAGS_ARGUMENT:
363                     case TAG_ROSTER_ARGUMENT:
364                         process_argument(line[argument_prefix_len],
365                                          &line[argument_prefix_len+1]);
366                         /* Apply this type to subsequent lines. */
367                         linetype = nexttype;
368                         break;
369                     default:
370                         linetype = nexttype;
371                         break;
372                 }
373                 (void) free(line);
374             }
375         }
376         (void) fclose(fp);
377     }
378 }
379
380         /* Determine which (if any) type of argument is
381          * indicated by the contents of the current line.
382          * Arguments are assumed to be surrounded by
383          * square brackets.
384          */
385 static ArgType
386 classify_arg(const char *line)
387 {
388     /* Valid arguments must have at least one character beyond
389      * the prefix.
390      */
391     static const size_t min_argument_length = 1+sizeof(argument_prefix)-1;
392     size_t line_length = strlen(line);
393
394     /* Check for a line of the form:
395      *            :-argument
396      */
397     if((strncmp(line,argument_prefix,argument_prefix_len) == 0) &&
398                    (line_length >= min_argument_length)){
399         char argument_letter = line[argument_prefix_len];
400         switch(argument_letter){
401             case TAGS_ARGUMENT:
402             case MOVES_ARGUMENT:
403             case POSITIONS_ARGUMENT:
404             case ENDINGS_ARGUMENT:
405             case TAG_EXTRACTION_ARGUMENT:
406             case LINE_WIDTH_ARGUMENT:
407             case OUTPUT_FORMAT_ARGUMENT:
408             case SEVEN_TAG_ROSTER_ARGUMENT:
409             case FILE_OF_ARGUMENTS_ARGUMENT:
410             case NON_MATCHING_GAMES_ARGUMENT:
411             case DONT_KEEP_COMMENTS_ARGUMENT:
412             case DONT_KEEP_DUPLICATES_ARGUMENT:
413             case DONT_KEEP_NAGS_ARGUMENT:
414             case DONT_MATCH_PERMUTATIONS_ARGUMENT:
415             case OUTPUT_FEN_STRING_ARGUMENT:
416             case CHECK_ONLY_ARGUMENT:
417             case KEEP_SILENT_ARGUMENT:
418             case USE_SOUNDEX_ARGUMENT:
419             case MATCH_CHECKMATE_ARGUMENT:
420             case SUPPRESS_ORIGINALS_ARGUMENT:
421             case DONT_KEEP_VARIATIONS_ARGUMENT:
422             case WRITE_TO_OUTPUT_FILE_ARGUMENT:
423             case WRITE_TO_LOG_FILE_ARGUMENT:
424             case APPEND_TO_LOG_FILE_ARGUMENT:
425             case APPEND_TO_OUTPUT_FILE_ARGUMENT:
426             case DUPLICATES_FILE_ARGUMENT:
427             case USE_ECO_FILE_ARGUMENT:
428             case CHECK_FILE_ARGUMENT:
429             case FILE_OF_FILES_ARGUMENT:
430             case BOUNDS_ARGUMENT:
431             case GAMES_PER_FILE_ARGUMENT:
432             case ECO_OUTPUT_LEVEL_ARGUMENT:
433             case HELP_ARGUMENT:
434             case ALTERNATIVE_HELP_ARGUMENT:
435             case TAG_ROSTER_ARGUMENT:
436             case LONG_FORM_ARGUMENT:
437                 return (ArgType) argument_letter;
438             default:
439                 fprintf(GlobalState.logfile,
440                         "Unrecognized argument: %s in the argument file.\n",
441                         line);
442                 exit(1);
443                 return NO_ARGUMENT_MATCH;
444         }
445     }
446     else{
447         return NO_ARGUMENT_MATCH;
448     }
449 }
450
451     /* Process the argument character and its associated value.
452      * This function processes arguments from the command line and
453      * from an argument file associated with the -A argument.
454      *
455      * An argument -ofile.pgn would be passed in as:
456      *                'o' and "file.pgn".
457      * A zero-length string for associated_value is not necessarily
458      * an error, e.g. -e has an optional following filename.
459      * NB: If the associated_value is to be used beyond this function,
460      * it must be copied.
461      */
462 void
463 process_argument(char arg_letter,const char *associated_value)
464 {   /* Provide an alias for associated_value because it will
465      * often represent a file name.
466      */
467     const char *filename = skip_leading_spaces(associated_value);
468
469     switch(arg_letter){
470         case WRITE_TO_OUTPUT_FILE_ARGUMENT:
471         case APPEND_TO_OUTPUT_FILE_ARGUMENT:
472           if(GlobalState.ECO_level > 0){
473               fprintf(GlobalState.logfile,"-%c conflicts with -E\n",
474                       arg_letter);
475           }
476           else if(GlobalState.games_per_file > 0){
477               fprintf(GlobalState.logfile,"-%c conflicts with -#\n",
478                       arg_letter);
479           }
480           else if(GlobalState.output_filename != NULL){
481               fprintf(GlobalState.logfile,
482                       "-%c: File %s has already been selected for output.\n",
483                       arg_letter,GlobalState.output_filename);
484               exit(1);
485           }
486           else if(*filename == '\0'){
487               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",arg_letter);
488               exit(1);
489           }
490           else{
491               if(GlobalState.outputfile != NULL){
492                   (void) fclose(GlobalState.outputfile);
493               }
494               if(arg_letter == WRITE_TO_OUTPUT_FILE_ARGUMENT){
495                   GlobalState.outputfile = must_open_file(filename,"w");
496               }
497               else{
498                   GlobalState.outputfile = must_open_file(filename,"a");
499               }
500               GlobalState.output_filename = filename;
501           }
502           break;
503         case WRITE_TO_LOG_FILE_ARGUMENT:
504         case APPEND_TO_LOG_FILE_ARGUMENT:
505           /* Take precautions against multiple log files. */
506           if((GlobalState.logfile != stderr) && (GlobalState.logfile != NULL)){
507                 (void) fclose(GlobalState.logfile);
508           }
509           if(arg_letter == WRITE_TO_LOG_FILE_ARGUMENT){
510                 GlobalState.logfile = fopen(filename,"w");
511           }
512           else{
513                 GlobalState.logfile = fopen(filename,"a");
514           }
515           if(GlobalState.logfile == NULL){
516                 fprintf(stderr,"Unable to open %s for writing.\n",filename);
517                 GlobalState.logfile = stderr;
518           }
519           break;
520         case DUPLICATES_FILE_ARGUMENT:
521           if(*filename == '\0'){
522               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",arg_letter);
523               exit(1);
524           }
525           else if(GlobalState.suppress_duplicates){
526               fprintf(GlobalState.logfile,
527                       "-%c clashes with the -%c flag.\n",arg_letter,
528                       DONT_KEEP_DUPLICATES_ARGUMENT);
529               exit(1);
530           }
531           else{
532               GlobalState.duplicate_file = must_open_file(filename,"w");
533           }
534           break;
535         case USE_ECO_FILE_ARGUMENT:
536           GlobalState.add_ECO = TRUE;
537           if(*filename != '\0'){
538               GlobalState.eco_file = copy_string(filename);
539           }
540           else if((filename = getenv("ECO_FILE")) != NULL){
541               GlobalState.eco_file = filename;
542           }
543           else{
544               /* Use the default which is already set up. */
545           }
546           initEcoTable();
547           break;
548         case ECO_OUTPUT_LEVEL_ARGUMENT:
549           {   unsigned level;
550
551               if(GlobalState.output_filename != NULL){
552                   fprintf(GlobalState.logfile,
553                           "-%c: File %s has already been selected for output.\n",
554                           arg_letter,
555                           GlobalState.output_filename);
556                   exit(1);
557               }
558               else if(GlobalState.games_per_file > 0){
559                   fprintf(GlobalState.logfile,
560                             "-%c conflicts with -#.\n",
561                             arg_letter);
562                   exit(1);
563               }
564               else if(sscanf(associated_value,"%u",&level) != 1){
565                     fprintf(GlobalState.logfile,
566                             "-%c requires a number attached, e.g., -%c1.\n",
567                             arg_letter,arg_letter);
568                      exit(1);
569               }
570               else if((level < MIN_ECO_LEVEL) || (level > MAX_ECO_LEVEL)){
571                     fprintf(GlobalState.logfile,
572                             "-%c level should be between %u and %u.\n",
573                             MIN_ECO_LEVEL,MAX_ECO_LEVEL,arg_letter);
574                     exit(1);
575               }
576               else{
577                     GlobalState.ECO_level = level;
578               }
579           }
580           break;
581         case CHECK_FILE_ARGUMENT:
582           if(*filename != '\0'){
583               /* See if it is a single PGN file, or a list
584                * of files.
585                */
586               size_t len = strlen(filename);
587               /* Check for a .PGN suffix. */
588               const char *suffix = output_file_suffix(SAN);
589
590               if((len > strlen(suffix)) &&
591                     (stringcompare(&filename[len-strlen(suffix)],
592                                             suffix) == 0)){
593                   add_filename_to_source_list(filename,CHECKFILE);
594               }
595               else{
596                   FILE *fp = must_open_file(filename,"r");
597                   add_filename_list_from_file(fp,CHECKFILE);
598                   (void) fclose(fp);
599               }
600           }
601           break;
602         case FILE_OF_FILES_ARGUMENT:
603           if(*filename != '\0'){
604               FILE *fp = must_open_file(filename,"r");
605               add_filename_list_from_file(fp,NORMALFILE);
606               (void) fclose(fp);
607           }
608           else{
609               fprintf(GlobalState.logfile,"Filename expected with -%c\n",
610                       arg_letter);
611           }
612           break;
613         case BOUNDS_ARGUMENT:
614           {
615               /* Bounds on the number of moves are to be found.
616                * "l#" means less-than-or-equal-to.
617                * "g#" means greater-than-or-equal-to.
618                * Otherwise "#" (or "e#") means that number.
619                */
620               /* Equal by default. */
621               char which = 'e';
622               unsigned value;
623               Boolean Ok = TRUE;
624               const char *bound = associated_value;
625
626               switch(*bound){
627                 case 'l':
628                 case 'u':
629                 case 'e':
630                   which = *bound;
631                   bound++;
632                   break;
633                 default:
634                   if(!isdigit((int) *bound)){
635                       fprintf(GlobalState.logfile,
636                               "-%c must be followed by e, l, or u.\n",
637                               arg_letter);
638                       Ok = FALSE;
639                   }
640                   break;
641               }
642               if(Ok && (sscanf(bound,"%u",&value) == 1)){
643                 GlobalState.check_move_bounds = TRUE;
644                 switch(which){
645                   case 'e':
646                         GlobalState.lower_move_bound = value; 
647                         GlobalState.upper_move_bound = value; 
648                         break;
649                   case 'l':
650                     if(value <= GlobalState.upper_move_bound){
651                         GlobalState.lower_move_bound = value; 
652                     }
653                     else{
654                         fprintf(GlobalState.logfile,
655                            "Lower bound is greater than the upper bound; -%c ignored.\n",
656                            arg_letter);
657                         Ok = FALSE;
658                     }
659                     break;
660                   case 'u':
661                     if(value >= GlobalState.lower_move_bound){
662                         GlobalState.upper_move_bound = value; 
663                     }
664                     else{
665                         fprintf(GlobalState.logfile,
666                            "Upper bound is smaller than the lower bound; -%c ignored.\n",
667                            arg_letter);
668                         Ok = FALSE;
669                     }
670                     break;
671                 }
672              }
673              else{
674                fprintf(GlobalState.logfile,
675                        "-%c should be in the form -%c[elu]number.\n",
676                        arg_letter,arg_letter);
677                Ok = FALSE;
678              }
679              if(!Ok){
680                 exit(1);
681              }
682          }
683          break;
684         case GAMES_PER_FILE_ARGUMENT:
685           if(GlobalState.ECO_level > 0){
686               fprintf(GlobalState.logfile,
687                       "-%c conflicts with -E.\n",arg_letter);
688               exit(1);
689           }
690           else if(GlobalState.output_filename != NULL){
691               fprintf(GlobalState.logfile,
692                         "-%c: File %s has already been selected for output.\n",
693                         arg_letter,
694                         GlobalState.output_filename);
695               exit(1);
696           }
697           else if(sscanf(associated_value,"%u",
698                        &GlobalState.games_per_file) != 1){
699             fprintf(GlobalState.logfile,
700                     "-%c should be followed by an unsigned integer.\n",
701                     arg_letter);
702             exit(1);
703           }
704           else{
705             /* Value set. */
706           }
707           break;
708         case FILE_OF_ARGUMENTS_ARGUMENT:
709           if(*filename != '\0'){
710               /* @@@ Potentially recursive call. Is this safe? */
711               read_args_file(filename);
712           }
713           else{
714               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",
715                       arg_letter);
716           }
717           break;
718         case NON_MATCHING_GAMES_ARGUMENT:
719           if(*filename != '\0'){
720               if(GlobalState.non_matching_file != NULL){
721                   (void) fclose(GlobalState.non_matching_file);
722               }
723               GlobalState.non_matching_file = must_open_file(filename,"w");
724           }
725           else{
726               fprintf(GlobalState.logfile,"Usage: -%cfilename.\n",arg_letter);
727               exit(1);
728           }
729           break;
730         case TAG_EXTRACTION_ARGUMENT:
731             /* A single tag extraction criterion. */
732             extract_tag_argument(associated_value);
733             break;
734         case LINE_WIDTH_ARGUMENT:
735             { /* Specify an output line width. */
736               unsigned length;
737
738               if(sscanf(associated_value,"%u",&length) > 0){
739                   set_output_line_length(length);
740               }
741               else{
742                   fprintf(GlobalState.logfile,
743                           "-%c should be followed by an unsigned integer.\n",
744                           arg_letter);
745                   exit(1);
746               }
747             }
748             break;
749         case HELP_ARGUMENT:
750             usage_and_exit();
751             break;
752         case OUTPUT_FORMAT_ARGUMENT:
753             /* Whether to use the source form of moves or
754              * rewrite them into another format.
755              */
756             {
757                 OutputFormat format = which_output_format(associated_value);
758                 if(format == UCI) {
759                     /* Rewrite the game in a format suitable for input to
760                      * a UCI-compatible engine.
761                      * This is actually LALG but involves adjusting a lot of
762                      * the other statuses, too.
763                      */
764                     GlobalState.keep_NAGs = FALSE;
765                     GlobalState.keep_comments = FALSE;
766                     GlobalState.keep_move_numbers = FALSE;
767                     GlobalState.keep_checks = FALSE;
768                     GlobalState.keep_variations = FALSE;
769                     set_output_line_length(5000);
770                     format = LALG;
771                 }
772                 GlobalState.output_format = format;
773             }
774             break;
775         case SEVEN_TAG_ROSTER_ARGUMENT:
776             if(GlobalState.tag_output_format == ALL_TAGS ||
777                GlobalState.tag_output_format == SEVEN_TAG_ROSTER) {
778                 GlobalState.tag_output_format = SEVEN_TAG_ROSTER;
779             }
780             else {
781                 fprintf(GlobalState.logfile,
782                         "-%c clashes with another argument.\n",
783                         SEVEN_TAG_ROSTER_ARGUMENT);
784                 exit(1);
785             }
786             break;
787         case DONT_KEEP_COMMENTS_ARGUMENT:
788             GlobalState.keep_comments = FALSE;
789             break;
790         case DONT_KEEP_DUPLICATES_ARGUMENT:
791             /* Make sure that this doesn't clash with -d. */
792             if(GlobalState.duplicate_file == NULL){
793                 GlobalState.suppress_duplicates = TRUE;
794             }
795             else{
796                 fprintf(GlobalState.logfile,
797                             "-%c clashes with -%c flag.\n",
798                             DONT_KEEP_DUPLICATES_ARGUMENT,
799                             DUPLICATES_FILE_ARGUMENT);
800                 exit(1);
801             }
802             break;
803         case DONT_MATCH_PERMUTATIONS_ARGUMENT:
804             GlobalState.match_permutations = FALSE;
805             break;
806         case DONT_KEEP_NAGS_ARGUMENT:
807             GlobalState.keep_NAGs = FALSE;
808             break;
809         case OUTPUT_FEN_STRING_ARGUMENT:
810             /* Output a FEN string of the final position.
811              * This is displayed in a comment.
812              */
813             if(GlobalState.add_FEN_comments) {
814                 /* Already implied. */
815                 GlobalState.output_FEN_string = FALSE;
816             }
817             else {
818                 GlobalState.output_FEN_string = TRUE;
819             }
820             break;
821         case CHECK_ONLY_ARGUMENT:
822             /* Report errors, but don't convert. */
823             GlobalState.check_only = TRUE;
824             break;
825         case KEEP_SILENT_ARGUMENT:
826             /* Turn off progress reporting. */
827             GlobalState.verbose = FALSE;
828             break;
829         case USE_SOUNDEX_ARGUMENT:
830             /* Use soundex matches for player tags. */
831             GlobalState.use_soundex = TRUE;
832             break;
833         case MATCH_CHECKMATE_ARGUMENT:
834             /* Match only games that end in checkmate. */
835             GlobalState.match_only_checkmate = TRUE;
836             break;
837         case SUPPRESS_ORIGINALS_ARGUMENT:
838             GlobalState.suppress_originals = TRUE;
839             break;
840         case DONT_KEEP_VARIATIONS_ARGUMENT:
841             GlobalState.keep_variations = FALSE;
842             break;
843         case USE_VIRTUAL_HASH_TABLE_ARGUMENT:
844             GlobalState.use_virtual_hash_table = TRUE;
845             break;
846
847         case TAGS_ARGUMENT:
848             if(*filename != '\0'){
849                 read_tag_file(filename);
850             }
851             break;
852         case TAG_ROSTER_ARGUMENT:
853             if(*filename != '\0'){
854                 read_tag_roster_file(filename);
855             }
856             break;
857         case MOVES_ARGUMENT:
858             if(*filename != '\0'){
859                 /* Where the list of variations of interest are kept. */
860                 FILE *variation_file = must_open_file(filename,"r");
861                 /* We wish to search for particular variations. */
862                 add_textual_variations_from_file(variation_file);
863                 fclose(variation_file);
864             }
865             break;
866         case POSITIONS_ARGUMENT:
867             if(*filename != '\0'){
868                 FILE *variation_file = must_open_file(filename,"r");
869                 /* We wish to search for positional variations. */
870                 add_positional_variations_from_file(variation_file);
871                 fclose(variation_file);
872             }
873             break;
874         case ENDINGS_ARGUMENT:
875             if(*filename != '\0'){
876                 if(!build_endings(filename)){
877                     exit(1);
878                 }
879             }
880             break;
881         default:
882             fprintf(GlobalState.logfile,
883                     "Unrecognized argument -%c\n", arg_letter);
884     }
885 }
886
887     /* The argument has been expressed in a long-form, i.e. prefixed
888      * by --
889      * Decode and act on the argument.
890      * The associated_value will only be required by some arguments.
891      * Return whether one or both were required.
892      */
893 int
894 process_long_form_argument(const char *argument, const char *associated_value)
895 {
896     if(stringcompare(argument, "addhashcode") == 0) {
897         GlobalState.add_hashcode_tag = TRUE;
898         return 1;
899     }
900     else if(stringcompare(argument, "append") == 0) {
901         process_argument(APPEND_TO_OUTPUT_FILE_ARGUMENT, associated_value);
902         return 2;
903     }
904     else if(stringcompare(argument, "checkfile") == 0) {
905         process_argument(CHECK_FILE_ARGUMENT, associated_value);
906         return 2;
907     }
908     else if(stringcompare(argument, "checkmate") == 0) {
909         process_argument(MATCH_CHECKMATE_ARGUMENT, "");
910         return 1;
911     }
912     else if(stringcompare(argument, "dumpeco") == 0) {
913         GlobalState.dump_eco = TRUE;
914         return 1;
915     }
916     else if(stringcompare(argument, "duplicates") == 0) {
917         process_argument(DUPLICATES_FILE_ARGUMENT, associated_value);
918         return 2;
919     }
920     else if(stringcompare(argument, "evaluation") == 0) {
921         /* Output an evaluation is required with each move. */
922         GlobalState.output_evaluation = TRUE;
923         return 1;
924     }
925     else if(stringcompare(argument, "fencomments") == 0) {
926         /* Output an evaluation is required with each move. */
927         GlobalState.add_FEN_comments = TRUE;
928         /* Turn off any separate setting of output_FEN_comment. */
929         GlobalState.output_FEN_string = FALSE;
930         return 1;
931     }
932     else if(stringcompare(argument, "fuzzydepth") == 0) {
933         /* Extract the depth. */
934         int depth = 0;
935
936         if(sscanf(associated_value, "%d",&depth) == 1){
937             if(depth >= 0) {
938                 GlobalState.fuzzy_match_duplicates = TRUE;
939                 GlobalState.fuzzy_match_depth = depth;
940             }
941             else {
942                 fprintf(GlobalState.logfile,
943                         "--%s requires a number greater than or equal to zero.\n", argument);
944                 exit(1);
945             }
946         }
947         else {
948             fprintf(GlobalState.logfile,
949                     "--%s requires a number following it.\n", argument);
950             exit(1);
951         }
952         return 2;
953     }
954     else if(stringcompare(argument, "help") == 0) {
955         process_argument(HELP_ARGUMENT, "");
956         return 1;
957     }
958     else if(stringcompare(argument, "keepbroken") == 0) {
959         GlobalState.keep_broken_games = TRUE;
960         return 1;
961     }
962     else if(stringcompare(argument, "linelength") == 0) {
963         process_argument(LINE_WIDTH_ARGUMENT,
964                          associated_value);
965         return 2;
966     }
967     else if(stringcompare(argument, "markmatches") == 0) {
968         if(*associated_value != '\0') {
969             GlobalState.add_position_match_comments = TRUE;
970             GlobalState.position_match_comment = copy_string(associated_value);
971         }
972         else {
973               fprintf(GlobalState.logfile,
974                         "--markmatches requires a comment string following it.\n");
975               exit(1);
976         }
977         return 2;
978     }
979     else if(stringcompare(argument, "nochecks") == 0) {
980         GlobalState.keep_checks = FALSE;
981         return 1;
982     }
983     else if(stringcompare(argument, "nocomments") == 0) {
984         process_argument(DONT_KEEP_COMMENTS_ARGUMENT, "");
985         return 1;
986     }
987     else if(stringcompare(argument, "noduplicates") == 0) {
988         process_argument(DONT_KEEP_DUPLICATES_ARGUMENT, "");
989         return 1;
990     }
991     else if(stringcompare(argument, "nomovenumbers") == 0) {
992         GlobalState.keep_move_numbers = FALSE;
993         return 1;
994     }
995     else if(stringcompare(argument, "nonags") == 0) {
996         process_argument(DONT_KEEP_NAGS_ARGUMENT, "");
997         return 1;
998     }
999     else if(stringcompare(argument, "noresults") == 0) {
1000         GlobalState.keep_results = FALSE;
1001         return 1;
1002     }
1003     else if(stringcompare(argument, "notags") == 0) {
1004         if(GlobalState.tag_output_format == ALL_TAGS ||
1005            GlobalState.tag_output_format == NO_TAGS) {
1006             GlobalState.tag_output_format = NO_TAGS;
1007         }
1008         else {
1009             fprintf(GlobalState.logfile,
1010                     "--notags clashes with another argument.\n");
1011             exit(1);
1012         }
1013         return 1;
1014     }
1015     else if(stringcompare(argument, "nounique") == 0) {
1016         process_argument(SUPPRESS_ORIGINALS_ARGUMENT, "");
1017         return 1;
1018     }
1019     else if(stringcompare(argument, "novars") == 0) {
1020         process_argument(DONT_KEEP_VARIATIONS_ARGUMENT, "");
1021         return 1;
1022     }
1023     else if(stringcompare(argument, "selectonly") == 0) {
1024           unsigned long selection = 0;
1025
1026           /* Extract the selected match number. */
1027           if(sscanf(associated_value, "%lu",&selection) == 1){
1028               if(selection > 0) {
1029                   GlobalState.matching_game_number = selection;
1030               }
1031               else {
1032                   fprintf(GlobalState.logfile,
1033                         "--%s requires a number greater than zero.\n", argument);
1034                   exit(1);
1035               }
1036           }
1037           else {
1038               fprintf(GlobalState.logfile,
1039                         "--%s requires a number greater than zero following it.\n", argument);
1040               exit(1);
1041           }
1042           return 2;
1043     }
1044     else if(stringcompare(argument, "output") == 0) {
1045         process_argument(WRITE_TO_OUTPUT_FILE_ARGUMENT, associated_value);
1046         return 2;
1047     }
1048     else if(stringcompare(argument, "plylimit") == 0) {
1049           int limit = 0;
1050
1051           /* Extract the limit. */
1052           if(sscanf(associated_value, "%d",&limit) == 1){
1053               if(limit >= 0) {
1054                   GlobalState.output_ply_limit = limit;
1055               }
1056               else {
1057                   fprintf(GlobalState.logfile,
1058                         "--%s requires a number greater than or equal to zero.\n", argument);
1059                   exit(1);
1060               }
1061           }
1062           else {
1063               fprintf(GlobalState.logfile,
1064                         "--%s requires a number following it.\n", argument);
1065               exit(1);
1066           }
1067           return 2;
1068     }
1069     else if(stringcompare(argument, "seven") == 0) {
1070         process_argument(SEVEN_TAG_ROSTER_ARGUMENT, "");
1071         return 1;
1072     }
1073     else if(stringcompare(argument, "stalemate") == 0) {
1074         GlobalState.match_only_stalemate = TRUE;
1075         return 1;
1076     }
1077     else if(stringcompare(argument, "totalplycount") == 0) {
1078         GlobalState.output_total_plycount = TRUE;
1079         return 1;
1080     }
1081     else if(stringcompare(argument, "version") == 0) {
1082         fprintf(GlobalState.logfile, "pgn-extract %s\n", CURRENT_VERSION);
1083         exit(0);
1084         return 1;
1085     }
1086     else {
1087         fprintf(GlobalState.logfile,
1088                 "Unrecognised long-form argument: --%s\n",
1089                 argument);
1090         exit(1);
1091         return 1;
1092     }
1093 }