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