JAL-629 Doing things with the command line arguments
[jalview.git] / src / jalview / bin / ArgParser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.bin;
22
23 import java.net.URLDecoder;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.EnumSet;
28 import java.util.Enumeration;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33
34 import jalview.util.Platform;
35
36 public class ArgParser
37 {
38   private static final String NEGATESTRING = "no";
39
40   private static final String DEFAULTLINKEDID = "";
41
42   private static enum Opt
43   {
44     BOOLEAN, STRING, UNARY, MULTI, LINKED, ORDERED
45   }
46
47   public enum Arg
48   {
49     /*
50     NOCALCULATION, NOMENUBAR, NOSTATUS, SHOWOVERVIEW, ANNOTATIONS, COLOUR,
51     FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, NOANNOTATION, NOANNOTATION2,
52     NODISPLAY, NOGUI, NONEWS, NOQUESTIONNAIRE, NOSORTBYTREE, NOUSAGESTATS,
53     OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, SORTBYTREE, TREE, VDOC,
54     VSESS;
55     */
56     HELP("h"), CALCULATION, MENUBAR, STATUS, SHOWOVERVIEW, ANNOTATIONS,
57     COLOUR, FEATURES, GROOVY, GROUPS, HEADLESS, JABAWS, ANNOTATION,
58     ANNOTATION2, DISPLAY, GUI, NEWS, NOQUESTIONNAIRE, SORTBYTREE,
59     USAGESTATS, OPEN, OPEN2, PROPS, QUESTIONNAIRE, SETPROP, TREE, VDOC,
60     VSESS, OUTPUT, OUTPUTTYPE, SSANNOTATION, NOTEMPFAC, TEMPFAC,
61     TEMPFAC_LABEL, TEMPFAC_DESC, TEMPFAC_SHADING, TITLE;
62
63     static
64     {
65       HELP.setOptions(Opt.UNARY);
66       CALCULATION.setOptions(true, Opt.BOOLEAN); // default "true" implies only
67                                                  // expecting "--nocalculation"
68       MENUBAR.setOptions(true, Opt.BOOLEAN);
69       STATUS.setOptions(true, Opt.BOOLEAN);
70       SHOWOVERVIEW.setOptions(Opt.UNARY, Opt.LINKED);
71       ANNOTATIONS.setOptions(Opt.STRING, Opt.LINKED);
72       COLOUR.setOptions(Opt.STRING, Opt.LINKED);
73       FEATURES.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
74       GROOVY.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
75       GROUPS.setOptions(Opt.STRING, Opt.LINKED);
76       HEADLESS.setOptions(Opt.UNARY);
77       JABAWS.setOptions(Opt.STRING);
78       ANNOTATION.setOptions(true, Opt.BOOLEAN);
79       ANNOTATION2.setOptions(true, Opt.BOOLEAN);
80       DISPLAY.setOptions(true, Opt.BOOLEAN);
81       GUI.setOptions(true, Opt.BOOLEAN);
82       NEWS.setOptions(true, Opt.BOOLEAN);
83       NOQUESTIONNAIRE.setOptions(Opt.UNARY); // unary as --questionnaire=val
84                                              // expects a string value
85       SORTBYTREE.setOptions(true, Opt.BOOLEAN);
86       USAGESTATS.setOptions(true, Opt.BOOLEAN);
87       OPEN.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
88       OPEN2.setOptions(Opt.STRING, Opt.LINKED);
89       PROPS.setOptions(Opt.STRING);
90       QUESTIONNAIRE.setOptions(Opt.STRING);
91       SETPROP.setOptions(Opt.STRING);
92       TREE.setOptions(Opt.STRING);
93
94       VDOC.setOptions(Opt.UNARY);
95       VSESS.setOptions(Opt.UNARY);
96
97       OUTPUT.setOptions(Opt.STRING, Opt.LINKED);
98       OUTPUTTYPE.setOptions(Opt.STRING, Opt.LINKED, Opt.MULTI);
99
100       SSANNOTATION.setOptions(Opt.BOOLEAN, Opt.LINKED);
101       NOTEMPFAC.setOptions(Opt.UNARY, Opt.LINKED);
102       TEMPFAC.setOptions(Opt.STRING, Opt.LINKED);
103       TEMPFAC_LABEL.setOptions(Opt.STRING, Opt.LINKED);
104       TEMPFAC_DESC.setOptions(Opt.STRING, Opt.LINKED);
105       TEMPFAC_SHADING.setOptions(Opt.STRING, Opt.LINKED);
106       TITLE.setOptions(Opt.STRING, Opt.LINKED);
107     }
108
109     private final String[] argNames;
110
111     private Opt[] argOptions;
112
113     private boolean defaultBoolValue = false;
114
115     public String toLongString()
116     {
117       StringBuilder sb = new StringBuilder();
118       sb.append("Arg: ").append(this.name());
119       for (String name : getNames())
120       {
121         sb.append(", '").append(name).append("'");
122       }
123       sb.append("\nOptions: ");
124       boolean first = true;
125       for (Opt o : argOptions)
126       {
127         if (!first)
128         {
129           sb.append(", ");
130         }
131         sb.append(o.toString());
132         first = false;
133       }
134       sb.append("\n");
135       return sb.toString();
136     }
137
138     private Arg()
139     {
140       this(new String[0]);
141     }
142
143     private Arg(String... names)
144     {
145       int length = (names == null || names.length == 0
146               || (names.length == 1 && names[0] == null)) ? 1
147                       : names.length + 1;
148       this.argNames = new String[length];
149       this.argNames[0] = this.getName();
150       if (length > 1)
151         System.arraycopy(names, 0, this.argNames, 1, names.length);
152     }
153
154     public String[] getNames()
155     {
156       return argNames;
157     }
158
159     public String getName()
160     {
161       return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
162     }
163
164     @Override
165     public final String toString()
166     {
167       return getName();
168     }
169
170     public boolean hasOption(Opt o)
171     {
172       if (argOptions == null)
173         return false;
174       for (Opt option : argOptions)
175       {
176         if (o == option)
177           return true;
178       }
179       return false;
180     }
181
182     protected void setOptions(Opt... options)
183     {
184       setOptions(false, options);
185     }
186
187     protected void setOptions(boolean defaultBoolValue, Opt... options)
188     {
189       this.defaultBoolValue = defaultBoolValue;
190       argOptions = options;
191     }
192
193     protected boolean getDefaultBoolValue()
194     {
195       return defaultBoolValue;
196     }
197   }
198
199   public static class ArgValues
200   {
201     private Arg arg;
202
203     private int argCount = 0;
204
205     private boolean boolValue = false;
206
207     private boolean negated = false;
208
209     private List<String> argsList;
210
211     protected ArgValues(Arg a)
212     {
213       this.arg = a;
214       this.argsList = new ArrayList<String>();
215       this.boolValue = arg.getDefaultBoolValue();
216     }
217
218     public Arg arg()
219     {
220       return arg;
221     }
222
223     protected int getCount()
224     {
225       return argCount;
226     }
227
228     protected void incrementCount()
229     {
230       argCount++;
231     }
232
233     protected void setNegated(boolean b)
234     {
235       this.negated = b;
236     }
237
238     protected boolean isNegated()
239     {
240       return this.negated;
241     }
242
243     protected void setBoolean(boolean b)
244     {
245       this.boolValue = b;
246     }
247
248     protected boolean getBoolean()
249     {
250       return this.boolValue;
251     }
252
253     @Override
254     public String toString()
255     {
256       if (argsList == null)
257         return null;
258       StringBuilder sb = new StringBuilder();
259       sb.append(arg.toLongString());
260       if (arg.hasOption(Opt.BOOLEAN) || arg.hasOption(Opt.UNARY))
261         sb.append("Boolean: ").append(boolValue).append("; Default: ")
262                 .append(arg.getDefaultBoolValue()).append("; Negated: ")
263                 .append(negated).append("\n");
264       if (arg.hasOption(Opt.STRING))
265       {
266         sb.append("Values:");
267         boolean first = true;
268         for (String v : argsList)
269         {
270           if (!first)
271             sb.append(",");
272           sb.append("\n  '");
273           sb.append(v).append("'");
274           first = false;
275         }
276         sb.append("\n");
277       }
278       sb.append("Count: ").append(argCount).append("\n");
279       return sb.toString();
280     }
281
282     protected void addValue()
283     {
284       addValue(null);
285     }
286
287     protected void addValue(String val)
288     {
289       addValue(val, false);
290     }
291
292     protected void addValue(String val, boolean noDuplicates)
293     {
294       if ((!arg.hasOption(Opt.MULTI) && argsList.size() > 0)
295               || (noDuplicates && argsList.contains(val)))
296         return;
297       if (argsList == null)
298       {
299         Console.warn("** inst");
300         argsList = new ArrayList<String>();
301       }
302       argsList.add(val);
303     }
304
305     protected boolean hasValue(String val)
306     {
307       return argsList.contains(val);
308     }
309
310     protected String getValue()
311     {
312       if (arg.hasOption(Opt.MULTI))
313         Console.warn("Requesting single value for multi value argument");
314       return argsList.size() > 0 ? argsList.get(0) : null;
315     }
316
317     protected List<String> getValues()
318     {
319       return argsList;
320     }
321   }
322
323   // old style
324   private List<String> vargs = null;
325
326   private boolean isApplet;
327
328   // private AppletParams appletParams;
329
330   public boolean isApplet()
331   {
332     return isApplet;
333   }
334
335   public String nextValue()
336   {
337     return vargs.remove(0);
338   }
339
340   public int getSize()
341   {
342     return vargs.size();
343   }
344
345   public String getValue(String arg)
346   {
347     return getValue(arg, false);
348   }
349
350   public String getValue(String arg, boolean utf8decode)
351   {
352     int index = vargs.indexOf(arg);
353     String dc = null, ret = null;
354     if (index != -1)
355     {
356       ret = vargs.get(index + 1).toString();
357       vargs.remove(index);
358       vargs.remove(index);
359       if (utf8decode && ret != null)
360       {
361         try
362         {
363           dc = URLDecoder.decode(ret, "UTF-8");
364           ret = dc;
365         } catch (Exception e)
366         {
367           // TODO: log failure to decode
368         }
369       }
370     }
371     return ret;
372   }
373
374   /*
375   public Object getAppletValue(String key, String def, boolean asString)
376   {
377     Object value;
378     return (appletParams == null ? null
379             : (value = appletParams.get(key.toLowerCase())) == null ? def
380                     : asString ? "" + value : value);
381   }
382   */
383
384   // new style
385   private static final Map<String, Arg> argMap;
386
387   private Map<String, HashMap<Arg, ArgValues>> linkedArgs = new HashMap<>();
388
389   private List<String> linkedOrder = null;
390
391   private List<Arg> argList;
392
393   static
394   {
395     argMap = new HashMap<>();
396     for (Arg a : EnumSet.allOf(Arg.class))
397     {
398       ARGNAME: for (String argName : a.getNames())
399       {
400         if (argMap.containsKey(argName))
401         {
402           Console.warn("Trying to add argument name multiple times: '"
403                   + argName + "'"); // RESTORE THIS WHEN MERGED
404           if (argMap.get(argName) != a)
405           {
406             Console.error(
407                     "Trying to add argument name multiple times for different Args: '"
408                             + argMap.get(argName).getName() + ":" + argName
409                             + "' and '" + a.getName() + ":" + argName
410                             + "'");
411           }
412           continue ARGNAME;
413         }
414         argMap.put(argName, a);
415       }
416     }
417   }
418
419   public ArgParser(String[] args)
420   {
421     // old style
422     vargs = new ArrayList<>();
423     isApplet = (args.length > 0 && args[0].startsWith("<applet"));
424     if (isApplet)
425     {
426       // appletParams = AppletParams.getAppletParams(args, vargs);
427     }
428     else
429     {
430       if (Platform.isJS())
431
432       {
433         isApplet = true;
434         // appletParams =
435         // AppletParams.getAppletParams(Platform.getAppletInfoAsMap(), vargs);
436       }
437       for (int i = 0; i < args.length; i++)
438       {
439         String arg = args[i].trim();
440         if (arg.charAt(0) == '-')
441         {
442           arg = arg.substring(1);
443         }
444         vargs.add(arg);
445       }
446     }
447
448     // new style
449     Enumeration<String> argE = Collections.enumeration(Arrays.asList(args));
450     ARG: while (argE.hasMoreElements())
451     {
452       String arg = argE.nextElement();
453       String argName = null;
454       String val = null;
455       String linkedId = null;
456       if (arg.startsWith("--"))
457       {
458         int equalPos = arg.indexOf('=');
459         if (equalPos > -1)
460         {
461           argName = arg.substring(2, equalPos);
462           val = arg.substring(equalPos + 1);
463         }
464         else
465         {
466           argName = arg.substring(2);
467         }
468         int idOpen = argName.indexOf('[');
469         int idClose = argName.indexOf(']');
470
471         if (idOpen > -1 && idClose == argName.length() - 1)
472         {
473           linkedId = argName.substring(idOpen + 1, idClose);
474           argName = argName.substring(0, idOpen);
475         }
476
477         Arg a = argMap.get(argName);
478         // check for boolean prepended by "no"
479         boolean negated = false;
480         if (a == null && argName.startsWith(NEGATESTRING) && argMap
481                 .containsKey(argName.substring(NEGATESTRING.length())))
482         {
483           argName = argName.substring(NEGATESTRING.length());
484           a = argMap.get(argName);
485           negated = true;
486         }
487
488         // check for config errors
489         if (a == null)
490         {
491           // arg not found
492           Console.error("Argument '" + arg + "' not recognised. Ignoring.");
493           continue ARG;
494         }
495         if (!a.hasOption(Opt.BOOLEAN) && negated)
496         {
497           // used "no" with a non-boolean option
498           Console.error("Argument '--" + NEGATESTRING + argName
499                   + "' not a boolean option. Ignoring.");
500           continue ARG;
501         }
502         if (!a.hasOption(Opt.STRING) && equalPos > -1)
503         {
504           // set --argname=value when arg does not accept values
505           Console.error("Argument '--" + argName
506                   + "' does not expect a value (given as '" + arg
507                   + "').  Ignoring.");
508           continue ARG;
509         }
510         if (!a.hasOption(Opt.LINKED) && linkedId != null)
511         {
512           // set --argname[linkedId] when arg does not use linkedIds
513           Console.error("Argument '--" + argName
514                   + "' does not expect a linked id (given as '" + arg
515                   + "'). Ignoring.");
516           continue ARG;
517         }
518
519         if (a.hasOption(Opt.STRING) && equalPos == -1)
520         {
521           // take next arg as value if required, and '=' was not found
522           if (!argE.hasMoreElements())
523           {
524             // no value to take for arg, which wants a value
525             Console.error("Argument '" + a.getName()
526                     + "' requires a value, none given. Ignoring.");
527             continue ARG;
528           }
529           val = argE.nextElement();
530         }
531
532         // use default linkedId for linked arguments
533         if (a.hasOption(Opt.LINKED) && linkedId == null)
534           linkedId = DEFAULTLINKEDID;
535
536         if (!linkedArgs.containsKey(linkedId))
537           linkedArgs.put(linkedId, new HashMap<>());
538
539         Map<Arg, ArgValues> valuesMap = linkedArgs.get(linkedId);
540         if (!valuesMap.containsKey(a))
541           valuesMap.put(a, new ArgValues(a));
542
543         ArgValues values = valuesMap.get(a);
544         if (values == null)
545         {
546           values = new ArgValues(a);
547         }
548         // store appropriate value
549         if (a.hasOption(Opt.STRING))
550         {
551           values.addValue(val);
552         }
553         else if (a.hasOption(Opt.BOOLEAN))
554         {
555           values.setBoolean(!negated);
556           values.setNegated(negated);
557         }
558         else if (a.hasOption(Opt.UNARY))
559         {
560           values.setBoolean(true);
561         }
562         values.incrementCount();
563
564         // store in appropriate place
565         if (a.hasOption(Opt.LINKED))
566         {
567           // allow a default linked id for single usage
568           if (linkedId == null)
569             linkedId = DEFAULTLINKEDID;
570           // store the order of linkedIds
571           if (linkedOrder == null)
572             linkedOrder = new ArrayList<>();
573           if (!linkedOrder.contains(linkedId))
574             linkedOrder.add(linkedId);
575         }
576         // store the ArgValues
577         valuesMap.put(a, values);
578
579         // store arg in the list of args
580         if (argList == null)
581           argList = new ArrayList<>();
582         if (!argList.contains(a))
583           argList.add(a);
584       }
585     }
586   }
587
588   public boolean isSet(Arg a)
589   {
590     return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
591   }
592
593   public boolean isSet(String linkedId, Arg a)
594   {
595     Map<Arg, ArgValues> m = linkedArgs.get(linkedId);
596     return m == null ? false : m.containsKey(a);
597   }
598
599   public boolean getBool(Arg a)
600   {
601     if (!a.hasOption(Opt.BOOLEAN))
602     {
603       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
604               + "'.");
605     }
606     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
607   }
608
609   public boolean getBool(String linkedId, Arg a)
610   {
611     Map<Arg, ArgValues> m = linkedArgs.get(linkedId);
612     if (m == null)
613       return a.getDefaultBoolValue();
614     ArgValues v = m.get(a);
615     return v == null ? a.getDefaultBoolValue() : v.getBoolean();
616   }
617
618   public List<String> linkedIds()
619   {
620     return linkedOrder;
621   }
622
623   public HashMap<Arg, ArgValues> linkedArgs(String id)
624   {
625     return linkedArgs.get(id);
626   }
627
628   @Override
629   public String toString()
630   {
631     StringBuilder sb = new StringBuilder();
632     sb.append("UNLINKED\n");
633     sb.append(argMapToString(linkedArgs.get(null)));
634     if (linkedIds() != null)
635     {
636       sb.append("LINKED\n");
637       for (String id : linkedIds())
638       {
639         // already listed these as UNLINKED args
640         if (id == null)
641           continue;
642
643         Map<Arg, ArgValues> m = linkedArgs(id);
644         sb.append("ID: '").append(id).append("'\n");
645         sb.append(argMapToString(m));
646       }
647     }
648     return sb.toString();
649   }
650
651   private static String argMapToString(Map<Arg, ArgValues> m)
652   {
653     if (m == null)
654       return null;
655     StringBuilder sb = new StringBuilder();
656     for (Arg a : m.keySet())
657     {
658       ArgValues v = m.get(a);
659       sb.append(v.toString());
660       sb.append("\n");
661     }
662     return sb.toString();
663   }
664 }