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