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
59 private int opennewLinkedIdCounter = 0;
61 // the linked id prefix used for --opennew files
62 protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:";
64 // the counter used for {n} substitutions
65 private int linkedIdAutoCounter = 0;
67 // the linked id substitution string used to increment the idCounter (and use
68 // the incremented value)
69 private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
71 // the linked id substitution string used to use the idCounter
72 private static final String LINKEDIDAUTOCOUNTER = "{n}";
74 // the linked id substitution string used to use the base filename of --open
76 private static final String LINKEDIDBASENAME = "{basename}";
78 // the linked id substitution string used to use the dir path of --open
80 private static final String LINKEDIDDIRNAME = "{dirname}";
82 // the current argfile
83 private String argFile = null;
85 // the linked id substitution string used to use the dir path of the latest
87 private static final String ARGFILEBASENAME = "{argfilebasename}";
89 // the linked id substitution string used to use the dir path of the latest
91 private static final String ARGFILEDIRNAME = "{argfiledirname}";
93 // flag to say whether {n} subtitutions in output filenames should be made.
94 // Turn on and off with --subs and --nosubs
95 private boolean substitutions = false;
97 protected static final Map<String, Arg> argMap;
99 protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
101 protected List<String> linkedOrder = null;
103 protected List<Arg> argList;
105 private static final char ARGFILECOMMENT = '#';
109 argMap = new HashMap<>();
110 for (Arg a : EnumSet.allOf(Arg.class))
112 for (String argName : a.getNames())
114 if (argMap.containsKey(argName))
116 Console.warn("Trying to add argument name multiple times: '"
117 + argName + "'"); // RESTORE THIS WHEN
119 if (argMap.get(argName) != a)
122 "Trying to add argument name multiple times for different Args: '"
123 + argMap.get(argName).getName() + ":" + argName
124 + "' and '" + a.getName() + ":" + argName
129 argMap.put(argName, a);
134 public ArgParser(String[] args)
139 public ArgParser(String[] args, boolean initsubstitutions)
141 // Make a mutable new ArrayList so that shell globbing parser works.
142 // (When shell file globbing is used, there are a sequence of non-Arg
143 // arguments (which are the expanded globbed filenames) that need to be
144 // consumed by the --open/--argfile/etc Arg which is most easily done by
145 // removing these filenames from the list one at a time. This can't be done
146 // with an ArrayList made with only Arrays.asList(String[] args). )
147 this(new ArrayList<>(Arrays.asList(args)), initsubstitutions);
150 public ArgParser(List<String> args, boolean initsubstitutions)
152 this(args, initsubstitutions, false);
155 public ArgParser(List<String> args, boolean initsubstitutions,
156 boolean allowPrivate)
158 // do nothing if there are no "--" args and some "-" args
161 for (String arg : args)
163 if (arg.startsWith(DOUBLEDASH))
168 else if (arg.startsWith("-"))
175 // leave it to the old style -- parse an empty list
176 parse(new ArrayList<String>(), false, false);
179 parse(args, initsubstitutions, allowPrivate);
182 private void parse(List<String> args, boolean initsubstitutions,
183 boolean allowPrivate)
185 this.substitutions = initsubstitutions;
187 boolean openEachInitialFilenames = true;
188 for (int i = 0; i < args.size(); i++)
190 String arg = args.get(i);
192 // If the first arguments do not start with "--" or "-" or is "open" and
193 // is a filename that exists it is probably a file/list of files to open
194 // so we fake an Arg.OPEN argument and when adding files only add the
195 // single arg[i] and increment the defaultLinkedIdCounter so that each of
196 // these files is opened separately.
197 if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
198 && !arg.startsWith("-") && new File(arg).exists())
200 arg = Arg.OPENNEW.argString();
204 openEachInitialFilenames = false;
207 String argName = null;
209 List<String> globVals = null; // for Opt.GLOB only
210 SubVals globSubVals = null; // also for use by Opt.GLOB only
211 String linkedId = null;
212 if (arg.startsWith(DOUBLEDASH))
214 int equalPos = arg.indexOf(EQUALS);
217 argName = arg.substring(DOUBLEDASH.length(), equalPos);
218 val = arg.substring(equalPos + 1);
222 argName = arg.substring(DOUBLEDASH.length());
224 int idOpen = argName.indexOf('[');
225 int idClose = argName.indexOf(']');
227 if (idOpen > -1 && idClose == argName.length() - 1)
229 linkedId = argName.substring(idOpen + 1, idClose);
230 argName = argName.substring(0, idOpen);
233 Arg a = argMap.get(argName);
234 // check for boolean prepended by "no"
235 boolean negated = false;
236 if (a == null && argName.startsWith(NEGATESTRING) && argMap
237 .containsKey(argName.substring(NEGATESTRING.length())))
239 argName = argName.substring(NEGATESTRING.length());
240 a = argMap.get(argName);
244 // check for config errors
248 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
251 if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
254 "Argument '" + a.argString() + "' is private. Ignoring.");
257 if (!a.hasOption(Opt.BOOLEAN) && negated)
259 // used "no" with a non-boolean option
260 Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
261 + "' not a boolean option. Ignoring.");
264 if (!a.hasOption(Opt.STRING) && equalPos > -1)
266 // set --argname=value when arg does not accept values
267 Console.error("Argument '" + a.argString()
268 + "' does not expect a value (given as '" + arg
272 if (!a.hasOption(Opt.LINKED) && linkedId != null)
274 // set --argname[linkedId] when arg does not use linkedIds
275 Console.error("Argument '" + a.argString()
276 + "' does not expect a linked id (given as '" + arg
282 if (a.hasOption(Opt.STRING))
286 if (a.hasOption(Opt.GLOB))
288 // strip off and save the SubVals to be added individually later
289 globSubVals = new SubVals(val);
290 // make substitutions before looking for files
291 String fileGlob = makeSubstitutions(globSubVals.getContent(),
293 globVals = FileUtils.getFilenamesFromGlob(fileGlob);
297 // val is already set -- will be saved in the ArgValue later in
303 // There is no "=" so value is next arg or args (possibly shell
305 if ((openEachInitialFilenames ? i : i + 1) >= args.size())
307 // no value to take for arg, which wants a value
308 Console.error("Argument '" + a.getName()
309 + "' requires a value, none given. Ignoring.");
312 // deal with bash globs here (--arg val* is expanded before reaching
313 // the JVM). Note that SubVals cannot be used in this case.
314 // If using the --arg=val then the glob is preserved and Java globs
315 // will be used later. SubVals can be used.
316 if (a.hasOption(Opt.GLOB))
318 // if this is the first argument with a file list at the start of
319 // the args we add filenames from index i instead of i+1
320 globVals = getShellGlobbedFilenameValues(a, args,
321 openEachInitialFilenames ? i : i + 1);
325 val = args.get(i + 1);
330 // make NOACTION adjustments
331 // default and auto counter increments
332 if (a == Arg.INCREMENT)
334 defaultLinkedIdCounter++;
336 else if (a == Arg.NPP)
338 linkedIdAutoCounter++;
340 else if (a == Arg.SUBSTITUTIONS)
342 substitutions = !negated;
344 else if (a == Arg.SETARGFILE)
348 else if (a == Arg.UNSETARGFILE)
353 String autoCounterString = null;
354 boolean usingAutoCounterLinkedId = false;
355 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
356 .append(Integer.toString(defaultLinkedIdCounter))
358 boolean usingDefaultLinkedId = false;
359 if (a.hasOption(Opt.LINKED))
361 if (linkedId == null)
363 if (a == Arg.OPENNEW)
365 // use the next default prefixed OPENNEWLINKEDID
366 linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX)
367 .append(Integer.toString(opennewLinkedIdCounter))
369 opennewLinkedIdCounter++;
373 // use default linkedId for linked arguments
374 linkedId = defaultLinkedId;
375 usingDefaultLinkedId = true;
376 Console.debug("Changing linkedId to '" + linkedId + "' from "
380 else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
382 // turn {n} to the autoCounter
383 autoCounterString = Integer.toString(linkedIdAutoCounter);
384 linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
386 usingAutoCounterLinkedId = true;
388 "Changing linkedId to '" + linkedId + "' from " + arg);
390 else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
392 // turn {++n} to the incremented autoCounter
393 autoCounterString = Integer.toString(++linkedIdAutoCounter);
394 linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
396 usingAutoCounterLinkedId = true;
398 "Changing linkedId to '" + linkedId + "' from " + arg);
402 if (!linkedArgs.containsKey(linkedId))
403 linkedArgs.put(linkedId, new ArgValuesMap());
405 // do not continue for NOACTION args
406 if (a.hasOption(Opt.NOACTION))
409 ArgValuesMap avm = linkedArgs.get(linkedId);
411 // not dealing with both NODUPLICATEVALUES and GLOB
412 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
414 Console.error("Argument '" + a.argString()
415 + "' cannot contain a duplicate value ('" + val
416 + "'). Ignoring this and subsequent occurrences.");
420 // check for unique id
421 SubVals idsv = new SubVals(val);
422 String id = idsv.get(ArgValues.ID);
423 if (id != null && avm.hasId(a, id))
425 Console.error("Argument '" + a.argString()
426 + "' has a duplicate id ('" + id + "'). Ignoring.");
430 boolean argIndexIncremented = false;
431 ArgValues avs = avm.getOrCreateArgValues(a);
433 // store appropriate String value(s)
434 if (a.hasOption(Opt.STRING))
436 if (a.hasOption(Opt.GLOB) && globVals != null
437 && globVals.size() > 0)
439 for (String v : globVals)
441 v = makeSubstitutions(v, linkedId);
442 SubVals vsv = new SubVals(globSubVals, v);
443 avs.addValue(vsv, v, argIndex++);
444 argIndexIncremented = true;
449 avs.addValue(makeSubstitutions(val, linkedId), argIndex);
452 else if (a.hasOption(Opt.BOOLEAN))
454 avs.setBoolean(!negated, argIndex);
455 avs.setNegated(negated);
457 else if (a.hasOption(Opt.UNARY))
459 avs.setBoolean(true, argIndex);
461 avs.incrementCount();
462 if (!argIndexIncremented)
465 // store in appropriate place
466 if (a.hasOption(Opt.LINKED))
468 // store the order of linkedIds
469 if (linkedOrder == null)
470 linkedOrder = new ArrayList<>();
471 if (!linkedOrder.contains(linkedId))
472 linkedOrder.add(linkedId);
475 // store arg in the list of args used
477 argList = new ArrayList<>();
478 if (!argList.contains(a))
485 public String makeSubstitutions(String val, String linkedId)
487 if (!this.substitutions || val == null)
492 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
494 int closeBracket = val.indexOf(']');
495 if (val.length() == closeBracket)
497 subvals = val.substring(0, closeBracket + 1);
498 rest = val.substring(closeBracket + 1);
505 if (rest.contains(LINKEDIDAUTOCOUNTER))
506 rest = rest.replace(LINKEDIDAUTOCOUNTER,
507 String.valueOf(linkedIdAutoCounter));
508 if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
509 rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
510 String.valueOf(++linkedIdAutoCounter));
511 if (rest.contains(DEFAULTLINKEDIDCOUNTER))
512 rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
513 String.valueOf(defaultLinkedIdCounter));
514 ArgValuesMap avm = linkedArgs.get(linkedId);
517 if (rest.contains(LINKEDIDBASENAME))
519 rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
521 if (rest.contains(LINKEDIDDIRNAME))
523 rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
528 if (rest.contains(ARGFILEBASENAME))
530 rest = rest.replace(ARGFILEBASENAME,
531 FileUtils.getBasename(new File(argFile)));
533 if (rest.contains(ARGFILEDIRNAME))
535 rest = rest.replace(ARGFILEDIRNAME,
536 FileUtils.getDirname(new File(argFile)));
540 return new StringBuilder(subvals).append(rest).toString();
544 * A helper method to take a list of String args where we're expecting
545 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
546 * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
547 * "file2", "file3"} *and remove these from the original list object* so that
548 * processing can continue from where it has left off, e.g. args has become
549 * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
550 * carries on from the next --arg if available.
552 protected static List<String> getShellGlobbedFilenameValues(Arg a,
553 List<String> args, int i)
555 List<String> vals = new ArrayList<>();
556 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
558 vals.add(FileUtils.substituteHomeDir(args.remove(i)));
559 if (!a.hasOption(Opt.GLOB))
565 public boolean isSet(Arg a)
567 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
570 public boolean isSet(String linkedId, Arg a)
572 ArgValuesMap avm = linkedArgs.get(linkedId);
573 return avm == null ? false : avm.containsArg(a);
576 public boolean getBool(Arg a)
578 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
580 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
583 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
586 public boolean getBool(String linkedId, Arg a)
588 ArgValuesMap avm = linkedArgs.get(linkedId);
590 return a.getDefaultBoolValue();
591 ArgValues avs = avm.getArgValues(a);
592 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
595 public List<String> linkedIds()
600 public ArgValuesMap linkedArgs(String id)
602 return linkedArgs.get(id);
606 public String toString()
608 StringBuilder sb = new StringBuilder();
609 sb.append("UNLINKED\n");
610 sb.append(argValuesMapToString(linkedArgs.get(null)));
611 if (linkedIds() != null)
613 sb.append("LINKED\n");
614 for (String id : linkedIds())
616 // already listed these as UNLINKED args
620 ArgValuesMap avm = linkedArgs(id);
621 sb.append("ID: '").append(id).append("'\n");
622 sb.append(argValuesMapToString(avm));
625 return sb.toString();
628 private static String argValuesMapToString(ArgValuesMap avm)
632 StringBuilder sb = new StringBuilder();
633 for (Arg a : avm.getArgKeys())
635 ArgValues v = avm.getArgValues(a);
636 sb.append(v.toString());
639 return sb.toString();
642 public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
643 boolean initsubstitutions)
645 List<File> argFiles = new ArrayList<>();
647 for (String pattern : argFilenameGlobs)
649 // I don't think we want to dedup files, making life easier
650 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
653 return parseArgFileList(argFiles, initsubstitutions);
656 public static ArgParser parseArgFileList(List<File> argFiles,
657 boolean initsubstitutions)
659 List<String> argsList = new ArrayList<>();
660 for (File argFile : argFiles)
662 if (!argFile.exists())
664 String message = Arg.ARGFILE.argString() + EQUALS + "\""
665 + argFile.getPath() + "\": File does not exist.";
666 Jalview.exit(message, 2);
670 String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
671 .append(EQUALS).append(argFile.getCanonicalPath())
673 argsList.add(setargfile);
674 argsList.addAll(readArgFile(argFile));
675 argsList.add(Arg.UNSETARGFILE.argString());
676 } catch (IOException e)
678 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
679 + "\": File could not be read.";
680 Jalview.exit(message, 3);
683 // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
685 return new ArgParser(argsList, initsubstitutions, true);
688 protected static List<String> readArgFile(File argFile)
690 List<String> args = new ArrayList<>();
691 if (argFile != null && argFile.exists())
695 for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
697 if (line != null && line.length() > 0
698 && line.charAt(0) != ARGFILECOMMENT)
701 } catch (IOException e)
703 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
704 + "\": File could not be read.";
705 Console.debug(message, e);
706 Jalview.exit(message, 3);
712 public static enum Position
717 public static String getValueFromSubValOrArg(ArgValuesMap avm, Arg a,
720 return getFromSubValArgOrPref(avm, a, sv, null, null, null);
723 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
724 SubVals sv, String key, String pref, String def)
726 return getFromSubValArgOrPref(avm, a, Position.FIRST, null, sv, key,
730 public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
731 Position pos, ArgValue av, SubVals sv, String key, String pref,
736 if (sv != null && sv.has(key) && sv.get(key) != null)
738 if (avm != null && avm.containsArg(a))
741 if (pos == Position.FIRST && avm.getValue(a) != null)
742 return avm.getValue(a);
743 else if (pos == Position.BEFORE
744 && avm.getClosestPreviousArgValueOfArg(av, a) != null)
745 return avm.getClosestPreviousArgValueOfArg(av, a).getValue();
746 else if (pos == Position.AFTER
747 && avm.getClosestNextArgValueOfArg(av, a) != null)
748 return avm.getClosestNextArgValueOfArg(av, a).getValue();
750 return pref != null ? Cache.getDefault(pref, def) : def;
753 public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a,
756 return getFromSubValArgOrPref(avm, a, sv, null, null, false);
759 public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a,
760 SubVals sv, String key, String pref, boolean def)
764 if (sv != null && sv.has(key) && sv.get(key) != null)
765 return sv.get(key).toLowerCase(Locale.ROOT).equals("true");
766 if (avm != null && avm.containsArg(a))
767 return avm.getBoolean(a);
768 return pref != null ? Cache.getDefault(pref, def) : def;