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