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;
40 import jalview.util.HttpUtils;
42 public class ArgParser
44 protected static final String DOUBLEDASH = "--";
46 protected static final char EQUALS = '=';
48 protected static final String NEGATESTRING = "no";
50 // the default linked id prefix used for no id (not even square braces)
51 protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
53 // the counter added to the default linked id prefix
54 private int defaultLinkedIdCounter = 0;
56 // the substitution string used to use the defaultLinkedIdCounter
57 private static final String DEFAULTLINKEDIDCOUNTER = "{}";
59 // the counter added to the default linked id prefix. NOW using
60 // linkedIdAutoCounter
61 // private int openLinkedIdCounter = 0;
63 // the linked id prefix used for --open files. NOW the same as DEFAULT
64 protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX;
66 // the counter used for {n} substitutions
67 private int linkedIdAutoCounter = 0;
69 // the linked id substitution string used to increment the idCounter (and use
70 // the incremented value)
71 private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
73 // the linked id substitution string used to use the idCounter
74 private static final String LINKEDIDAUTOCOUNTER = "{n}";
76 // the linked id substitution string used to use the base filename of --append
78 private static final String LINKEDIDBASENAME = "{basename}";
80 // the linked id substitution string used to use the dir path of --append
82 private static final String LINKEDIDDIRNAME = "{dirname}";
84 // the current argfile
85 private String argFile = null;
87 // the linked id substitution string used to use the dir path of the latest
89 private static final String ARGFILEBASENAME = "{argfilebasename}";
91 // the linked id substitution string used to use the dir path of the latest
93 private static final String ARGFILEDIRNAME = "{argfiledirname}";
95 // flag to say whether {n} subtitutions in output filenames should be made.
96 // Turn on and off with --subs and --nosubs
97 private boolean substitutions = false;
99 protected static final Map<String, Arg> argMap;
101 protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
103 protected List<String> linkedOrder = null;
105 protected List<Arg> argList;
107 private static final char ARGFILECOMMENT = '#';
111 argMap = new HashMap<>();
112 for (Arg a : EnumSet.allOf(Arg.class))
114 for (String argName : a.getNames())
116 if (argMap.containsKey(argName))
118 Console.warn("Trying to add argument name multiple times: '"
119 + argName + "'"); // RESTORE THIS WHEN
121 if (argMap.get(argName) != a)
124 "Trying to add argument name multiple times for different Args: '"
125 + argMap.get(argName).getName() + ":" + argName
126 + "' and '" + a.getName() + ":" + argName
131 argMap.put(argName, a);
136 public ArgParser(String[] args)
141 public ArgParser(String[] args, boolean initsubstitutions)
143 // Make a mutable new ArrayList so that shell globbing parser works.
144 // (When shell file globbing is used, there are a sequence of non-Arg
145 // arguments (which are the expanded globbed filenames) that need to be
146 // consumed by the --append/--argfile/etc Arg which is most easily done by
147 // removing these filenames from the list one at a time. This can't be done
148 // with an ArrayList made with only Arrays.asList(String[] args). )
149 this(new ArrayList<>(Arrays.asList(args)), initsubstitutions);
152 public ArgParser(List<String> args, boolean initsubstitutions)
154 this(args, initsubstitutions, false);
157 public ArgParser(List<String> args, boolean initsubstitutions,
158 boolean allowPrivate)
160 // do nothing if there are no "--" args and (some "-" args || >0 arg is
164 for (String arg : args)
166 if (arg.startsWith(DOUBLEDASH))
171 else if (arg.startsWith("-") || arg.equals("open"))
178 // leave it to the old style -- parse an empty list
179 parse(new ArrayList<String>(), false, false);
182 parse(args, initsubstitutions, allowPrivate);
185 private void parse(List<String> args, boolean initsubstitutions,
186 boolean allowPrivate)
188 this.substitutions = initsubstitutions;
190 boolean openEachInitialFilenames = true;
191 for (int i = 0; i < args.size(); i++)
193 String arg = args.get(i);
195 // If the first arguments do not start with "--" or "-" or is "open" and
196 // is a filename that exists it is probably a file/list of files to open
197 // so we fake an Arg.OPEN argument and when adding files only add the
198 // single arg[i] and increment the defaultLinkedIdCounter so that each of
199 // these files is opened separately.
200 if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
201 && !arg.startsWith("-") && (new File(arg).exists()
202 || HttpUtils.startsWithHttpOrHttps(arg)))
204 arg = Arg.OPEN.argString();
208 openEachInitialFilenames = false;
211 String argName = null;
213 List<String> globVals = null; // for Opt.GLOB only
214 SubVals globSubVals = null; // also for use by Opt.GLOB only
215 String linkedId = null;
216 if (arg.startsWith(DOUBLEDASH))
218 int equalPos = arg.indexOf(EQUALS);
221 argName = arg.substring(DOUBLEDASH.length(), equalPos);
222 val = arg.substring(equalPos + 1);
226 argName = arg.substring(DOUBLEDASH.length());
228 int idOpen = argName.indexOf('[');
229 int idClose = argName.indexOf(']');
231 if (idOpen > -1 && idClose == argName.length() - 1)
233 linkedId = argName.substring(idOpen + 1, idClose);
234 argName = argName.substring(0, idOpen);
237 Arg a = argMap.get(argName);
238 // check for boolean prepended by "no"
239 boolean negated = false;
240 if (a == null && argName.startsWith(NEGATESTRING) && argMap
241 .containsKey(argName.substring(NEGATESTRING.length())))
243 argName = argName.substring(NEGATESTRING.length());
244 a = argMap.get(argName);
248 // check for config errors
252 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
255 if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
258 "Argument '" + a.argString() + "' is private. Ignoring.");
261 if (!a.hasOption(Opt.BOOLEAN) && negated)
263 // used "no" with a non-boolean option
264 Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
265 + "' not a boolean option. Ignoring.");
268 if (!a.hasOption(Opt.STRING) && equalPos > -1)
270 // set --argname=value when arg does not accept values
271 Console.error("Argument '" + a.argString()
272 + "' does not expect a value (given as '" + arg
276 if (!a.hasOption(Opt.LINKED) && linkedId != null)
278 // set --argname[linkedId] when arg does not use linkedIds
279 Console.error("Argument '" + a.argString()
280 + "' does not expect a linked id (given as '" + arg
286 if (a.hasOption(Opt.STRING))
290 if (a.hasOption(Opt.GLOB))
292 // strip off and save the SubVals to be added individually later
293 globSubVals = new SubVals(val);
294 // make substitutions before looking for files
295 String fileGlob = makeSubstitutions(globSubVals.getContent(),
297 globVals = FileUtils.getFilenamesFromGlob(fileGlob);
301 // val is already set -- will be saved in the ArgValue later in
307 // There is no "=" so value is next arg or args (possibly shell
309 if ((openEachInitialFilenames ? i : i + 1) >= args.size())
311 // no value to take for arg, which wants a value
312 Console.error("Argument '" + a.getName()
313 + "' requires a value, none given. Ignoring.");
316 // deal with bash globs here (--arg val* is expanded before reaching
317 // the JVM). Note that SubVals cannot be used in this case.
318 // If using the --arg=val then the glob is preserved and Java globs
319 // will be used later. SubVals can be used.
320 if (a.hasOption(Opt.GLOB))
322 // if this is the first argument with a file list at the start of
323 // the args we add filenames from index i instead of i+1
324 globVals = getShellGlobbedFilenameValues(a, args,
325 openEachInitialFilenames ? i : i + 1);
329 val = args.get(i + 1);
334 // make NOACTION adjustments
335 // default and auto counter increments
336 if (a == Arg.NEWFRAME)
338 defaultLinkedIdCounter++;
340 else if (a == Arg.NPP)
342 linkedIdAutoCounter++;
344 else if (a == Arg.SUBSTITUTIONS)
346 substitutions = !negated;
348 else if (a == Arg.SETARGFILE)
352 else if (a == Arg.UNSETARGFILE)
357 String autoCounterString = null;
358 boolean usingAutoCounterLinkedId = false;
359 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
360 .append(Integer.toString(defaultLinkedIdCounter))
362 boolean usingDefaultLinkedId = false;
363 if (a.hasOption(Opt.LINKED))
365 if (linkedId == null)
369 // use the next default prefixed OPENLINKEDID
370 // NOW using the linkedIdAutoCounter
371 defaultLinkedIdCounter++;
372 linkedId = new StringBuilder(OPENLINKEDIDPREFIX)
373 .append(Integer.toString(defaultLinkedIdCounter))
378 // use default linkedId for linked arguments
379 linkedId = defaultLinkedId;
380 usingDefaultLinkedId = true;
381 Console.debug("Changing linkedId to '" + linkedId + "' from "
385 else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
387 // turn {n} to the autoCounter
388 autoCounterString = Integer.toString(linkedIdAutoCounter);
389 linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
391 usingAutoCounterLinkedId = true;
393 "Changing linkedId to '" + linkedId + "' from " + arg);
395 else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
397 // turn {++n} to the incremented autoCounter
398 autoCounterString = Integer.toString(++linkedIdAutoCounter);
399 linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
401 usingAutoCounterLinkedId = true;
403 "Changing linkedId to '" + linkedId + "' from " + arg);
407 if (!linkedArgs.containsKey(linkedId))
408 linkedArgs.put(linkedId, new ArgValuesMap());
410 // do not continue for NOACTION args
411 if (a.hasOption(Opt.NOACTION))
414 ArgValuesMap avm = linkedArgs.get(linkedId);
416 // not dealing with both NODUPLICATEVALUES and GLOB
417 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
419 Console.error("Argument '" + a.argString()
420 + "' cannot contain a duplicate value ('" + val
421 + "'). Ignoring this and subsequent occurrences.");
425 // check for unique id
426 SubVals idsv = new SubVals(val);
427 String id = idsv.get(ArgValues.ID);
428 if (id != null && avm.hasId(a, id))
430 Console.error("Argument '" + a.argString()
431 + "' has a duplicate id ('" + id + "'). Ignoring.");
435 boolean argIndexIncremented = false;
436 ArgValues avs = avm.getOrCreateArgValues(a);
438 // store appropriate String value(s)
439 if (a.hasOption(Opt.STRING))
441 if (a.hasOption(Opt.GLOB) && globVals != null
442 && globVals.size() > 0)
444 for (String v : globVals)
446 v = makeSubstitutions(v, linkedId);
447 SubVals vsv = new SubVals(globSubVals, v);
448 avs.addValue(vsv, v, argIndex++);
449 argIndexIncremented = true;
454 avs.addValue(makeSubstitutions(val, linkedId), argIndex);
457 else if (a.hasOption(Opt.BOOLEAN))
459 avs.setBoolean(!negated, argIndex);
460 avs.setNegated(negated);
462 else if (a.hasOption(Opt.UNARY))
464 avs.setBoolean(true, argIndex);
466 avs.incrementCount();
467 if (!argIndexIncremented)
470 // store in appropriate place
471 if (a.hasOption(Opt.LINKED))
473 // store the order of linkedIds
474 if (linkedOrder == null)
475 linkedOrder = new ArrayList<>();
476 if (!linkedOrder.contains(linkedId))
477 linkedOrder.add(linkedId);
480 // store arg in the list of args used
482 argList = new ArrayList<>();
483 if (!argList.contains(a))
490 public String makeSubstitutions(String val, String linkedId)
492 if (!this.substitutions || val == null)
497 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
499 int closeBracket = val.indexOf(']');
500 if (val.length() == closeBracket)
502 subvals = val.substring(0, closeBracket + 1);
503 rest = val.substring(closeBracket + 1);
510 if (rest.contains(LINKEDIDAUTOCOUNTER))
511 rest = rest.replace(LINKEDIDAUTOCOUNTER,
512 String.valueOf(linkedIdAutoCounter));
513 if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
514 rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
515 String.valueOf(++linkedIdAutoCounter));
516 if (rest.contains(DEFAULTLINKEDIDCOUNTER))
517 rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
518 String.valueOf(defaultLinkedIdCounter));
519 ArgValuesMap avm = linkedArgs.get(linkedId);
522 if (rest.contains(LINKEDIDBASENAME))
524 rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
526 if (rest.contains(LINKEDIDDIRNAME))
528 rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
533 if (rest.contains(ARGFILEBASENAME))
535 rest = rest.replace(ARGFILEBASENAME,
536 FileUtils.getBasename(new File(argFile)));
538 if (rest.contains(ARGFILEDIRNAME))
540 rest = rest.replace(ARGFILEDIRNAME,
541 FileUtils.getDirname(new File(argFile)));
545 return new StringBuilder(subvals).append(rest).toString();
549 * A helper method to take a list of String args where we're expecting
550 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
551 * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
552 * "file2", "file3"} *and remove these from the original list object* so that
553 * processing can continue from where it has left off, e.g. args has become
554 * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
555 * carries on from the next --arg if available.
557 protected static List<String> getShellGlobbedFilenameValues(Arg a,
558 List<String> args, int i)
560 List<String> vals = new ArrayList<>();
561 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
563 vals.add(FileUtils.substituteHomeDir(args.remove(i)));
564 if (!a.hasOption(Opt.GLOB))
570 public boolean isSet(Arg a)
572 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
575 public boolean isSet(String linkedId, Arg a)
577 ArgValuesMap avm = linkedArgs.get(linkedId);
578 return avm == null ? false : avm.containsArg(a);
581 public boolean getBool(Arg a)
583 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
585 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
588 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
591 public boolean getBool(String linkedId, Arg a)
593 ArgValuesMap avm = linkedArgs.get(linkedId);
595 return a.getDefaultBoolValue();
596 ArgValues avs = avm.getArgValues(a);
597 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
600 public List<String> linkedIds()
605 public ArgValuesMap linkedArgs(String id)
607 return linkedArgs.get(id);
611 public String toString()
613 StringBuilder sb = new StringBuilder();
614 sb.append("UNLINKED\n");
615 sb.append(argValuesMapToString(linkedArgs.get(null)));
616 if (linkedIds() != null)
618 sb.append("LINKED\n");
619 for (String id : linkedIds())
621 // already listed these as UNLINKED args
625 ArgValuesMap avm = linkedArgs(id);
626 sb.append("ID: '").append(id).append("'\n");
627 sb.append(argValuesMapToString(avm));
630 return sb.toString();
633 private static String argValuesMapToString(ArgValuesMap avm)
637 StringBuilder sb = new StringBuilder();
638 for (Arg a : avm.getArgKeys())
640 ArgValues v = avm.getArgValues(a);
641 sb.append(v.toString());
644 return sb.toString();
647 public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
648 boolean initsubstitutions)
650 List<File> argFiles = new ArrayList<>();
652 for (String pattern : argFilenameGlobs)
654 // I don't think we want to dedup files, making life easier
655 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
658 return parseArgFileList(argFiles, initsubstitutions);
661 public static ArgParser parseArgFileList(List<File> argFiles,
662 boolean initsubstitutions)
664 List<String> argsList = new ArrayList<>();
665 for (File argFile : argFiles)
667 if (!argFile.exists())
669 String message = Arg.ARGFILE.argString() + EQUALS + "\""
670 + argFile.getPath() + "\": File does not exist.";
671 Jalview.exit(message, 2);
675 String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
676 .append(EQUALS).append(argFile.getCanonicalPath())
678 argsList.add(setargfile);
679 argsList.addAll(readArgFile(argFile));
680 argsList.add(Arg.UNSETARGFILE.argString());
681 } catch (IOException e)
683 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
684 + "\": File could not be read.";
685 Jalview.exit(message, 3);
688 // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
690 return new ArgParser(argsList, initsubstitutions, true);
693 protected static List<String> readArgFile(File argFile)
695 List<String> args = new ArrayList<>();
696 if (argFile != null && argFile.exists())
700 for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
702 if (line != null && line.length() > 0
703 && line.charAt(0) != ARGFILECOMMENT)
706 } catch (IOException e)
708 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
709 + "\": File could not be read.";
710 Console.debug(message, e);
711 Jalview.exit(message, 3);
717 public static enum Position
722 public static String getValueFromSubValOrArg(ArgValuesMap avm, Arg a,
725 return getFromSubValArgOrPref(avm, a, sv, null, null, null);
728 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
729 SubVals sv, String key, String pref, String def)
731 return getFromSubValArgOrPref(avm, a, Position.FIRST, null, sv, key,
735 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
736 Position pos, ArgValue av, SubVals sv, String key, String pref,
741 if (sv != null && sv.has(key) && sv.get(key) != null)
743 if (avm != null && avm.containsArg(a))
746 if (pos == Position.FIRST && avm.getValue(a) != null)
747 return avm.getValue(a);
748 else if (pos == Position.BEFORE
749 && avm.getClosestPreviousArgValueOfArg(av, a) != null)
750 return avm.getClosestPreviousArgValueOfArg(av, a).getValue();
751 else if (pos == Position.AFTER
752 && avm.getClosestNextArgValueOfArg(av, a) != null)
753 return avm.getClosestNextArgValueOfArg(av, a).getValue();
755 return pref != null ? Cache.getDefault(pref, def) : def;
758 public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
761 return getFromSubValArgOrPref(avm, a, sv, null, null, false);
764 public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
765 SubVals sv, String key, String pref, boolean def)
769 if (sv != null && sv.has(key) && sv.get(key) != null)
770 return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
771 if (avm != null && avm.containsArg(a))
772 return avm.getBoolean(a);
773 return pref != null ? Cache.getDefault(pref, def) : def;