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