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 || >0 arg is
163 for (String arg : args)
165 if (arg.startsWith(DOUBLEDASH))
170 else if (arg.startsWith("-") || arg.equals("open"))
177 // leave it to the old style -- parse an empty list
178 parse(new ArrayList<String>(), false, false);
181 parse(args, initsubstitutions, allowPrivate);
184 private void parse(List<String> args, boolean initsubstitutions,
185 boolean allowPrivate)
187 this.substitutions = initsubstitutions;
189 boolean openEachInitialFilenames = true;
190 for (int i = 0; i < args.size(); i++)
192 String arg = args.get(i);
194 // If the first arguments do not start with "--" or "-" or is "open" and
195 // is a filename that exists it is probably a file/list of files to open
196 // so we fake an Arg.OPEN argument and when adding files only add the
197 // single arg[i] and increment the defaultLinkedIdCounter so that each of
198 // these files is opened separately.
199 if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
200 && !arg.startsWith("-") && new File(arg).exists())
202 arg = Arg.OPEN.argString();
206 openEachInitialFilenames = false;
209 String argName = null;
211 List<String> globVals = null; // for Opt.GLOB only
212 SubVals globSubVals = null; // also for use by Opt.GLOB only
213 String linkedId = null;
214 if (arg.startsWith(DOUBLEDASH))
216 int equalPos = arg.indexOf(EQUALS);
219 argName = arg.substring(DOUBLEDASH.length(), equalPos);
220 val = arg.substring(equalPos + 1);
224 argName = arg.substring(DOUBLEDASH.length());
226 int idOpen = argName.indexOf('[');
227 int idClose = argName.indexOf(']');
229 if (idOpen > -1 && idClose == argName.length() - 1)
231 linkedId = argName.substring(idOpen + 1, idClose);
232 argName = argName.substring(0, idOpen);
235 Arg a = argMap.get(argName);
236 // check for boolean prepended by "no"
237 boolean negated = false;
238 if (a == null && argName.startsWith(NEGATESTRING) && argMap
239 .containsKey(argName.substring(NEGATESTRING.length())))
241 argName = argName.substring(NEGATESTRING.length());
242 a = argMap.get(argName);
246 // check for config errors
250 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
253 if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
256 "Argument '" + a.argString() + "' is private. Ignoring.");
259 if (!a.hasOption(Opt.BOOLEAN) && negated)
261 // used "no" with a non-boolean option
262 Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
263 + "' not a boolean option. Ignoring.");
266 if (!a.hasOption(Opt.STRING) && equalPos > -1)
268 // set --argname=value when arg does not accept values
269 Console.error("Argument '" + a.argString()
270 + "' does not expect a value (given as '" + arg
274 if (!a.hasOption(Opt.LINKED) && linkedId != null)
276 // set --argname[linkedId] when arg does not use linkedIds
277 Console.error("Argument '" + a.argString()
278 + "' does not expect a linked id (given as '" + arg
284 if (a.hasOption(Opt.STRING))
288 if (a.hasOption(Opt.GLOB))
290 // strip off and save the SubVals to be added individually later
291 globSubVals = new SubVals(val);
292 // make substitutions before looking for files
293 String fileGlob = makeSubstitutions(globSubVals.getContent(),
295 globVals = FileUtils.getFilenamesFromGlob(fileGlob);
299 // val is already set -- will be saved in the ArgValue later in
305 // There is no "=" so value is next arg or args (possibly shell
307 if ((openEachInitialFilenames ? i : i + 1) >= args.size())
309 // no value to take for arg, which wants a value
310 Console.error("Argument '" + a.getName()
311 + "' requires a value, none given. Ignoring.");
314 // deal with bash globs here (--arg val* is expanded before reaching
315 // the JVM). Note that SubVals cannot be used in this case.
316 // If using the --arg=val then the glob is preserved and Java globs
317 // will be used later. SubVals can be used.
318 if (a.hasOption(Opt.GLOB))
320 // if this is the first argument with a file list at the start of
321 // the args we add filenames from index i instead of i+1
322 globVals = getShellGlobbedFilenameValues(a, args,
323 openEachInitialFilenames ? i : i + 1);
327 val = args.get(i + 1);
332 // make NOACTION adjustments
333 // default and auto counter increments
334 if (a == Arg.INCREMENT)
336 defaultLinkedIdCounter++;
338 else if (a == Arg.NPP)
340 linkedIdAutoCounter++;
342 else if (a == Arg.SUBSTITUTIONS)
344 substitutions = !negated;
346 else if (a == Arg.SETARGFILE)
350 else if (a == Arg.UNSETARGFILE)
355 String autoCounterString = null;
356 boolean usingAutoCounterLinkedId = false;
357 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
358 .append(Integer.toString(defaultLinkedIdCounter))
360 boolean usingDefaultLinkedId = false;
361 if (a.hasOption(Opt.LINKED))
363 if (linkedId == null)
367 // use the next default prefixed OPENLINKEDID
368 // NOW using the linkedIdAutoCounter
369 defaultLinkedIdCounter++;
370 linkedId = new StringBuilder(OPENLINKEDIDPREFIX)
371 .append(Integer.toString(defaultLinkedIdCounter))
376 // use default linkedId for linked arguments
377 linkedId = defaultLinkedId;
378 usingDefaultLinkedId = true;
379 Console.debug("Changing linkedId to '" + linkedId + "' from "
383 else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
385 // turn {n} to the autoCounter
386 autoCounterString = Integer.toString(linkedIdAutoCounter);
387 linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
389 usingAutoCounterLinkedId = true;
391 "Changing linkedId to '" + linkedId + "' from " + arg);
393 else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
395 // turn {++n} to the incremented autoCounter
396 autoCounterString = Integer.toString(++linkedIdAutoCounter);
397 linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
399 usingAutoCounterLinkedId = true;
401 "Changing linkedId to '" + linkedId + "' from " + arg);
405 if (!linkedArgs.containsKey(linkedId))
406 linkedArgs.put(linkedId, new ArgValuesMap());
408 // do not continue for NOACTION args
409 if (a.hasOption(Opt.NOACTION))
412 ArgValuesMap avm = linkedArgs.get(linkedId);
414 // not dealing with both NODUPLICATEVALUES and GLOB
415 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
417 Console.error("Argument '" + a.argString()
418 + "' cannot contain a duplicate value ('" + val
419 + "'). Ignoring this and subsequent occurrences.");
423 // check for unique id
424 SubVals idsv = new SubVals(val);
425 String id = idsv.get(ArgValues.ID);
426 if (id != null && avm.hasId(a, id))
428 Console.error("Argument '" + a.argString()
429 + "' has a duplicate id ('" + id + "'). Ignoring.");
433 boolean argIndexIncremented = false;
434 ArgValues avs = avm.getOrCreateArgValues(a);
436 // store appropriate String value(s)
437 if (a.hasOption(Opt.STRING))
439 if (a.hasOption(Opt.GLOB) && globVals != null
440 && globVals.size() > 0)
442 for (String v : globVals)
444 v = makeSubstitutions(v, linkedId);
445 SubVals vsv = new SubVals(globSubVals, v);
446 avs.addValue(vsv, v, argIndex++);
447 argIndexIncremented = true;
452 avs.addValue(makeSubstitutions(val, linkedId), argIndex);
455 else if (a.hasOption(Opt.BOOLEAN))
457 avs.setBoolean(!negated, argIndex);
458 avs.setNegated(negated);
460 else if (a.hasOption(Opt.UNARY))
462 avs.setBoolean(true, argIndex);
464 avs.incrementCount();
465 if (!argIndexIncremented)
468 // store in appropriate place
469 if (a.hasOption(Opt.LINKED))
471 // store the order of linkedIds
472 if (linkedOrder == null)
473 linkedOrder = new ArrayList<>();
474 if (!linkedOrder.contains(linkedId))
475 linkedOrder.add(linkedId);
478 // store arg in the list of args used
480 argList = new ArrayList<>();
481 if (!argList.contains(a))
488 public String makeSubstitutions(String val, String linkedId)
490 if (!this.substitutions || val == null)
495 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
497 int closeBracket = val.indexOf(']');
498 if (val.length() == closeBracket)
500 subvals = val.substring(0, closeBracket + 1);
501 rest = val.substring(closeBracket + 1);
508 if (rest.contains(LINKEDIDAUTOCOUNTER))
509 rest = rest.replace(LINKEDIDAUTOCOUNTER,
510 String.valueOf(linkedIdAutoCounter));
511 if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
512 rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
513 String.valueOf(++linkedIdAutoCounter));
514 if (rest.contains(DEFAULTLINKEDIDCOUNTER))
515 rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
516 String.valueOf(defaultLinkedIdCounter));
517 ArgValuesMap avm = linkedArgs.get(linkedId);
520 if (rest.contains(LINKEDIDBASENAME))
522 rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
524 if (rest.contains(LINKEDIDDIRNAME))
526 rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
531 if (rest.contains(ARGFILEBASENAME))
533 rest = rest.replace(ARGFILEBASENAME,
534 FileUtils.getBasename(new File(argFile)));
536 if (rest.contains(ARGFILEDIRNAME))
538 rest = rest.replace(ARGFILEDIRNAME,
539 FileUtils.getDirname(new File(argFile)));
543 return new StringBuilder(subvals).append(rest).toString();
547 * A helper method to take a list of String args where we're expecting
548 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
549 * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
550 * "file2", "file3"} *and remove these from the original list object* so that
551 * processing can continue from where it has left off, e.g. args has become
552 * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
553 * carries on from the next --arg if available.
555 protected static List<String> getShellGlobbedFilenameValues(Arg a,
556 List<String> args, int i)
558 List<String> vals = new ArrayList<>();
559 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
561 vals.add(FileUtils.substituteHomeDir(args.remove(i)));
562 if (!a.hasOption(Opt.GLOB))
568 public boolean isSet(Arg a)
570 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
573 public boolean isSet(String linkedId, Arg a)
575 ArgValuesMap avm = linkedArgs.get(linkedId);
576 return avm == null ? false : avm.containsArg(a);
579 public boolean getBool(Arg a)
581 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
583 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
586 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
589 public boolean getBool(String linkedId, Arg a)
591 ArgValuesMap avm = linkedArgs.get(linkedId);
593 return a.getDefaultBoolValue();
594 ArgValues avs = avm.getArgValues(a);
595 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
598 public List<String> linkedIds()
603 public ArgValuesMap linkedArgs(String id)
605 return linkedArgs.get(id);
609 public String toString()
611 StringBuilder sb = new StringBuilder();
612 sb.append("UNLINKED\n");
613 sb.append(argValuesMapToString(linkedArgs.get(null)));
614 if (linkedIds() != null)
616 sb.append("LINKED\n");
617 for (String id : linkedIds())
619 // already listed these as UNLINKED args
623 ArgValuesMap avm = linkedArgs(id);
624 sb.append("ID: '").append(id).append("'\n");
625 sb.append(argValuesMapToString(avm));
628 return sb.toString();
631 private static String argValuesMapToString(ArgValuesMap avm)
635 StringBuilder sb = new StringBuilder();
636 for (Arg a : avm.getArgKeys())
638 ArgValues v = avm.getArgValues(a);
639 sb.append(v.toString());
642 return sb.toString();
645 public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
646 boolean initsubstitutions)
648 List<File> argFiles = new ArrayList<>();
650 for (String pattern : argFilenameGlobs)
652 // I don't think we want to dedup files, making life easier
653 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
656 return parseArgFileList(argFiles, initsubstitutions);
659 public static ArgParser parseArgFileList(List<File> argFiles,
660 boolean initsubstitutions)
662 List<String> argsList = new ArrayList<>();
663 for (File argFile : argFiles)
665 if (!argFile.exists())
667 String message = Arg.ARGFILE.argString() + EQUALS + "\""
668 + argFile.getPath() + "\": File does not exist.";
669 Jalview.exit(message, 2);
673 String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
674 .append(EQUALS).append(argFile.getCanonicalPath())
676 argsList.add(setargfile);
677 argsList.addAll(readArgFile(argFile));
678 argsList.add(Arg.UNSETARGFILE.argString());
679 } catch (IOException e)
681 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
682 + "\": File could not be read.";
683 Jalview.exit(message, 3);
686 // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
688 return new ArgParser(argsList, initsubstitutions, true);
691 protected static List<String> readArgFile(File argFile)
693 List<String> args = new ArrayList<>();
694 if (argFile != null && argFile.exists())
698 for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
700 if (line != null && line.length() > 0
701 && line.charAt(0) != ARGFILECOMMENT)
704 } catch (IOException e)
706 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
707 + "\": File could not be read.";
708 Console.debug(message, e);
709 Jalview.exit(message, 3);
715 public static enum Position
720 public static String getValueFromSubValOrArg(ArgValuesMap avm, Arg a,
723 return getFromSubValArgOrPref(avm, a, sv, null, null, null);
726 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
727 SubVals sv, String key, String pref, String def)
729 return getFromSubValArgOrPref(avm, a, Position.FIRST, null, sv, key,
733 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
734 Position pos, ArgValue av, SubVals sv, String key, String pref,
739 if (sv != null && sv.has(key) && sv.get(key) != null)
741 if (avm != null && avm.containsArg(a))
744 if (pos == Position.FIRST && avm.getValue(a) != null)
745 return avm.getValue(a);
746 else if (pos == Position.BEFORE
747 && avm.getClosestPreviousArgValueOfArg(av, a) != null)
748 return avm.getClosestPreviousArgValueOfArg(av, a).getValue();
749 else if (pos == Position.AFTER
750 && avm.getClosestNextArgValueOfArg(av, a) != null)
751 return avm.getClosestNextArgValueOfArg(av, a).getValue();
753 return pref != null ? Cache.getDefault(pref, def) : def;
756 public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
759 return getFromSubValArgOrPref(avm, a, sv, null, null, false);
762 public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
763 SubVals sv, String key, String pref, boolean def)
767 if (sv != null && sv.has(key) && sv.get(key) != null)
768 return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
769 if (avm != null && avm.containsArg(a))
770 return avm.getBoolean(a);
771 return pref != null ? Cache.getDefault(pref, def) : def;