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 used to increment the idCounter (and use the incremented
54 private static final String INCREMENTAUTOCOUNTERLINKEDID = "{++n}";
56 // the linked id used to use the idCounter
57 private static final String AUTOCOUNTERLINKEDID = "{n}";
59 private int idCounter = 0;
61 // flag to say whether {n} subtitutions in output filenames should be made.
62 // Turn on and off with --subs and --nosubs
63 private boolean substitutions = false;
65 protected static final Map<String, Arg> argMap;
67 protected Map<String, ArgValuesMap> linkedArgs = new HashMap<>();
69 protected List<String> linkedOrder = null;
71 protected List<Arg> argList;
75 argMap = new HashMap<>();
76 for (Arg a : EnumSet.allOf(Arg.class))
78 for (String argName : a.getNames())
80 if (argMap.containsKey(argName))
82 Console.warn("Trying to add argument name multiple times: '"
83 + argName + "'"); // RESTORE THIS WHEN MERGED
84 if (argMap.get(argName) != a)
87 "Trying to add argument name multiple times for different Args: '"
88 + argMap.get(argName).getName() + ":" + argName
89 + "' and '" + a.getName() + ":" + argName
94 argMap.put(argName, a);
99 public ArgParser(String[] args)
101 // Make a mutable new ArrayList so that shell globbing parser works.
102 // (When shell file globbing is used, there are a sequence of non-Arg
103 // arguments (which are the expanded globbed filenames) that need to be
104 // consumed by the --open/--argfile/etc Arg which is most easily done by
105 // removing these filenames from the list one at a time. This can't be done
106 // with an ArrayList made with only Arrays.asList(String[] args). )
107 this(new ArrayList<>(Arrays.asList(args)));
110 public ArgParser(List<String> args)
115 private void parse(List<String> args)
118 boolean initialFilenameArgs = true;
119 for (int i = 0; i < args.size(); i++)
121 String arg = args.get(i);
123 // If the first arguments do not start with "--" or "-" or is "open" and
124 // is a filename that exists it is probably a file/list of files to open
125 // so we fake an Arg.OPEN argument and when adding files only add the
126 // single arg[i] and increment the defaultLinkedIdCounter so that each of
127 // these files is opened separately.
128 if (initialFilenameArgs && !arg.startsWith(DOUBLEDASH)
129 && !arg.startsWith("-") && new File(arg).exists())
131 arg = DOUBLEDASH + Arg.OPEN.getName();
132 Console.debug("Adding argument '" + args.get(i)
133 + "' as a file to be opened");
137 initialFilenameArgs = false;
140 String argName = null;
142 List<String> vals = null; // for Opt.GLOB only
143 String linkedId = null;
144 if (arg.startsWith(DOUBLEDASH))
146 int equalPos = arg.indexOf('=');
149 argName = arg.substring(DOUBLEDASH.length(), equalPos);
150 val = arg.substring(equalPos + 1);
154 argName = arg.substring(DOUBLEDASH.length());
156 int idOpen = argName.indexOf('[');
157 int idClose = argName.indexOf(']');
159 if (idOpen > -1 && idClose == argName.length() - 1)
161 linkedId = argName.substring(idOpen + 1, idClose);
162 argName = argName.substring(0, idOpen);
165 Arg a = argMap.get(argName);
166 // check for boolean prepended by "no"
167 boolean negated = false;
168 if (a == null && argName.startsWith(NEGATESTRING) && argMap
169 .containsKey(argName.substring(NEGATESTRING.length())))
171 argName = argName.substring(NEGATESTRING.length());
172 a = argMap.get(argName);
176 // check for config errors
180 Console.error("Argument '" + arg + "' not recognised. Ignoring.");
183 if (!a.hasOption(Opt.BOOLEAN) && negated)
185 // used "no" with a non-boolean option
186 Console.error("Argument '--" + NEGATESTRING + argName
187 + "' not a boolean option. Ignoring.");
190 if (!a.hasOption(Opt.STRING) && equalPos > -1)
192 // set --argname=value when arg does not accept values
193 Console.error("Argument '--" + argName
194 + "' does not expect a value (given as '" + arg
198 if (!a.hasOption(Opt.LINKED) && linkedId != null)
200 // set --argname[linkedId] when arg does not use linkedIds
201 Console.error("Argument '--" + argName
202 + "' does not expect a linked id (given as '" + arg
207 if (a.hasOption(Opt.STRING) && equalPos == -1)
209 // take next arg as value if required, and '=' was not found
210 // if (!argE.hasMoreElements())
211 if (i + 1 >= args.size())
213 // no value to take for arg, which wants a value
214 Console.error("Argument '" + a.getName()
215 + "' requires a value, none given. Ignoring.");
218 // deal with bash globs here (--arg val* is expanded before reaching
219 // the JVM). Note that SubVals cannot be used in this case.
220 // If using the --arg=val then the glob is preserved and Java globs
221 // will be used later. SubVals can be used.
222 if (a.hasOption(Opt.GLOB))
224 // if this is the first argument with a file list at the start of
225 // the args we add filenames from index i instead of i+1
226 // and assume they should be opened separately
227 if (initialFilenameArgs)
230 defaultLinkedIdCounter++;
234 vals = getShellGlobbedFilenameValues(a, args, i + 1);
239 val = args.get(i + 1);
243 // make NOACTION adjustments
244 // default and auto counter increments
245 if (a == Arg.INCREMENT)
247 defaultLinkedIdCounter++;
249 else if (a == Arg.NPP)
253 else if (a == Arg.SUBSTITUTIONS)
255 substitutions = !negated;
258 String autoCounterString = null;
259 boolean usingAutoCounterLinkedId = false;
260 String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX)
261 .append(Integer.toString(defaultLinkedIdCounter))
263 boolean usingDefaultLinkedId = false;
264 if (a.hasOption(Opt.LINKED))
266 if (linkedId == null)
268 // use default linkedId for linked arguments
269 linkedId = defaultLinkedId;
270 usingDefaultLinkedId = true;
272 "Changing linkedId to '" + linkedId + "' from " + arg);
274 else if (linkedId.equals(AUTOCOUNTERLINKEDID))
276 // turn {n} to the autoCounter
277 autoCounterString = Integer.toString(idCounter);
278 linkedId = autoCounterString;
279 usingAutoCounterLinkedId = true;
281 "Changing linkedId to '" + linkedId + "' from " + arg);
283 else if (linkedId.equals(INCREMENTAUTOCOUNTERLINKEDID))
285 // turn {++n} to the incremented autoCounter
286 autoCounterString = Integer.toString(++idCounter);
287 linkedId = autoCounterString;
288 usingAutoCounterLinkedId = true;
290 "Changing linkedId to '" + linkedId + "' from " + arg);
294 if (!linkedArgs.containsKey(linkedId))
295 linkedArgs.put(linkedId, new ArgValuesMap());
297 // do not continue for NOACTION args
298 if (a.hasOption(Opt.NOACTION))
301 ArgValuesMap avm = linkedArgs.get(linkedId);
303 // not dealing with both NODUPLICATEVALUES and GLOB
304 if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
306 Console.error("Argument '--" + argName
307 + "' cannot contain a duplicate value ('" + val
308 + "'). Ignoring this and subsequent occurrences.");
312 // check for unique id
313 SubVals sv = ArgParser.getSubVals(val);
314 String id = sv.get(ArgValues.ID);
315 if (id != null && avm.hasId(a, id))
317 Console.error("Argument '--" + argName + "' has a duplicate id ('"
318 + id + "'). Ignoring.");
322 ArgValues avs = avm.getOrCreateArgValues(a);
325 avs = new ArgValues(a);
328 boolean argIndexIncremented = false;
329 // store appropriate value
330 if (a.hasOption(Opt.STRING))
332 if (a.hasOption(Opt.GLOB) && vals != null && vals.size() > 0)
334 for (String v : vals)
336 avs.addValue(makeSubstitutions(v), argIndex++);
337 argIndexIncremented = true;
342 avs.addValue(makeSubstitutions(val), argIndex);
345 else if (a.hasOption(Opt.BOOLEAN))
347 avs.setBoolean(!negated, argIndex);
348 avs.setNegated(negated);
350 else if (a.hasOption(Opt.UNARY))
352 avs.setBoolean(true, argIndex);
354 avs.incrementCount();
355 if (!argIndexIncremented)
358 // store in appropriate place
359 if (a.hasOption(Opt.LINKED))
361 // allow a default linked id for single usage
362 if (linkedId == null)
363 linkedId = defaultLinkedId;
364 // store the order of linkedIds
365 if (linkedOrder == null)
366 linkedOrder = new ArrayList<>();
367 if (!linkedOrder.contains(linkedId))
368 linkedOrder.add(linkedId);
371 // store arg in the list of args used
373 argList = new ArrayList<>();
374 if (!argList.contains(a))
380 private String makeSubstitutions(String val)
382 if (!this.substitutions)
387 if (val.indexOf('[') == 0 && val.indexOf(']') > 1)
389 int closeBracket = val.indexOf(']');
390 if (val.length() == closeBracket)
392 subvals = val.substring(0, closeBracket + 1);
393 rest = val.substring(closeBracket + 1);
400 rest.replace(AUTOCOUNTERLINKEDID, String.valueOf(idCounter));
401 rest.replace(INCREMENTAUTOCOUNTERLINKEDID, String.valueOf(++idCounter));
402 rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
404 return new StringBuilder(subvals).append(rest).toString();
408 * A helper method to take a list of String args where we're expecting
409 * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"}
410 * and the index of the globbed arg, here 1. It returns a
411 * List<String> {"file1", "file2", "file3"}
412 * *and remove these from the original list object* so that processing
413 * can continue from where it has left off, e.g. args has become
414 * {"--previousargs", "--arg", "--otheroptionsornot"}
415 * so the next increment carries on from the next --arg if available.
417 protected static List<String> getShellGlobbedFilenameValues(Arg a,
418 List<String> args, int i)
420 List<String> vals = new ArrayList<>();
421 while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH))
423 vals.add(args.remove(i));
424 if (!a.hasOption(Opt.GLOB))
430 public boolean isSet(Arg a)
432 return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a);
435 public boolean isSet(String linkedId, Arg a)
437 ArgValuesMap avm = linkedArgs.get(linkedId);
438 return avm == null ? false : avm.containsArg(a);
441 public boolean getBool(Arg a)
443 if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY))
445 Console.warn("Getting boolean from non boolean Arg '" + a.getName()
448 return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a);
451 public boolean getBool(String linkedId, Arg a)
453 ArgValuesMap avm = linkedArgs.get(linkedId);
455 return a.getDefaultBoolValue();
456 ArgValues avs = avm.getArgValues(a);
457 return avs == null ? a.getDefaultBoolValue() : avs.getBoolean();
460 public List<String> linkedIds()
465 public ArgValuesMap linkedArgs(String id)
467 return linkedArgs.get(id);
471 public String toString()
473 StringBuilder sb = new StringBuilder();
474 sb.append("UNLINKED\n");
475 sb.append(argValuesMapToString(linkedArgs.get(null)));
476 if (linkedIds() != null)
478 sb.append("LINKED\n");
479 for (String id : linkedIds())
481 // already listed these as UNLINKED args
485 ArgValuesMap avm = linkedArgs(id);
486 sb.append("ID: '").append(id).append("'\n");
487 sb.append(argValuesMapToString(avm));
490 return sb.toString();
493 private static String argValuesMapToString(ArgValuesMap avm)
497 StringBuilder sb = new StringBuilder();
498 for (Arg a : avm.getArgKeys())
500 ArgValues v = avm.getArgValues(a);
501 sb.append(v.toString());
504 return sb.toString();
507 public static SubVals getSubVals(String item)
509 return new SubVals(item);
512 public static ArgParser parseArgFiles(List<String> argFilenameGlobs)
514 List<File> argFiles = new ArrayList<>();
516 for (String pattern : argFilenameGlobs)
518 // I don't think we want to dedup files, making life easier
519 argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
522 return parseArgFileList(argFiles);
525 public static ArgParser parseArgFileList(List<File> argFiles)
527 List<String> argsList = new ArrayList<>();
528 for (File argFile : argFiles)
530 if (!argFile.exists())
532 String message = DOUBLEDASH
533 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
534 + argFile.getPath() + "\": File does not exist.";
535 Jalview.exit(message, 2);
539 argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
540 } catch (IOException e)
542 String message = DOUBLEDASH
543 + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
544 + argFile.getPath() + "\": File could not be read.";
545 Jalview.exit(message, 3);
548 return new ArgParser(argsList);