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