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