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