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