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;
34 import jalview.bin.Console;
35 import jalview.bin.Jalview;
36 import jalview.bin.argparser.Arg.Opt;
37 import jalview.util.FileUtils;
39 public class ArgParser
41 protected static final String DOUBLEDASH = "--";
43 protected static final char EQUALS = '=';
45 protected static final String NEGATESTRING = "no";
47 // the default linked id prefix used for no id (not even square braces)
48 protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
50 // the counter added to the default linked id prefix
51 private int defaultLinkedIdCounter = 0;
53 // the substitution string used to use the defaultLinkedIdCounter
54 private static final String DEFAULTLINKEDIDCOUNTER = "{}";
56 // the counter added to the default linked id prefix
57 private int opennewLinkedIdCounter = 0;
59 // the linked id prefix used for --opennew files
60 protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:";
62 // the counter used for {n} substitutions
63 private int linkedIdAutoCounter = 0;
65 // the linked id substitution string used to increment the idCounter (and use
66 // the incremented value)
67 private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}";
69 // the linked id substitution string used to use the idCounter
70 private static final String LINKEDIDAUTOCOUNTER = "{n}";
72 // the linked id substitution string used to use the base filename of --open
74 private static final String LINKEDIDBASENAME = "{basename}";
76 // the linked id substitution string used to use the dir path of --open
78 private static final String LINKEDIDDIRNAME = "{dirname}";
80 // the current argfile
81 private String argFile = null;
83 // the linked id substitution string used to use the dir path of the latest
85 private static final String ARGFILEBASENAME = "{argfilebasename}";
87 // the linked id substitution string used to use the dir path of the latest
89 private static final String ARGFILEDIRNAME = "{argfiledirname}";
91 // flag to say whether {n} subtitutions in output filenames should be made.
92 // Turn on and off with --subs and --nosubs
93 private boolean substitutions = false;
95 protected static final Map<String, Arg> argMap;
97 protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
99 protected List<String> linkedOrder = null;
101 protected List<Arg> argList;
103 private static final char ARGFILECOMMENT = '#';
107 argMap = new HashMap<>();
108 for (Arg a : EnumSet.allOf(Arg.class))
110 for (String argName : a.getNames())
112 if (argMap.containsKey(argName))
114 Console.warn("Trying to add argument name multiple times: '"
115 + argName + "'"); // RESTORE THIS WHEN
117 if (argMap.get(argName) != a)
120 "Trying to add argument name multiple times for different Args: '"
121 + argMap.get(argName).getName() + ":" + argName
122 + "' and '" + a.getName() + ":" + argName
127 argMap.put(argName, a);
132 public ArgParser(String[] args)
137 public ArgParser(String[] args, boolean initsubstitutions)
139 // Make a mutable new ArrayList so that shell globbing parser works.
140 // (When shell file globbing is used, there are a sequence of non-Arg
141 // arguments (which are the expanded globbed filenames) that need to be
142 // consumed by the --open/--argfile/etc Arg which is most easily done by
143 // removing these filenames from the list one at a time. This can't be done
144 // with an ArrayList made with only Arrays.asList(String[] args). )
145 this(new ArrayList<>(Arrays.asList(args)), initsubstitutions);
148 public ArgParser(List<String> args, boolean initsubstitutions)
150 this(args, initsubstitutions, false);
153 public ArgParser(List<String> args, boolean initsubstitutions,
154 boolean allowPrivate)
156 // do nothing if there are no "--" args and some "-" args
159 for (String arg : args)
161 if (arg.startsWith(DOUBLEDASH))
166 else if (arg.startsWith("-"))
173 // leave it to the old style -- parse an empty list
174 parse(new ArrayList<String>(), false, false);
177 parse(args, initsubstitutions, allowPrivate);
180 private void parse(List<String> args, boolean initsubstitutions,
181 boolean allowPrivate)
183 this.substitutions = initsubstitutions;
185 boolean openEachInitialFilenames = true;
186 for (int i = 0; i < args.size(); i++)
188 String arg = args.get(i);
190 // If the first arguments do not start with "--" or "-" or is "open" and
191 // is a filename that exists it is probably a file/list of files to open
192 // so we fake an Arg.OPEN argument and when adding files only add the
193 // single arg[i] and increment the defaultLinkedIdCounter so that each of
194 // these files is opened separately.
195 if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
196 && !arg.startsWith("-") && new File(arg).exists())
198 arg = Arg.OPENNEW.argString();
202 openEachInitialFilenames = false;
205 String argName = null;
207 List<String> globVals = null; // for Opt.GLOB only
208 SubVals globSubVals = null; // also for use by Opt.GLOB only
209 String linkedId = null;
210 if (arg.startsWith(DOUBLEDASH))
212 int equalPos = arg.indexOf(EQUALS);
215 argName = arg.substring(DOUBLEDASH.length(), equalPos);
216 val = arg.substring(equalPos + 1);
220 argName = arg.substring(DOUBLEDASH.length());
222 int idOpen = argName.indexOf('[');
223 int idClose = argName.indexOf(']');
225 if (idOpen > -1 && idClose == argName.length() - 1)
227 linkedId = argName.substring(idOpen + 1, idClose);
228 argName = argName.substring(0, idOpen);
231 Arg a = argMap.get(argName);
232 // check for boolean prepended by "no"
233 boolean negated = false;
234 if (a == null && argName.startsWith(NEGATESTRING) && argMap
235 .containsKey(argName.substring(NEGATESTRING.length())))
237 argName = argName.substring(NEGATESTRING.length());
238 a = argMap.get(argName);
242 // check for config errors
246 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
249 if (a.hasOption(Opt.PRIVATE) && !allowPrivate)
252 "Argument '" + a.argString() + "' is private. Ignoring.");
255 if (!a.hasOption(Opt.BOOLEAN) && negated)
257 // used "no" with a non-boolean option
258 Console.error("Argument '" + DOUBLEDASH + NEGATESTRING + argName
259 + "' not a boolean option. Ignoring.");
262 if (!a.hasOption(Opt.STRING) && equalPos > -1)
264 // set --argname=value when arg does not accept values
265 Console.error("Argument '" + a.argString()
266 + "' does not expect a value (given as '" + arg
270 if (!a.hasOption(Opt.LINKED) && linkedId != null)
272 // set --argname[linkedId] when arg does not use linkedIds
273 Console.error("Argument '" + a.argString()
274 + "' does not expect a linked id (given as '" + arg
280 if (a.hasOption(Opt.STRING))
284 if (a.hasOption(Opt.GLOB))
286 // strip off and save the SubVals to be added individually later
287 globSubVals = new SubVals(val);
288 // make substitutions before looking for files
289 String fileGlob = makeSubstitutions(globSubVals.getContent(),
291 globVals = FileUtils.getFilenamesFromGlob(fileGlob);
295 // val is already set -- will be saved in the ArgValue later in
301 // There is no "=" so value is next arg or args (possibly shell
303 if ((openEachInitialFilenames ? i : i + 1) >= args.size())
305 // no value to take for arg, which wants a value
306 Console.error("Argument '" + a.getName()
307 + "' requires a value, none given. Ignoring.");
310 // deal with bash globs here (--arg val* is expanded before reaching
311 // the JVM). Note that SubVals cannot be used in this case.
312 // If using the --arg=val then the glob is preserved and Java globs
313 // will be used later. SubVals can be used.
314 if (a.hasOption(Opt.GLOB))
316 // if this is the first argument with a file list at the start of
317 // the args we add filenames from index i instead of i+1
318 globVals = getShellGlobbedFilenameValues(a, args,
319 openEachInitialFilenames ? i : i + 1);
323 val = args.get(i + 1);
328 // make NOACTION adjustments
329 // default and auto counter increments
330 if (a == Arg.INCREMENT)
332 defaultLinkedIdCounter++;
334 else if (a == Arg.NPP)
336 linkedIdAutoCounter++;
338 else if (a == Arg.SUBSTITUTIONS)
340 substitutions = !negated;
342 else if (a == Arg.SETARGFILE)
346 else if (a == Arg.UNSETARGFILE)
351 String autoCounterString = null;
352 boolean usingAutoCounterLinkedId = false;
353 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
354 .append(Integer.toString(defaultLinkedIdCounter))
356 boolean usingDefaultLinkedId = false;
357 if (a.hasOption(Opt.LINKED))
359 if (linkedId == null)
361 if (a == Arg.OPENNEW)
363 // use the next default prefixed OPENNEWLINKEDID
364 linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX)
365 .append(Integer.toString(opennewLinkedIdCounter))
367 opennewLinkedIdCounter++;
371 // use default linkedId for linked arguments
372 linkedId = defaultLinkedId;
373 usingDefaultLinkedId = true;
374 Console.debug("Changing linkedId to '" + linkedId + "' from "
378 else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
380 // turn {n} to the autoCounter
381 autoCounterString = Integer.toString(linkedIdAutoCounter);
382 linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
384 usingAutoCounterLinkedId = true;
386 "Changing linkedId to '" + linkedId + "' from " + arg);
388 else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
390 // turn {++n} to the incremented autoCounter
391 autoCounterString = Integer.toString(++linkedIdAutoCounter);
392 linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
394 usingAutoCounterLinkedId = true;
396 "Changing linkedId to '" + linkedId + "' from " + arg);
400 if (!linkedArgs.containsKey(linkedId))
401 linkedArgs.put(linkedId, new ArgValuesMap());
403 // do not continue for NOACTION args
404 if (a.hasOption(Opt.NOACTION))
407 ArgValuesMap avm = linkedArgs.get(linkedId);
409 // not dealing with both NODUPLICATEVALUES and GLOB
410 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
412 Console.error("Argument '" + a.argString()
413 + "' cannot contain a duplicate value ('" + val
414 + "'). Ignoring this and subsequent occurrences.");
418 // check for unique id
419 SubVals idsv = new SubVals(val);
420 String id = idsv.get(ArgValues.ID);
421 if (id != null && avm.hasId(a, id))
423 Console.error("Argument '" + a.argString()
424 + "' has a duplicate id ('" + id + "'). Ignoring.");
428 boolean argIndexIncremented = false;
429 ArgValues avs = avm.getOrCreateArgValues(a);
431 // store appropriate String value(s)
432 if (a.hasOption(Opt.STRING))
434 if (a.hasOption(Opt.GLOB) && globVals != null
435 && globVals.size() > 0)
437 for (String v : globVals)
439 v = makeSubstitutions(v, linkedId);
440 SubVals vsv = new SubVals(globSubVals, v);
441 avs.addValue(vsv, v, argIndex++);
442 argIndexIncremented = true;
447 avs.addValue(makeSubstitutions(val, linkedId), argIndex);
450 else if (a.hasOption(Opt.BOOLEAN))
452 avs.setBoolean(!negated, argIndex);
453 avs.setNegated(negated);
455 else if (a.hasOption(Opt.UNARY))
457 avs.setBoolean(true, argIndex);
459 avs.incrementCount();
460 if (!argIndexIncremented)
463 // store in appropriate place
464 if (a.hasOption(Opt.LINKED))
466 // store the order of linkedIds
467 if (linkedOrder == null)
468 linkedOrder = new ArrayList<>();
469 if (!linkedOrder.contains(linkedId))
470 linkedOrder.add(linkedId);
473 // store arg in the list of args used
475 argList = new ArrayList<>();
476 if (!argList.contains(a))
483 private String makeSubstitutions(String val, String linkedId)
485 if (!this.substitutions)
490 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
492 int closeBracket = val.indexOf(']');
493 if (val.length() == closeBracket)
495 subvals = val.substring(0, closeBracket + 1);
496 rest = val.substring(closeBracket + 1);
503 if (rest.contains(LINKEDIDAUTOCOUNTER))
504 rest = rest.replace(LINKEDIDAUTOCOUNTER,
505 String.valueOf(linkedIdAutoCounter));
506 if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
507 rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
508 String.valueOf(++linkedIdAutoCounter));
509 if (rest.contains(DEFAULTLINKEDIDCOUNTER))
510 rest = rest.replace(DEFAULTLINKEDIDCOUNTER,
511 String.valueOf(defaultLinkedIdCounter));
512 ArgValuesMap avm = linkedArgs.get(linkedId);
515 if (rest.contains(LINKEDIDBASENAME))
517 rest = rest.replace(LINKEDIDBASENAME, avm.getBasename());
519 if (rest.contains(LINKEDIDDIRNAME))
521 rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname());
526 if (rest.contains(ARGFILEBASENAME))
528 rest = rest.replace(ARGFILEBASENAME,
529 FileUtils.getBasename(new File(argFile)));
531 if (rest.contains(ARGFILEDIRNAME))
533 rest = rest.replace(ARGFILEDIRNAME,
534 FileUtils.getDirname(new File(argFile)));
538 return new StringBuilder(subvals).append(rest).toString();
542 * A helper method to take a list of String args where we're expecting
543 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
544 * and the index of the globbed arg, here 1. It returns a List<String> {"file1",
545 * "file2", "file3"} *and remove these from the original list object* so that
546 * processing can continue from where it has left off, e.g. args has become
547 * {"--previousargs", "--arg", "--otheroptionsornot"} so the next increment
548 * carries on from the next --arg if available.
550 protected static List<String> getShellGlobbedFilenameValues(Arg a,
551 List<String> args, int i)
553 List<String> vals = new ArrayList<>();
554 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
556 vals.add(FileUtils.substituteHomeDir(args.remove(i)));
557 if (!a.hasOption(Opt.GLOB))
563 public boolean isSet(Arg a)
565 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
568 public boolean isSet(String linkedId, Arg a)
570 ArgValuesMap avm = linkedArgs.get(linkedId);
571 return avm == null ? false : avm.containsArg(a);
574 public boolean getBool(Arg a)
576 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
578 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
581 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
584 public boolean getBool(String linkedId, Arg a)
586 ArgValuesMap avm = linkedArgs.get(linkedId);
588 return a.getDefaultBoolValue();
589 ArgValues avs = avm.getArgValues(a);
590 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
593 public List<String> linkedIds()
598 public ArgValuesMap linkedArgs(String id)
600 return linkedArgs.get(id);
604 public String toString()
606 StringBuilder sb = new StringBuilder();
607 sb.append("UNLINKED\n");
608 sb.append(argValuesMapToString(linkedArgs.get(null)));
609 if (linkedIds() != null)
611 sb.append("LINKED\n");
612 for (String id : linkedIds())
614 // already listed these as UNLINKED args
618 ArgValuesMap avm = linkedArgs(id);
619 sb.append("ID: '").append(id).append("'\n");
620 sb.append(argValuesMapToString(avm));
623 return sb.toString();
626 private static String argValuesMapToString(ArgValuesMap avm)
630 StringBuilder sb = new StringBuilder();
631 for (Arg a : avm.getArgKeys())
633 ArgValues v = avm.getArgValues(a);
634 sb.append(v.toString());
637 return sb.toString();
640 public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
641 boolean initsubstitutions)
643 List<File> argFiles = new ArrayList<>();
645 for (String pattern : argFilenameGlobs)
647 // I don't think we want to dedup files, making life easier
648 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
651 return parseArgFileList(argFiles, initsubstitutions);
654 public static ArgParser parseArgFileList(List<File> argFiles,
655 boolean initsubstitutions)
657 List<String> argsList = new ArrayList<>();
658 for (File argFile : argFiles)
660 if (!argFile.exists())
662 String message = Arg.ARGFILE.argString() + EQUALS + "\""
663 + argFile.getPath() + "\": File does not exist.";
664 Jalview.exit(message, 2);
668 String setargfile = new StringBuilder(Arg.SETARGFILE.argString())
669 .append(EQUALS).append(argFile.getCanonicalPath())
671 argsList.add(setargfile);
672 argsList.addAll(readArgFile(argFile));
673 argsList.add(Arg.UNSETARGFILE.argString());
674 } catch (IOException e)
676 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
677 + "\": File could not be read.";
678 Jalview.exit(message, 3);
681 // Third param "true" uses Opt.PRIVATE args --setargile=argfile and
683 return new ArgParser(argsList, initsubstitutions, true);
686 protected static List<String> readArgFile(File argFile)
688 List<String> args = new ArrayList<>();
689 if (argFile != null && argFile.exists())
693 for (String line : Files.readAllLines(Paths.get(argFile.getPath())))
695 if (line != null && line.length() > 0
696 && line.charAt(0) != ARGFILECOMMENT)
699 } catch (IOException e)
701 String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
702 + "\": File could not be read.";
703 Console.debug(message, e);
704 Jalview.exit(message, 3);