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