fe73053b33a219a8f8d191eaf3c1d3d940af2f35
[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     {
322       this.bootstrapArgs = bsa;
323     }
324     else
325     {
326       this.bootstrapArgs = BootstrapArgs.getBootstrapArgs(args);
327     }
328     parse(args, initsubstitutions, allowPrivate);
329   }
330
331   private void parse(List<String> args, boolean initsubstitutions,
332           boolean allowPrivate)
333   {
334     this.substitutions = initsubstitutions;
335
336     /*
337      *  If the first argument does not start with "--" or "-" or is not "open",
338      *  and is a filename that exists or a URL, it is probably a file/list of
339      *  files to open so we insert an Arg.OPEN argument before it. This will
340      *  mean the list of files at the start of the arguments are all opened
341      *  separately.
342      */
343     if (args.size() > 0)
344     {
345       String arg0 = args.get(0);
346       if (arg0 != null
347               && (!arg0.startsWith(DOUBLEDASH) && !arg0.startsWith("-")
348                       && !arg0.equals("open") && (new File(arg0).exists()
349                               || HttpUtils.startsWithHttpOrHttps(arg0))))
350       {
351         // insert "--open" at the start
352         args.add(0, Arg.OPEN.argString());
353       }
354     }
355
356     for (int i = 0; i < args.size(); i++)
357     {
358       String arg = args.get(i);
359
360       // look for double-dash, e.g. --arg
361       if (arg.startsWith(DOUBLEDASH))
362       {
363         String argName = null;
364         String val = null;
365         List<String> globVals = null; // for Opt.GLOB only
366         SubVals globSubVals = null; // also for use by Opt.GLOB only
367         String linkedId = null;
368         Type type = null;
369
370         // look for equals e.g. --arg=value
371         int equalPos = arg.indexOf(EQUALS);
372         if (equalPos > -1)
373         {
374           argName = arg.substring(DOUBLEDASH.length(), equalPos);
375           val = arg.substring(equalPos + 1);
376         }
377         else
378         {
379           argName = arg.substring(DOUBLEDASH.length());
380         }
381
382         // look for linked ID e.g. --arg[linkedID]
383         int idOpen = argName.indexOf('[');
384         int idClose = argName.indexOf(']');
385         if (idOpen > -1 && idClose == argName.length() - 1)
386         {
387           linkedId = argName.substring(idOpen + 1, idClose);
388           argName = argName.substring(0, idOpen);
389         }
390
391         // look for type modification e.g. --help-opening
392         int dashPos = argName.indexOf(SINGLEDASH);
393         if (dashPos > -1)
394         {
395           String potentialArgName = argName.substring(0, dashPos);
396           Arg potentialArg = argMap.get(potentialArgName);
397           if (potentialArg != null && potentialArg.hasOption(Opt.HASTYPE))
398           {
399             String typeName = argName.substring(dashPos + 1);
400             try
401             {
402               type = Type.valueOf(typeName);
403             } catch (IllegalArgumentException e)
404             {
405               type = Type.INVALID;
406             }
407             argName = argName.substring(0, dashPos);
408           }
409         }
410
411         Arg a = argMap.get(argName);
412         // check for boolean prepended by "no" e.g. --nowrap
413         boolean negated = false;
414         if (a == null)
415         {
416           if (argName.startsWith(NEGATESTRING) && argMap
417                   .containsKey(argName.substring(NEGATESTRING.length())))
418           {
419             argName = argName.substring(NEGATESTRING.length());
420             a = argMap.get(argName);
421             negated = true;
422           }
423           else
424           {
425             // after all other args, look for Opt.PREFIXKEV args if still not
426             // found
427             for (Arg potentialArg : EnumSet.allOf(Arg.class))
428             {
429               if (potentialArg.hasOption(Opt.PREFIXKEV) && argName != null
430                       && argName.startsWith(potentialArg.getName())
431                       && equalPos > -1)
432               {
433                 val = argName.substring(potentialArg.getName().length())
434                         + EQUALS + val;
435                 argName = argName.substring(0,
436                         potentialArg.getName().length());
437                 a = potentialArg;
438                 break;
439               }
440             }
441           }
442         }
443
444         // check for config errors
445         if (a == null)
446         {
447           // arg not found
448           Console.error("Argument '" + arg + "' not recognised.  Exiting.");
449           Jalview.exit(
450                   "Invalid argument used." + System.lineSeparator() + "Use"
451                           + System.lineSeparator() + "jalview "
452                           + Arg.HELP.argString() + System.lineSeparator()
453                           + "for a usage statement.",
454                   ExitCode.INVALID_ARGUMENT);
455           continue;
456         }
457         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
458         {
459           Console.error(
460                   "Argument '" + a.argString() + "' is private. Ignoring.");
461           continue;
462         }
463         if (!a.hasOption(Opt.BOOLEAN) && negated)
464         {
465           // used "no" with a non-boolean option
466           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
467                   + "' not a boolean option. Ignoring.");
468           continue;
469         }
470         if (!a.hasOption(Opt.STRING) && equalPos > -1)
471         {
472           // set --argname=value when arg does not accept values
473           Console.error("Argument '" + a.argString()
474                   + "' does not expect a value (given as '" + arg
475                   + "').  Ignoring.");
476           continue;
477         }
478         if (!a.hasOption(Opt.LINKED) && linkedId != null)
479         {
480           // set --argname[linkedId] when arg does not use linkedIds
481           Console.error("Argument '" + a.argString()
482                   + "' does not expect a linked id (given as '" + arg
483                   + "'). Ignoring.");
484           continue;
485         }
486
487         // String value(s)
488         if (a.hasOption(Opt.STRING))
489         {
490           if (equalPos >= 0)
491           {
492             if (a.hasOption(Opt.GLOB))
493             {
494               // strip off and save the SubVals to be added individually later
495               globSubVals = new SubVals(val);
496               // make substitutions before looking for files
497               String fileGlob = makeSubstitutions(globSubVals.getContent(),
498                       linkedId);
499               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
500             }
501             else
502             {
503               // val is already set -- will be saved in the ArgValue later in
504               // the normal way
505             }
506           }
507           else
508           {
509             // There is no "=" so value is next arg or args (possibly shell
510             // glob-expanded)
511             if (i + 1 >= args.size())
512             {
513               // no value to take for arg, which wants a value
514               Console.error("Argument '" + a.getName()
515                       + "' requires a value, none given. Ignoring.");
516               continue;
517             }
518             // deal with bash globs here (--arg val* is expanded before reaching
519             // the JVM). Note that SubVals cannot be used in this case.
520             // If using the --arg=val then the glob is preserved and Java globs
521             // will be used later. SubVals can be used.
522             if (a.hasOption(Opt.GLOB))
523             {
524               // if this is the first argument with a file list at the start of
525               // the args we add filenames from index i instead of i+1
526               globVals = getShellGlobbedFilenameValues(a, args, i + 1);
527             }
528             else
529             {
530               val = args.get(i + 1);
531             }
532           }
533         }
534
535         // make NOACTION adjustments
536         // default and auto counter increments
537         if (a == Arg.NPP)
538         {
539           linkedIdAutoCounter++;
540         }
541         else if (a == Arg.SUBSTITUTIONS)
542         {
543           substitutions = !negated;
544         }
545         else if (a == Arg.SETARGFILE)
546         {
547           argFile = val;
548         }
549         else if (a == Arg.UNSETARGFILE)
550         {
551           argFile = null;
552         }
553         else if (a == Arg.ALL)
554         {
555           allLinkedIds = !negated;
556         }
557         else if (a == Arg.ALLSTRUCTURES)
558         {
559           allStructures = !negated;
560         }
561
562         if (a.hasOption(Opt.STORED))
563         {
564           // reset the lastOpenedLinkedIds list
565           this.storedLinkedIds = new ArrayList<>();
566         }
567
568         // this is probably only Arg.NEW and Arg.OPEN
569         if (a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
570         {
571           // use the next default prefixed OPENLINKEDID
572           defaultLinkedId(true);
573         }
574
575         String autoCounterString = null;
576         String defaultLinkedId = defaultLinkedId(false);
577         boolean usingDefaultLinkedId = false;
578         if (a.hasOption(Opt.LINKED))
579         {
580           if (linkedId == null)
581           {
582             if (a.hasOption(Opt.OUTPUTFILE) && a.hasOption(Opt.ALLOWMULTIID)
583                     && val.contains(MATCHALLLINKEDIDS))
584             {
585               // --output=*.ext is shorthand for --output {basename}.ext
586               // --output=*/*.ext is shorthand for
587               // --output {dirname}/{basename}.ext
588               // (or --image=*.ext)
589               linkedId = allLinkedIds ? MATCHALLLINKEDIDS
590                       : MATCHOPENEDLINKEDIDS;
591               val = FileUtils.convertWildcardsToPath(val, MATCHALLLINKEDIDS,
592                       LINKEDIDDIRNAME, LINKEDIDBASENAME);
593             }
594             else if (allLinkedIds && a.hasOption(Opt.ALLOWMULTIID))
595             {
596               linkedId = MATCHALLLINKEDIDS;
597             }
598             else if (a.hasOption(Opt.ALLOWMULTIID)
599                     && this.storedLinkedIds != null
600                     && this.storedLinkedIds.size() > 0)
601             {
602               linkedId = MATCHOPENEDLINKEDIDS;
603             }
604             else
605             {
606               // use default linkedId for linked arguments
607               linkedId = defaultLinkedId;
608               usingDefaultLinkedId = true;
609               Console.debug("Changing linkedId to '" + linkedId + "' from "
610                       + arg);
611             }
612           }
613           else
614           {
615             if (linkedId.contains(LINKEDIDAUTOCOUNTER))
616             {
617               // turn {n} to the autoCounter
618               autoCounterString = Integer.toString(linkedIdAutoCounter);
619               linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
620                       autoCounterString);
621               Console.debug("Changing linkedId to '" + linkedId + "' from "
622                       + arg);
623             }
624             if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
625             {
626               // turn {++n} to the incremented autoCounter
627               autoCounterString = Integer.toString(++linkedIdAutoCounter);
628               linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
629                       autoCounterString);
630               Console.debug("Changing linkedId to '" + linkedId + "' from "
631                       + arg);
632             }
633           }
634         }
635
636         // do not continue in this block for NOACTION args
637         if (a.hasOption(Opt.NOACTION))
638           continue;
639
640         ArgValuesMap avm = getOrCreateLinkedArgValuesMap(linkedId);
641
642         // not dealing with both NODUPLICATEVALUES and GLOB
643         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
644         {
645           Console.error("Argument '" + a.argString()
646                   + "' cannot contain a duplicate value ('" + val
647                   + "'). Ignoring this and subsequent occurrences.");
648           continue;
649         }
650
651         // check for unique id
652         SubVals subvals = new SubVals(val);
653         boolean addNewSubVals = false;
654         String id = subvals.get(ArgValues.ID);
655         if (id != null && avm.hasId(a, id))
656         {
657           Console.error("Argument '" + a.argString()
658                   + "' has a duplicate id ('" + id + "'). Ignoring.");
659           continue;
660         }
661
662         // set allstructures to all non-primary structure options in this linked
663         // id if --allstructures has been set
664         if (allStructures && (a.hasType(Type.STRUCTURE)
665         // || a.getType() == Type.STRUCTUREIMAGE)
666         ) && !a.hasOption(Opt.PRIMARY))
667         {
668           if (!subvals.has(Arg.ALLSTRUCTURES.getName()))
669           // && !subvals.has("structureid"))
670           {
671             subvals.put(Arg.ALLSTRUCTURES.getName(), "true");
672             addNewSubVals = true;
673           }
674         }
675
676         ArgValues avs = avm.getOrCreateArgValues(a);
677
678         // store appropriate String value(s)
679         if (a.hasOption(Opt.STRING))
680         {
681           if (a.hasOption(Opt.GLOB) && globVals != null
682                   && globVals.size() > 0)
683           {
684             Enumeration<String> gve = Collections.enumeration(globVals);
685             while (gve.hasMoreElements())
686             {
687               String v = gve.nextElement();
688               SubVals vsv = new SubVals(globSubVals, v);
689               addValue(linkedId, type, avs, vsv, v, argIndex++, true);
690               // if we're using defaultLinkedId and the arg increments the
691               // counter:
692               if (gve.hasMoreElements() && usingDefaultLinkedId
693                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
694               {
695                 // increment the default linkedId
696                 linkedId = defaultLinkedId(true);
697                 // get new avm and avs
698                 avm = linkedArgs.get(linkedId);
699                 avs = avm.getOrCreateArgValues(a);
700               }
701             }
702           }
703           else
704           {
705             // addValue(linkedId, type, avs, val, argIndex, true);
706             addValue(linkedId, type, avs, addNewSubVals ? subvals : null,
707                     val, argIndex, true);
708           }
709         }
710         else if (a.hasOption(Opt.BOOLEAN))
711         {
712           setBoolean(linkedId, type, avs, !negated, argIndex);
713           setNegated(linkedId, avs, negated);
714         }
715         else if (a.hasOption(Opt.UNARY))
716         {
717           setBoolean(linkedId, type, avs, true, argIndex);
718         }
719
720         // remove the '*' or 'open*' linkedId that should be empty if it was
721         // created
722         if ((MATCHALLLINKEDIDS.equals(linkedId)
723                 || MATCHOPENEDLINKEDIDS.equals(linkedId))
724                 && linkedArgs.containsKey(linkedId))
725         {
726           linkedArgs.remove(linkedId);
727         }
728       }
729     }
730   }
731
732   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
733   {
734     Arg a = avs.arg();
735     incrementCount(linkedId, avs);
736     argIndex++;
737
738     // store in appropriate place
739     if (a.hasOption(Opt.LINKED))
740     {
741       // store the order of linkedIds
742       if (!linkedOrder.contains(linkedId))
743         linkedOrder.add(linkedId);
744     }
745
746     // store arg in the list of args used
747     if (!argList.contains(a))
748       argList.add(a);
749   }
750
751   private String defaultLinkedId(boolean increment)
752   {
753     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
754             .append(Integer.toString(defaultLinkedIdCounter)).toString();
755     if (increment)
756     {
757       while (linkedArgs.containsKey(defaultLinkedId))
758       {
759         defaultLinkedIdCounter++;
760         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
761                 .append(Integer.toString(defaultLinkedIdCounter))
762                 .toString();
763       }
764     }
765     getOrCreateLinkedArgValuesMap(defaultLinkedId);
766     return defaultLinkedId;
767   }
768
769   public String makeSubstitutions(String val, String linkedId)
770   {
771     return makeSubstitutions(val, linkedId, false);
772   }
773
774   public String makeSubstitutions(String val, String linkedId,
775           boolean onthefly)
776   {
777     if (!this.substitutions || val == null)
778       return val;
779
780     String subvals;
781     String rest;
782     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
783     {
784       int closeBracket = val.indexOf(']');
785       if (val.length() == closeBracket)
786         return val;
787       subvals = val.substring(0, closeBracket + 1);
788       rest = val.substring(closeBracket + 1);
789     }
790     else
791     {
792       subvals = "";
793       rest = val;
794     }
795     if (rest.contains(LINKEDIDAUTOCOUNTER))
796     {
797       rest = rest.replace(LINKEDIDAUTOCOUNTER,
798               String.valueOf(linkedIdAutoCounter));
799     }
800     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
801     {
802       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
803               String.valueOf(++linkedIdAutoCounter));
804     }
805     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
806     {
807       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
808               String.valueOf(defaultLinkedIdCounter));
809     }
810     ArgValuesMap avm = linkedArgs.get(linkedId);
811     if (avm != null)
812     {
813       if (rest.contains(LINKEDIDBASENAME))
814       {
815         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
816       }
817       if (rest.contains(LINKEDIDEXTENSION))
818       {
819         rest = rest.replace(LINKEDIDEXTENSION, avm.getExtension());
820       }
821       if (rest.contains(LINKEDIDDIRNAME))
822       {
823         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
824       }
825     }
826     if (argFile != null)
827     {
828       if (rest.contains(ARGFILEBASENAME))
829       {
830         rest = rest.replace(ARGFILEBASENAME,
831                 FileUtils.getBasename(new File(argFile)));
832       }
833       if (rest.contains(ARGFILEDIRNAME))
834       {
835         rest = rest.replace(ARGFILEDIRNAME,
836                 FileUtils.getDirname(new File(argFile)));
837       }
838     }
839     if (onthefly)
840     {
841       if (rest.contains(ONTHEFLYCOUNTER))
842       {
843         rest = rest.replace(ONTHEFLYCOUNTER,
844                 String.valueOf(ontheflyCounter));
845       }
846       if (rest.contains(INCREMENTONTHEFLYCOUNTER))
847       {
848         rest = rest.replace(INCREMENTONTHEFLYCOUNTER,
849                 String.valueOf(++ontheflyCounter));
850       }
851       if (currentStructureFilename != null)
852       {
853         if (rest.contains(STRUCTUREBASENAME))
854         {
855           rest = rest.replace(STRUCTUREBASENAME, FileUtils
856                   .getBasename(new File(currentStructureFilename)));
857         }
858         if (rest.contains(STRUCTUREDIRNAME))
859         {
860           rest = rest.replace(STRUCTUREDIRNAME,
861                   FileUtils.getDirname(new File(currentStructureFilename)));
862         }
863       }
864     }
865
866     return new StringBuilder(subvals).append(rest).toString();
867   }
868
869   /*
870    * A helper method to take a list of String args where we're expecting
871    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
872    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
873    * "file2", "file3"} *and remove these from the original list object* so that
874    * processing can continue from where it has left off, e.g. args has become
875    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
876    * carries on from the next --arg if available.
877    */
878   protected static List<String> getShellGlobbedFilenameValues(Arg a,
879           List<String> args, int i)
880   {
881     List<String> vals = new ArrayList<>();
882     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
883     {
884       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
885       if (!a.hasOption(Opt.GLOB))
886         break;
887     }
888     return vals;
889   }
890
891   public BootstrapArgs getBootstrapArgs()
892   {
893     return bootstrapArgs;
894   }
895
896   public boolean isSet(Arg a)
897   {
898     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
899   }
900
901   public boolean isSetAtAll(Arg a)
902   {
903     for (String linkedId : linkedOrder)
904     {
905       if (isSet(linkedId, a))
906         return true;
907     }
908     return false;
909   }
910
911   public boolean isSet(String linkedId, Arg a)
912   {
913     ArgValuesMap avm = linkedArgs.get(linkedId);
914     return avm == null ? false : avm.containsArg(a);
915   }
916
917   public boolean getBoolean(Arg a)
918   {
919     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
920     {
921       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
922               + "'.");
923     }
924     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
925   }
926
927   public boolean getBool(String linkedId, Arg a)
928   {
929     ArgValuesMap avm = linkedArgs.get(linkedId);
930     if (avm == null)
931       return a.getDefaultBoolValue();
932     ArgValues avs = avm.getArgValues(a);
933     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
934   }
935
936   public List<String> getLinkedIds()
937   {
938     return linkedOrder;
939   }
940
941   public ArgValuesMap getLinkedArgs(String id)
942   {
943     return linkedArgs.get(id);
944   }
945
946   @Override
947   public String toString()
948   {
949     StringBuilder sb = new StringBuilder();
950     sb.append("UNLINKED\n");
951     sb.append(argValuesMapToString(linkedArgs.get(null)));
952     if (getLinkedIds() != null)
953     {
954       sb.append("LINKED\n");
955       for (String id : getLinkedIds())
956       {
957         // already listed these as UNLINKED args
958         if (id == null)
959           continue;
960
961         ArgValuesMap avm = getLinkedArgs(id);
962         sb.append("ID: '").append(id).append("'\n");
963         sb.append(argValuesMapToString(avm));
964       }
965     }
966     return sb.toString();
967   }
968
969   private static String argValuesMapToString(ArgValuesMap avm)
970   {
971     if (avm == null)
972       return null;
973     StringBuilder sb = new StringBuilder();
974     for (Arg a : avm.getArgKeys())
975     {
976       ArgValues v = avm.getArgValues(a);
977       sb.append(v.toString());
978       sb.append("\n");
979     }
980     return sb.toString();
981   }
982
983   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
984           boolean initsubstitutions, BootstrapArgs bsa)
985   {
986     List<File> argFiles = new ArrayList<>();
987
988     for (String pattern : argFilenameGlobs)
989     {
990       // I don't think we want to dedup files, making life easier
991       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
992     }
993
994     return parseArgFileList(argFiles, initsubstitutions, bsa);
995   }
996
997   public static ArgParser parseArgFileList(List<File> argFiles,
998           boolean initsubstitutions, BootstrapArgs bsa)
999   {
1000     List<String> argsList = new ArrayList<>();
1001     for (File argFile : argFiles)
1002     {
1003       if (!argFile.exists())
1004       {
1005         String message = Arg.ARGFILE.argString() + EQUALS + "\""
1006                 + argFile.getPath() + "\": File does not exist.";
1007         Jalview.exit(message, ExitCode.FILE_NOT_FOUND);
1008       }
1009       try
1010       {
1011         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
1012                 .append(EQUALS).append(argFile.getCanonicalPath())
1013                 .toString();
1014         argsList.add(setargfile);
1015         argsList.addAll(readArgFile(argFile));
1016         argsList.add(Arg.UNSETARGFILE.argString());
1017       } catch (IOException e)
1018       {
1019         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
1020                 + "\": File could not be read.";
1021         Jalview.exit(message, ExitCode.FILE_NOT_READABLE);
1022       }
1023     }
1024     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
1025     // --unsetargfile
1026     return new ArgParser(argsList, initsubstitutions, true, bsa);
1027   }
1028
1029   protected static List<String> readArgFile(File argFile)
1030   {
1031     List<String> args = new ArrayList<>();
1032     if (argFile != null && argFile.exists())
1033     {
1034       try
1035       {
1036         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
1037         {
1038           if (line != null && line.length() > 0
1039                   && line.charAt(0) != ARGFILECOMMENT)
1040             args.add(line);
1041         }
1042       } catch (IOException e)
1043       {
1044         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
1045                 + "\": File could not be read.";
1046         Console.debug(message, e);
1047         Jalview.exit(message, ExitCode.FILE_NOT_READABLE);
1048       }
1049     }
1050     return args;
1051   }
1052
1053   // the following methods look for the "*" linkedId and add the argvalue to all
1054   // linkedId ArgValues if it does.
1055   /**
1056    * This version inserts the subvals sv into all created values
1057    */
1058   private void addValue(String linkedId, Type type, ArgValues avs,
1059           SubVals sv, String v, int argIndex, boolean doSubs)
1060   {
1061     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, sv, v, false,
1062             argIndex, doSubs);
1063   }
1064
1065   private void setBoolean(String linkedId, Type type, ArgValues avs,
1066           boolean b, int argIndex)
1067   {
1068     this.argValueOperation(Op.SETBOOLEAN, linkedId, type, avs, null, null,
1069             b, argIndex, false);
1070   }
1071
1072   private void setNegated(String linkedId, ArgValues avs, boolean b)
1073   {
1074     this.argValueOperation(Op.SETNEGATED, linkedId, null, avs, null, null,
1075             b, 0, false);
1076   }
1077
1078   private void incrementCount(String linkedId, ArgValues avs)
1079   {
1080     this.argValueOperation(Op.INCREMENTCOUNT, linkedId, null, avs, null,
1081             null, false, 0, false);
1082   }
1083
1084   private enum Op
1085   {
1086     ADDVALUE, SETBOOLEAN, SETNEGATED, INCREMENTCOUNT
1087   }
1088
1089   private void argValueOperation(Op op, String linkedId, Type type,
1090           ArgValues avs, SubVals sv, String v, boolean b, int argIndex,
1091           boolean doSubs)
1092   {
1093     // default to merge subvals if subvals are provided
1094     argValueOperation(op, linkedId, type, avs, sv, true, v, b, argIndex,
1095             doSubs);
1096   }
1097
1098   /**
1099    * The following operations look for the "*" and "open*" linkedIds and add the
1100    * argvalue to all appropriate linkedId ArgValues if it does. If subvals are
1101    * supplied, they are inserted into all new set values.
1102    * 
1103    * @param op
1104    *          The ArgParser.Op operation
1105    * @param linkedId
1106    *          The String linkedId from the ArgValuesMap
1107    * @param type
1108    *          The Arg.Type to attach to this ArgValue
1109    * @param avs
1110    *          The ArgValues for this linkedId
1111    * @param sv
1112    *          Use these SubVals on the ArgValue
1113    * @param merge
1114    *          Merge the SubVals with any existing on the value. False will
1115    *          replace unless sv is null
1116    * @param v
1117    *          The value of the ArgValue (may contain subvals).
1118    * @param b
1119    *          The boolean value of the ArgValue.
1120    * @param argIndex
1121    *          The argIndex for the ArgValue.
1122    * @param doSubs
1123    *          Whether to perform substitutions on the subvals and value.
1124    */
1125   private void argValueOperation(Op op, String linkedId, Type type,
1126           ArgValues avs, SubVals sv, boolean merge, String v, boolean b,
1127           int argIndex, boolean doSubs)
1128   {
1129     Arg a = avs.arg();
1130
1131     List<String> wildcardLinkedIds = null;
1132     if (a.hasOption(Opt.ALLOWMULTIID))
1133     {
1134       switch (linkedId)
1135       {
1136       case MATCHALLLINKEDIDS:
1137         wildcardLinkedIds = getLinkedIds();
1138         break;
1139       case MATCHOPENEDLINKEDIDS:
1140         wildcardLinkedIds = this.storedLinkedIds;
1141         break;
1142       }
1143     }
1144
1145     // if we're not a wildcard linkedId and the arg is marked to be stored, add
1146     // to storedLinkedIds
1147     if (linkedId != null && wildcardLinkedIds == null
1148             && a.hasOption(Opt.STORED)
1149             && !storedLinkedIds.contains(linkedId))
1150     {
1151       storedLinkedIds.add(linkedId);
1152     }
1153
1154     // if we are a wildcard linkedId, apply the arg and value to all appropriate
1155     // linkedIds
1156     if (wildcardLinkedIds != null)
1157     {
1158       for (String id : wildcardLinkedIds)
1159       {
1160         // skip incorrectly stored wildcard ids!
1161         if (id == null || MATCHALLLINKEDIDS.equals(id)
1162                 || MATCHOPENEDLINKEDIDS.equals(id))
1163         {
1164           continue;
1165         }
1166         ArgValuesMap avm = linkedArgs.get(id);
1167         // don't set an output if there isn't an input
1168         if (a.hasOption(Opt.REQUIREINPUT)
1169                 && !avm.hasArgWithOption(Opt.INPUT))
1170           continue;
1171
1172         ArgValues tavs = avm.getOrCreateArgValues(a);
1173         switch (op)
1174         {
1175
1176         case ADDVALUE:
1177           String val = v;
1178           if (sv != null)
1179           {
1180             if (doSubs)
1181             {
1182               sv = new SubVals(sv, val, merge);
1183               val = makeSubstitutions(sv.getContent(), id);
1184             }
1185             tavs.addValue(sv, type, val, argIndex, true);
1186           }
1187           else
1188           {
1189             if (doSubs)
1190             {
1191               val = makeSubstitutions(v, id);
1192             }
1193             tavs.addValue(type, val, argIndex, true);
1194           }
1195           finaliseStoringArgValue(id, tavs);
1196           break;
1197
1198         case SETBOOLEAN:
1199           tavs.setBoolean(type, b, argIndex, true);
1200           finaliseStoringArgValue(id, tavs);
1201           break;
1202
1203         case SETNEGATED:
1204           tavs.setNegated(b, true);
1205           break;
1206
1207         case INCREMENTCOUNT:
1208           tavs.incrementCount();
1209           break;
1210
1211         default:
1212           break;
1213
1214         }
1215
1216       }
1217     }
1218     else // no wildcard linkedId -- do it simpler
1219     {
1220       switch (op)
1221       {
1222       case ADDVALUE:
1223         String val = v;
1224         if (sv != null)
1225         {
1226           if (doSubs)
1227           {
1228             val = makeSubstitutions(v, linkedId);
1229             sv = new SubVals(sv, val);
1230           }
1231           avs.addValue(sv, type, val, argIndex, false);
1232         }
1233         else
1234         {
1235           if (doSubs)
1236           {
1237             val = makeSubstitutions(v, linkedId);
1238           }
1239           avs.addValue(type, val, argIndex, false);
1240         }
1241         finaliseStoringArgValue(linkedId, avs);
1242         break;
1243
1244       case SETBOOLEAN:
1245         avs.setBoolean(type, b, argIndex, false);
1246         finaliseStoringArgValue(linkedId, avs);
1247         break;
1248
1249       case SETNEGATED:
1250         avs.setNegated(b, false);
1251         break;
1252
1253       case INCREMENTCOUNT:
1254         avs.incrementCount();
1255         break;
1256
1257       default:
1258         break;
1259       }
1260     }
1261   }
1262
1263   private ArgValuesMap getOrCreateLinkedArgValuesMap(String linkedId)
1264   {
1265     if (linkedArgs.containsKey(linkedId)
1266             && linkedArgs.get(linkedId) != null)
1267       return linkedArgs.get(linkedId);
1268
1269     linkedArgs.put(linkedId, new ArgValuesMap(linkedId));
1270     return linkedArgs.get(linkedId);
1271   }
1272
1273   public boolean isOldStyle()
1274   {
1275     return oldArguments;
1276   }
1277
1278   public boolean isMixedStyle()
1279   {
1280     return mixedArguments;
1281   }
1282
1283   public String[] getMixedExamples()
1284   {
1285     return mixedExamples;
1286   }
1287
1288   public void setStructureFilename(String s)
1289   {
1290     this.currentStructureFilename = s;
1291   }
1292
1293 }