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.Console;
36 import jalview.bin.Jalview;
37 import jalview.bin.argparser.Arg.Opt;
38 import jalview.util.FileUtils;
40 public class ArgParser
42 protected static final String DOUBLEDASH = "--";
44 protected static final char EQUALS = '=';
46 protected static final String NEGATESTRING = "no";
48 // the default linked id prefix used for no id (not even square braces)
49 protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
51 // the counter added to the default linked id prefix
52 private int defaultLinkedIdCounter = 0;
54 // the linked id prefix used for --opennew files
55 protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:";
57 // the counter added to the default linked id prefix
58 private int opennewLinkedIdCounter = 0;
60 // the linked id substitution string used to increment the idCounter (and use
61 // the incremented value)
62 private static final String INCREMENTAUTOCOUNTERLINKEDID = "{++n}";
64 // the linked id substitution string used to use the idCounter
65 private static final String AUTOCOUNTERLINKEDID = "{n}";
67 // the linked id substitution string used to use the base filename of --open
69 private static final String LINKEDIDBASENAME = "{basename}";
71 // the linked id substitution string used to use the dir path of --open
73 private static final String LINKEDIDDIRNAME = "{dirname}";
75 // the current argfile
76 private String argFile = null;
78 // the linked id substitution string used to use the dir path of the latest
80 private static final String ARGFILEBASENAME = "{argfilebasename}";
82 // the linked id substitution string used to use the dir path of the latest
84 private static final String ARGFILEDIRNAME = "{argfiledirname}";
86 private int linkedIdAutoCounter = 0;
88 // flag to say whether {n} subtitutions in output filenames should be made.
89 // Turn on and off with --subs and --nosubs
90 private boolean substitutions = false;
92 protected static final Map<String, Arg> argMap;
94 protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
96 protected List<String> linkedOrder = null;
98 protected List<Arg> argList;
102 argMap = new HashMap<>();
103 for (Arg a : EnumSet.allOf(Arg.class))
105 for (String argName : a.getNames())
107 if (argMap.containsKey(argName))
109 Console.warn("Trying to add argument name multiple times: '"
110 + argName + "'"); // RESTORE THIS WHEN MERGED
111 if (argMap.get(argName) != a)
114 "Trying to add argument name multiple times for different Args: '"
115 + argMap.get(argName).getName() + ":" + argName
116 + "' and '" + a.getName() + ":" + argName
121 argMap.put(argName, a);
126 public ArgParser(String[] args)
128 // Make a mutable new ArrayList so that shell globbing parser works.
129 // (When shell file globbing is used, there are a sequence of non-Arg
130 // arguments (which are the expanded globbed filenames) that need to be
131 // consumed by the --open/--argfile/etc Arg which is most easily done by
132 // removing these filenames from the list one at a time. This can't be done
133 // with an ArrayList made with only Arrays.asList(String[] args). )
134 this(new ArrayList<>(Arrays.asList(args)));
137 public ArgParser(List<String> args)
142 public ArgParser(List<String> args, boolean allowPrivate)
144 // do nothing if there are no "--" args and some "-" args
147 for (String arg : args)
149 if (arg.startsWith(DOUBLEDASH))
154 else if (arg.startsWith("-"))
161 // leave it to the old style -- parse an empty list
162 parse(new ArrayList<String>(), allowPrivate);
165 parse(args, allowPrivate);
168 private void parse(List<String> args, boolean allowPrivate)
171 boolean openEachInitialFilenames = true;
172 for (int i = 0; i < args.size(); i++)
174 String arg = args.get(i);
176 // If the first arguments do not start with "--" or "-" or is "open" and
177 // is a filename that exists it is probably a file/list of files to open
178 // so we fake an Arg.OPEN argument and when adding files only add the
179 // single arg[i] and increment the defaultLinkedIdCounter so that each of
180 // these files is opened separately.
181 if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
182 && !arg.startsWith("-") && new File(arg).exists())
184 arg = DOUBLEDASH + Arg.OPENNEW.getName();
188 openEachInitialFilenames = false;
191 String argName = null;
193 List<String> globVals = null; // for Opt.GLOB only
194 SubVals globSubVals = null; // also for use by Opt.GLOB only
195 String linkedId = null;
196 if (arg.startsWith(DOUBLEDASH))
198 int equalPos = arg.indexOf(EQUALS);
201 argName = arg.substring(DOUBLEDASH.length(), equalPos);
202 val = arg.substring(equalPos + 1);
206 argName = arg.substring(DOUBLEDASH.length());
208 int idOpen = argName.indexOf('[');
209 int idClose = argName.indexOf(']');
211 if (idOpen > -1 && idClose == argName.length() - 1)
213 linkedId = argName.substring(idOpen + 1, idClose);
214 argName = argName.substring(0, idOpen);
217 Arg a = argMap.get(argName);
218 // check for boolean prepended by "no"
219 boolean negated = false;
220 if (a == null && argName.startsWith(NEGATESTRING) && argMap
221 .containsKey(argName.substring(NEGATESTRING.length())))
223 argName = argName.substring(NEGATESTRING.length());
224 a = argMap.get(argName);
228 // check for config errors
232 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
235 if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
237 Console.error("Argument '" + DOUBLEDASH + argName
238 + "' is private. Ignoring.");
241 if (!a.hasOption(Opt.BOOLEAN) && negated)
243 // used "no" with a non-boolean option
244 Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
245 + "' not a boolean option. Ignoring.");
248 if (!a.hasOption(Opt.STRING) && equalPos > -1)
250 // set --argname=value when arg does not accept values
251 Console.error("Argument '" + DOUBLEDASH + argName
252 + "' does not expect a value (given as '" + arg
256 if (!a.hasOption(Opt.LINKED) && linkedId != null)
258 // set --argname[linkedId] when arg does not use linkedIds
259 Console.error("Argument '" + DOUBLEDASH + argName
260 + "' does not expect a linked id (given as '" + arg
266 if (a.hasOption(Opt.STRING))
270 if (a.hasOption(Opt.GLOB))
272 // strip off and save the SubVals to be added individually later
273 globSubVals = ArgParser.getSubVals(val);
274 // make substitutions before looking for files
275 String fileGlob = makeSubstitutions(globSubVals.getContent(),
277 globVals = FileUtils.getFilenamesFromGlob(fileGlob);
281 // val is already set -- will be saved in the ArgValue later in
287 // There is no "=" so value is next arg or args (possibly shell
289 if ((openEachInitialFilenames ? i : i + 1) >= args.size())
291 // no value to take for arg, which wants a value
292 Console.error("Argument '" + a.getName()
293 + "' requires a value, none given. Ignoring.");
296 // deal with bash globs here (--arg val* is expanded before reaching
297 // the JVM). Note that SubVals cannot be used in this case.
298 // If using the --arg=val then the glob is preserved and Java globs
299 // will be used later. SubVals can be used.
300 if (a.hasOption(Opt.GLOB))
302 // if this is the first argument with a file list at the start of
303 // the args we add filenames from index i instead of i+1
304 globVals = getShellGlobbedFilenameValues(a, args,
305 openEachInitialFilenames ? i : i + 1);
309 val = args.get(i + 1);
314 // make NOACTION adjustments
315 // default and auto counter increments
316 if (a == Arg.INCREMENT)
318 defaultLinkedIdCounter++;
320 else if (a == Arg.NPP)
322 linkedIdAutoCounter++;
324 else if (a == Arg.SUBSTITUTIONS)
326 substitutions = !negated;
328 else if (a == Arg.SETARGFILE)
332 else if (a == Arg.UNSETARGFILE)
337 String autoCounterString = null;
338 boolean usingAutoCounterLinkedId = false;
339 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
340 .append(Integer.toString(defaultLinkedIdCounter))
342 boolean usingDefaultLinkedId = false;
343 if (a.hasOption(Opt.LINKED))
345 if (linkedId == null)
347 if (a == Arg.OPENNEW)
349 // use the next default prefixed OPENNEWLINKEDID
350 linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX)
351 .append(Integer.toString(opennewLinkedIdCounter))
353 opennewLinkedIdCounter++;
357 // use default linkedId for linked arguments
358 linkedId = defaultLinkedId;
359 usingDefaultLinkedId = true;
360 Console.debug("Changing linkedId to '" + linkedId + "' from "
364 else if (linkedId.contains(AUTOCOUNTERLINKEDID))
366 // turn {n} to the autoCounter
367 autoCounterString = Integer.toString(linkedIdAutoCounter);
368 linkedId = linkedId.replace(AUTOCOUNTERLINKEDID,
370 usingAutoCounterLinkedId = true;
372 "Changing linkedId to '" + linkedId + "' from " + arg);
374 else if (linkedId.contains(INCREMENTAUTOCOUNTERLINKEDID))
376 // turn {++n} to the incremented autoCounter
377 autoCounterString = Integer.toString(++linkedIdAutoCounter);
378 linkedId = linkedId.replace(INCREMENTAUTOCOUNTERLINKEDID,
380 usingAutoCounterLinkedId = true;
382 "Changing linkedId to '" + linkedId + "' from " + arg);
386 if (!linkedArgs.containsKey(linkedId))
387 linkedArgs.put(linkedId, new ArgValuesMap());
389 // do not continue for NOACTION args
390 if (a.hasOption(Opt.NOACTION))
393 ArgValuesMap avm = linkedArgs.get(linkedId);
395 // not dealing with both NODUPLICATEVALUES and GLOB
396 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
398 Console.error("Argument '" + DOUBLEDASH + argName
399 + "' cannot contain a duplicate value ('" + val
400 + "'). Ignoring this and subsequent occurrences.");
404 // check for unique id
405 SubVals idsv = ArgParser.getSubVals(val);
406 String id = idsv.get(ArgValues.ID);
407 if (id != null && avm.hasId(a, id))
409 Console.error("Argument '" + DOUBLEDASH + argName
410 + "' has a duplicate id ('" + id + "'). Ignoring.");
414 boolean argIndexIncremented = false;
415 ArgValues avs = avm.getOrCreateArgValues(a);
417 // store appropriate String value(s)
418 if (a.hasOption(Opt.STRING))
420 if (a.hasOption(Opt.GLOB) && globVals != null
421 && globVals.size() > 0)
423 for (String v : globVals)
425 v = makeSubstitutions(v, linkedId);
426 SubVals vsv = new SubVals(globSubVals, v);
427 avs.addValue(vsv, v, argIndex++);
428 argIndexIncremented = true;
433 avs.addValue(makeSubstitutions(val, linkedId), argIndex);
436 else if (a.hasOption(Opt.BOOLEAN))
438 avs.setBoolean(!negated, argIndex);
439 avs.setNegated(negated);
441 else if (a.hasOption(Opt.UNARY))
443 avs.setBoolean(true, argIndex);
445 avs.incrementCount();
446 if (!argIndexIncremented)
449 // store in appropriate place
450 if (a.hasOption(Opt.LINKED))
452 // store the order of linkedIds
453 if (linkedOrder == null)
454 linkedOrder = new ArrayList<>();
455 if (!linkedOrder.contains(linkedId))
456 linkedOrder.add(linkedId);
459 // store arg in the list of args used
461 argList = new ArrayList<>();
462 if (!argList.contains(a))
469 private String makeSubstitutions(String val, String linkedId)
471 if (!this.substitutions)
476 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
478 int closeBracket = val.indexOf(']');
479 if (val.length() == closeBracket)
481 subvals = val.substring(0, closeBracket + 1);
482 rest = val.substring(closeBracket + 1);
489 if (rest.contains(AUTOCOUNTERLINKEDID))
490 rest = rest.replace(AUTOCOUNTERLINKEDID,
491 String.valueOf(linkedIdAutoCounter));
492 if (rest.contains(INCREMENTAUTOCOUNTERLINKEDID))
493 rest = rest.replace(INCREMENTAUTOCOUNTERLINKEDID,
494 String.valueOf(++linkedIdAutoCounter));
495 if (rest.contains("{}"))
496 rest = rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
497 ArgValuesMap avm = linkedArgs.get(linkedId);
500 if (rest.contains(LINKEDIDBASENAME))
502 rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
504 if (rest.contains(LINKEDIDDIRNAME))
506 rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
511 if (rest.contains(ARGFILEBASENAME))
513 rest = rest.replace(ARGFILEBASENAME,
514 FileUtils.getBasename(new File(argFile)));
516 if (rest.contains(ARGFILEDIRNAME))
518 rest = rest.replace(ARGFILEDIRNAME,
519 FileUtils.getDirname(new File(argFile)));
523 return new StringBuilder(subvals).append(rest).toString();
527 * A helper method to take a list of String args where we're expecting
528 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
529 * and the index of the globbed arg, here 1. It returns a
530 * List<String> {"file1", "file2", "file3"}
531 * *and remove these from the original list object* so that processing
532 * can continue from where it has left off, e.g. args has become
533 * {"--previousargs", "--arg", "--otheroptionsornot"}
534 * so the next increment carries on from the next --arg if available.
536 protected static List<String> getShellGlobbedFilenameValues(Arg a,
537 List<String> args, int i)
539 List<String> vals = new ArrayList<>();
540 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
542 vals.add(FileUtils.substituteHomeDir(args.remove(i)));
543 if (!a.hasOption(Opt.GLOB))
549 public boolean isSet(Arg a)
551 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
554 public boolean isSet(String linkedId, Arg a)
556 ArgValuesMap avm = linkedArgs.get(linkedId);
557 return avm == null ? false : avm.containsArg(a);
560 public boolean getBool(Arg a)
562 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
564 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
567 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
570 public boolean getBool(String linkedId, Arg a)
572 ArgValuesMap avm = linkedArgs.get(linkedId);
574 return a.getDefaultBoolValue();
575 ArgValues avs = avm.getArgValues(a);
576 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
579 public List<String> linkedIds()
584 public ArgValuesMap linkedArgs(String id)
586 return linkedArgs.get(id);
590 public String toString()
592 StringBuilder sb = new StringBuilder();
593 sb.append("UNLINKED\n");
594 sb.append(argValuesMapToString(linkedArgs.get(null)));
595 if (linkedIds() != null)
597 sb.append("LINKED\n");
598 for (String id : linkedIds())
600 // already listed these as UNLINKED args
604 ArgValuesMap avm = linkedArgs(id);
605 sb.append("ID: '").append(id).append("'\n");
606 sb.append(argValuesMapToString(avm));
609 return sb.toString();
612 private static String argValuesMapToString(ArgValuesMap avm)
616 StringBuilder sb = new StringBuilder();
617 for (Arg a : avm.getArgKeys())
619 ArgValues v = avm.getArgValues(a);
620 sb.append(v.toString());
623 return sb.toString();
626 public static SubVals getSubVals(String item)
628 return new SubVals(item);
631 public static ArgParser parseArgFiles(List<String> argFilenameGlobs)
633 List<File> argFiles = new ArrayList<>();
635 for (String pattern : argFilenameGlobs)
637 // I don't think we want to dedup files, making life easier
638 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
641 return parseArgFileList(argFiles);
644 public static ArgParser parseArgFileList(List<File> argFiles)
646 List<String> argsList = new ArrayList<>();
647 for (File argFile : argFiles)
649 if (!argFile.exists())
651 String message = DOUBLEDASH
652 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + EQUALS
653 + "\"" + argFile.getPath() + "\": File does not exist.";
654 Jalview.exit(message, 2);
658 String setargfile = new StringBuilder(ArgParser.DOUBLEDASH)
659 .append(Arg.SETARGFILE.getName()).append(EQUALS)
660 .append(argFile.getCanonicalPath()).toString();
661 argsList.add(setargfile);
662 argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
663 argsList.add(new StringBuilder(ArgParser.DOUBLEDASH)
664 .append(Arg.UNSETARGFILE.getName()).toString());
665 } catch (IOException e)
667 String message = DOUBLEDASH
668 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
669 + argFile.getPath() + "\": File could not be read.";
670 Jalview.exit(message, 3);
673 // Second param "true" uses Opt.PRIVATE args --setargile=argfile and
675 return new ArgParser(argsList, true);