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