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