JAL-629 Add some shortnames to args. Add Opt.OUTPUT and allow --output=*.ext to be...
[jalview.git] / src / jalview / bin / argparser / ArgParser.java
1 /*
2  * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3  * Copyright (C) $$Year-Rel$$ The Jalview Authors
4  * 
5  * This file is part of Jalview.
6  * 
7  * Jalview is free software: you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License 
9  * as published by the Free Software Foundation, either version 3
10  * of the License, or (at your option) any later version.
11  *  
12  * Jalview is distributed in the hope that it will be useful, but 
13  * WITHOUT ANY WARRANTY; without even the implied warranty 
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR 
15  * PURPOSE.  See the GNU General Public License for more details.
16  * 
17  * You should have received a copy of the GNU General Public License
18  * along with Jalview.  If not, see <http://www.gnu.org/licenses/>.
19  * The Jalview Authors are detailed in the 'AUTHORS' file.
20  */
21 package jalview.bin.argparser;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.nio.file.Files;
26 import java.nio.file.Paths;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.Collections;
30 import java.util.EnumSet;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36
37 import jalview.bin.Cache;
38 import jalview.bin.Console;
39 import jalview.bin.Jalview;
40 import jalview.bin.argparser.Arg.Opt;
41 import jalview.util.FileUtils;
42 import jalview.util.HttpUtils;
43
44 public class ArgParser
45 {
46   protected static final String DOUBLEDASH = "--";
47
48   protected static final char EQUALS = '=';
49
50   protected static final String NEGATESTRING = "no";
51
52   // the default linked id prefix used for no id (not even square braces)
53   protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
54
55   // the linkedId string used to match all linkedIds seen so far
56   protected static final String MATCHALLLINKEDIDS = "*";
57
58   // the counter added to the default linked id prefix
59   private int defaultLinkedIdCounter = 0;
60
61   // the substitution string used to use the defaultLinkedIdCounter
62   private static final String DEFAULTLINKEDIDCOUNTER = "{}";
63
64   // the counter added to the default linked id prefix. NOW using
65   // linkedIdAutoCounter
66   // private int openLinkedIdCounter = 0;
67
68   // the linked id prefix used for --open files. NOW the same as DEFAULT
69   protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX;
70
71   // the counter used for {n} substitutions
72   private int linkedIdAutoCounter = 0;
73
74   // the linked id substitution string used to increment the idCounter (and use
75   // the incremented value)
76   private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
77
78   // the linked id substitution string used to use the idCounter
79   private static final String LINKEDIDAUTOCOUNTER = "{n}";
80
81   // the linked id substitution string used to use the base filename of --append
82   // or --open
83   private static final String LINKEDIDBASENAME = "{basename}";
84
85   // the linked id substitution string used to use the dir path of --append
86   // or --open
87   private static final String LINKEDIDDIRNAME = "{dirname}";
88
89   // the current argfile
90   private String argFile = null;
91
92   // the linked id substitution string used to use the dir path of the latest
93   // --argfile name
94   private static final String ARGFILEBASENAME = "{argfilebasename}";
95
96   // the linked id substitution string used to use the dir path of the latest
97   // --argfile name
98   private static final String ARGFILEDIRNAME = "{argfiledirname}";
99
100   // an output file wildcard to signify --output=*.ext is really --all --output
101   // {basename}.ext
102   private static final String OUTPUTWILDCARD = "*.";
103
104   // flag to say whether {n} subtitutions in output filenames should be made.
105   // Turn on and off with --substitutions and --nosubstitutions
106   // Start with it on
107   private boolean substitutions = true;
108
109   // flag to say whether the default linkedId is the current default linked id
110   // or ALL linkedIds
111   private boolean allLinkedIds = false;
112
113   protected static final Map<String, Arg> argMap;
114
115   protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
116
117   protected List<String> linkedOrder = new ArrayList<>();
118
119   protected List<Arg> argList = new ArrayList<>();
120
121   private static final char ARGFILECOMMENT = '#';
122
123   private int argIndex = 0;
124
125   private BootstrapArgs bootstrapArgs = null;
126
127   static
128   {
129     argMap = new HashMap<>();
130     for (Arg a : EnumSet.allOf(Arg.class))
131     {
132       for (String argName : a.getNames())
133       {
134         if (argMap.containsKey(argName))
135         {
136           Console.warn("Trying to add argument name multiple times: '"
137                   + argName + "'"); // RESTORE THIS WHEN
138           // MERGED
139           if (argMap.get(argName) != a)
140           {
141             Console.error(
142                     "Trying to add argument name multiple times for different Args: '"
143                             + argMap.get(argName).getName() + ":" + argName
144                             + "' and '" + a.getName() + ":" + argName
145                             + "'");
146           }
147           continue;
148         }
149         argMap.put(argName, a);
150       }
151     }
152   }
153
154   public ArgParser(String[] args)
155   {
156     this(args, false, null);
157   }
158
159   public ArgParser(String[] args, boolean initsubstitutions,
160           BootstrapArgs bsa)
161   {
162     // Make a mutable new ArrayList so that shell globbing parser works.
163     // (When shell file globbing is used, there are a sequence of non-Arg
164     // arguments (which are the expanded globbed filenames) that need to be
165     // consumed by the --append/--argfile/etc Arg which is most easily done by
166     // removing these filenames from the list one at a time. This can't be done
167     // with an ArrayList made with only Arrays.asList(String[] args). )
168     this(new ArrayList<>(Arrays.asList(args)), initsubstitutions, false,
169             bsa);
170   }
171
172   public ArgParser(List<String> args, boolean initsubstitutions)
173   {
174     this(args, initsubstitutions, false, null);
175   }
176
177   public ArgParser(List<String> args, boolean initsubstitutions,
178           boolean allowPrivate, BootstrapArgs bsa)
179   {
180     // do nothing if there are no "--" args and (some "-" args || >0 arg is
181     // "open")
182     boolean d = false;
183     boolean dd = false;
184     for (String arg : args)
185     {
186       if (arg.startsWith(DOUBLEDASH))
187       {
188         dd = true;
189         break;
190       }
191       else if (arg.startsWith("-") || arg.equals("open"))
192       {
193         d = true;
194       }
195     }
196     if (d && !dd)
197     {
198       // leave it to the old style -- parse an empty list
199       parse(new ArrayList<String>(), false, false);
200       return;
201     }
202     if (bsa != null)
203       this.bootstrapArgs = bsa;
204     else
205       this.bootstrapArgs = BootstrapArgs.getBootstrapArgs(args);
206     parse(args, initsubstitutions, allowPrivate);
207   }
208
209   private void parse(List<String> args, boolean initsubstitutions,
210           boolean allowPrivate)
211   {
212     this.substitutions = initsubstitutions;
213     boolean openEachInitialFilenames = true;
214     for (int i = 0; i < args.size(); i++)
215     {
216       String arg = args.get(i);
217
218       // If the first arguments do not start with "--" or "-" or is not "open"
219       // and` is a filename that exists it is probably a file/list of files to
220       // open so we fake an Arg.OPEN argument and when adding files only add the
221       // single arg[i] and increment the defaultLinkedIdCounter so that each of
222       // these files is opened separately.
223       if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
224               && !arg.startsWith("-") && !arg.equals("open")
225               && (new File(arg).exists()
226                       || HttpUtils.startsWithHttpOrHttps(arg)))
227       {
228         arg = Arg.OPEN.argString();
229       }
230       else
231       {
232         openEachInitialFilenames = false;
233       }
234
235       String argName = null;
236       String val = null;
237       List<String> globVals = null; // for Opt.GLOB only
238       SubVals globSubVals = null; // also for use by Opt.GLOB only
239       String linkedId = null;
240       if (arg.startsWith(DOUBLEDASH))
241       {
242         int equalPos = arg.indexOf(EQUALS);
243         if (equalPos > -1)
244         {
245           argName = arg.substring(DOUBLEDASH.length(), equalPos);
246           val = arg.substring(equalPos + 1);
247         }
248         else
249         {
250           argName = arg.substring(DOUBLEDASH.length());
251         }
252         int idOpen = argName.indexOf('[');
253         int idClose = argName.indexOf(']');
254
255         if (idOpen > -1 && idClose == argName.length() - 1)
256         {
257           linkedId = argName.substring(idOpen + 1, idClose);
258           argName = argName.substring(0, idOpen);
259         }
260
261         Arg a = argMap.get(argName);
262         // check for boolean prepended by "no"
263         boolean negated = false;
264         if (a == null && argName.startsWith(NEGATESTRING) && argMap
265                 .containsKey(argName.substring(NEGATESTRING.length())))
266         {
267           argName = argName.substring(NEGATESTRING.length());
268           a = argMap.get(argName);
269           negated = true;
270         }
271
272         // check for config errors
273         if (a == null)
274         {
275           // arg not found
276           Console.error("Argument '" + arg + "' not recognised.  Exiting.");
277           Jalview.exit("Invalid argument used." + System.lineSeparator()
278                   + "Use" + System.lineSeparator() + "jalview "
279                   + Arg.HELP.argString() + System.lineSeparator()
280                   + "for a usage statement.", 13);
281           continue;
282         }
283         if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
284         {
285           Console.error(
286                   "Argument '" + a.argString() + "' is private. Ignoring.");
287           continue;
288         }
289         if (!a.hasOption(Opt.BOOLEAN) && negated)
290         {
291           // used "no" with a non-boolean option
292           Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
293                   + "' not a boolean option. Ignoring.");
294           continue;
295         }
296         if (!a.hasOption(Opt.STRING) && equalPos > -1)
297         {
298           // set --argname=value when arg does not accept values
299           Console.error("Argument '" + a.argString()
300                   + "' does not expect a value (given as '" + arg
301                   + "').  Ignoring.");
302           continue;
303         }
304         if (!a.hasOption(Opt.LINKED) && linkedId != null)
305         {
306           // set --argname[linkedId] when arg does not use linkedIds
307           Console.error("Argument '" + a.argString()
308                   + "' does not expect a linked id (given as '" + arg
309                   + "'). Ignoring.");
310           continue;
311         }
312
313         // String value(s)
314         if (a.hasOption(Opt.STRING))
315         {
316           if (equalPos >= 0)
317           {
318             if (a.hasOption(Opt.GLOB))
319             {
320               // strip off and save the SubVals to be added individually later
321               globSubVals = new SubVals(val);
322               // make substitutions before looking for files
323               String fileGlob = makeSubstitutions(globSubVals.getContent(),
324                       linkedId);
325               globVals = FileUtils.getFilenamesFromGlob(fileGlob);
326             }
327             else
328             {
329               // val is already set -- will be saved in the ArgValue later in
330               // the normal way
331             }
332           }
333           else
334           {
335             // There is no "=" so value is next arg or args (possibly shell
336             // glob-expanded)
337             if ((openEachInitialFilenames ? i : i + 1) >= args.size())
338             {
339               // no value to take for arg, which wants a value
340               Console.error("Argument '" + a.getName()
341                       + "' requires a value, none given. Ignoring.");
342               continue;
343             }
344             // deal with bash globs here (--arg val* is expanded before reaching
345             // the JVM). Note that SubVals cannot be used in this case.
346             // If using the --arg=val then the glob is preserved and Java globs
347             // will be used later. SubVals can be used.
348             if (a.hasOption(Opt.GLOB))
349             {
350               // if this is the first argument with a file list at the start of
351               // the args we add filenames from index i instead of i+1
352               globVals = getShellGlobbedFilenameValues(a, args,
353                       openEachInitialFilenames ? i : i + 1);
354             }
355             else
356             {
357               val = args.get(i + 1);
358             }
359           }
360         }
361
362         // make NOACTION adjustments
363         // default and auto counter increments
364         if (a == Arg.NPP)
365         {
366           linkedIdAutoCounter++;
367         }
368         else if (a == Arg.SUBSTITUTIONS)
369         {
370           substitutions = !negated;
371         }
372         else if (a == Arg.SETARGFILE)
373         {
374           argFile = val;
375         }
376         else if (a == Arg.UNSETARGFILE)
377         {
378           argFile = null;
379         }
380         else if (a == Arg.ALL)
381         {
382           allLinkedIds = !negated;
383         }
384
385         // this is probably only Arg.NEW and Arg.OPEN
386         if (a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
387         {
388           // use the next default prefixed OPENLINKEDID
389           defaultLinkedId(true);
390         }
391
392         String autoCounterString = null;
393         boolean usingAutoCounterLinkedId = false;
394         String defaultLinkedId = defaultLinkedId(false);
395         boolean usingDefaultLinkedId = false;
396         if (a.hasOption(Opt.LINKED))
397         {
398           if (linkedId == null)
399           {
400             if (a.hasOption(Opt.OUTPUT) && a.hasOption(Opt.ALLOWALL)
401                     && val.startsWith(OUTPUTWILDCARD))
402             {
403               // --output=*.ext is shorthand for --all --output {basename}.ext
404               // (or --image=*.ext)
405               allLinkedIds = true;
406               linkedId = MATCHALLLINKEDIDS;
407               String oldval = val;
408               val = LINKEDIDBASENAME
409                       + val.substring(OUTPUTWILDCARD.length() - 1);
410             }
411             else if (allLinkedIds && a.hasOption(Opt.ALLOWALL))
412             {
413               linkedId = MATCHALLLINKEDIDS;
414             }
415             else
416             {
417               // use default linkedId for linked arguments
418               linkedId = defaultLinkedId;
419               usingDefaultLinkedId = true;
420               Console.debug("Changing linkedId to '" + linkedId + "' from "
421                       + arg);
422             }
423           }
424           else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
425           {
426             // turn {n} to the autoCounter
427             autoCounterString = Integer.toString(linkedIdAutoCounter);
428             linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
429                     autoCounterString);
430             usingAutoCounterLinkedId = true;
431             Console.debug(
432                     "Changing linkedId to '" + linkedId + "' from " + arg);
433           }
434           else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
435           {
436             // turn {++n} to the incremented autoCounter
437             autoCounterString = Integer.toString(++linkedIdAutoCounter);
438             linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
439                     autoCounterString);
440             usingAutoCounterLinkedId = true;
441             Console.debug(
442                     "Changing linkedId to '" + linkedId + "' from " + arg);
443           }
444         }
445
446         // do not continue in this block for NOACTION args
447         if (a.hasOption(Opt.NOACTION))
448           continue;
449
450         ArgValuesMap avm = getOrCreateLinkedArgValuesMap(linkedId);
451
452         // not dealing with both NODUPLICATEVALUES and GLOB
453         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
454         {
455           Console.error("Argument '" + a.argString()
456                   + "' cannot contain a duplicate value ('" + val
457                   + "'). Ignoring this and subsequent occurrences.");
458           continue;
459         }
460
461         // check for unique id
462         SubVals idsv = new SubVals(val);
463         String id = idsv.get(ArgValues.ID);
464         if (id != null && avm.hasId(a, id))
465         {
466           Console.error("Argument '" + a.argString()
467                   + "' has a duplicate id ('" + id + "'). Ignoring.");
468           continue;
469         }
470
471         /* TODO
472          * Change all avs.addValue() avs.setBoolean avs.setNegated() avs.incrementCount calls to checkfor linkedId == "*"
473          * DONE, need to check
474          */
475         ArgValues avs = avm.getOrCreateArgValues(a);
476
477         // store appropriate String value(s)
478         if (a.hasOption(Opt.STRING))
479         {
480           if (a.hasOption(Opt.GLOB) && globVals != null
481                   && globVals.size() > 0)
482           {
483             Enumeration<String> gve = Collections.enumeration(globVals);
484             while (gve.hasMoreElements())
485             {
486               String v = gve.nextElement();
487               SubVals vsv = new SubVals(globSubVals, v);
488               addValue(linkedId, avs, vsv, v, argIndex++, true);
489               // if we're using defaultLinkedId and the arg increments the
490               // counter:
491               if (gve.hasMoreElements() && usingDefaultLinkedId
492                       && a.hasOption(Opt.INCREMENTDEFAULTCOUNTER))
493               {
494                 // increment the default linkedId
495                 linkedId = defaultLinkedId(true);
496                 // get new avm and avs
497                 avm = linkedArgs.get(linkedId);
498                 avs = avm.getOrCreateArgValues(a);
499               }
500             }
501           }
502           else
503           {
504             addValue(linkedId, avs, val, argIndex, true);
505           }
506         }
507         else if (a.hasOption(Opt.BOOLEAN))
508         {
509           setBoolean(linkedId, avs, !negated, argIndex);
510           setNegated(linkedId, avs, negated);
511         }
512         else if (a.hasOption(Opt.UNARY))
513         {
514           setBoolean(linkedId, avs, true, argIndex);
515         }
516
517         // remove the '*' linkedId that should be empty if it was created
518         if (MATCHALLLINKEDIDS.equals(linkedId)
519                 && linkedArgs.containsKey(linkedId))
520         {
521           linkedArgs.remove(linkedId);
522         }
523       }
524     }
525   }
526
527   private void finaliseStoringArgValue(String linkedId, ArgValues avs)
528   {
529     Arg a = avs.arg();
530     incrementCount(linkedId, avs);
531     argIndex++;
532
533     // store in appropriate place
534     if (a.hasOption(Opt.LINKED))
535     {
536       // store the order of linkedIds
537       if (!linkedOrder.contains(linkedId))
538         linkedOrder.add(linkedId);
539     }
540
541     // store arg in the list of args used
542     if (!argList.contains(a))
543       argList.add(a);
544   }
545
546   private String defaultLinkedId(boolean increment)
547   {
548     String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
549             .append(Integer.toString(defaultLinkedIdCounter)).toString();
550     if (increment)
551     {
552       while (linkedArgs.containsKey(defaultLinkedId))
553       {
554         defaultLinkedIdCounter++;
555         defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
556                 .append(Integer.toString(defaultLinkedIdCounter))
557                 .toString();
558       }
559     }
560     getOrCreateLinkedArgValuesMap(defaultLinkedId);
561     return defaultLinkedId;
562   }
563
564   public String makeSubstitutions(String val, String linkedId)
565   {
566     if (!this.substitutions || val == null)
567       return val;
568
569     String subvals;
570     String rest;
571     if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
572     {
573       int closeBracket = val.indexOf(']');
574       if (val.length() == closeBracket)
575         return val;
576       subvals = val.substring(0, closeBracket + 1);
577       rest = val.substring(closeBracket + 1);
578     }
579     else
580     {
581       subvals = "";
582       rest = val;
583     }
584     if (rest.contains(LINKEDIDAUTOCOUNTER))
585       rest = rest.replace(LINKEDIDAUTOCOUNTER,
586               String.valueOf(linkedIdAutoCounter));
587     if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
588       rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
589               String.valueOf(++linkedIdAutoCounter));
590     if (rest.contains(DEFAULTLINKEDIDCOUNTER))
591       rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
592               String.valueOf(defaultLinkedIdCounter));
593     ArgValuesMap avm = linkedArgs.get(linkedId);
594     if (avm != null)
595     {
596       if (rest.contains(LINKEDIDBASENAME))
597       {
598         rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
599       }
600       if (rest.contains(LINKEDIDDIRNAME))
601       {
602         rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
603       }
604     }
605     if (argFile != null)
606     {
607       if (rest.contains(ARGFILEBASENAME))
608       {
609         rest = rest.replace(ARGFILEBASENAME,
610                 FileUtils.getBasename(new File(argFile)));
611       }
612       if (rest.contains(ARGFILEDIRNAME))
613       {
614         rest = rest.replace(ARGFILEDIRNAME,
615                 FileUtils.getDirname(new File(argFile)));
616       }
617     }
618
619     return new StringBuilder(subvals).append(rest).toString();
620   }
621
622   /*
623    * A helper method to take a list of String args where we're expecting
624    * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
625    * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
626    * "file2", "file3"} *and remove these from the original list object* so that
627    * processing can continue from where it has left off, e.g. args has become
628    * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
629    * carries on from the next --arg if available.
630    */
631   protected static List<String> getShellGlobbedFilenameValues(Arg a,
632           List<String> args, int i)
633   {
634     List<String> vals = new ArrayList<>();
635     while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
636     {
637       vals.add(FileUtils.substituteHomeDir(args.remove(i)));
638       if (!a.hasOption(Opt.GLOB))
639         break;
640     }
641     return vals;
642   }
643
644   public BootstrapArgs getBootstrapArgs()
645   {
646     return bootstrapArgs;
647   }
648
649   public boolean isSet(Arg a)
650   {
651     return a.hasOption(Opt.LINKED) ? isSetAtAll(a) : isSet(null, a);
652   }
653
654   public boolean isSetAtAll(Arg a)
655   {
656     for (String linkedId : linkedOrder)
657     {
658       if (isSet(linkedId, a))
659         return true;
660     }
661     return false;
662   }
663
664   public boolean isSet(String linkedId, Arg a)
665   {
666     ArgValuesMap avm = linkedArgs.get(linkedId);
667     return avm == null ? false : avm.containsArg(a);
668   }
669
670   public boolean getBoolean(Arg a)
671   {
672     if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
673     {
674       Console.warn("Getting boolean from non boolean Arg '" + a.getName()
675               + "'.");
676     }
677     return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
678   }
679
680   public boolean getBool(String linkedId, Arg a)
681   {
682     ArgValuesMap avm = linkedArgs.get(linkedId);
683     if (avm == null)
684       return a.getDefaultBoolValue();
685     ArgValues avs = avm.getArgValues(a);
686     return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
687   }
688
689   public List<String> getLinkedIds()
690   {
691     return linkedOrder;
692   }
693
694   public ArgValuesMap getLinkedArgs(String id)
695   {
696     return linkedArgs.get(id);
697   }
698
699   @Override
700   public String toString()
701   {
702     StringBuilder sb = new StringBuilder();
703     sb.append("UNLINKED\n");
704     sb.append(argValuesMapToString(linkedArgs.get(null)));
705     if (getLinkedIds() != null)
706     {
707       sb.append("LINKED\n");
708       for (String id : getLinkedIds())
709       {
710         // already listed these as UNLINKED args
711         if (id == null)
712           continue;
713
714         ArgValuesMap avm = getLinkedArgs(id);
715         sb.append("ID: '").append(id).append("'\n");
716         sb.append(argValuesMapToString(avm));
717       }
718     }
719     return sb.toString();
720   }
721
722   private static String argValuesMapToString(ArgValuesMap avm)
723   {
724     if (avm == null)
725       return null;
726     StringBuilder sb = new StringBuilder();
727     for (Arg a : avm.getArgKeys())
728     {
729       ArgValues v = avm.getArgValues(a);
730       sb.append(v.toString());
731       sb.append("\n");
732     }
733     return sb.toString();
734   }
735
736   public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
737           boolean initsubstitutions, BootstrapArgs bsa)
738   {
739     List<File> argFiles = new ArrayList<>();
740
741     for (String pattern : argFilenameGlobs)
742     {
743       // I don't think we want to dedup files, making life easier
744       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
745     }
746
747     return parseArgFileList(argFiles, initsubstitutions, bsa);
748   }
749
750   public static ArgParser parseArgFileList(List<File> argFiles,
751           boolean initsubstitutions, BootstrapArgs bsa)
752   {
753     List<String> argsList = new ArrayList<>();
754     for (File argFile : argFiles)
755     {
756       if (!argFile.exists())
757       {
758         String message = Arg.ARGFILE.argString() + EQUALS + "\""
759                 + argFile.getPath() + "\": File does not exist.";
760         Jalview.exit(message, 2);
761       }
762       try
763       {
764         String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
765                 .append(EQUALS).append(argFile.getCanonicalPath())
766                 .toString();
767         argsList.add(setargfile);
768         argsList.addAll(readArgFile(argFile));
769         argsList.add(Arg.UNSETARGFILE.argString());
770       } catch (IOException e)
771       {
772         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
773                 + "\": File could not be read.";
774         Jalview.exit(message, 3);
775       }
776     }
777     // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
778     // --unsetargfile
779     return new ArgParser(argsList, initsubstitutions, true, bsa);
780   }
781
782   protected static List<String> readArgFile(File argFile)
783   {
784     List<String> args = new ArrayList<>();
785     if (argFile != null && argFile.exists())
786     {
787       try
788       {
789         for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
790         {
791           if (line != null && line.length() > 0
792                   && line.charAt(0) != ARGFILECOMMENT)
793             args.add(line);
794         }
795       } catch (IOException e)
796       {
797         String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
798                 + "\": File could not be read.";
799         Console.debug(message, e);
800         Jalview.exit(message, 3);
801       }
802     }
803     return args;
804   }
805
806   public static enum Position
807   {
808     FIRST, BEFORE, AFTER
809   }
810
811   // get from following Arg of type a or subval of same name (lowercase)
812   public static String getValueFromSubValOrArg(ArgValuesMap avm,
813           ArgValue av, Arg a, SubVals sv)
814   {
815     return getFromSubValArgOrPref(avm, av, a, sv, null, null, null);
816   }
817
818   // get from following Arg of type a or subval key or preference pref or
819   // default def
820   public static String getFromSubValArgOrPref(ArgValuesMap avm, ArgValue av,
821           Arg a, SubVals sv, String key, String pref, String def)
822   {
823     return getFromSubValArgOrPref(avm, a, Position.AFTER, av, sv, key, pref,
824             def);
825   }
826
827   // get from following(AFTER), first occurence of (FIRST) or previous (BEFORE)
828   // Arg of type a or subval key or preference pref or default def
829   public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
830           Position pos, ArgValue av, SubVals sv, String key, String pref,
831           String def)
832   {
833     return getFromSubValArgOrPrefWithSubstitutions(null, avm, a, pos, av,
834             sv, key, pref, def);
835   }
836
837   public static String getFromSubValArgOrPrefWithSubstitutions(ArgParser ap,
838           ArgValuesMap avm, Arg a, Position pos, ArgValue av, SubVals sv,
839           String key, String pref, String def)
840   {
841     if (key == null)
842       key = a.getName();
843     String value = null;
844     if (sv != null && sv.has(key) && sv.get(key) != null)
845     {
846       value = ap == null ? sv.get(key)
847               : sv.getWithSubstitutions(ap, avm.getLinkedId(), key);
848     }
849     else if (avm != null && avm.containsArg(a))
850     {
851       if (pos == Position.FIRST && avm.getValue(a) != null)
852         value = avm.getValue(a);
853       else if (pos == Position.BEFORE
854               && avm.getClosestPreviousArgValueOfArg(av, a) != null)
855         value = avm.getClosestPreviousArgValueOfArg(av, a).getValue();
856       else if (pos == Position.AFTER
857               && avm.getClosestNextArgValueOfArg(av, a) != null)
858         value = avm.getClosestNextArgValueOfArg(av, a).getValue();
859     }
860     else
861     {
862       value = pref != null ? Cache.getDefault(pref, def) : def;
863     }
864     return value;
865   }
866
867   public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
868           SubVals sv)
869   {
870     return getFromSubValArgOrPref(avm, a, sv, null, null, false);
871   }
872
873   public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
874           SubVals sv, String key, String pref, boolean def)
875   {
876     if ((key == null && a == null) || (sv == null && a == null))
877       return false;
878
879     boolean usingArgKey = false;
880     if (key == null)
881     {
882       key = a.getName();
883       usingArgKey = true;
884     }
885
886     String nokey = ArgParser.NEGATESTRING + key;
887
888     // look for key or nokey in subvals first (if using Arg check options)
889     if (sv != null)
890     {
891       // check for true boolean
892       if (sv.has(key) && sv.get(key) != null)
893       {
894         if (usingArgKey)
895         {
896           if (!(a.hasOption(Opt.BOOLEAN) || a.hasOption(Opt.UNARY)))
897           {
898             Console.debug(
899                     "Looking for boolean in subval from non-boolean/non-unary Arg "
900                             + a.getName());
901             return false;
902           }
903         }
904         return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
905       }
906
907       // check for negative boolean (subval "no..." will be "true")
908       if (sv.has(nokey) && sv.get(nokey) != null)
909       {
910         if (usingArgKey)
911         {
912           if (!(a.hasOption(Opt.BOOLEAN)))
913           {
914             Console.debug(
915                     "Looking for negative boolean in subval from non-boolean Arg "
916                             + a.getName());
917             return false;
918           }
919         }
920         return !sv.get(nokey).toLowerCase(Locale.ROOT).equals("true");
921       }
922     }
923
924     // check argvalues
925     if (avm != null && avm.containsArg(a))
926       return avm.getBoolean(a);
927
928     // return preference or default
929     return pref != null ? Cache.getDefault(pref, def) : def;
930   }
931
932   // the following methods look for the "*" linkedId and add the argvalue to all
933   // linkedId ArgValues if it does
934   private void addValue(String linkedId, ArgValues avs, SubVals sv,
935           String v, int argIndex, boolean doSubs)
936   {
937     Arg a = avs.arg();
938     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
939     {
940       for (String id : getLinkedIds())
941       {
942         if (id == null || MATCHALLLINKEDIDS.equals(id))
943           continue;
944         ArgValuesMap avm = linkedArgs.get(id);
945         if (a.hasOption(Opt.REQUIREINPUT)
946                 && !avm.hasArgWithOption(Opt.INPUT))
947           continue;
948         ArgValues tavs = avm.getOrCreateArgValues(a);
949         String val = v;
950         if (doSubs)
951         {
952           val = makeSubstitutions(v, id);
953           sv = new SubVals(sv, val);
954         }
955         tavs.addValue(sv, val, argIndex);
956         finaliseStoringArgValue(id, tavs);
957       }
958     }
959     else
960     {
961       String val = v;
962       if (doSubs)
963       {
964         val = makeSubstitutions(v, linkedId);
965         sv = new SubVals(sv, val);
966       }
967       avs.addValue(sv, val, argIndex);
968       finaliseStoringArgValue(linkedId, avs);
969     }
970   }
971
972   private void addValue(String linkedId, ArgValues avs, String v,
973           int argIndex, boolean doSubs)
974   {
975     Arg a = avs.arg();
976     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
977     {
978       for (String id : getLinkedIds())
979       {
980         if (id == null || MATCHALLLINKEDIDS.equals(id))
981           continue;
982         ArgValuesMap avm = linkedArgs.get(id);
983         // don't set an output if there isn't an input
984         if (a.hasOption(Opt.REQUIREINPUT)
985                 && !avm.hasArgWithOption(Opt.INPUT))
986           continue;
987         ArgValues tavs = avm.getOrCreateArgValues(a);
988         String val = doSubs ? makeSubstitutions(v, id) : v;
989         tavs.addValue(val, argIndex);
990         finaliseStoringArgValue(id, tavs);
991       }
992     }
993     else
994     {
995       String val = doSubs ? makeSubstitutions(v, linkedId) : v;
996       avs.addValue(val, argIndex);
997       finaliseStoringArgValue(linkedId, avs);
998     }
999   }
1000
1001   private void setBoolean(String linkedId, ArgValues avs, boolean b,
1002           int argIndex)
1003   {
1004     Arg a = avs.arg();
1005     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
1006     {
1007       for (String id : getLinkedIds())
1008       {
1009         if (id == null || MATCHALLLINKEDIDS.equals(id))
1010           continue;
1011         ArgValuesMap avm = linkedArgs.get(id);
1012         if (a.hasOption(Opt.REQUIREINPUT)
1013                 && !avm.hasArgWithOption(Opt.INPUT))
1014           continue;
1015         ArgValues tavs = avm.getOrCreateArgValues(a);
1016         tavs.setBoolean(b, argIndex);
1017         finaliseStoringArgValue(id, tavs);
1018       }
1019     }
1020     else
1021     {
1022       avs.setBoolean(b, argIndex);
1023       finaliseStoringArgValue(linkedId, avs);
1024     }
1025   }
1026
1027   private void setNegated(String linkedId, ArgValues avs, boolean b)
1028   {
1029     Arg a = avs.arg();
1030     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
1031     {
1032       for (String id : getLinkedIds())
1033       {
1034         if (id == null || MATCHALLLINKEDIDS.equals(id))
1035           continue;
1036         ArgValuesMap avm = linkedArgs.get(id);
1037         if (a.hasOption(Opt.REQUIREINPUT)
1038                 && !avm.hasArgWithOption(Opt.INPUT))
1039           continue;
1040         ArgValues tavs = avm.getOrCreateArgValues(a);
1041         tavs.setNegated(b);
1042       }
1043     }
1044     else
1045     {
1046       avs.setNegated(b);
1047     }
1048   }
1049
1050   private void incrementCount(String linkedId, ArgValues avs)
1051   {
1052     Arg a = avs.arg();
1053     if (MATCHALLLINKEDIDS.equals(linkedId) && a.hasOption(Opt.ALLOWALL))
1054     {
1055       for (String id : getLinkedIds())
1056       {
1057         if (id == null || MATCHALLLINKEDIDS.equals(id))
1058           continue;
1059         ArgValuesMap avm = linkedArgs.get(id);
1060         if (a.hasOption(Opt.REQUIREINPUT)
1061                 && !avm.hasArgWithOption(Opt.INPUT))
1062           continue;
1063         ArgValues tavs = avm.getOrCreateArgValues(a);
1064         tavs.incrementCount();
1065       }
1066     }
1067     else
1068     {
1069       avs.incrementCount();
1070     }
1071   }
1072
1073   private ArgValuesMap getOrCreateLinkedArgValuesMap(String linkedId)
1074   {
1075     if (linkedArgs.containsKey(linkedId)
1076             && linkedArgs.get(linkedId) != null)
1077       return linkedArgs.get(linkedId);
1078
1079     linkedArgs.put(linkedId, new ArgValuesMap(linkedId));
1080     return linkedArgs.get(linkedId);
1081   }
1082
1083 }