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