]> gitweb.fperrin.net Git - Dictionary.git/blob - jars/icu4j-52_1/tools/build/src/com/ibm/icu/dev/tool/docs/CodeMangler.java
Added flags.
[Dictionary.git] / jars / icu4j-52_1 / tools / build / src / com / ibm / icu / dev / tool / docs / CodeMangler.java
1 /**
2 *******************************************************************************
3 * Copyright (C) 2004-2012, International Business Machines Corporation and    *
4 * others. All Rights Reserved.                                                *
5 *******************************************************************************
6 */
7
8 package com.ibm.icu.dev.tool.docs;
9
10 import java.io.BufferedReader;
11 import java.io.File;
12 import java.io.FileInputStream;
13 import java.io.FileOutputStream;
14 import java.io.FilenameFilter;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.InputStreamReader;
18 import java.io.PrintStream;
19 import java.util.ArrayList;
20 import java.util.HashMap;
21 import java.util.Iterator;
22 import java.util.Map;
23 import java.util.TreeMap;
24
25
26 /**
27  * A simple facility for adding C-like preprocessing to .java files.
28  * This only understands a subset of the C preprocessing syntax.
29  * Its used to manage files that with only small differences can be
30  * compiled for different JVMs.  This changes files in place, 
31  * commenting out lines based on the current flag settings.
32  */
33 public class CodeMangler {
34     private File indir;        // root of input
35     private File outdir;       // root of output
36     private String suffix;     // suffix to process, default '.jpp'
37     private boolean recurse;   // true if recurse on directories
38     private boolean force;     // true if force reprocess of files
39     private boolean clean;     // true if output is to be cleaned
40     private boolean timestamp; // true if we read/write timestamp
41     private boolean nonames;   // true if no names in header
42     private HashMap map;       // defines
43     private ArrayList names;   // files/directories to process
44     private String header;     // sorted list of defines passed in
45
46     private boolean verbose; // true if we emit debug output
47
48     private static final String IGNORE_PREFIX = "//##";
49     private static final String HEADER_PREFIX = "//##header";
50
51     public static void main(String[] args) {
52         new CodeMangler(args).run();
53     }
54
55     private static final String usage = "Usage:\n" +
56         "    CodeMangler [flags] file... dir... @argfile... \n" +
57         "-in[dir] path          - root directory of input files, otherwise use current directory\n" +
58         "-out[dir] path         - root directory of output files, otherwise use input directory\n" +
59         "-s[uffix] string       - suffix of inputfiles to process, otherwise use '.java' (directories only)\n" +
60         "-c[lean]               - remove all control flags from code on output (does not proceed if overwriting)\n" +
61         "-r[ecurse]             - if present, recursively process subdirectories\n" +
62         "-f[orce]               - force reprocessing of files even if timestamp and headers match\n" +
63         "-t[imestamp]           - expect/write timestamp in header\n" +
64         "-dNAME[=VALUE]         - define NAME with optional value VALUE\n" +
65         "  (or -d NAME[=VALUE])\n" +
66         "-n                     - do not put NAME/VALUE in header\n" +
67         "-help                  - print this usage message and exit.\n" +
68         "\n" +
69         "For file arguments, output '.java' files using the same path/name under the output directory.\n" +
70         "For directory arguments, process all files with the defined suffix in the directory.\n" +
71         "  (if recursing, do the same for all files recursively under each directory)\n" +
72         "For @argfile arguments, read the specified text file (strip the '@'), and process each line of that file as \n" +
73         "an argument.\n" +
74         "\n" +
75         "Directives are one of the following:\n" +
76         "  #ifdef, #ifndef, #else, #endif, #if, #elif, #define, #undef\n" +
77         "These may optionally be preceeded by whitespace or //.\n" +
78         "#if, #elif args are of the form 'key == value' or 'key != value'.\n" +
79         "Only exact character match key with value is performed.\n" +
80         "#define args are 'key [==] value', the '==' is optional.\n";
81
82     CodeMangler(String[] args) {
83         map = new HashMap();
84         names = new ArrayList();
85         suffix = ".java";
86         clean = false;
87         timestamp = false;
88
89         String inname = null;
90         String outname = null;
91         boolean processArgs = true;
92         String arg = null;
93         try {
94             for (int i = 0; i < args.length; ++i) {
95                 arg = args[i];
96                 if ("--".equals(arg)) {
97                     processArgs = false;
98                 } else if (processArgs && arg.charAt(0) == '-') {
99                     if (arg.startsWith("-in")) {
100                         inname = args[++i];
101                     } else if (arg.startsWith("-out")) {
102                         outname = args[++i];
103                     } else if (arg.startsWith("-d")) {
104                         String id = arg.substring(2);
105                         if (id.length() == 0) {
106                             id = args[++i];
107                         }
108                         String val = "";
109                         int ix = id.indexOf('=');
110                         if (ix >= 0) {
111                             val = id.substring(ix+1);
112                             id = id.substring(0,ix);
113                         }
114                         map.put(id, val);
115                     } else if (arg.startsWith("-s")) {
116                         suffix = args[++i];
117                     } else if (arg.startsWith("-r")) {
118                         recurse = true;
119                     } else if (arg.startsWith("-f")) {
120                         force = true;
121                     } else if (arg.startsWith("-c")) {
122                         clean = true;
123                     } else if (arg.startsWith("-t")) {
124                         timestamp = true;
125                     } else if (arg.startsWith("-h")) {
126                         System.out.print(usage);
127                         break; // stop before processing arguments, so we will do nothing
128                     } else if (arg.startsWith("-v")) {
129                         verbose = true;
130                     } else if (arg.startsWith("-n")) {
131                         nonames = true;
132                     } else {
133                         System.err.println("Error: unrecognized argument '" + arg + "'");
134                         System.err.println(usage);
135                         throw new IllegalArgumentException(arg);
136                     }
137                 } else {
138                     if (arg.charAt(0) == '@') {
139                         File argfile = new File(arg.substring(1));
140                         if (argfile.exists() && !argfile.isDirectory()) {
141                             BufferedReader br = null;
142                             try {
143                                 br = new BufferedReader(new InputStreamReader(new FileInputStream(argfile)));
144                                 ArrayList list = new ArrayList();
145                                 for (int x = 0; x < args.length; ++x) {
146                                     list.add(args[x]);
147                                 }
148                                 String line;
149                                 while (null != (line = br.readLine())) {
150                                     line = line.trim();
151                                     if (line.length() > 0 && line.charAt(0) != '#') {
152                                         if (verbose) System.out.println("adding argument: " + line);
153                                         list.add(line);
154                                     }
155                                 }
156                                 args = (String[])list.toArray(new String[list.size()]);
157                             }
158                             catch (IOException e) {
159                                 System.err.println("error reading arg file: " + e);
160                             }
161                             finally {
162                                 if (br != null) {
163                                     try {
164                                         br.close();
165                                     } catch (Exception e){
166                                         // ignore
167                                     }
168                                 }
169                             }
170                         }
171                     } else {
172                         names.add(arg);
173                     }
174                 }
175             }
176         } catch (IndexOutOfBoundsException e) {
177             String msg = "Error: argument '" + arg + "' missing value";
178             System.err.println(msg);
179             System.err.println(usage);
180             throw new IllegalArgumentException(msg);
181         }
182
183         String username = System.getProperty("user.dir");
184         if (inname == null) {
185             inname = username;
186         } else if (!(inname.startsWith("\\") || inname.startsWith("/"))) {
187             inname = username + File.separator + inname;
188         }
189         indir = new File(inname);
190         try {
191             indir = indir.getCanonicalFile();
192         }
193         catch (IOException e) {
194             // continue, but most likely we'll fail later
195         }
196         if (!indir.exists()) {
197             throw new IllegalArgumentException("Input directory '" + indir.getAbsolutePath() + "' does not exist.");
198         } else if (!indir.isDirectory()) {
199             throw new IllegalArgumentException("Input path '" + indir.getAbsolutePath() + "' is not a directory.");
200         }
201         if (verbose) System.out.println("indir: " + indir.getAbsolutePath());
202
203         if (outname == null) {
204             outname = inname;
205         } else if (!(outname.startsWith("\\") || outname.startsWith("/"))) {
206             outname = username + File.separator + outname;
207         }
208         outdir = new File(outname);
209         try {
210             outdir = outdir.getCanonicalFile();
211         }
212         catch (IOException e) {
213             // continue, but most likely we'll fail later
214         }
215         if (!outdir.exists()) {
216             throw new IllegalArgumentException("Output directory '" + outdir.getAbsolutePath() + "' does not exist.");
217         } else if (!outdir.isDirectory()) {
218             throw new IllegalArgumentException("Output path '" + outdir.getAbsolutePath() + "' is not a directory.");
219         }
220         if (verbose) System.out.println("outdir: " + outdir.getAbsolutePath());
221
222         if (clean && suffix.equals(".java")) {
223             try {
224                 if (outdir.getCanonicalPath().equals(indir.getCanonicalPath())) {
225                     throw new IllegalArgumentException("Cannot use 'clean' to overwrite .java files in same directory tree");
226                 }
227             }
228             catch (IOException e) {
229                 System.err.println("possible overwrite, error: " + e.getMessage());
230                 throw new IllegalArgumentException("Cannot use 'clean' to overrwrite .java files");
231             }
232         }
233
234         if (names.isEmpty()) {
235             names.add(".");
236         }
237
238         TreeMap sort = new TreeMap(String.CASE_INSENSITIVE_ORDER);
239         sort.putAll(map);
240         Iterator iter = sort.entrySet().iterator();
241         StringBuffer buf = new StringBuffer();
242         if (!nonames) {
243             while (iter.hasNext()) {
244                 Map.Entry e = (Map.Entry)iter.next();
245                 if (buf.length() > 0) {
246                     buf.append(", ");
247                 }
248                 buf.append(e.getKey());
249                 String v = (String)e.getValue();
250                 if (v != null && v.length() > 0) {
251                     buf.append('=');
252                     buf.append(v);
253                 }
254             }
255         }
256         header = buf.toString();
257     }
258
259     public int run() {
260         return process("", (String[])names.toArray(new String[names.size()]));
261     }
262
263     public int process(String path, String[] filenames) {
264         if (verbose) System.out.println("path: '" + path + "'");
265         int count = 0;
266         for (int i = 0; i < filenames.length; ++i) {
267             if (verbose) System.out.println("name " + i + " of " + filenames.length + ": '" + filenames[i] + "'");
268             String name = path + filenames[i];
269             File fin = new File(indir, name);
270             try {
271                 fin = fin.getCanonicalFile();
272             }
273             catch (IOException e) {
274             }
275             if (!fin.exists()) {
276                 System.err.println("File " + fin.getAbsolutePath() + " does not exist.");
277                 continue;
278             }
279             if (fin.isFile()) {
280                 if (verbose) System.out.println("processing file: '" + fin.getAbsolutePath() + "'");
281                 String oname;
282                 int ix = name.lastIndexOf(".");
283                 if (ix != -1) {
284                     oname = name.substring(0, ix);
285                 } else {
286                     oname = name;
287                 }
288                 oname += ".java";
289                 File fout = new File(outdir, oname);
290                 if (processFile(fin, fout)) {
291                     ++count;
292                 }
293             } else if (fin.isDirectory()) {
294                 if (verbose) System.out.println("recursing on directory '" + fin.getAbsolutePath() + "'");
295                 String npath = ".".equals(name) ? path : path + fin.getName() + File.separator;
296                 count += process(npath, fin.list(filter)); // recursive call
297             }
298         }
299         return count;
300     }
301
302                 
303     private final FilenameFilter filter = new FilenameFilter() {
304             public boolean accept(File dir, String name) {
305                 File f = new File(dir, name);
306                 return (f.isFile() && name.endsWith(suffix)) || (f.isDirectory() && recurse);
307             }
308         };
309
310     public boolean processFile(File infile, File outfile) {
311         File backup = null;
312
313         class State {
314             int lc;
315             String line;
316             boolean emit = true;
317             boolean tripped;
318             private State next;
319
320             public String toString() {
321                 return "line " + lc 
322                     + ": '" + line 
323                     + "' (emit: " + emit 
324                     + " tripped: " + tripped 
325                     + ")";
326             }
327
328             void trip(boolean trip) {
329                 if (!tripped & trip) {
330                     tripped = true;
331                     emit = next != null ? next.emit : true;
332                 } else {
333                     emit = false;
334                 }
335             }
336                         
337             State push(int lc, String line, boolean trip) {
338                 this.lc = lc;
339                 this.line = line;
340                 State ret = new State();
341                 ret.next = this;
342                 ret.emit = this.emit & trip;
343                 ret.tripped = trip;
344                 return ret;
345             }
346
347             State pop() {
348                 return next;
349             }
350         }
351           
352         HashMap oldMap = null;
353         
354         long outModTime = 0;
355
356         try {
357             PrintStream outstream = null;
358             InputStream instream = new FileInputStream(infile);
359
360             BufferedReader reader = new BufferedReader(new InputStreamReader(instream));
361             int lc = 0;
362             State state = new State();
363             String line;
364             while ((line = reader.readLine()) != null) {
365                 if (lc == 0) { // check and write header for output file if needed
366                     boolean hasHeader = line.startsWith(HEADER_PREFIX);
367                     if (hasHeader && !force) {
368                         long expectLastModified = ((infile.lastModified() + 999)/1000)*1000;
369                         String headerline = HEADER_PREFIX;
370                         if (header.length() > 0) {
371                             headerline += " ";
372                             headerline += header;
373                         }
374                         if (timestamp) {
375                             headerline += " ";
376                             headerline += String.valueOf(expectLastModified);
377                         }
378                         if (line.equals(headerline)) {
379                             if (verbose) System.out.println("no changes necessary to " + infile.getCanonicalPath());
380                             reader.close();
381                             return false; // nothing to do
382                         }
383                         if (verbose) {
384                             System.out.println("  old header:  " + line);
385                             System.out.println("  != expected: " + headerline);
386                         }
387                     }
388
389                     // create output file directory structure
390                     String outpname = outfile.getParent();
391                     if (outpname != null) {
392                         File outp = new File(outpname);
393                         if (!(outp.exists() || outp.mkdirs())) {
394                             System.err.println("could not create directory: '" + outpname + "'");
395                             reader.close();
396                             return false;
397                         }
398                     }
399
400                     // if we're overwriting, use a temporary file
401                     if (suffix.equals(".java")) {
402                         backup = outfile;
403                         try {
404                             outfile = File.createTempFile(outfile.getName(), null, outfile.getParentFile());
405                         }
406                         catch (IOException ex) {
407                             System.err.println(ex.getMessage());
408                             reader.close();
409                             return false;
410                         }
411                     }
412
413                     outModTime = ((outfile.lastModified()+999)/1000)*1000; // round up
414                     outstream = new PrintStream(new FileOutputStream(outfile));
415                     String headerline = HEADER_PREFIX;
416                     if (header.length() > 0) {
417                         headerline += " ";
418                         headerline += header;
419                     }
420                     if (timestamp) {
421                         headerline += " ";
422                         headerline += String.valueOf(outModTime);
423                     }
424                     outstream.println(headerline);
425                     if (verbose) System.out.println("header: " + headerline);
426
427                     // discard the old header if we had one, otherwise match this line like any other
428                     if (hasHeader) {
429                         ++lc; // mark as having read a line so we never reexecute this block
430                         continue;
431                     }
432                 }
433                 
434                 String[] res = new String[3];
435                 if (patMatch(line, res)) {
436                     String lead = res[0];
437                     String key = res[1];
438                     String val = res[2];
439
440                     if (verbose) System.out.println("directive: " + line
441                                                     + " key: '" + key
442                                                     + "' val: '" + val 
443                                                     + "' " + state);
444                     if (key.equals("ifdef")) {
445                         state = state.push(lc, line, map.get(val) != null);
446                     } else if (key.equals("ifndef")) {
447                         state = state.push(lc, line, map.get(val) == null);
448                     } else if (key.equals("else")) {
449                         state.trip(true);
450                     } else if (key.equals("endif")) {
451                         state = state.pop();
452                     } else if (key.equals("undef")) {
453                         if (state.emit) {
454                             if (oldMap == null) {
455                                 oldMap = (HashMap)map.clone();
456                             }
457                             map.remove(val);
458                         }
459                     } else if (key.equals("define")) {
460                         if (pat2Match(val, res)) {
461                             String key2 = res[0];
462                             String val2 = res[2];
463
464                             if (verbose) System.out.println("val2: '" + val2 
465                                                             + "' key2: '" + key2 
466                                                             + "'");
467                             if (state.emit) {
468                                 if (oldMap == null) {
469                                     oldMap = (HashMap)map.clone();
470                                 }
471                                 map.put(key2, val2);
472                             }
473                         }
474                     } else { // #if, #elif
475                         // only top level OR (||) operator is supported for now
476                         int count = 1;
477                         int index = 0;
478                         while ((index = val.indexOf("||", index)) > 0) {
479                             count++;
480                             index++;
481                         }
482                         String[] expressions = new String[count];
483                         if (count == 1) {
484                             expressions[0] = val;
485                         } else {
486                             int start = 0;
487                             index = 0;
488                             count = 0;
489                             while (true) {
490                                 index = val.indexOf("||", start);
491                                 if (index > 0) {
492                                     expressions[count++] = val.substring(start, index);
493                                     start = index + 2;
494                                 } else {
495                                     expressions[count++] = val.substring(start);
496                                     break;
497                                 }
498                             }
499                         }
500                         boolean eval = false;
501                         for (count = 0; count < expressions.length && !eval; count++) {
502                             if (pat2Match(expressions[count], res)) {
503                                 String key2 = res[0];
504                                 String val2 = res[2];
505
506                                 if (key2.equals("defined")) {
507                                     // defined command
508                                     if (verbose) System.out.println(
509                                             "index: '" + count
510                                             + "' val2: '" + val2 
511                                             + "' key2: '" + key2 
512                                             + "'");
513                                     eval = map.containsKey(val2);
514                                 } else {
515                                     boolean neq = false;
516                                     if (res[1].equals("!=")) {
517                                         neq = true;
518                                     } else if (!res[1].equals("==")) {
519                                         System.err.println("Invalid expression: '" + val);
520                                     }
521                                     if (verbose) System.out.println(
522                                             "index: '" + count
523                                             + "' val2: '" + val2 
524                                             + "' neq: '" + neq 
525                                             + "' key2: '" + key2 
526                                             + "'");
527                                     eval = (val2.equals(map.get(key2)) != neq);
528                                 }
529                             }
530                         }
531                         if (key.equals("if")) {
532                             state = state.push(lc, line, eval);
533                         } else if (key.equals("elif")) {
534                             state.trip(eval);
535                         }
536                     }
537                     if (!clean) {
538                         lc++;
539                         if (!lead.equals("//")) {
540                             outstream.print("//");
541                             line = line.substring(lead.length());
542                         }
543                         outstream.println(line);
544                     }
545                     continue;
546                 }
547
548                 lc++;
549                 String found = pat3Match(line);
550                 boolean hasIgnore = found != null;
551                 if (state.emit == hasIgnore) {
552                     if (state.emit) {
553                         line = line.substring(found.length());
554                     } else {
555                         line = IGNORE_PREFIX + line;
556                     }
557                 } else if (hasIgnore && !found.equals(IGNORE_PREFIX)) {
558                     line = IGNORE_PREFIX + line.substring(found.length());
559                 }
560                 if (!clean || state.emit) {
561                     outstream.println(line);
562                 }
563             }
564
565             state = state.pop();
566             if (state != null) {
567                 System.err.println("Error: unclosed directive(s):");
568                 do {
569                     System.err.println(state);
570                 } while ((state = state.pop()) != null);
571                 System.err.println(" in file: " + outfile.getCanonicalPath());
572                 if (oldMap != null) {
573                     map = oldMap;
574                 }
575                 reader.close();
576                 outstream.close();
577                 return false;
578             }
579                 
580             outstream.close();
581             instream.close();
582
583             if (backup != null) {
584                 if (backup.exists()) {
585                     backup.delete();
586                 }
587                 outfile.renameTo(backup);
588             }
589
590             if (timestamp) {
591                 outfile.setLastModified(outModTime); // synch with timestamp
592             }
593
594             if (oldMap != null) {
595                 map = oldMap;
596             }
597         }
598         catch (IOException e) {
599             System.err.println(e);
600             return false;
601         }
602         return true;
603     }
604
605
606     /**
607      * Perform same operation as matching on pat.  on exit
608      * leadKeyValue contains the three strings lead, key, and value.
609      * 'lead' is the portion before the #ifdef directive.  'key' is
610      * the directive.  'value' is the portion after the directive.  if
611      * there is a match, return true, else return false.
612      */
613     static boolean patMatch(String line, String[] leadKeyValue) {
614         if (line.length() == 0) {
615             return false;
616         }
617         if (!line.endsWith("\n")) {
618             line = line + '\n';
619         }
620         int mark = 0;
621         int state = 0;
622         loop: for (int i = 0; i < line.length(); ++i) {
623             char c = line.charAt(i);
624             switch (state) {
625             case 0: // at start of line, haven't seen anything but whitespace yet
626                 if (c == ' ' || c == '\t' || c == '\r') continue;
627                 if (c == '/') { state = 1; continue; }
628                 if (c == '#') { state = 4; continue; }
629                 return false;
630             case 1: // have seen a single slash after start of line
631                 if (c == '/') { state = 2; continue; }
632                 return false;
633             case 2: // have seen two or more slashes
634                 if (c == '/') continue;
635                 if (c == ' ' || c == '\t' || c == '\r') { state = 3; continue; }
636                 if (c == '#') { state = 4; continue; }
637                 return false;
638             case 3: // have seen a space after two or more slashes
639                 if (c == ' ' || c == '\t' || c == '\r') continue;
640                 if (c == '#') { state = 4; continue; }
641                 return false;
642             case 4: // have seen a '#' 
643                 leadKeyValue[0] = line.substring(mark, i-1);
644                 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { mark = i; state = 5; continue; }
645                 return false;
646             case 5: // an ascii char followed the '#'
647                 if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) continue;
648                 if (c == ' ' || c == '\t' || c == '\n') {
649                     String key = line.substring(mark, i).toLowerCase();
650                     if (key.equals("ifdef") ||
651                         key.equals("ifndef") ||
652                         key.equals("else") ||
653                         key.equals("endif") ||
654                         key.equals("undef") ||
655                         key.equals("define") ||
656                         key.equals("if") ||
657                         key.equals("elif")) {
658                         leadKeyValue[1] = key;
659                         mark = i;
660                         state = 6;
661                         break loop;
662                     }
663                 }
664                 return false;
665             default:
666                 throw new IllegalStateException();
667             }
668         }
669         if (state == 6) {
670             leadKeyValue[2] = line.substring(mark, line.length()).trim();
671             return true;
672         }
673         return false; // never reached, does the compiler know this?
674     }
675
676     /**
677      * Perform same operation as matching on pat2.  on exit
678      * keyRelValue contains the three strings key, rel, and value.
679      * 'key' is the portion before the relation (or final word).  'rel' is
680      * the relation, if present, either == or !=.  'value' is the final
681      * word.  if there is a match, return true, else return false.
682      */
683     static boolean pat2Match(String line, String[] keyRelVal) {
684
685         if (line.length() == 0) {
686             return false;
687         }
688         keyRelVal[0] = keyRelVal[1] = keyRelVal[2] = "";
689         int mark = 0;
690         int state = 0;
691         String command = null;
692         loop: for (int i = 0; i < line.length(); ++i) {
693             char c = line.charAt(i);
694             switch (state) {
695             case 0: // saw beginning or space, no rel yet
696                 if (c == ' ' || c == '\t' || c == '\n') {
697                     continue;
698                 }
699                 if ((c == '!' || c == '=')) {
700                     return false;
701                 }
702                 state = 1;
703                 continue;
704             case 1: // saw start of a word
705                 if (c == ' ' || c == '\t') {
706                     state = 2;
707                 }
708                 else if (c == '(') {
709                     command = line.substring(0, i).trim();
710                     if (!command.equals("defined")) {
711                         return false;
712                     }
713                     keyRelVal[0] = command;
714                     state = 2;
715                 }
716                 else if (c == '!' || c == '=') {
717                     state = 3;
718                 }
719                 continue;
720             case 2: // saw end of word, and space
721                 if (c == ' ' || c == '\t') {
722                     continue;
723                 }
724                 else if (command == null && c == '(') {
725                     continue;
726                 }
727                 else if (c == '!' || c == '=') {
728                     state = 3;
729                     continue;
730                 }
731                 keyRelVal[0] = line.substring(0, i-1).trim();
732                 mark = i;
733                 state = 4;
734                 break loop;
735             case 3: // saw end of word, and '!' or '='
736                 if (c == '=') {
737                     keyRelVal[0] = line.substring(0, i-1).trim();
738                     keyRelVal[1] = line.substring(i-1, i+1);
739                     mark = i+1;
740                     state = 4;
741                     break loop;
742                 }
743                 return false;
744             default:
745                 break;
746             }
747         }
748         switch (state) {
749         case 0: 
750             return false; // found nothing
751         case 1: 
752         case 2:
753             keyRelVal[0] = line.trim(); break; // found only a word
754         case 3:
755             return false; // found a word and '!' or '=" then end of line, incomplete
756         case 4:
757             keyRelVal[2] = line.substring(mark).trim(); // found a word, possible rel, and who knows what
758             if (command != null) {
759                 int len = keyRelVal[2].length();
760                 if (keyRelVal[2].charAt(len - 1) != ')') {
761                     // closing parenthesis is missing
762                     return false;
763                 }
764                 keyRelVal[2] = keyRelVal[2].substring(0, len - 1).trim();
765             }
766             break;
767         default: 
768             throw new IllegalStateException();
769         }
770         return true;
771     }
772
773     static String pat3Match(String line) {
774         int state = 0;
775         loop: for (int i = 0; i < line.length(); ++i) {
776             char c = line.charAt(i);
777             switch(state) {
778             case 0: if (c == ' ' || c == '\t') continue;
779                 if (c == '/') { state = 1; continue; }
780                 break loop;
781             case 1:
782                 if (c == '/') { state = 2; continue; }
783                 break loop;
784             case 2:
785                 if (c == '#') { state = 3; continue; }
786                 break loop;
787             case 3:
788                 if (c == '#') return line.substring(0, i+1);
789                 break loop;
790             default:
791                 break loop;
792             }
793         }
794         return null;
795     }
796 }