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