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