2 * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
3 * Copyright (C) $$Year-Rel$$ The Jalview Authors
5 * This file is part of Jalview.
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.
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.
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.
21 package jalview.bin.argparser;
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.EnumSet;
30 import java.util.HashMap;
31 import java.util.List;
32 import java.util.Locale;
35 import jalview.bin.Cache;
36 import jalview.bin.Console;
37 import jalview.bin.Jalview;
38 import jalview.bin.argparser.Arg.Opt;
39 import jalview.util.FileUtils;
41 public class ArgParser
43 protected static final String DOUBLEDASH = "--";
45 protected static final char EQUALS = '=';
47 protected static final String NEGATESTRING = "no";
49 // the default linked id prefix used for no id (not even square braces)
50 protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
52 // the counter added to the default linked id prefix
53 private int defaultLinkedIdCounter = 0;
55 // the substitution string used to use the defaultLinkedIdCounter
56 private static final String DEFAULTLINKEDIDCOUNTER = "{}";
58 // the counter added to the default linked id prefix. NOW using
59 // linkedIdAutoCounter
60 // private int openLinkedIdCounter = 0;
62 // the linked id prefix used for --open files. NOW the same as DEFAULT
63 protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX;
65 // the counter used for {n} substitutions
66 private int linkedIdAutoCounter = 0;
68 // the linked id substitution string used to increment the idCounter (and use
69 // the incremented value)
70 private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
72 // the linked id substitution string used to use the idCounter
73 private static final String LINKEDIDAUTOCOUNTER = "{n}";
75 // the linked id substitution string used to use the base filename of --append
77 private static final String LINKEDIDBASENAME = "{basename}";
79 // the linked id substitution string used to use the dir path of --append
81 private static final String LINKEDIDDIRNAME = "{dirname}";
83 // the current argfile
84 private String argFile = null;
86 // the linked id substitution string used to use the dir path of the latest
88 private static final String ARGFILEBASENAME = "{argfilebasename}";
90 // the linked id substitution string used to use the dir path of the latest
92 private static final String ARGFILEDIRNAME = "{argfiledirname}";
94 // flag to say whether {n} subtitutions in output filenames should be made.
95 // Turn on and off with --subs and --nosubs
96 private boolean substitutions = false;
98 protected static final Map<String, Arg> argMap;
100 protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
102 protected List<String> linkedOrder = null;
104 protected List<Arg> argList;
106 private static final char ARGFILECOMMENT = '#';
110 argMap = new HashMap<>();
111 for (Arg a : EnumSet.allOf(Arg.class))
113 for (String argName : a.getNames())
115 if (argMap.containsKey(argName))
117 Console.warn("Trying to add argument name multiple times: '"
118 + argName + "'"); // RESTORE THIS WHEN
120 if (argMap.get(argName) != a)
123 "Trying to add argument name multiple times for different Args: '"
124 + argMap.get(argName).getName() + ":" + argName
125 + "' and '" + a.getName() + ":" + argName
130 argMap.put(argName, a);
135 public ArgParser(String[] args)
140 public ArgParser(String[] args, boolean initsubstitutions)
142 // Make a mutable new ArrayList so that shell globbing parser works.
143 // (When shell file globbing is used, there are a sequence of non-Arg
144 // arguments (which are the expanded globbed filenames) that need to be
145 // consumed by the --append/--argfile/etc Arg which is most easily done by
146 // removing these filenames from the list one at a time. This can't be done
147 // with an ArrayList made with only Arrays.asList(String[] args). )
148 this(new ArrayList<>(Arrays.asList(args)), initsubstitutions);
151 public ArgParser(List<String> args, boolean initsubstitutions)
153 this(args, initsubstitutions, false);
156 public ArgParser(List<String> args, boolean initsubstitutions,
157 boolean allowPrivate)
159 // do nothing if there are no "--" args and some "-" args
162 for (String arg : args)
164 if (arg.startsWith(DOUBLEDASH))
169 else if (arg.startsWith("-"))
176 // leave it to the old style -- parse an empty list
177 parse(new ArrayList<String>(), false, false);
180 parse(args, initsubstitutions, allowPrivate);
183 private void parse(List<String> args, boolean initsubstitutions,
184 boolean allowPrivate)
186 this.substitutions = initsubstitutions;
188 boolean openEachInitialFilenames = true;
189 for (int i = 0; i < args.size(); i++)
191 String arg = args.get(i);
193 // If the first arguments do not start with "--" or "-" or is "open" and
194 // is a filename that exists it is probably a file/list of files to open
195 // so we fake an Arg.OPEN argument and when adding files only add the
196 // single arg[i] and increment the defaultLinkedIdCounter so that each of
197 // these files is opened separately.
198 if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
199 && !arg.startsWith("-") && new File(arg).exists())
201 arg = Arg.OPEN.argString();
205 openEachInitialFilenames = false;
208 String argName = null;
210 List<String> globVals = null; // for Opt.GLOB only
211 SubVals globSubVals = null; // also for use by Opt.GLOB only
212 String linkedId = null;
213 if (arg.startsWith(DOUBLEDASH))
215 int equalPos = arg.indexOf(EQUALS);
218 argName = arg.substring(DOUBLEDASH.length(), equalPos);
219 val = arg.substring(equalPos + 1);
223 argName = arg.substring(DOUBLEDASH.length());
225 int idOpen = argName.indexOf('[');
226 int idClose = argName.indexOf(']');
228 if (idOpen > -1 && idClose == argName.length() - 1)
230 linkedId = argName.substring(idOpen + 1, idClose);
231 argName = argName.substring(0, idOpen);
234 Arg a = argMap.get(argName);
235 // check for boolean prepended by "no"
236 boolean negated = false;
237 if (a == null && argName.startsWith(NEGATESTRING) && argMap
238 .containsKey(argName.substring(NEGATESTRING.length())))
240 argName = argName.substring(NEGATESTRING.length());
241 a = argMap.get(argName);
245 // check for config errors
249 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
252 if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
255 "Argument '" + a.argString() + "' is private. Ignoring.");
258 if (!a.hasOption(Opt.BOOLEAN) && negated)
260 // used "no" with a non-boolean option
261 Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
262 + "' not a boolean option. Ignoring.");
265 if (!a.hasOption(Opt.STRING) && equalPos > -1)
267 // set --argname=value when arg does not accept values
268 Console.error("Argument '" + a.argString()
269 + "' does not expect a value (given as '" + arg
273 if (!a.hasOption(Opt.LINKED) && linkedId != null)
275 // set --argname[linkedId] when arg does not use linkedIds
276 Console.error("Argument '" + a.argString()
277 + "' does not expect a linked id (given as '" + arg
283 if (a.hasOption(Opt.STRING))
287 if (a.hasOption(Opt.GLOB))
289 // strip off and save the SubVals to be added individually later
290 globSubVals = new SubVals(val);
291 // make substitutions before looking for files
292 String fileGlob = makeSubstitutions(globSubVals.getContent(),
294 globVals = FileUtils.getFilenamesFromGlob(fileGlob);
298 // val is already set -- will be saved in the ArgValue later in
304 // There is no "=" so value is next arg or args (possibly shell
306 if ((openEachInitialFilenames ? i : i + 1) >= args.size())
308 // no value to take for arg, which wants a value
309 Console.error("Argument '" + a.getName()
310 + "' requires a value, none given. Ignoring.");
313 // deal with bash globs here (--arg val* is expanded before reaching
314 // the JVM). Note that SubVals cannot be used in this case.
315 // If using the --arg=val then the glob is preserved and Java globs
316 // will be used later. SubVals can be used.
317 if (a.hasOption(Opt.GLOB))
319 // if this is the first argument with a file list at the start of
320 // the args we add filenames from index i instead of i+1
321 globVals = getShellGlobbedFilenameValues(a, args,
322 openEachInitialFilenames ? i : i + 1);
326 val = args.get(i + 1);
331 // make NOACTION adjustments
332 // default and auto counter increments
333 if (a == Arg.INCREMENT)
335 defaultLinkedIdCounter++;
337 else if (a == Arg.NPP)
339 linkedIdAutoCounter++;
341 else if (a == Arg.SUBSTITUTIONS)
343 substitutions = !negated;
345 else if (a == Arg.SETARGFILE)
349 else if (a == Arg.UNSETARGFILE)
354 String autoCounterString = null;
355 boolean usingAutoCounterLinkedId = false;
356 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
357 .append(Integer.toString(defaultLinkedIdCounter))
359 boolean usingDefaultLinkedId = false;
360 if (a.hasOption(Opt.LINKED))
362 if (linkedId == null)
366 // use the next default prefixed OPENLINKEDID
367 // NOW using the linkedIdAutoCounter
368 defaultLinkedIdCounter++;
369 linkedId = new StringBuilder(OPENLINKEDIDPREFIX)
370 .append(Integer.toString(defaultLinkedIdCounter))
375 // use default linkedId for linked arguments
376 linkedId = defaultLinkedId;
377 usingDefaultLinkedId = true;
378 Console.debug("Changing linkedId to '" + linkedId + "' from "
382 else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
384 // turn {n} to the autoCounter
385 autoCounterString = Integer.toString(linkedIdAutoCounter);
386 linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
388 usingAutoCounterLinkedId = true;
390 "Changing linkedId to '" + linkedId + "' from " + arg);
392 else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
394 // turn {++n} to the incremented autoCounter
395 autoCounterString = Integer.toString(++linkedIdAutoCounter);
396 linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
398 usingAutoCounterLinkedId = true;
400 "Changing linkedId to '" + linkedId + "' from " + arg);
404 if (!linkedArgs.containsKey(linkedId))
405 linkedArgs.put(linkedId, new ArgValuesMap());
407 // do not continue for NOACTION args
408 if (a.hasOption(Opt.NOACTION))
411 ArgValuesMap avm = linkedArgs.get(linkedId);
413 // not dealing with both NODUPLICATEVALUES and GLOB
414 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
416 Console.error("Argument '" + a.argString()
417 + "' cannot contain a duplicate value ('" + val
418 + "'). Ignoring this and subsequent occurrences.");
422 // check for unique id
423 SubVals idsv = new SubVals(val);
424 String id = idsv.get(ArgValues.ID);
425 if (id != null && avm.hasId(a, id))
427 Console.error("Argument '" + a.argString()
428 + "' has a duplicate id ('" + id + "'). Ignoring.");
432 boolean argIndexIncremented = false;
433 ArgValues avs = avm.getOrCreateArgValues(a);
435 // store appropriate String value(s)
436 if (a.hasOption(Opt.STRING))
438 if (a.hasOption(Opt.GLOB) && globVals != null
439 && globVals.size() > 0)
441 for (String v : globVals)
443 v = makeSubstitutions(v, linkedId);
444 SubVals vsv = new SubVals(globSubVals, v);
445 avs.addValue(vsv, v, argIndex++);
446 argIndexIncremented = true;
451 avs.addValue(makeSubstitutions(val, linkedId), argIndex);
454 else if (a.hasOption(Opt.BOOLEAN))
456 avs.setBoolean(!negated, argIndex);
457 avs.setNegated(negated);
459 else if (a.hasOption(Opt.UNARY))
461 avs.setBoolean(true, argIndex);
463 avs.incrementCount();
464 if (!argIndexIncremented)
467 // store in appropriate place
468 if (a.hasOption(Opt.LINKED))
470 // store the order of linkedIds
471 if (linkedOrder == null)
472 linkedOrder = new ArrayList<>();
473 if (!linkedOrder.contains(linkedId))
474 linkedOrder.add(linkedId);
477 // store arg in the list of args used
479 argList = new ArrayList<>();
480 if (!argList.contains(a))
487 public String makeSubstitutions(String val, String linkedId)
489 if (!this.substitutions || val == null)
494 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
496 int closeBracket = val.indexOf(']');
497 if (val.length() == closeBracket)
499 subvals = val.substring(0, closeBracket + 1);
500 rest = val.substring(closeBracket + 1);
507 if (rest.contains(LINKEDIDAUTOCOUNTER))
508 rest = rest.replace(LINKEDIDAUTOCOUNTER,
509 String.valueOf(linkedIdAutoCounter));
510 if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
511 rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
512 String.valueOf(++linkedIdAutoCounter));
513 if (rest.contains(DEFAULTLINKEDIDCOUNTER))
514 rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
515 String.valueOf(defaultLinkedIdCounter));
516 ArgValuesMap avm = linkedArgs.get(linkedId);
519 if (rest.contains(LINKEDIDBASENAME))
521 rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
523 if (rest.contains(LINKEDIDDIRNAME))
525 rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
530 if (rest.contains(ARGFILEBASENAME))
532 rest = rest.replace(ARGFILEBASENAME,
533 FileUtils.getBasename(new File(argFile)));
535 if (rest.contains(ARGFILEDIRNAME))
537 rest = rest.replace(ARGFILEDIRNAME,
538 FileUtils.getDirname(new File(argFile)));
542 return new StringBuilder(subvals).append(rest).toString();
546 * A helper method to take a list of String args where we're expecting
547 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
548 * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
549 * "file2", "file3"} *and remove these from the original list object* so that
550 * processing can continue from where it has left off, e.g. args has become
551 * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
552 * carries on from the next --arg if available.
554 protected static List<String> getShellGlobbedFilenameValues(Arg a,
555 List<String> args, int i)
557 List<String> vals = new ArrayList<>();
558 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
560 vals.add(FileUtils.substituteHomeDir(args.remove(i)));
561 if (!a.hasOption(Opt.GLOB))
567 public boolean isSet(Arg a)
569 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
572 public boolean isSet(String linkedId, Arg a)
574 ArgValuesMap avm = linkedArgs.get(linkedId);
575 return avm == null ? false : avm.containsArg(a);
578 public boolean getBool(Arg a)
580 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
582 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
585 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
588 public boolean getBool(String linkedId, Arg a)
590 ArgValuesMap avm = linkedArgs.get(linkedId);
592 return a.getDefaultBoolValue();
593 ArgValues avs = avm.getArgValues(a);
594 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
597 public List<String> linkedIds()
602 public ArgValuesMap linkedArgs(String id)
604 return linkedArgs.get(id);
608 public String toString()
610 StringBuilder sb = new StringBuilder();
611 sb.append("UNLINKED\n");
612 sb.append(argValuesMapToString(linkedArgs.get(null)));
613 if (linkedIds() != null)
615 sb.append("LINKED\n");
616 for (String id : linkedIds())
618 // already listed these as UNLINKED args
622 ArgValuesMap avm = linkedArgs(id);
623 sb.append("ID: '").append(id).append("'\n");
624 sb.append(argValuesMapToString(avm));
627 return sb.toString();
630 private static String argValuesMapToString(ArgValuesMap avm)
634 StringBuilder sb = new StringBuilder();
635 for (Arg a : avm.getArgKeys())
637 ArgValues v = avm.getArgValues(a);
638 sb.append(v.toString());
641 return sb.toString();
644 public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
645 boolean initsubstitutions)
647 List<File> argFiles = new ArrayList<>();
649 for (String pattern : argFilenameGlobs)
651 // I don't think we want to dedup files, making life easier
652 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
655 return parseArgFileList(argFiles, initsubstitutions);
658 public static ArgParser parseArgFileList(List<File> argFiles,
659 boolean initsubstitutions)
661 List<String> argsList = new ArrayList<>();
662 for (File argFile : argFiles)
664 if (!argFile.exists())
666 String message = Arg.ARGFILE.argString() + EQUALS + "\""
667 + argFile.getPath() + "\": File does not exist.";
668 Jalview.exit(message, 2);
672 String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
673 .append(EQUALS).append(argFile.getCanonicalPath())
675 argsList.add(setargfile);
676 argsList.addAll(readArgFile(argFile));
677 argsList.add(Arg.UNSETARGFILE.argString());
678 } catch (IOException e)
680 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
681 + "\": File could not be read.";
682 Jalview.exit(message, 3);
685 // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
687 return new ArgParser(argsList, initsubstitutions, true);
690 protected static List<String> readArgFile(File argFile)
692 List<String> args = new ArrayList<>();
693 if (argFile != null && argFile.exists())
697 for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
699 if (line != null && line.length() > 0
700 && line.charAt(0) != ARGFILECOMMENT)
703 } catch (IOException e)
705 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
706 + "\": File could not be read.";
707 Console.debug(message, e);
708 Jalview.exit(message, 3);
714 public static enum Position
719 public static String getValueFromSubValOrArg(ArgValuesMap avm, Arg a,
722 return getFromSubValArgOrPref(avm, a, sv, null, null, null);
725 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
726 SubVals sv, String key, String pref, String def)
728 return getFromSubValArgOrPref(avm, a, Position.FIRST, null, sv, key,
732 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
733 Position pos, ArgValue av, SubVals sv, String key, String pref,
738 if (sv != null && sv.has(key) && sv.get(key) != null)
740 if (avm != null && avm.containsArg(a))
743 if (pos == Position.FIRST && avm.getValue(a) != null)
744 return avm.getValue(a);
745 else if (pos == Position.BEFORE
746 && avm.getClosestPreviousArgValueOfArg(av, a) != null)
747 return avm.getClosestPreviousArgValueOfArg(av, a).getValue();
748 else if (pos == Position.AFTER
749 && avm.getClosestNextArgValueOfArg(av, a) != null)
750 return avm.getClosestNextArgValueOfArg(av, a).getValue();
752 return pref != null ? Cache.getDefault(pref, def) : def;
755 public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
758 return getFromSubValArgOrPref(avm, a, sv, null, null, false);
761 public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
762 SubVals sv, String key, String pref, boolean def)
766 if (sv != null && sv.has(key) && sv.get(key) != null)
767 return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
768 if (avm != null && avm.containsArg(a))
769 return avm.getBoolean(a);
770 return pref != null ? Cache.getDefault(pref, def) : def;