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