JAL-629 Added --opennew --nonews --nosplash. Added java globbing for = e.g. --open...
[jalview.git] / src / jalview / bin / argparser / 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.argparser;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.nio.file.Files;
26 import java.nio.file.Paths;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.EnumSet;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.Map;
34
35 import jalview.bin.Console;
36 import jalview.bin.Jalview;
37 import jalview.bin.argparser.Arg.Opt;
38 import jalview.util.FileUtils;
39
40 public class ArgParser
41 {
42   protected static final String DOUBLEDASH = "--";
43
44   protected static final String NEGATESTRING = "no";
45
46   // the default linked id prefix used for no id (not even square braces)
47   protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
48
49   // the counter added to the default linked id prefix
50   private int defaultLinkedIdCounter = 0;
51
52   // the linked id prefix used for --opennew files
53   protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:";
54
55   // the counter added to the default linked id prefix
56   private int opennewLinkedIdCounter = 0;
57
58   // the linked id used to increment the idCounter (and use the incremented
59   // value)
60   private static final String INCREMENTAUTOCOUNTERLINKEDID = "{++n}";
61
62   // the linked id used to use the idCounter
63   private static final String AUTOCOUNTERLINKEDID = "{n}";
64
65   private int linkedIdAutoCounter = 0;
66
67   // flag to say whether {n} subtitutions in output filenames should be made.
68   // Turn on and off with --subs and --nosubs
69   private boolean substitutions = false;
70
71   protected static final Map<String, Arg> argMap;
72
73   protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
74
75   protected List<String> linkedOrder = null;
76
77   protected List<Arg> argList;
78
79   static
80   {
81     argMap = new HashMap<>();
82     for (Arg a : EnumSet.allOf(Arg.class))
83     {
84       for (String argName : a.getNames())
85       {
86         if (argMap.containsKey(argName))
87         {
88           Console.warn("Trying to add argument name multiple times: '"
89                   + argName + "'"); // RESTORE THIS WHEN MERGED
90           if (argMap.get(argName) != a)
91           {
92             Console.error(
93                     "Trying to add argument name multiple times for different Args: '"
94                             + argMap.get(argName).getName() + ":" + argName
95                             + "' and '" + a.getName() + ":" + argName
96                             + "'");
97           }
98           continue;
99         }
100         argMap.put(argName, a);
101       }
102     }
103   }
104
105   public ArgParser(String[] args)
106   {
107     // Make a mutable new ArrayList so that shell globbing parser works.
108     // (When shell file globbing is used, there are a sequence of non-Arg
109     // arguments (which are the expanded globbed filenames) that need to be
110     // consumed by the --open/--argfile/etc Arg which is most easily done by
111     // removing these filenames from the list one at a time. This can't be done
112     // with an ArrayList made with only Arrays.asList(String[] args). )
113     this(new ArrayList<>(Arrays.asList(args)));
114   }
115
116   public ArgParser(List<String> args)
117   {
118     parse(args);
119   }
120
121   private void parse(List<String> args)
122   {
123     int argIndex = 0;
124     boolean openEachInitialFilenames = true;
125     for (int i = 0; i < args.size(); i++)
126     {
127       String arg = args.get(i);
128
129       // If the first arguments do not start with "--" or "-" or is "open" and
130       // is a filename that exists it is probably a file/list of files to open
131       // so we fake an Arg.OPEN argument and when adding files only add the
132       // single arg[i] and increment the defaultLinkedIdCounter so that each of
133       // these files is opened separately.
134       if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
135               && !arg.startsWith("-") && new File(arg).exists())
136       {
137         arg = DOUBLEDASH + Arg.OPENNEW.getName();
138       }
139       else
140       {
141         openEachInitialFilenames = false;
142       }
143
144       String argName = null;
145       String val = null;
146       List<String> globVals = null; // for Opt.GLOB only
147       SubVals globSubVals = null; // also for use by Opt.GLOB only
148       String linkedId = null;
149       if (arg.startsWith(DOUBLEDASH))
150       {
151         int equalPos = arg.indexOf('=');
152         if (equalPos > -1)
153         {
154           argName = arg.substring(DOUBLEDASH.length(), equalPos);
155           val = arg.substring(equalPos + 1);
156         }
157         else
158         {
159           argName = arg.substring(DOUBLEDASH.length());
160         }
161         int idOpen = argName.indexOf('[');
162         int idClose = argName.indexOf(']');
163
164         if (idOpen > -1 && idClose == argName.length() - 1)
165         {
166           linkedId = argName.substring(idOpen + 1, idClose);
167           argName = argName.substring(0, idOpen);
168         }
169
170         Arg a = argMap.get(argName);
171         // check for boolean prepended by "no"
172         boolean negated = false;
173         if (a == null && argName.startsWith(NEGATESTRING) && argMap
174                 .containsKey(argName.substring(NEGATESTRING.length())))
175         {
176           argName = argName.substring(NEGATESTRING.length());
177           a = argMap.get(argName);
178           negated = true;
179         }
180
181         // check for config errors
182         if (a == null)
183         {
184           // arg not found
185           Console.error("Argument '" + arg + "' not recognised. Ignoring.");
186           continue;
187         }
188         if (!a.hasOption(Opt.BOOLEAN) && negated)
189         {
190           // used "no" with a non-boolean option
191           Console.error("Argument '--" + NEGATESTRING + argName
192                   + "' not a boolean option. Ignoring.");
193           continue;
194         }
195         if (!a.hasOption(Opt.STRING) && equalPos > -1)
196         {
197           // set --argname=value when arg does not accept values
198           Console.error("Argument '--" + argName
199                   + "' does not expect a value (given as '" + arg
200                   + "').  Ignoring.");
201           continue;
202         }
203         if (!a.hasOption(Opt.LINKED) && linkedId != null)
204         {
205           // set --argname[linkedId] when arg does not use linkedIds
206           Console.error("Argument '--" + argName
207                   + "' does not expect a linked id (given as '" + arg
208                   + "'). Ignoring.");
209           continue;
210         }
211
212         // String value(s)
213         if (a.hasOption(Opt.STRING))
214         {
215           if (equalPos >= 0)
216           {
217             if (a.hasOption(Opt.GLOB))
218             {
219               // strip off and save the SubVals to be added individually later
220               globSubVals = ArgParser.getSubVals(val);
221               globVals = FileUtils
222                       .getFilenamesFromGlob(globSubVals.getContent());
223             }
224             else
225             {
226               // val is already set -- will be saved in the ArgValue later in
227               // the normal way
228             }
229           }
230           else
231           {
232             // There is no "=" so value is next arg or args (possibly shell
233             // glob-expanded)
234             if (i + 1 >= args.size())
235             {
236               // no value to take for arg, which wants a value
237               Console.error("Argument '" + a.getName()
238                       + "' requires a value, none given. Ignoring.");
239               continue;
240             }
241             // deal with bash globs here (--arg val* is expanded before reaching
242             // the JVM). Note that SubVals cannot be used in this case.
243             // If using the --arg=val then the glob is preserved and Java globs
244             // will be used later. SubVals can be used.
245             if (a.hasOption(Opt.GLOB))
246             {
247               // if this is the first argument with a file list at the start of
248               // the args we add filenames from index i instead of i+1
249               globVals = getShellGlobbedFilenameValues(a, args,
250                       openEachInitialFilenames ? i : i + 1);
251             }
252             else
253             {
254               val = args.get(i + 1);
255             }
256           }
257         }
258
259         // make NOACTION adjustments
260         // default and auto counter increments
261         if (a == Arg.INCREMENT)
262         {
263           defaultLinkedIdCounter++;
264         }
265         else if (a == Arg.NPP)
266         {
267           linkedIdAutoCounter++;
268         }
269         else if (a == Arg.SUBSTITUTIONS)
270         {
271           substitutions = !negated;
272         }
273
274         String autoCounterString = null;
275         boolean usingAutoCounterLinkedId = false;
276         String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
277                 .append(Integer.toString(defaultLinkedIdCounter))
278                 .toString();
279         boolean usingDefaultLinkedId = false;
280         if (a.hasOption(Opt.LINKED))
281         {
282           if (linkedId == null)
283           {
284             if (a == Arg.OPENNEW)
285             {
286               // use the next default prefixed OPENNEWLINKEDID
287               linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX)
288                       .append(Integer.toString(opennewLinkedIdCounter))
289                       .toString();
290               opennewLinkedIdCounter++;
291             }
292             else
293             {
294               // use default linkedId for linked arguments
295               linkedId = defaultLinkedId;
296               usingDefaultLinkedId = true;
297               Console.debug("Changing linkedId to '" + linkedId + "' from "
298                       + arg);
299             }
300           }
301           else if (linkedId.contains(AUTOCOUNTERLINKEDID))
302           {
303             // turn {n} to the autoCounter
304             autoCounterString = Integer.toString(linkedIdAutoCounter);
305             linkedId = linkedId.replace(AUTOCOUNTERLINKEDID,
306                     autoCounterString);
307             usingAutoCounterLinkedId = true;
308             Console.debug(
309                     "Changing linkedId to '" + linkedId + "' from " + arg);
310           }
311           else if (linkedId.contains(INCREMENTAUTOCOUNTERLINKEDID))
312           {
313             // turn {++n} to the incremented autoCounter
314             autoCounterString = Integer.toString(++linkedIdAutoCounter);
315             linkedId = linkedId.replace(INCREMENTAUTOCOUNTERLINKEDID,
316                     autoCounterString);
317             usingAutoCounterLinkedId = true;
318             Console.debug(
319                     "Changing linkedId to '" + linkedId + "' from " + arg);
320           }
321         }
322
323         if (!linkedArgs.containsKey(linkedId))
324           linkedArgs.put(linkedId, new ArgValuesMap());
325
326         // do not continue for NOACTION args
327         if (a.hasOption(Opt.NOACTION))
328           continue;
329
330         ArgValuesMap avm = linkedArgs.get(linkedId);
331
332         // not dealing with both NODUPLICATEVALUES and GLOB
333         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
334         {
335           Console.error("Argument '--" + argName
336                   + "' cannot contain a duplicate value ('" + val
337                   + "'). Ignoring this and subsequent occurrences.");
338           continue;
339         }
340
341         // check for unique id
342         SubVals idsv = ArgParser.getSubVals(val);
343         String id = idsv.get(ArgValues.ID);
344         if (id != null && avm.hasId(a, id))
345         {
346           Console.error("Argument '--" + argName + "' has a duplicate id ('"
347                   + id + "'). Ignoring.");
348           continue;
349         }
350
351         boolean argIndexIncremented = false;
352         ArgValues avs = avm.getOrCreateArgValues(a);
353
354         // store appropriate String value(s)
355         if (a.hasOption(Opt.STRING))
356         {
357           if (a.hasOption(Opt.GLOB) && globVals != null
358                   && globVals.size() > 0)
359           {
360             for (String v : globVals)
361             {
362               v = makeSubstitutions(v);
363               SubVals vsv = new SubVals(globSubVals == null ? null
364                       : globSubVals.getSubValsMap(), v);
365               avs.addValue(vsv, v, argIndex++);
366               argIndexIncremented = true;
367             }
368           }
369           else
370           {
371             avs.addValue(makeSubstitutions(val), argIndex);
372           }
373         }
374         else if (a.hasOption(Opt.BOOLEAN))
375         {
376           avs.setBoolean(!negated, argIndex);
377           avs.setNegated(negated);
378         }
379         else if (a.hasOption(Opt.UNARY))
380         {
381           avs.setBoolean(true, argIndex);
382         }
383         avs.incrementCount();
384         if (!argIndexIncremented)
385           argIndex++;
386
387         // store in appropriate place
388         if (a.hasOption(Opt.LINKED))
389         {
390           // store the order of linkedIds
391           if (linkedOrder == null)
392             linkedOrder = new ArrayList<>();
393           if (!linkedOrder.contains(linkedId))
394             linkedOrder.add(linkedId);
395         }
396
397         // store arg in the list of args used
398         if (argList == null)
399           argList = new ArrayList<>();
400         if (!argList.contains(a))
401           argList.add(a);
402
403       }
404     }
405   }
406
407   private String makeSubstitutions(String val)
408   {
409     if (!this.substitutions)
410       return val;
411
412     String subvals;
413     String rest;
414     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
415     {
416       int closeBracket = val.indexOf(']');
417       if (val.length() == closeBracket)
418         return val;
419       subvals = val.substring(0, closeBracket + 1);
420       rest = val.substring(closeBracket + 1);
421     }
422     else
423     {
424       subvals = "";
425       rest = val;
426     }
427     if ((rest.contains(AUTOCOUNTERLINKEDID)))
428       rest = rest.replace(AUTOCOUNTERLINKEDID,
429               String.valueOf(linkedIdAutoCounter));
430     if ((rest.contains(INCREMENTAUTOCOUNTERLINKEDID)))
431       rest = rest.replace(INCREMENTAUTOCOUNTERLINKEDID,
432               String.valueOf(++linkedIdAutoCounter));
433     if ((rest.contains("{}")))
434       rest = rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
435
436     return new StringBuilder(subvals).append(rest).toString();
437   }
438
439   /*
440    * A helper method to take a list of String args where we're expecting
441    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
442    * and the index of the globbed arg, here 1.  It returns a
443    * List<String> {"file1", "file2", "file3"}
444    * *and remove these from the original list object* so that processing
445    * can continue from where it has left off, e.g. args has become
446    * {"--previousargs", "--arg", "--otheroptionsornot"}
447    * so the next increment carries on from the next --arg if available.
448    */
449   protected static List<String> getShellGlobbedFilenameValues(Arg a,
450           List<String> args, int i)
451   {
452     List<String> vals = new ArrayList<>();
453     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
454     {
455       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
456       if (!a.hasOption(Opt.GLOB))
457         break;
458     }
459     return vals;
460   }
461
462   public boolean isSet(Arg a)
463   {
464     return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
465   }
466
467   public boolean isSet(String linkedId, Arg a)
468   {
469     ArgValuesMap avm = linkedArgs.get(linkedId);
470     return avm == null ? false : avm.containsArg(a);
471   }
472
473   public boolean getBool(Arg a)
474   {
475     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
476     {
477       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
478               + "'.");
479     }
480     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
481   }
482
483   public boolean getBool(String linkedId, Arg a)
484   {
485     ArgValuesMap avm = linkedArgs.get(linkedId);
486     if (avm == null)
487       return a.getDefaultBoolValue();
488     ArgValues avs = avm.getArgValues(a);
489     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
490   }
491
492   public List<String> linkedIds()
493   {
494     return linkedOrder;
495   }
496
497   public ArgValuesMap linkedArgs(String id)
498   {
499     return linkedArgs.get(id);
500   }
501
502   @Override
503   public String toString()
504   {
505     StringBuilder sb = new StringBuilder();
506     sb.append("UNLINKED\n");
507     sb.append(argValuesMapToString(linkedArgs.get(null)));
508     if (linkedIds() != null)
509     {
510       sb.append("LINKED\n");
511       for (String id : linkedIds())
512       {
513         // already listed these as UNLINKED args
514         if (id == null)
515           continue;
516
517         ArgValuesMap avm = linkedArgs(id);
518         sb.append("ID: '").append(id).append("'\n");
519         sb.append(argValuesMapToString(avm));
520       }
521     }
522     return sb.toString();
523   }
524
525   private static String argValuesMapToString(ArgValuesMap avm)
526   {
527     if (avm == null)
528       return null;
529     StringBuilder sb = new StringBuilder();
530     for (Arg a : avm.getArgKeys())
531     {
532       ArgValues v = avm.getArgValues(a);
533       sb.append(v.toString());
534       sb.append("\n");
535     }
536     return sb.toString();
537   }
538
539   public static SubVals getSubVals(String item)
540   {
541     return new SubVals(item);
542   }
543
544   public static ArgParser parseArgFiles(List<String> argFilenameGlobs)
545   {
546     List<File> argFiles = new ArrayList<>();
547
548     for (String pattern : argFilenameGlobs)
549     {
550       // I don't think we want to dedup files, making life easier
551       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
552     }
553
554     return parseArgFileList(argFiles);
555   }
556
557   public static ArgParser parseArgFileList(List<File> argFiles)
558   {
559     List<String> argsList = new ArrayList<>();
560     for (File argFile : argFiles)
561     {
562       if (!argFile.exists())
563       {
564         String message = DOUBLEDASH
565                 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
566                 + argFile.getPath() + "\": File does not exist.";
567         Jalview.exit(message, 2);
568       }
569       try
570       {
571         argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
572       } catch (IOException e)
573       {
574         String message = DOUBLEDASH
575                 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
576                 + argFile.getPath() + "\": File could not be read.";
577         Jalview.exit(message, 3);
578       }
579     }
580     return new ArgParser(argsList);
581   }
582
583 }