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