JAL-629 Added 'Type' to args and argvalues
[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             argName = argName.substring(0, dashPos);
287             String typeName = argName.substring(dashPos + 1);
288             type = Type.valueOf(typeName);
289           }
290         }
291
292         Arg a = argMap.get(argName);
293         // check for boolean prepended by "no" e.g. --nowrap
294         boolean negated = false;
295         if (a == null && argName.startsWith(NEGATESTRING) && argMap
296                 .containsKey(argName.substring(NEGATESTRING.length())))
297         {
298           argName = argName.substring(NEGATESTRING.length());
299           a = argMap.get(argName);
300           negated = true;
301         }
302
303         // check for config errors
304         if (a == null)
305         {
306           // arg not found
307           Console.error("Argument '" + arg + "' not recognised.  Exiting.");
308           Jalview.exit("Invalid argument used." + System.lineSeparator()
309                   + "Use" + System.lineSeparator() + "jalview "
310                   + Arg.HELP.argString() + System.lineSeparator()
311                   + "for a usage statement.", 13);
312           continue;
313         }
314         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
315         {
316           Console.error(
317                   "Argument '" + a.argString() + "' is private. Ignoring.");
318           continue;
319         }
320         if (!a.hasOption(Opt.BOOLEAN) && negated)
321         {
322           // used "no" with a non-boolean option
323           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
324                   + "' not a boolean option. Ignoring.");
325           continue;
326         }
327         if (!a.hasOption(Opt.STRING) && equalPos > -1)
328         {
329           // set --argname=value when arg does not accept values
330           Console.error("Argument '" + a.argString()
331                   + "' does not expect a value (given as '" + arg
332                   + "').  Ignoring.");
333           continue;
334         }
335         if (!a.hasOption(Opt.LINKED) && linkedId != null)
336         {
337           // set --argname[linkedId] when arg does not use linkedIds
338           Console.error("Argument '" + a.argString()
339                   + "' does not expect a linked id (given as '" + arg
340                   + "'). Ignoring.");
341           continue;
342         }
343
344         // String value(s)
345         if (a.hasOption(Opt.STRING))
346         {
347           if (equalPos >= 0)
348           {
349             if (a.hasOption(Opt.GLOB))
350             {
351               // strip off and save the SubVals to be added individually later
352               globSubVals = new SubVals(val);
353               // make substitutions before looking for files
354               String fileGlob = makeSubstitutions(globSubVals.getContent(),
355                       linkedId);
356               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
357             }
358             else
359             {
360               // val is already set -- will be saved in the ArgValue later in
361               // the normal way
362             }
363           }
364           else
365           {
366             // There is no "=" so value is next arg or args (possibly shell
367             // glob-expanded)
368             if ((openEachInitialFilenames ? i : i + 1) >= args.size())
369             {
370               // no value to take for arg, which wants a value
371               Console.error("Argument '" + a.getName()
372                       + "' requires a value, none given. Ignoring.");
373               continue;
374             }
375             // deal with bash globs here (--arg val* is expanded before reaching
376             // the JVM). Note that SubVals cannot be used in this case.
377             // If using the --arg=val then the glob is preserved and Java globs
378             // will be used later. SubVals can be used.
379             if (a.hasOption(Opt.GLOB))
380             {
381               // if this is the first argument with a file list at the start of
382               // the args we add filenames from index i instead of i+1
383               globVals = getShellGlobbedFilenameValues(a, args,
384                       openEachInitialFilenames ? i : i + 1);
385             }
386             else
387             {
388               val = args.get(i + 1);
389             }
390           }
391         }
392
393         // make NOACTION adjustments
394         // default and auto counter increments
395         if (a == Arg.NPP)
396         {
397           linkedIdAutoCounter++;
398         }
399         else if (a == Arg.SUBSTITUTIONS)
400         {
401           substitutions = !negated;
402         }
403         else if (a == Arg.SETARGFILE)
404         {
405           argFile = val;
406         }
407         else if (a == Arg.UNSETARGFILE)
408         {
409           argFile = null;
410         }
411         else if (a == Arg.ALL)
412         {
413           allLinkedIds = !negated;
414           openedLinkedIds = false;
415         }
416         else if (a == Arg.OPENED)
417         {
418           openedLinkedIds = !negated;
419           allLinkedIds = false;
420         }
421
422         if (a.hasOption(Opt.STORED))
423         {
424           // reset the lastOpenedLinkedIds list
425           this.storedLinkedIds = new ArrayList<>();
426         }
427
428         // this is probably only Arg.NEW and Arg.OPEN
429         if (a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
430         {
431           // use the next default prefixed OPENLINKEDID
432           defaultLinkedId(true);
433         }
434
435         String autoCounterString = null;
436         String defaultLinkedId = defaultLinkedId(false);
437         boolean usingDefaultLinkedId = false;
438         if (a.hasOption(Opt.LINKED))
439         {
440           if (linkedId == null)
441           {
442             if (a.hasOption(Opt.OUTPUT) && a.hasOption(Opt.ALLOWALL)
443                     && val.startsWith(MATCHALLLINKEDIDS))
444             {
445               // --output=*.ext is shorthand for --all --output {basename}.ext
446               // (or --image=*.ext)
447               allLinkedIds = true;
448               openedLinkedIds = false;
449               linkedId = MATCHALLLINKEDIDS;
450               val = LINKEDIDDIRNAME + File.separator + LINKEDIDBASENAME
451                       + val.substring(MATCHALLLINKEDIDS.length());
452             }
453             else if (a.hasOption(Opt.OUTPUT) && a.hasOption(Opt.ALLOWALL)
454                     && val.startsWith(MATCHOPENEDLINKEDIDS))
455             {
456               // --output=open*.ext is shorthand for --opened --output
457               // {basename}.ext
458               // (or --image=open*.ext)
459               openedLinkedIds = true;
460               allLinkedIds = false;
461               linkedId = MATCHOPENEDLINKEDIDS;
462               val = LINKEDIDDIRNAME + File.separator + LINKEDIDBASENAME
463                       + val.substring(MATCHOPENEDLINKEDIDS.length());
464             }
465             else if (allLinkedIds && a.hasOption(Opt.ALLOWALL))
466             {
467               linkedId = MATCHALLLINKEDIDS;
468             }
469             else if (openedLinkedIds && a.hasOption(Opt.ALLOWALL))
470             {
471               linkedId = MATCHOPENEDLINKEDIDS;
472             }
473             else
474             {
475               // use default linkedId for linked arguments
476               linkedId = defaultLinkedId;
477               usingDefaultLinkedId = true;
478               Console.debug("Changing linkedId to '" + linkedId + "' from "
479                       + arg);
480             }
481           }
482           else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
483           {
484             // turn {n} to the autoCounter
485             autoCounterString = Integer.toString(linkedIdAutoCounter);
486             linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
487                     autoCounterString);
488             Console.debug(
489                     "Changing linkedId to '" + linkedId + "' from " + arg);
490           }
491           else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
492           {
493             // turn {++n} to the incremented autoCounter
494             autoCounterString = Integer.toString(++linkedIdAutoCounter);
495             linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
496                     autoCounterString);
497             Console.debug(
498                     "Changing linkedId to '" + linkedId + "' from " + arg);
499           }
500         }
501
502         // do not continue in this block for NOACTION args
503         if (a.hasOption(Opt.NOACTION))
504           continue;
505
506         ArgValuesMap avm = getOrCreateLinkedArgValuesMap(linkedId);
507
508         // not dealing with both NODUPLICATEVALUES and GLOB
509         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
510         {
511           Console.error("Argument '" + a.argString()
512                   + "' cannot contain a duplicate value ('" + val
513                   + "'). Ignoring this and subsequent occurrences.");
514           continue;
515         }
516
517         // check for unique id
518         SubVals idsv = new SubVals(val);
519         String id = idsv.get(ArgValues.ID);
520         if (id != null && avm.hasId(a, id))
521         {
522           Console.error("Argument '" + a.argString()
523                   + "' has a duplicate id ('" + id + "'). Ignoring.");
524           continue;
525         }
526
527         /* TODO
528          * Change all avs.addValue() avs.setBoolean avs.setNegated() avs.incrementCount calls to checkfor linkedId == "*"
529          * DONE, need to check
530          */
531         ArgValues avs = avm.getOrCreateArgValues(a);
532
533         // store appropriate String value(s)
534         if (a.hasOption(Opt.STRING))
535         {
536           if (a.hasOption(Opt.GLOB) && globVals != null
537                   && globVals.size() > 0)
538           {
539             Enumeration<String> gve = Collections.enumeration(globVals);
540             while (gve.hasMoreElements())
541             {
542               String v = gve.nextElement();
543               SubVals vsv = new SubVals(globSubVals, v);
544               addValue(linkedId, type, avs, vsv, v, argIndex++, true);
545               // if we're using defaultLinkedId and the arg increments the
546               // counter:
547               if (gve.hasMoreElements() && usingDefaultLinkedId
548                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
549               {
550                 // increment the default linkedId
551                 linkedId = defaultLinkedId(true);
552                 // get new avm and avs
553                 avm = linkedArgs.get(linkedId);
554                 avs = avm.getOrCreateArgValues(a);
555               }
556             }
557           }
558           else
559           {
560             addValue(linkedId, type, avs, val, argIndex, true);
561           }
562         }
563         else if (a.hasOption(Opt.BOOLEAN))
564         {
565           setBoolean(linkedId, type, avs, !negated, argIndex);
566           setNegated(linkedId, avs, negated);
567         }
568         else if (a.hasOption(Opt.UNARY))
569         {
570           setBoolean(linkedId, type, avs, true, argIndex);
571         }
572
573         // remove the '*' or 'open*' linkedId that should be empty if it was
574         // created
575         if ((MATCHALLLINKEDIDS.equals(linkedId)
576                 && linkedArgs.containsKey(linkedId))
577                 || (MATCHOPENEDLINKEDIDS.equals(linkedId)
578                         && linkedArgs.containsKey(linkedId)))
579         {
580           linkedArgs.remove(linkedId);
581         }
582       }
583     }
584   }
585
586   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
587   {
588     Arg a = avs.arg();
589     incrementCount(linkedId, avs);
590     argIndex++;
591
592     // store in appropriate place
593     if (a.hasOption(Opt.LINKED))
594     {
595       // store the order of linkedIds
596       if (!linkedOrder.contains(linkedId))
597         linkedOrder.add(linkedId);
598     }
599
600     // store arg in the list of args used
601     if (!argList.contains(a))
602       argList.add(a);
603   }
604
605   private String defaultLinkedId(boolean increment)
606   {
607     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
608             .append(Integer.toString(defaultLinkedIdCounter)).toString();
609     if (increment)
610     {
611       while (linkedArgs.containsKey(defaultLinkedId))
612       {
613         defaultLinkedIdCounter++;
614         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
615                 .append(Integer.toString(defaultLinkedIdCounter))
616                 .toString();
617       }
618     }
619     getOrCreateLinkedArgValuesMap(defaultLinkedId);
620     return defaultLinkedId;
621   }
622
623   public String makeSubstitutions(String val, String linkedId)
624   {
625     if (!this.substitutions || val == null)
626       return val;
627
628     String subvals;
629     String rest;
630     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
631     {
632       int closeBracket = val.indexOf(']');
633       if (val.length() == closeBracket)
634         return val;
635       subvals = val.substring(0, closeBracket + 1);
636       rest = val.substring(closeBracket + 1);
637     }
638     else
639     {
640       subvals = "";
641       rest = val;
642     }
643     if (rest.contains(LINKEDIDAUTOCOUNTER))
644       rest = rest.replace(LINKEDIDAUTOCOUNTER,
645               String.valueOf(linkedIdAutoCounter));
646     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
647       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
648               String.valueOf(++linkedIdAutoCounter));
649     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
650       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
651               String.valueOf(defaultLinkedIdCounter));
652     ArgValuesMap avm = linkedArgs.get(linkedId);
653     if (avm != null)
654     {
655       if (rest.contains(LINKEDIDBASENAME))
656       {
657         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
658       }
659       if (rest.contains(LINKEDIDEXTENSION))
660       {
661         rest = rest.replace(LINKEDIDEXTENSION, avm.getExtension());
662       }
663       if (rest.contains(LINKEDIDDIRNAME))
664       {
665         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
666       }
667     }
668     if (argFile != null)
669     {
670       if (rest.contains(ARGFILEBASENAME))
671       {
672         rest = rest.replace(ARGFILEBASENAME,
673                 FileUtils.getBasename(new File(argFile)));
674       }
675       if (rest.contains(ARGFILEDIRNAME))
676       {
677         rest = rest.replace(ARGFILEDIRNAME,
678                 FileUtils.getDirname(new File(argFile)));
679       }
680     }
681
682     return new StringBuilder(subvals).append(rest).toString();
683   }
684
685   /*
686    * A helper method to take a list of String args where we're expecting
687    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
688    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
689    * "file2", "file3"} *and remove these from the original list object* so that
690    * processing can continue from where it has left off, e.g. args has become
691    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
692    * carries on from the next --arg if available.
693    */
694   protected static List<String> getShellGlobbedFilenameValues(Arg a,
695           List<String> args, int i)
696   {
697     List<String> vals = new ArrayList<>();
698     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
699     {
700       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
701       if (!a.hasOption(Opt.GLOB))
702         break;
703     }
704     return vals;
705   }
706
707   public BootstrapArgs getBootstrapArgs()
708   {
709     return bootstrapArgs;
710   }
711
712   public boolean isSet(Arg a)
713   {
714     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
715   }
716
717   public boolean isSetAtAll(Arg a)
718   {
719     for (String linkedId : linkedOrder)
720     {
721       if (isSet(linkedId, a))
722         return true;
723     }
724     return false;
725   }
726
727   public boolean isSet(String linkedId, Arg a)
728   {
729     ArgValuesMap avm = linkedArgs.get(linkedId);
730     return avm == null ? false : avm.containsArg(a);
731   }
732
733   public boolean getBoolean(Arg a)
734   {
735     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
736     {
737       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
738               + "'.");
739     }
740     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
741   }
742
743   public boolean getBool(String linkedId, Arg a)
744   {
745     ArgValuesMap avm = linkedArgs.get(linkedId);
746     if (avm == null)
747       return a.getDefaultBoolValue();
748     ArgValues avs = avm.getArgValues(a);
749     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
750   }
751
752   public List<String> getLinkedIds()
753   {
754     return linkedOrder;
755   }
756
757   public ArgValuesMap getLinkedArgs(String id)
758   {
759     return linkedArgs.get(id);
760   }
761
762   @Override
763   public String toString()
764   {
765     StringBuilder sb = new StringBuilder();
766     sb.append("UNLINKED\n");
767     sb.append(argValuesMapToString(linkedArgs.get(null)));
768     if (getLinkedIds() != null)
769     {
770       sb.append("LINKED\n");
771       for (String id : getLinkedIds())
772       {
773         // already listed these as UNLINKED args
774         if (id == null)
775           continue;
776
777         ArgValuesMap avm = getLinkedArgs(id);
778         sb.append("ID: '").append(id).append("'\n");
779         sb.append(argValuesMapToString(avm));
780       }
781     }
782     return sb.toString();
783   }
784
785   private static String argValuesMapToString(ArgValuesMap avm)
786   {
787     if (avm == null)
788       return null;
789     StringBuilder sb = new StringBuilder();
790     for (Arg a : avm.getArgKeys())
791     {
792       ArgValues v = avm.getArgValues(a);
793       sb.append(v.toString());
794       sb.append("\n");
795     }
796     return sb.toString();
797   }
798
799   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
800           boolean initsubstitutions, BootstrapArgs bsa)
801   {
802     List<File> argFiles = new ArrayList<>();
803
804     for (String pattern : argFilenameGlobs)
805     {
806       // I don't think we want to dedup files, making life easier
807       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
808     }
809
810     return parseArgFileList(argFiles, initsubstitutions, bsa);
811   }
812
813   public static ArgParser parseArgFileList(List<File> argFiles,
814           boolean initsubstitutions, BootstrapArgs bsa)
815   {
816     List<String> argsList = new ArrayList<>();
817     for (File argFile : argFiles)
818     {
819       if (!argFile.exists())
820       {
821         String message = Arg.ARGFILE.argString() + EQUALS + "\""
822                 + argFile.getPath() + "\": File does not exist.";
823         Jalview.exit(message, 2);
824       }
825       try
826       {
827         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
828                 .append(EQUALS).append(argFile.getCanonicalPath())
829                 .toString();
830         argsList.add(setargfile);
831         argsList.addAll(readArgFile(argFile));
832         argsList.add(Arg.UNSETARGFILE.argString());
833       } catch (IOException e)
834       {
835         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
836                 + "\": File could not be read.";
837         Jalview.exit(message, 3);
838       }
839     }
840     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
841     // --unsetargfile
842     return new ArgParser(argsList, initsubstitutions, true, bsa);
843   }
844
845   protected static List<String> readArgFile(File argFile)
846   {
847     List<String> args = new ArrayList<>();
848     if (argFile != null && argFile.exists())
849     {
850       try
851       {
852         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
853         {
854           if (line != null && line.length() > 0
855                   && line.charAt(0) != ARGFILECOMMENT)
856             args.add(line);
857         }
858       } catch (IOException e)
859       {
860         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
861                 + "\": File could not be read.";
862         Console.debug(message, e);
863         Jalview.exit(message, 3);
864       }
865     }
866     return args;
867   }
868
869   public static enum Position
870   {
871     FIRST, BEFORE, AFTER
872   }
873
874   // get from following Arg of type a or subval of same name (lowercase)
875   public static String getValueFromSubValOrArg(ArgValuesMap avm,
876           ArgValue av, Arg a, SubVals sv)
877   {
878     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
879   }
880
881   // get from following Arg of type a or subval key or preference pref or
882   // default def
883   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
884           Arg a, SubVals sv, String key, String pref, String def)
885   {
886     return getFromSubValArgOrPref(avm, a, Position.AFTER, av, sv, key, pref,
887             def);
888   }
889
890   // get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
891   // Arg of type a or subval key or preference pref or default def
892   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
893           Position pos, ArgValue av, SubVals sv, String key, String pref,
894           String def)
895   {
896     return getFromSubValArgOrPrefWithSubstitutions(null, avm, a, pos, av,
897             sv, key, pref, def);
898   }
899
900   public static String getFromSubValArgOrPrefWithSubstitutions(ArgParser ap,
901           ArgValuesMap avm, Arg a, Position pos, ArgValue av, SubVals sv,
902           String key, String pref, String def)
903   {
904     if (key == null)
905       key = a.getName();
906     String value = null;
907     if (sv != null && sv.has(key) && sv.get(key) != null)
908     {
909       value = ap == null ? sv.get(key)
910               : sv.getWithSubstitutions(ap, avm.getLinkedId(), key);
911     }
912     else if (avm != null && avm.containsArg(a))
913     {
914       if (pos == Position.FIRST && avm.getValue(a) != null)
915         value = avm.getValue(a);
916       else if (pos == Position.BEFORE
917               && avm.getClosestPreviousArgValueOfArg(av, a) != null)
918         value = avm.getClosestPreviousArgValueOfArg(av, a).getValue();
919       else if (pos == Position.AFTER
920               && avm.getClosestNextArgValueOfArg(av, a) != null)
921         value = avm.getClosestNextArgValueOfArg(av, a).getValue();
922     }
923     else
924     {
925       value = pref != null ? Cache.getDefault(pref, def) : def;
926     }
927     return value;
928   }
929
930   public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
931           SubVals sv)
932   {
933     return getFromSubValArgOrPref(avm, a, sv, null, null, false);
934   }
935
936   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
937           SubVals sv, String key, String pref, boolean def)
938   {
939     if ((key == null && a == null) || (sv == null && a == null))
940       return false;
941
942     boolean usingArgKey = false;
943     if (key == null)
944     {
945       key = a.getName();
946       usingArgKey = true;
947     }
948
949     String nokey = ArgParser.NEGATESTRING + key;
950
951     // look for key or nokey in subvals first (if using Arg check options)
952     if (sv != null)
953     {
954       // check for true boolean
955       if (sv.has(key) && sv.get(key) != null)
956       {
957         if (usingArgKey)
958         {
959           if (!(a.hasOption(Opt.BOOLEAN) || a.hasOption(Opt.UNARY)))
960           {
961             Console.debug(
962                     "Looking for boolean in subval from non-boolean/non-unary Arg "
963                             + a.getName());
964             return false;
965           }
966         }
967         return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
968       }
969
970       // check for negative boolean (subval "no..." will be "true")
971       if (sv.has(nokey) && sv.get(nokey) != null)
972       {
973         if (usingArgKey)
974         {
975           if (!(a.hasOption(Opt.BOOLEAN)))
976           {
977             Console.debug(
978                     "Looking for negative boolean in subval from non-boolean Arg "
979                             + a.getName());
980             return false;
981           }
982         }
983         return !sv.get(nokey).toLowerCase(Locale.ROOT).equals("true");
984       }
985     }
986
987     // check argvalues
988     if (avm != null && avm.containsArg(a))
989       return avm.getBoolean(a);
990
991     // return preference or default
992     return pref != null ? Cache.getDefault(pref, def) : def;
993   }
994
995   // the following methods look for the "*" linkedId and add the argvalue to all
996   // linkedId ArgValues if it does.
997   // This version inserts the subvals sv into all created values
998   private void addValue(String linkedId, Type type, ArgValues avs,
999           SubVals sv, String v, int argIndex, boolean doSubs)
1000   {
1001     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, sv, v, false,
1002             argIndex, doSubs);
1003   }
1004
1005   private void addValue(String linkedId, Type type, ArgValues avs, String v,
1006           int argIndex, boolean doSubs)
1007   {
1008     this.argValueOperation(Op.ADDVALUE, linkedId, type, avs, null, v, false,
1009             argIndex, doSubs);
1010   }
1011
1012   private void setBoolean(String linkedId, Type type, ArgValues avs,
1013           boolean b, int argIndex)
1014   {
1015     this.argValueOperation(Op.SETBOOLEAN, linkedId, type, avs, null, null,
1016             b, argIndex, false);
1017   }
1018
1019   private void setNegated(String linkedId, ArgValues avs, boolean b)
1020   {
1021     this.argValueOperation(Op.SETNEGATED, linkedId, null, avs, null, null,
1022             b, 0, false);
1023   }
1024
1025   private void incrementCount(String linkedId, ArgValues avs)
1026   {
1027     this.argValueOperation(Op.INCREMENTCOUNT, linkedId, null, avs, null,
1028             null, false, 0, false);
1029   }
1030
1031   private enum Op
1032   {
1033     ADDVALUE, SETBOOLEAN, SETNEGATED, INCREMENTCOUNT
1034   }
1035
1036   // The following operations look for the "*" and "open*" linkedIds and add the
1037   // argvalue to all appropriate linkedId ArgValues if it does.
1038   // If subvals are supplied, they are inserted into all new set values.
1039   private void argValueOperation(Op op, String linkedId, Type type,
1040           ArgValues avs, SubVals sv, String v, boolean b, int argIndex,
1041           boolean doSubs)
1042   {
1043     Arg a = avs.arg();
1044
1045     List<String> wildcardLinkedIds = null;
1046     if (a.hasOption(Opt.ALLOWALL))
1047     {
1048       switch (linkedId)
1049       {
1050       case MATCHALLLINKEDIDS:
1051         wildcardLinkedIds = getLinkedIds();
1052         break;
1053       case MATCHOPENEDLINKEDIDS:
1054         wildcardLinkedIds = this.storedLinkedIds;
1055         break;
1056       }
1057     }
1058
1059     // if we're not a wildcard linkedId and the arg is marked to be stored, add
1060     // to storedLinkedIds
1061     if (linkedId != null && wildcardLinkedIds == null
1062             && a.hasOption(Opt.STORED)
1063             && !storedLinkedIds.contains(linkedId))
1064     {
1065       storedLinkedIds.add(linkedId);
1066     }
1067
1068     // if we are a wildcard linkedId, apply the arg and value to all appropriate
1069     // linkedIds
1070     if (wildcardLinkedIds != null)
1071     {
1072       for (String id : wildcardLinkedIds)
1073       {
1074         // skip incorrectly stored wildcard ids!
1075         if (id == null || MATCHALLLINKEDIDS.equals(id)
1076                 || MATCHOPENEDLINKEDIDS.equals(id))
1077           continue;
1078         ArgValuesMap avm = linkedArgs.get(id);
1079         // don't set an output if there isn't an input
1080         if (a.hasOption(Opt.REQUIREINPUT)
1081                 && !avm.hasArgWithOption(Opt.INPUT))
1082           continue;
1083
1084         ArgValues tavs = avm.getOrCreateArgValues(a);
1085         switch (op)
1086         {
1087
1088         case ADDVALUE:
1089           String val = v;
1090           if (sv != null)
1091           {
1092             if (doSubs)
1093             {
1094               val = makeSubstitutions(v, id);
1095               sv = new SubVals(sv, val);
1096             }
1097             tavs.addValue(sv, type, val, argIndex, true);
1098           }
1099           else
1100           {
1101             if (doSubs)
1102             {
1103               val = makeSubstitutions(v, id);
1104             }
1105             tavs.addValue(type, val, argIndex, true);
1106           }
1107           finaliseStoringArgValue(id, tavs);
1108           break;
1109
1110         case SETBOOLEAN:
1111           tavs.setBoolean(type, b, argIndex, true);
1112           finaliseStoringArgValue(id, tavs);
1113           break;
1114
1115         case SETNEGATED:
1116           tavs.setNegated(b, true);
1117           break;
1118
1119         case INCREMENTCOUNT:
1120           tavs.incrementCount();
1121           break;
1122
1123         default:
1124           break;
1125
1126         }
1127
1128       }
1129     }
1130     else // no wildcard linkedId -- do it simpler
1131     {
1132       switch (op)
1133       {
1134       case ADDVALUE:
1135         String val = v;
1136         if (sv != null)
1137         {
1138           if (doSubs)
1139           {
1140             val = makeSubstitutions(v, linkedId);
1141             sv = new SubVals(sv, val);
1142           }
1143           avs.addValue(sv, type, val, argIndex, false);
1144         }
1145         else
1146         {
1147           if (doSubs)
1148           {
1149             val = makeSubstitutions(v, linkedId);
1150           }
1151           avs.addValue(type, val, argIndex, false);
1152         }
1153         finaliseStoringArgValue(linkedId, avs);
1154         break;
1155
1156       case SETBOOLEAN:
1157         avs.setBoolean(type, b, argIndex, false);
1158         finaliseStoringArgValue(linkedId, avs);
1159         break;
1160
1161       case SETNEGATED:
1162         avs.setNegated(b, false);
1163         break;
1164
1165       case INCREMENTCOUNT:
1166         avs.incrementCount();
1167         break;
1168
1169       default:
1170         break;
1171       }
1172     }
1173   }
1174
1175   private ArgValuesMap getOrCreateLinkedArgValuesMap(String linkedId)
1176   {
1177     if (linkedArgs.containsKey(linkedId)
1178             && linkedArgs.get(linkedId) != null)
1179       return linkedArgs.get(linkedId);
1180
1181     linkedArgs.put(linkedId, new ArgValuesMap(linkedId));
1182     return linkedArgs.get(linkedId);
1183   }
1184
1185 }