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