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