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 String NEGATESTRING = "no";
46 // the default linked id prefix used for no id (not even square braces)
47 protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:";
49 // the counter added to the default linked id prefix
50 private int defaultLinkedIdCounter = 0;
52 // the linked id prefix used for --opennew files
53 protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:";
55 // the counter added to the default linked id prefix
56 private int opennewLinkedIdCounter = 0;
58 // the linked id used to increment the idCounter (and use the incremented
60 private static final String INCREMENTAUTOCOUNTERLINKEDID = "{++n}";
62 // the linked id used to use the idCounter
63 private static final String AUTOCOUNTERLINKEDID = "{n}";
65 private int linkedIdAutoCounter = 0;
67 // flag to say whether {n} subtitutions in output filenames should be made.
68 // Turn on and off with --subs and --nosubs
69 private boolean substitutions = false;
71 protected static final Map<String, Arg> argMap;
73 protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
75 protected List<String> linkedOrder = null;
77 protected List<Arg> argList;
81 argMap = new HashMap<>();
82 for (Arg a : EnumSet.allOf(Arg.class))
84 for (String argName : a.getNames())
86 if (argMap.containsKey(argName))
88 Console.warn("Trying to add argument name multiple times: '"
89 + argName + "'"); // RESTORE THIS WHEN MERGED
90 if (argMap.get(argName) != a)
93 "Trying to add argument name multiple times for different Args: '"
94 + argMap.get(argName).getName() + ":" + argName
95 + "' and '" + a.getName() + ":" + argName
100 argMap.put(argName, a);
105 public ArgParser(String[] args)
107 // Make a mutable new ArrayList so that shell globbing parser works.
108 // (When shell file globbing is used, there are a sequence of non-Arg
109 // arguments (which are the expanded globbed filenames) that need to be
110 // consumed by the --open/--argfile/etc Arg which is most easily done by
111 // removing these filenames from the list one at a time. This can't be done
112 // with an ArrayList made with only Arrays.asList(String[] args). )
113 this(new ArrayList<>(Arrays.asList(args)));
116 public ArgParser(List<String> args)
118 // do nothing if there are no "--" args and some "-" args
121 for (String arg : args)
123 if (arg.startsWith(DOUBLEDASH))
128 else if (arg.startsWith("-"))
135 // leave it to the old style -- parse an empty list
136 parse(new ArrayList<String>());
142 private void parse(List<String> args)
145 boolean openEachInitialFilenames = true;
146 for (int i = 0; i < args.size(); i++)
148 String arg = args.get(i);
150 // If the first arguments do not start with "--" or "-" or is "open" and
151 // is a filename that exists it is probably a file/list of files to open
152 // so we fake an Arg.OPEN argument and when adding files only add the
153 // single arg[i] and increment the defaultLinkedIdCounter so that each of
154 // these files is opened separately.
155 if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
156 && !arg.startsWith("-") && new File(arg).exists())
158 arg = DOUBLEDASH + Arg.OPENNEW.getName();
162 openEachInitialFilenames = false;
165 String argName = null;
167 List<String> globVals = null; // for Opt.GLOB only
168 SubVals globSubVals = null; // also for use by Opt.GLOB only
169 String linkedId = null;
170 if (arg.startsWith(DOUBLEDASH))
172 int equalPos = arg.indexOf('=');
175 argName = arg.substring(DOUBLEDASH.length(), equalPos);
176 val = arg.substring(equalPos + 1);
180 argName = arg.substring(DOUBLEDASH.length());
182 int idOpen = argName.indexOf('[');
183 int idClose = argName.indexOf(']');
185 if (idOpen > -1 && idClose == argName.length() - 1)
187 linkedId = argName.substring(idOpen + 1, idClose);
188 argName = argName.substring(0, idOpen);
191 Arg a = argMap.get(argName);
192 // check for boolean prepended by "no"
193 boolean negated = false;
194 if (a == null && argName.startsWith(NEGATESTRING) && argMap
195 .containsKey(argName.substring(NEGATESTRING.length())))
197 argName = argName.substring(NEGATESTRING.length());
198 a = argMap.get(argName);
202 // check for config errors
206 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
209 if (!a.hasOption(Opt.BOOLEAN) && negated)
211 // used "no" with a non-boolean option
212 Console.error("Argument '--" + NEGATESTRING + argName
213 + "' not a boolean option. Ignoring.");
216 if (!a.hasOption(Opt.STRING) && equalPos > -1)
218 // set --argname=value when arg does not accept values
219 Console.error("Argument '--" + argName
220 + "' does not expect a value (given as '" + arg
224 if (!a.hasOption(Opt.LINKED) && linkedId != null)
226 // set --argname[linkedId] when arg does not use linkedIds
227 Console.error("Argument '--" + argName
228 + "' does not expect a linked id (given as '" + arg
234 if (a.hasOption(Opt.STRING))
238 if (a.hasOption(Opt.GLOB))
240 // strip off and save the SubVals to be added individually later
241 globSubVals = ArgParser.getSubVals(val);
243 .getFilenamesFromGlob(globSubVals.getContent());
247 // val is already set -- will be saved in the ArgValue later in
253 // There is no "=" so value is next arg or args (possibly shell
255 if ((openEachInitialFilenames ? i : i + 1) >= args.size())
257 // no value to take for arg, which wants a value
258 Console.error("Argument '" + a.getName()
259 + "' requires a value, none given. Ignoring.");
262 // deal with bash globs here (--arg val* is expanded before reaching
263 // the JVM). Note that SubVals cannot be used in this case.
264 // If using the --arg=val then the glob is preserved and Java globs
265 // will be used later. SubVals can be used.
266 if (a.hasOption(Opt.GLOB))
268 // if this is the first argument with a file list at the start of
269 // the args we add filenames from index i instead of i+1
270 globVals = getShellGlobbedFilenameValues(a, args,
271 openEachInitialFilenames ? i : i + 1);
275 val = args.get(i + 1);
280 // make NOACTION adjustments
281 // default and auto counter increments
282 if (a == Arg.INCREMENT)
284 defaultLinkedIdCounter++;
286 else if (a == Arg.NPP)
288 linkedIdAutoCounter++;
290 else if (a == Arg.SUBSTITUTIONS)
292 substitutions = !negated;
295 String autoCounterString = null;
296 boolean usingAutoCounterLinkedId = false;
297 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
298 .append(Integer.toString(defaultLinkedIdCounter))
300 boolean usingDefaultLinkedId = false;
301 if (a.hasOption(Opt.LINKED))
303 if (linkedId == null)
305 if (a == Arg.OPENNEW)
307 // use the next default prefixed OPENNEWLINKEDID
308 linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX)
309 .append(Integer.toString(opennewLinkedIdCounter))
311 opennewLinkedIdCounter++;
315 // use default linkedId for linked arguments
316 linkedId = defaultLinkedId;
317 usingDefaultLinkedId = true;
318 Console.debug("Changing linkedId to '" + linkedId + "' from "
322 else if (linkedId.contains(AUTOCOUNTERLINKEDID))
324 // turn {n} to the autoCounter
325 autoCounterString = Integer.toString(linkedIdAutoCounter);
326 linkedId = linkedId.replace(AUTOCOUNTERLINKEDID,
328 usingAutoCounterLinkedId = true;
330 "Changing linkedId to '" + linkedId + "' from " + arg);
332 else if (linkedId.contains(INCREMENTAUTOCOUNTERLINKEDID))
334 // turn {++n} to the incremented autoCounter
335 autoCounterString = Integer.toString(++linkedIdAutoCounter);
336 linkedId = linkedId.replace(INCREMENTAUTOCOUNTERLINKEDID,
338 usingAutoCounterLinkedId = true;
340 "Changing linkedId to '" + linkedId + "' from " + arg);
344 if (!linkedArgs.containsKey(linkedId))
345 linkedArgs.put(linkedId, new ArgValuesMap());
347 // do not continue for NOACTION args
348 if (a.hasOption(Opt.NOACTION))
351 ArgValuesMap avm = linkedArgs.get(linkedId);
353 // not dealing with both NODUPLICATEVALUES and GLOB
354 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
356 Console.error("Argument '--" + argName
357 + "' cannot contain a duplicate value ('" + val
358 + "'). Ignoring this and subsequent occurrences.");
362 // check for unique id
363 SubVals idsv = ArgParser.getSubVals(val);
364 String id = idsv.get(ArgValues.ID);
365 if (id != null && avm.hasId(a, id))
367 Console.error("Argument '--" + argName + "' has a duplicate id ('"
368 + id + "'). Ignoring.");
372 boolean argIndexIncremented = false;
373 ArgValues avs = avm.getOrCreateArgValues(a);
375 // store appropriate String value(s)
376 if (a.hasOption(Opt.STRING))
378 if (a.hasOption(Opt.GLOB) && globVals != null
379 && globVals.size() > 0)
381 for (String v : globVals)
383 v = makeSubstitutions(v);
384 SubVals vsv = new SubVals(globSubVals, v);
385 avs.addValue(vsv, v, argIndex++);
386 argIndexIncremented = true;
391 avs.addValue(makeSubstitutions(val), argIndex);
394 else if (a.hasOption(Opt.BOOLEAN))
396 avs.setBoolean(!negated, argIndex);
397 avs.setNegated(negated);
399 else if (a.hasOption(Opt.UNARY))
401 avs.setBoolean(true, argIndex);
403 avs.incrementCount();
404 if (!argIndexIncremented)
407 // store in appropriate place
408 if (a.hasOption(Opt.LINKED))
410 // store the order of linkedIds
411 if (linkedOrder == null)
412 linkedOrder = new ArrayList<>();
413 if (!linkedOrder.contains(linkedId))
414 linkedOrder.add(linkedId);
417 // store arg in the list of args used
419 argList = new ArrayList<>();
420 if (!argList.contains(a))
427 private String makeSubstitutions(String val)
429 if (!this.substitutions)
434 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
436 int closeBracket = val.indexOf(']');
437 if (val.length() == closeBracket)
439 subvals = val.substring(0, closeBracket + 1);
440 rest = val.substring(closeBracket + 1);
447 if ((rest.contains(AUTOCOUNTERLINKEDID)))
448 rest = rest.replace(AUTOCOUNTERLINKEDID,
449 String.valueOf(linkedIdAutoCounter));
450 if ((rest.contains(INCREMENTAUTOCOUNTERLINKEDID)))
451 rest = rest.replace(INCREMENTAUTOCOUNTERLINKEDID,
452 String.valueOf(++linkedIdAutoCounter));
453 if ((rest.contains("{}")))
454 rest = rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
456 return new StringBuilder(subvals).append(rest).toString();
460 * A helper method to take a list of String args where we're expecting
461 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
462 * and the index of the globbed arg, here 1. It returns a
463 * List<String> {"file1", "file2", "file3"}
464 * *and remove these from the original list object* so that processing
465 * can continue from where it has left off, e.g. args has become
466 * {"--previousargs", "--arg", "--otheroptionsornot"}
467 * so the next increment carries on from the next --arg if available.
469 protected static List<String> getShellGlobbedFilenameValues(Arg a,
470 List<String> args, int i)
472 List<String> vals = new ArrayList<>();
473 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
475 vals.add(FileUtils.substituteHomeDir(args.remove(i)));
476 if (!a.hasOption(Opt.GLOB))
482 public boolean isSet(Arg a)
484 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
487 public boolean isSet(String linkedId, Arg a)
489 ArgValuesMap avm = linkedArgs.get(linkedId);
490 return avm == null ? false : avm.containsArg(a);
493 public boolean getBool(Arg a)
495 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
497 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
500 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
503 public boolean getBool(String linkedId, Arg a)
505 ArgValuesMap avm = linkedArgs.get(linkedId);
507 return a.getDefaultBoolValue();
508 ArgValues avs = avm.getArgValues(a);
509 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
512 public List<String> linkedIds()
517 public ArgValuesMap linkedArgs(String id)
519 return linkedArgs.get(id);
523 public String toString()
525 StringBuilder sb = new StringBuilder();
526 sb.append("UNLINKED\n");
527 sb.append(argValuesMapToString(linkedArgs.get(null)));
528 if (linkedIds() != null)
530 sb.append("LINKED\n");
531 for (String id : linkedIds())
533 // already listed these as UNLINKED args
537 ArgValuesMap avm = linkedArgs(id);
538 sb.append("ID: '").append(id).append("'\n");
539 sb.append(argValuesMapToString(avm));
542 return sb.toString();
545 private static String argValuesMapToString(ArgValuesMap avm)
549 StringBuilder sb = new StringBuilder();
550 for (Arg a : avm.getArgKeys())
552 ArgValues v = avm.getArgValues(a);
553 sb.append(v.toString());
556 return sb.toString();
559 public static SubVals getSubVals(String item)
561 return new SubVals(item);
564 public static ArgParser parseArgFiles(List<String> argFilenameGlobs)
566 List<File> argFiles = new ArrayList<>();
568 for (String pattern : argFilenameGlobs)
570 // I don't think we want to dedup files, making life easier
571 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
574 return parseArgFileList(argFiles);
577 public static ArgParser parseArgFileList(List<File> argFiles)
579 List<String> argsList = new ArrayList<>();
580 for (File argFile : argFiles)
582 if (!argFile.exists())
584 String message = DOUBLEDASH
585 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
586 + argFile.getPath() + "\": File does not exist.";
587 Jalview.exit(message, 2);
591 argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
592 } catch (IOException e)
594 String message = DOUBLEDASH
595 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
596 + argFile.getPath() + "\": File could not be read.";
597 Jalview.exit(message, 3);
600 return new ArgParser(argsList);