JAL-4277 Corrected choice of all or last opened linkedIds. Better wildcard syntactic...
[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.Collections;
30 import java.util.EnumSet;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36
37 import jalview.bin.Cache;
38 import jalview.bin.Console;
39 import jalview.bin.Jalview;
40 import jalview.bin.argparser.Arg.Opt;
41 import jalview.bin.argparser.Arg.Type;
42 import jalview.util.FileUtils;
43 import jalview.util.HttpUtils;
44
45 public class ArgParser
46 {
47   protected static final String SINGLEDASH = "-";
48
49   protected static final String DOUBLEDASH = "--";
50
51   public static final char EQUALS = '=';
52
53   public static final String STDOUTFILENAME = "-";
54
55   protected static final String NEGATESTRING = "no";
56
57   /**
58    * the default linked id prefix used for no id (ie when not even square braces
59    * are provided)
60    */
61   protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
62
63   /**
64    * the linkedId string used to match all linkedIds seen so far
65    */
66   protected static final String MATCHALLLINKEDIDS = "*";
67
68   /**
69    * the linkedId string used to match all of the last --open'ed linkedIds
70    */
71   protected static final String MATCHOPENEDLINKEDIDS = "open*";
72
73   /**
74    * the counter added to the default linked id prefix
75    */
76   private int defaultLinkedIdCounter = 0;
77
78   /**
79    * the substitution string used to use the defaultLinkedIdCounter
80    */
81   private static final String DEFAULTLINKEDIDCOUNTER = "{}";
82
83   /**
84    * the linked id prefix used for --open files. NOW the same as DEFAULT
85    */
86   protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX;
87
88   /**
89    * the counter used for {n} substitutions
90    */
91   private int linkedIdAutoCounter = 0;
92
93   /**
94    * the linked id substitution string used to increment the idCounter (and use
95    * the incremented value)
96    */
97   private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
98
99   /**
100    * the linked id substitution string used to use the idCounter
101    */
102   private static final String LINKEDIDAUTOCOUNTER = "{n}";
103
104   /**
105    * the linked id substitution string used to use the filename extension of
106    * --append or --open
107    */
108   private static final String LINKEDIDEXTENSION = "{extension}";
109
110   /**
111    * the linked id substitution string used to use the base filename of --append
112    */
113   /** or --open */
114   private static final String LINKEDIDBASENAME = "{basename}";
115
116   /**
117    * the linked id substitution string used to use the dir path of --append or
118    * --open
119    */
120   private static final String LINKEDIDDIRNAME = "{dirname}";
121
122   /**
123    * the current argfile
124    */
125   private String argFile = null;
126
127   /**
128    * the linked id substitution string used to use the dir path of the latest
129    */
130   /** --argfile name */
131   private static final String ARGFILEBASENAME = "{argfilebasename}";
132
133   /**
134    * the linked id substitution string used to use the dir path of the latest
135    * --argfile name
136    */
137   private static final String ARGFILEDIRNAME = "{argfiledirname}";
138
139   /**
140    * flag to say whether {n} subtitutions in output filenames should be made.
141    * Turn on and off with --substitutions and --nosubstitutions Start with it on
142    */
143   private boolean substitutions = true;
144
145   /**
146    * flag to say whether the default linkedId is the current default linked id
147    *
148    * or ALL linkedIds
149    */
150   private boolean allLinkedIds = false;
151
152   /**
153    * flag to say whether the structure arguments should be applied to all
154    * structures with this linked id
155    */
156   private boolean allStructures = false;
157
158   protected static final Map<String, Arg> argMap;
159
160   protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
161
162   protected List<String> linkedOrder = new ArrayList<>();
163
164   protected List<String> storedLinkedIds = new ArrayList<>();
165
166   protected List<Arg> argList = new ArrayList<>();
167
168   private static final char ARGFILECOMMENT = '#';
169
170   private int argIndex = 0;
171
172   private BootstrapArgs bootstrapArgs = null;
173
174   static
175   {
176     argMap = new HashMap<>();
177     for (Arg a : EnumSet.allOf(Arg.class))
178     {
179       for (String argName : a.getNames())
180       {
181         if (argMap.containsKey(argName))
182         {
183           Console.warn("Trying to add argument name multiple times: '"
184                   + argName + "'");
185           if (argMap.get(argName) != a)
186           {
187             Console.error(
188                     "Trying to add argument name multiple times for different Args: '"
189                             + argMap.get(argName).getName() + ":" + argName
190                             + "' and '" + a.getName() + ":" + argName
191                             + "'");
192           }
193           continue;
194         }
195         argMap.put(argName, a);
196       }
197     }
198   }
199
200   public ArgParser(String[] args)
201   {
202     this(args, false, null);
203   }
204
205   public ArgParser(String[] args, boolean initsubstitutions,
206           BootstrapArgs bsa)
207   {
208     /*
209      *  Make a mutable new ArrayList so that shell globbing parser works.
210      * (When shell file globbing is used, there are a sequence of non-Arg
211      * arguments (which are the expanded globbed filenames) that need to be
212      * consumed by the --append/--argfile/etc Arg which is most easily done
213      * by removing these filenames from the list one at a time. This can't be
214      * done with an ArrayList made with only Arrays.asList(String[] args) as
215      * that is not mutable. )
216      */
217     this(new ArrayList<>(Arrays.asList(args)), initsubstitutions, false,
218             bsa);
219   }
220
221   public ArgParser(List<String> args, boolean initsubstitutions)
222   {
223     this(args, initsubstitutions, false, null);
224   }
225
226   public ArgParser(List<String> args, boolean initsubstitutions,
227           boolean allowPrivate, BootstrapArgs bsa)
228   {
229     // do nothing if there are no "--" args and (some "-" args || >0 arg is
230     // "open")
231     boolean d = false;
232     boolean dd = false;
233     for (String arg : args)
234     {
235       if (arg.startsWith(DOUBLEDASH))
236       {
237         dd = true;
238         break;
239       }
240       else if (arg.startsWith("-") || arg.equals("open"))
241       {
242         d = true;
243       }
244     }
245     if (d && !dd)
246     {
247       // leave it to the old style -- parse an empty list
248       parse(new ArrayList<String>(), false, false);
249       return;
250     }
251     if (bsa != null)
252       this.bootstrapArgs = bsa;
253     else
254       this.bootstrapArgs = BootstrapArgs.getBootstrapArgs(args);
255     parse(args, initsubstitutions, allowPrivate);
256   }
257
258   private void parse(List<String> args, boolean initsubstitutions,
259           boolean allowPrivate)
260   {
261     this.substitutions = initsubstitutions;
262
263     /*
264      *  If the first argument does not start with "--" or "-" or is not "open",
265      *  and is a filename that exists or a URL, it is probably a file/list of
266      *  files to open so we insert an Arg.OPEN argument before it. This will
267      *  mean the list of files at the start of the arguments are all opened
268      *  separately.
269      */
270     if (args.size() > 0)
271     {
272       String arg0 = args.get(0);
273       if (arg0 != null
274               && (!arg0.startsWith(DOUBLEDASH) && !arg0.startsWith("-")
275                       && !arg0.equals("open") && (new File(arg0).exists()
276                               || HttpUtils.startsWithHttpOrHttps(arg0))))
277       {
278         // insert "--open" at the start
279         args.add(0, Arg.OPEN.argString());
280       }
281     }
282
283     for (int i = 0; i < args.size(); i++)
284     {
285       String arg = args.get(i);
286
287       // look for double-dash, e.g. --arg
288       if (arg.startsWith(DOUBLEDASH))
289       {
290         String argName = null;
291         String val = null;
292         List<String> globVals = null; // for Opt.GLOB only
293         SubVals globSubVals = null; // also for use by Opt.GLOB only
294         String linkedId = null;
295         Type type = null;
296
297         // look for equals e.g. --arg=value
298         int equalPos = arg.indexOf(EQUALS);
299         if (equalPos > -1)
300         {
301           argName = arg.substring(DOUBLEDASH.length(), equalPos);
302           val = arg.substring(equalPos + 1);
303         }
304         else
305         {
306           argName = arg.substring(DOUBLEDASH.length());
307         }
308
309         // look for linked ID e.g. --arg[linkedID]
310         int idOpen = argName.indexOf('[');
311         int idClose = argName.indexOf(']');
312         if (idOpen > -1 && idClose == argName.length() - 1)
313         {
314           linkedId = argName.substring(idOpen + 1, idClose);
315           argName = argName.substring(0, idOpen);
316         }
317
318         // look for type modification e.g. --help-opening
319         int dashPos = argName.indexOf(SINGLEDASH);
320         if (dashPos > -1)
321         {
322           String potentialArgName = argName.substring(0, dashPos);
323           Arg potentialArg = argMap.get(potentialArgName);
324           if (potentialArg != null && potentialArg.hasOption(Opt.HASTYPE))
325           {
326             String typeName = argName.substring(dashPos + 1);
327             try
328             {
329               type = Type.valueOf(typeName);
330             } catch (IllegalArgumentException e)
331             {
332               type = Type.INVALID;
333             }
334             argName = argName.substring(0, dashPos);
335           }
336         }
337
338         Arg a = argMap.get(argName);
339         // check for boolean prepended by "no" e.g. --nowrap
340         boolean negated = false;
341         if (a == null)
342         {
343           if (argName.startsWith(NEGATESTRING) && argMap
344                   .containsKey(argName.substring(NEGATESTRING.length())))
345           {
346             argName = argName.substring(NEGATESTRING.length());
347             a = argMap.get(argName);
348             negated = true;
349           }
350           else
351           {
352             // after all other args, look for Opt.PREFIXKEV args if still not
353             // found
354             for (Arg potentialArg : EnumSet.allOf(Arg.class))
355             {
356               if (potentialArg.hasOption(Opt.PREFIXKEV) && argName != null
357                       && argName.startsWith(potentialArg.getName())
358                       && equalPos > -1)
359               {
360                 val = argName.substring(potentialArg.getName().length())
361                         + EQUALS + val;
362                 argName = argName.substring(0,
363                         potentialArg.getName().length());
364                 a = potentialArg;
365                 break;
366               }
367             }
368           }
369         }
370
371         // check for config errors
372         if (a == null)
373         {
374           // arg not found
375           Console.error("Argument '" + arg + "' not recognised.  Exiting.");
376           Jalview.exit("Invalid argument used." + System.lineSeparator()
377                   + "Use" + System.lineSeparator() + "jalview "
378                   + Arg.HELP.argString() + System.lineSeparator()
379                   + "for a usage statement.", 13);
380           continue;
381         }
382         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
383         {
384           Console.error(
385                   "Argument '" + a.argString() + "' is private. Ignoring.");
386           continue;
387         }
388         if (!a.hasOption(Opt.BOOLEAN) && negated)
389         {
390           // used "no" with a non-boolean option
391           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
392                   + "' not a boolean option. Ignoring.");
393           continue;
394         }
395         if (!a.hasOption(Opt.STRING) && equalPos > -1)
396         {
397           // set --argname=value when arg does not accept values
398           Console.error("Argument '" + a.argString()
399                   + "' does not expect a value (given as '" + arg
400                   + "').  Ignoring.");
401           continue;
402         }
403         if (!a.hasOption(Opt.LINKED) && linkedId != null)
404         {
405           // set --argname[linkedId] when arg does not use linkedIds
406           Console.error("Argument '" + a.argString()
407                   + "' does not expect a linked id (given as '" + arg
408                   + "'). Ignoring.");
409           continue;
410         }
411
412         // String value(s)
413         if (a.hasOption(Opt.STRING))
414         {
415           if (equalPos >= 0)
416           {
417             if (a.hasOption(Opt.GLOB))
418             {
419               // strip off and save the SubVals to be added individually later
420               globSubVals = new SubVals(val);
421               // make substitutions before looking for files
422               String fileGlob = makeSubstitutions(globSubVals.getContent(),
423                       linkedId);
424               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
425             }
426             else
427             {
428               // val is already set -- will be saved in the ArgValue later in
429               // the normal way
430             }
431           }
432           else
433           {
434             // There is no "=" so value is next arg or args (possibly shell
435             // glob-expanded)
436             if (i + 1 >= args.size())
437             {
438               // no value to take for arg, which wants a value
439               Console.error("Argument '" + a.getName()
440                       + "' requires a value, none given. Ignoring.");
441               continue;
442             }
443             // deal with bash globs here (--arg val* is expanded before reaching
444             // the JVM). Note that SubVals cannot be used in this case.
445             // If using the --arg=val then the glob is preserved and Java globs
446             // will be used later. SubVals can be used.
447             if (a.hasOption(Opt.GLOB))
448             {
449               // if this is the first argument with a file list at the start of
450               // the args we add filenames from index i instead of i+1
451               globVals = getShellGlobbedFilenameValues(a, args, i + 1);
452             }
453             else
454             {
455               val = args.get(i + 1);
456             }
457           }
458         }
459
460         // make NOACTION adjustments
461         // default and auto counter increments
462         if (a == Arg.NPP)
463         {
464           linkedIdAutoCounter++;
465         }
466         else if (a == Arg.SUBSTITUTIONS)
467         {
468           substitutions = !negated;
469         }
470         else if (a == Arg.SETARGFILE)
471         {
472           argFile = val;
473         }
474         else if (a == Arg.UNSETARGFILE)
475         {
476           argFile = null;
477         }
478         else if (a == Arg.ALL)
479         {
480           allLinkedIds = !negated;
481         }
482         else if (a == Arg.ALLSTRUCTURES)
483         {
484           allStructures = !negated;
485         }
486
487         if (a.hasOption(Opt.STORED))
488         {
489           // reset the lastOpenedLinkedIds list
490           this.storedLinkedIds = new ArrayList<>();
491         }
492
493         // this is probably only Arg.NEW and Arg.OPEN
494         if (a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
495         {
496           // use the next default prefixed OPENLINKEDID
497           defaultLinkedId(true);
498         }
499
500         String autoCounterString = null;
501         String defaultLinkedId = defaultLinkedId(false);
502         boolean usingDefaultLinkedId = false;
503         if (a.hasOption(Opt.LINKED))
504         {
505           if (linkedId == null)
506           {
507             if (a.hasOption(Opt.OUTPUTFILE) && a.hasOption(Opt.ALLOWMULTIID)
508                     && val.contains(MATCHALLLINKEDIDS))
509             {
510               // --output=*.ext is shorthand for --output {basename}.ext
511               // --output=*/*.ext is shorthand for
512               // --output {dirname}/{basename}.ext
513               // (or --image=*.ext)
514               linkedId = allLinkedIds ? MATCHALLLINKEDIDS
515                       : MATCHOPENEDLINKEDIDS;
516               val = FileUtils.convertWildcardsToPath(val, MATCHALLLINKEDIDS,
517                       LINKEDIDDIRNAME, LINKEDIDBASENAME);
518             }
519             else if (allLinkedIds && a.hasOption(Opt.ALLOWMULTIID))
520             {
521               linkedId = MATCHALLLINKEDIDS;
522             }
523             else if (a.hasOption(Opt.ALLOWMULTIID)
524                     && this.storedLinkedIds != null
525                     && this.storedLinkedIds.size() > 0)
526             {
527               linkedId = MATCHOPENEDLINKEDIDS;
528             }
529             else
530             {
531               // use default linkedId for linked arguments
532               linkedId = defaultLinkedId;
533               usingDefaultLinkedId = true;
534               Console.debug("Changing linkedId to '" + linkedId + "' from "
535                       + arg);
536             }
537           }
538           else
539           {
540             if (linkedId.contains(LINKEDIDAUTOCOUNTER))
541             {
542               // turn {n} to the autoCounter
543               autoCounterString = Integer.toString(linkedIdAutoCounter);
544               linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
545                       autoCounterString);
546               Console.debug("Changing linkedId to '" + linkedId + "' from "
547                       + arg);
548             }
549             if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
550             {
551               // turn {++n} to the incremented autoCounter
552               autoCounterString = Integer.toString(++linkedIdAutoCounter);
553               linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
554                       autoCounterString);
555               Console.debug("Changing linkedId to '" + linkedId + "' from "
556                       + arg);
557             }
558           }
559         }
560
561         // do not continue in this block for NOACTION args
562         if (a.hasOption(Opt.NOACTION))
563           continue;
564
565         ArgValuesMap avm = getOrCreateLinkedArgValuesMap(linkedId);
566
567         // not dealing with both NODUPLICATEVALUES and GLOB
568         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
569         {
570           Console.error("Argument '" + a.argString()
571                   + "' cannot contain a duplicate value ('" + val
572                   + "'). Ignoring this and subsequent occurrences.");
573           continue;
574         }
575
576         // check for unique id
577         SubVals subvals = new SubVals(val);
578         boolean addNewSubVals = false;
579         String id = subvals.get(ArgValues.ID);
580         if (id != null && avm.hasId(a, id))
581         {
582           Console.error("Argument '" + a.argString()
583                   + "' has a duplicate id ('" + id + "'). Ignoring.");
584           continue;
585         }
586
587         // set allstructures to all non-primary structure options in this linked
588         // id if --allstructures has been set
589         if (allStructures
590                 && (a.getType() == Type.STRUCTURE
591                         || a.getType() == Type.STRUCTUREIMAGE)
592                 && !a.hasOption(Opt.PRIMARY))
593         {
594           if (!subvals.has(Arg.ALLSTRUCTURES.getName()))
595           // && !subvals.has("structureid"))
596           {
597             subvals.put(Arg.ALLSTRUCTURES.getName(), "true");
598             addNewSubVals = true;
599           }
600         }
601
602         ArgValues avs = avm.getOrCreateArgValues(a);
603
604         // store appropriate String value(s)
605         if (a.hasOption(Opt.STRING))
606         {
607           if (a.hasOption(Opt.GLOB) && globVals != null
608                   && globVals.size() > 0)
609           {
610             Enumeration<String> gve = Collections.enumeration(globVals);
611             while (gve.hasMoreElements())
612             {
613               String v = gve.nextElement();
614               SubVals vsv = new SubVals(globSubVals, v);
615               addValue(linkedId, type, avs, vsv, v, argIndex++, true);
616               // if we're using defaultLinkedId and the arg increments the
617               // counter:
618               if (gve.hasMoreElements() && usingDefaultLinkedId
619                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
620               {
621                 // increment the default linkedId
622                 linkedId = defaultLinkedId(true);
623                 // get new avm and avs
624                 avm = linkedArgs.get(linkedId);
625                 avs = avm.getOrCreateArgValues(a);
626               }
627             }
628           }
629           else
630           {
631             // addValue(linkedId, type, avs, val, argIndex, true);
632             addValue(linkedId, type, avs, addNewSubVals ? subvals : null,
633                     val, argIndex, true);
634           }
635         }
636         else if (a.hasOption(Opt.BOOLEAN))
637         {
638           setBoolean(linkedId, type, avs, !negated, argIndex);
639           setNegated(linkedId, avs, negated);
640         }
641         else if (a.hasOption(Opt.UNARY))
642         {
643           setBoolean(linkedId, type, avs, true, argIndex);
644         }
645
646         // remove the '*' or 'open*' linkedId that should be empty if it was
647         // created
648         if ((MATCHALLLINKEDIDS.equals(linkedId)
649                 || MATCHOPENEDLINKEDIDS.equals(linkedId))
650                 && linkedArgs.containsKey(linkedId))
651         {
652           linkedArgs.remove(linkedId);
653         }
654       }
655     }
656   }
657
658   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
659   {
660     Arg a = avs.arg();
661     incrementCount(linkedId, avs);
662     argIndex++;
663
664     // store in appropriate place
665     if (a.hasOption(Opt.LINKED))
666     {
667       // store the order of linkedIds
668       if (!linkedOrder.contains(linkedId))
669         linkedOrder.add(linkedId);
670     }
671
672     // store arg in the list of args used
673     if (!argList.contains(a))
674       argList.add(a);
675   }
676
677   private String defaultLinkedId(boolean increment)
678   {
679     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
680             .append(Integer.toString(defaultLinkedIdCounter)).toString();
681     if (increment)
682     {
683       while (linkedArgs.containsKey(defaultLinkedId))
684       {
685         defaultLinkedIdCounter++;
686         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
687                 .append(Integer.toString(defaultLinkedIdCounter))
688                 .toString();
689       }
690     }
691     getOrCreateLinkedArgValuesMap(defaultLinkedId);
692     return defaultLinkedId;
693   }
694
695   public String makeSubstitutions(String val, String linkedId)
696   {
697     if (!this.substitutions || val == null)
698       return val;
699
700     String subvals;
701     String rest;
702     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
703     {
704       int closeBracket = val.indexOf(']');
705       if (val.length() == closeBracket)
706         return val;
707       subvals = val.substring(0, closeBracket + 1);
708       rest = val.substring(closeBracket + 1);
709     }
710     else
711     {
712       subvals = "";
713       rest = val;
714     }
715     if (rest.contains(LINKEDIDAUTOCOUNTER))
716       rest = rest.replace(LINKEDIDAUTOCOUNTER,
717               String.valueOf(linkedIdAutoCounter));
718     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
719       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
720               String.valueOf(++linkedIdAutoCounter));
721     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
722       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
723               String.valueOf(defaultLinkedIdCounter));
724     ArgValuesMap avm = linkedArgs.get(linkedId);
725     if (avm != null)
726     {
727       if (rest.contains(LINKEDIDBASENAME))
728       {
729         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
730       }
731       if (rest.contains(LINKEDIDEXTENSION))
732       {
733         rest = rest.replace(LINKEDIDEXTENSION, avm.getExtension());
734       }
735       if (rest.contains(LINKEDIDDIRNAME))
736       {
737         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
738       }
739     }
740     if (argFile != null)
741     {
742       if (rest.contains(ARGFILEBASENAME))
743       {
744         rest = rest.replace(ARGFILEBASENAME,
745                 FileUtils.getBasename(new File(argFile)));
746       }
747       if (rest.contains(ARGFILEDIRNAME))
748       {
749         rest = rest.replace(ARGFILEDIRNAME,
750                 FileUtils.getDirname(new File(argFile)));
751       }
752     }
753
754     return new StringBuilder(subvals).append(rest).toString();
755   }
756
757   /*
758    * A helper method to take a list of String args where we're expecting
759    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
760    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
761    * "file2", "file3"} *and remove these from the original list object* so that
762    * processing can continue from where it has left off, e.g. args has become
763    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
764    * carries on from the next --arg if available.
765    */
766   protected static List<String> getShellGlobbedFilenameValues(Arg a,
767           List<String> args, int i)
768   {
769     List<String> vals = new ArrayList<>();
770     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
771     {
772       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
773       if (!a.hasOption(Opt.GLOB))
774         break;
775     }
776     return vals;
777   }
778
779   public BootstrapArgs getBootstrapArgs()
780   {
781     return bootstrapArgs;
782   }
783
784   public boolean isSet(Arg a)
785   {
786     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
787   }
788
789   public boolean isSetAtAll(Arg a)
790   {
791     for (String linkedId : linkedOrder)
792     {
793       if (isSet(linkedId, a))
794         return true;
795     }
796     return false;
797   }
798
799   public boolean isSet(String linkedId, Arg a)
800   {
801     ArgValuesMap avm = linkedArgs.get(linkedId);
802     return avm == null ? false : avm.containsArg(a);
803   }
804
805   public boolean getBoolean(Arg a)
806   {
807     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
808     {
809       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
810               + "'.");
811     }
812     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
813   }
814
815   public boolean getBool(String linkedId, Arg a)
816   {
817     ArgValuesMap avm = linkedArgs.get(linkedId);
818     if (avm == null)
819       return a.getDefaultBoolValue();
820     ArgValues avs = avm.getArgValues(a);
821     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
822   }
823
824   public List<String> getLinkedIds()
825   {
826     return linkedOrder;
827   }
828
829   public ArgValuesMap getLinkedArgs(String id)
830   {
831     return linkedArgs.get(id);
832   }
833
834   @Override
835   public String toString()
836   {
837     StringBuilder sb = new StringBuilder();
838     sb.append("UNLINKED\n");
839     sb.append(argValuesMapToString(linkedArgs.get(null)));
840     if (getLinkedIds() != null)
841     {
842       sb.append("LINKED\n");
843       for (String id : getLinkedIds())
844       {
845         // already listed these as UNLINKED args
846         if (id == null)
847           continue;
848
849         ArgValuesMap avm = getLinkedArgs(id);
850         sb.append("ID: '").append(id).append("'\n");
851         sb.append(argValuesMapToString(avm));
852       }
853     }
854     return sb.toString();
855   }
856
857   private static String argValuesMapToString(ArgValuesMap avm)
858   {
859     if (avm == null)
860       return null;
861     StringBuilder sb = new StringBuilder();
862     for (Arg a : avm.getArgKeys())
863     {
864       ArgValues v = avm.getArgValues(a);
865       sb.append(v.toString());
866       sb.append("\n");
867     }
868     return sb.toString();
869   }
870
871   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
872           boolean initsubstitutions, BootstrapArgs bsa)
873   {
874     List<File> argFiles = new ArrayList<>();
875
876     for (String pattern : argFilenameGlobs)
877     {
878       // I don't think we want to dedup files, making life easier
879       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
880     }
881
882     return parseArgFileList(argFiles, initsubstitutions, bsa);
883   }
884
885   public static ArgParser parseArgFileList(List<File> argFiles,
886           boolean initsubstitutions, BootstrapArgs bsa)
887   {
888     List<String> argsList = new ArrayList<>();
889     for (File argFile : argFiles)
890     {
891       if (!argFile.exists())
892       {
893         String message = Arg.ARGFILE.argString() + EQUALS + "\""
894                 + argFile.getPath() + "\": File does not exist.";
895         Jalview.exit(message, 2);
896       }
897       try
898       {
899         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
900                 .append(EQUALS).append(argFile.getCanonicalPath())
901                 .toString();
902         argsList.add(setargfile);
903         argsList.addAll(readArgFile(argFile));
904         argsList.add(Arg.UNSETARGFILE.argString());
905       } catch (IOException e)
906       {
907         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
908                 + "\": File could not be read.";
909         Jalview.exit(message, 3);
910       }
911     }
912     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
913     // --unsetargfile
914     return new ArgParser(argsList, initsubstitutions, true, bsa);
915   }
916
917   protected static List<String> readArgFile(File argFile)
918   {
919     List<String> args = new ArrayList<>();
920     if (argFile != null && argFile.exists())
921     {
922       try
923       {
924         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
925         {
926           if (line != null && line.length() > 0
927                   && line.charAt(0) != ARGFILECOMMENT)
928             args.add(line);
929         }
930       } catch (IOException e)
931       {
932         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
933                 + "\": File could not be read.";
934         Console.debug(message, e);
935         Jalview.exit(message, 3);
936       }
937     }
938     return args;
939   }
940
941   public static enum Position
942   {
943     FIRST, BEFORE, AFTER
944   }
945
946   /**
947    * get from following Arg of type a or subval of same name (lowercase)
948    */
949   public static String getValueFromSubValOrArg(ArgValuesMap avm,
950           ArgValue av, Arg a, SubVals sv)
951   {
952     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
953   }
954
955   /**
956    * get from following Arg of type a or subval key or preference pref or
957    * default def
958    */
959   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
960           Arg a, SubVals sv, String key, String pref, String def)
961   {
962     return getFromSubValArgOrPref(avm, a, Position.AFTER, av, sv, key, pref,
963             def);
964   }
965
966   /**
967    * get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
968    * Arg of type a or subval key or preference pref or default def
969    */
970   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
971           Position pos, ArgValue av, SubVals sv, String key, String pref,
972           String def)
973   {
974     return getFromSubValArgOrPrefWithSubstitutions(null, avm, a, pos, av,
975             sv, key, pref, def);
976   }
977
978   public static String getFromSubValArgOrPrefWithSubstitutions(ArgParser ap,
979           ArgValuesMap avm, Arg a, Position pos, ArgValue av, SubVals sv,
980           String key, String pref, String def)
981   {
982     if (key == null)
983       key = a.getName();
984     String value = null;
985     if (sv != null && sv.has(key) && sv.get(key) != null)
986     {
987       value = ap == null ? sv.get(key)
988               : sv.getWithSubstitutions(ap, avm.getLinkedId(), key);
989     }
990     else if (avm != null && avm.containsArg(a))
991     {
992       if (pos == Position.FIRST && avm.getValue(a) != null)
993         value = avm.getValue(a);
994       else if (pos == Position.BEFORE
995               && avm.getClosestPreviousArgValueOfArg(av, a) != null)
996         value = avm.getClosestPreviousArgValueOfArg(av, a).getValue();
997       else if (pos == Position.AFTER
998               && avm.getClosestNextArgValueOfArg(av, a) != null)
999         value = avm.getClosestNextArgValueOfArg(av, a).getValue();
1000
1001       // look for allstructures subval for Type.STRUCTURE*
1002       Arg arg = av.getArg();
1003       if (value == null && arg.hasOption(Opt.PRIMARY)
1004               && arg.getType() == Type.STRUCTURE
1005               && !a.hasOption(Opt.PRIMARY) && (a.getType() == Type.STRUCTURE
1006                       || a.getType() == Type.STRUCTUREIMAGE))
1007       {
1008         ArgValue av2 = avm.getArgValueOfArgWithSubValKey(a,
1009                 Arg.ALLSTRUCTURES.getName());
1010         if (av2 != null)
1011         {
1012           value = av2.getValue();
1013         }
1014       }
1015     }
1016     if (value == null)
1017     {
1018       value = pref != null ? Cache.getDefault(pref, def) : def;
1019     }
1020     return value;
1021   }
1022
1023   public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
1024           SubVals sv)
1025   {
1026     return getFromSubValArgOrPref(avm, a, sv, null, null, false);
1027   }
1028
1029   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
1030           SubVals sv, String key, String pref, boolean def)
1031   {
1032     return getFromSubValArgOrPref(avm, a, sv, key, pref, def, false);
1033   }
1034
1035   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
1036           SubVals sv, String key, String pref, boolean def,
1037           boolean invertPref)
1038   {
1039     if ((key == null && a == null) || (sv == null && a == null))
1040       return false;
1041
1042     boolean usingArgKey = false;
1043     if (key == null)
1044     {
1045       key = a.getName();
1046       usingArgKey = true;
1047     }
1048
1049     String nokey = ArgParser.NEGATESTRING + key;
1050
1051     // look for key or nokey in subvals first (if using Arg check options)
1052     if (sv != null)
1053     {
1054       // check for true boolean
1055       if (sv.has(key) && sv.get(key) != null)
1056       {
1057         if (usingArgKey)
1058         {
1059           if (!(a.hasOption(Opt.BOOLEAN) || a.hasOption(Opt.UNARY)))
1060           {
1061             Console.debug(
1062                     "Looking for boolean in subval from non-boolean/non-unary Arg "
1063                             + a.getName());
1064             return false;
1065           }
1066         }
1067         return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
1068       }
1069
1070       // check for negative boolean (subval "no..." will be "true")
1071       if (sv.has(nokey) && sv.get(nokey) != null)
1072       {
1073         if (usingArgKey)
1074         {
1075           if (!(a.hasOption(Opt.BOOLEAN)))
1076           {
1077             Console.debug(
1078                     "Looking for negative boolean in subval from non-boolean Arg "
1079                             + a.getName());
1080             return false;
1081           }
1082         }
1083         return !sv.get(nokey).toLowerCase(Locale.ROOT).equals("true");
1084       }
1085     }
1086
1087     // check argvalues
1088     if (avm != null && avm.containsArg(a))
1089       return avm.getBoolean(a);
1090
1091     // return preference or default
1092     boolean prefVal = pref != null ? Cache.getDefault(pref, def) : false;
1093     return pref != null ? (invertPref ? !prefVal : prefVal) : def;
1094   }
1095
1096   // the following methods look for the "*" linkedId and add the argvalue to all
1097   // linkedId ArgValues if it does.
1098   /**
1099    * This version inserts the subvals sv into all created values
1100    */
1101   private void addValue(String linkedId, Type type, ArgValues avs,
1102           SubVals sv, String v, int argIndex, boolean doSubs)
1103   {
1104     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, sv, v, false,
1105             argIndex, doSubs);
1106   }
1107
1108   private void addValue(String linkedId, Type type, ArgValues avs, String v,
1109           int argIndex, boolean doSubs)
1110   {
1111     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, null, v, false,
1112             argIndex, doSubs);
1113   }
1114
1115   private void setBoolean(String linkedId, Type type, ArgValues avs,
1116           boolean b, int argIndex)
1117   {
1118     this.argValueOperation(Op.SETBOOLEAN, linkedId, type, avs, null, null,
1119             b, argIndex, false);
1120   }
1121
1122   private void setNegated(String linkedId, ArgValues avs, boolean b)
1123   {
1124     this.argValueOperation(Op.SETNEGATED, linkedId, null, avs, null, null,
1125             b, 0, false);
1126   }
1127
1128   private void incrementCount(String linkedId, ArgValues avs)
1129   {
1130     this.argValueOperation(Op.INCREMENTCOUNT, linkedId, null, avs, null,
1131             null, false, 0, false);
1132   }
1133
1134   private enum Op
1135   {
1136     ADDVALUE, SETBOOLEAN, SETNEGATED, INCREMENTCOUNT
1137   }
1138
1139   private void argValueOperation(Op op, String linkedId, Type type,
1140           ArgValues avs, SubVals sv, String v, boolean b, int argIndex,
1141           boolean doSubs)
1142   {
1143     // default to merge subvals if subvals are provided
1144     argValueOperation(op, linkedId, type, avs, sv, true, v, b, argIndex,
1145             doSubs);
1146   }
1147
1148   /**
1149    * The following operations look for the "*" and "open*" linkedIds and add the
1150    * argvalue to all appropriate linkedId ArgValues if it does. If subvals are
1151    * supplied, they are inserted into all new set values.
1152    * 
1153    * @param op
1154    *          The ArgParser.Op operation
1155    * @param linkedId
1156    *          The String linkedId from the ArgValuesMap
1157    * @param type
1158    *          The Arg.Type to attach to this ArgValue
1159    * @param avs
1160    *          The ArgValues for this linkedId
1161    * @param sv
1162    *          Use these SubVals on the ArgValue
1163    * @param merge
1164    *          Merge the SubVals with any existing on the value. False will
1165    *          replace unless sv is null
1166    * @param v
1167    *          The value of the ArgValue (may contain subvals).
1168    * @param b
1169    *          The boolean value of the ArgValue.
1170    * @param argIndex
1171    *          The argIndex for the ArgValue.
1172    * @param doSubs
1173    *          Whether to perform substitutions on the subvals and value.
1174    */
1175   private void argValueOperation(Op op, String linkedId, Type type,
1176           ArgValues avs, SubVals sv, boolean merge, String v, boolean b,
1177           int argIndex, boolean doSubs)
1178   {
1179     Arg a = avs.arg();
1180
1181     List<String> wildcardLinkedIds = null;
1182     if (a.hasOption(Opt.ALLOWMULTIID))
1183     {
1184       switch (linkedId)
1185       {
1186       case MATCHALLLINKEDIDS:
1187         wildcardLinkedIds = getLinkedIds();
1188         break;
1189       case MATCHOPENEDLINKEDIDS:
1190         wildcardLinkedIds = this.storedLinkedIds;
1191         break;
1192       }
1193     }
1194
1195     // if we're not a wildcard linkedId and the arg is marked to be stored, add
1196     // to storedLinkedIds
1197     if (linkedId != null && wildcardLinkedIds == null
1198             && a.hasOption(Opt.STORED)
1199             && !storedLinkedIds.contains(linkedId))
1200     {
1201       storedLinkedIds.add(linkedId);
1202     }
1203
1204     // if we are a wildcard linkedId, apply the arg and value to all appropriate
1205     // linkedIds
1206     if (wildcardLinkedIds != null)
1207     {
1208       for (String id : wildcardLinkedIds)
1209       {
1210         // skip incorrectly stored wildcard ids!
1211         if (id == null || MATCHALLLINKEDIDS.equals(id)
1212                 || MATCHOPENEDLINKEDIDS.equals(id))
1213         {
1214           continue;
1215         }
1216         ArgValuesMap avm = linkedArgs.get(id);
1217         // don't set an output if there isn't an input
1218         if (a.hasOption(Opt.REQUIREINPUT)
1219                 && !avm.hasArgWithOption(Opt.INPUT))
1220           continue;
1221
1222         ArgValues tavs = avm.getOrCreateArgValues(a);
1223         switch (op)
1224         {
1225
1226         case ADDVALUE:
1227           String val = v;
1228           if (sv != null)
1229           {
1230             if (doSubs)
1231             {
1232               sv = new SubVals(sv, val, merge);
1233               val = makeSubstitutions(sv.getContent(), id);
1234             }
1235             tavs.addValue(sv, type, val, argIndex, true);
1236           }
1237           else
1238           {
1239             if (doSubs)
1240             {
1241               val = makeSubstitutions(v, id);
1242             }
1243             tavs.addValue(type, val, argIndex, true);
1244           }
1245           finaliseStoringArgValue(id, tavs);
1246           break;
1247
1248         case SETBOOLEAN:
1249           tavs.setBoolean(type, b, argIndex, true);
1250           finaliseStoringArgValue(id, tavs);
1251           break;
1252
1253         case SETNEGATED:
1254           tavs.setNegated(b, true);
1255           break;
1256
1257         case INCREMENTCOUNT:
1258           tavs.incrementCount();
1259           break;
1260
1261         default:
1262           break;
1263
1264         }
1265
1266       }
1267     }
1268     else // no wildcard linkedId -- do it simpler
1269     {
1270       switch (op)
1271       {
1272       case ADDVALUE:
1273         String val = v;
1274         if (sv != null)
1275         {
1276           if (doSubs)
1277           {
1278             val = makeSubstitutions(v, linkedId);
1279             sv = new SubVals(sv, val);
1280           }
1281           avs.addValue(sv, type, val, argIndex, false);
1282         }
1283         else
1284         {
1285           if (doSubs)
1286           {
1287             val = makeSubstitutions(v, linkedId);
1288           }
1289           avs.addValue(type, val, argIndex, false);
1290         }
1291         finaliseStoringArgValue(linkedId, avs);
1292         break;
1293
1294       case SETBOOLEAN:
1295         avs.setBoolean(type, b, argIndex, false);
1296         finaliseStoringArgValue(linkedId, avs);
1297         break;
1298
1299       case SETNEGATED:
1300         avs.setNegated(b, false);
1301         break;
1302
1303       case INCREMENTCOUNT:
1304         avs.incrementCount();
1305         break;
1306
1307       default:
1308         break;
1309       }
1310     }
1311   }
1312
1313   private ArgValuesMap getOrCreateLinkedArgValuesMap(String linkedId)
1314   {
1315     if (linkedArgs.containsKey(linkedId)
1316             && linkedArgs.get(linkedId) != null)
1317       return linkedArgs.get(linkedId);
1318
1319     linkedArgs.put(linkedId, new ArgValuesMap(linkedId));
1320     return linkedArgs.get(linkedId);
1321   }
1322
1323 }