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