/* * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$) * Copyright (C) $$Year-Rel$$ The Jalview Authors * * This file is part of Jalview. * * Jalview is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * Jalview is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty * of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jalview. If not, see . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.bin.argparser; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import jalview.bin.Console; import jalview.bin.Jalview; import jalview.bin.argparser.Arg.Opt; import jalview.util.FileUtils; public class ArgParser { protected static final String DOUBLEDASH = "--"; protected static final String NEGATESTRING = "no"; // the default linked id prefix used for no id (not even square braces) protected static final String DEFAULTLINKEDIDPREFIX = "JALVIEW:"; // the counter added to the default linked id prefix private int defaultLinkedIdCounter = 0; // the linked id prefix used for --opennew files protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:"; // the counter added to the default linked id prefix private int opennewLinkedIdCounter = 0; // the linked id used to increment the idCounter (and use the incremented // value) private static final String INCREMENTAUTOCOUNTERLINKEDID = "{++n}"; // the linked id used to use the idCounter private static final String AUTOCOUNTERLINKEDID = "{n}"; private int idCounter = 0; // flag to say whether {n} subtitutions in output filenames should be made. // Turn on and off with --subs and --nosubs private boolean substitutions = false; protected static final Map argMap; protected Map linkedArgs = new HashMap<>(); protected List linkedOrder = null; protected List argList; static { argMap = new HashMap<>(); for (Arg a : EnumSet.allOf(Arg.class)) { for (String argName : a.getNames()) { if (argMap.containsKey(argName)) { Console.warn("Trying to add argument name multiple times: '" + argName + "'"); // RESTORE THIS WHEN MERGED if (argMap.get(argName) != a) { Console.error( "Trying to add argument name multiple times for different Args: '" + argMap.get(argName).getName() + ":" + argName + "' and '" + a.getName() + ":" + argName + "'"); } continue; } argMap.put(argName, a); } } } public ArgParser(String[] args) { // Make a mutable new ArrayList so that shell globbing parser works. // (When shell file globbing is used, there are a sequence of non-Arg // arguments (which are the expanded globbed filenames) that need to be // consumed by the --open/--argfile/etc Arg which is most easily done by // removing these filenames from the list one at a time. This can't be done // with an ArrayList made with only Arrays.asList(String[] args). ) this(new ArrayList<>(Arrays.asList(args))); } public ArgParser(List args) { parse(args); } private void parse(List args) { int argIndex = 0; boolean openEachInitialFilenames = true; for (int i = 0; i < args.size(); i++) { String arg = args.get(i); Console.debug("##### Looking at arg '" + arg + "'"); // If the first arguments do not start with "--" or "-" or is "open" and // is a filename that exists it is probably a file/list of files to open // so we fake an Arg.OPEN argument and when adding files only add the // single arg[i] and increment the defaultLinkedIdCounter so that each of // these files is opened separately. if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH) && !arg.startsWith("-") && new File(arg).exists()) { arg = DOUBLEDASH + Arg.OPENNEW.getName(); } else { openEachInitialFilenames = false; } String argName = null; String val = null; List vals = null; // for Opt.GLOB only String linkedId = null; if (arg.startsWith(DOUBLEDASH)) { int equalPos = arg.indexOf('='); if (equalPos > -1) { argName = arg.substring(DOUBLEDASH.length(), equalPos); val = arg.substring(equalPos + 1); } else { argName = arg.substring(DOUBLEDASH.length()); } int idOpen = argName.indexOf('['); int idClose = argName.indexOf(']'); if (idOpen > -1 && idClose == argName.length() - 1) { linkedId = argName.substring(idOpen + 1, idClose); argName = argName.substring(0, idOpen); } Arg a = argMap.get(argName); // check for boolean prepended by "no" boolean negated = false; if (a == null && argName.startsWith(NEGATESTRING) && argMap .containsKey(argName.substring(NEGATESTRING.length()))) { argName = argName.substring(NEGATESTRING.length()); a = argMap.get(argName); negated = true; } // check for config errors if (a == null) { // arg not found Console.error("Argument '" + arg + "' not recognised. Ignoring."); continue; } if (!a.hasOption(Opt.BOOLEAN) && negated) { // used "no" with a non-boolean option Console.error("Argument '--" + NEGATESTRING + argName + "' not a boolean option. Ignoring."); continue; } if (!a.hasOption(Opt.STRING) && equalPos > -1) { // set --argname=value when arg does not accept values Console.error("Argument '--" + argName + "' does not expect a value (given as '" + arg + "'). Ignoring."); continue; } if (!a.hasOption(Opt.LINKED) && linkedId != null) { // set --argname[linkedId] when arg does not use linkedIds Console.error("Argument '--" + argName + "' does not expect a linked id (given as '" + arg + "'). Ignoring."); continue; } if (a.hasOption(Opt.STRING) && equalPos == -1) { // take next arg as value if required, and '=' was not found // if (!argE.hasMoreElements()) if (i + 1 >= args.size()) { // no value to take for arg, which wants a value Console.error("Argument '" + a.getName() + "' requires a value, none given. Ignoring."); continue; } // deal with bash globs here (--arg val* is expanded before reaching // the JVM). Note that SubVals cannot be used in this case. // If using the --arg=val then the glob is preserved and Java globs // will be used later. SubVals can be used. if (a.hasOption(Opt.GLOB)) { // if this is the first argument with a file list at the start of // the args we add filenames from index i instead of i+1 vals = getShellGlobbedFilenameValues(a, args, openEachInitialFilenames ? i : i + 1); } else { val = args.get(i + 1); } } // make NOACTION adjustments // default and auto counter increments if (a == Arg.INCREMENT) { defaultLinkedIdCounter++; } else if (a == Arg.NPP) { idCounter++; } else if (a == Arg.SUBSTITUTIONS) { substitutions = !negated; } String autoCounterString = null; boolean usingAutoCounterLinkedId = false; String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX) .append(Integer.toString(defaultLinkedIdCounter)) .toString(); boolean usingDefaultLinkedId = false; if (a == Arg.OPENNEW) { linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX) .append(Integer.toString(opennewLinkedIdCounter)) .toString(); opennewLinkedIdCounter++; } else if (a.hasOption(Opt.LINKED)) { if (linkedId == null) { // use default linkedId for linked arguments linkedId = defaultLinkedId; usingDefaultLinkedId = true; Console.debug( "Changing linkedId to '" + linkedId + "' from " + arg); } else if (linkedId.equals(AUTOCOUNTERLINKEDID)) { // turn {n} to the autoCounter autoCounterString = Integer.toString(idCounter); linkedId = autoCounterString; usingAutoCounterLinkedId = true; Console.debug( "Changing linkedId to '" + linkedId + "' from " + arg); } else if (linkedId.equals(INCREMENTAUTOCOUNTERLINKEDID)) { // turn {++n} to the incremented autoCounter autoCounterString = Integer.toString(++idCounter); linkedId = autoCounterString; usingAutoCounterLinkedId = true; Console.debug( "Changing linkedId to '" + linkedId + "' from " + arg); } } if (!linkedArgs.containsKey(linkedId)) linkedArgs.put(linkedId, new ArgValuesMap()); // do not continue for NOACTION args if (a.hasOption(Opt.NOACTION)) continue; ArgValuesMap avm = linkedArgs.get(linkedId); // not dealing with both NODUPLICATEVALUES and GLOB if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val)) { Console.error("Argument '--" + argName + "' cannot contain a duplicate value ('" + val + "'). Ignoring this and subsequent occurrences."); continue; } // check for unique id SubVals sv = ArgParser.getSubVals(val); String id = sv.get(ArgValues.ID); if (id != null && avm.hasId(a, id)) { Console.error("Argument '--" + argName + "' has a duplicate id ('" + id + "'). Ignoring."); continue; } boolean argIndexIncremented = false; ArgValues avs = avm.getOrCreateArgValues(a); // store appropriate value if (a.hasOption(Opt.STRING)) { if (a.hasOption(Opt.GLOB) && vals != null && vals.size() > 0) { for (String v : vals) { avs.addValue(makeSubstitutions(v), argIndex++); argIndexIncremented = true; } } else { avs.addValue(makeSubstitutions(val), argIndex); } } else if (a.hasOption(Opt.BOOLEAN)) { avs.setBoolean(!negated, argIndex); avs.setNegated(negated); } else if (a.hasOption(Opt.UNARY)) { avs.setBoolean(true, argIndex); } avs.incrementCount(); if (!argIndexIncremented) argIndex++; // store in appropriate place if (a.hasOption(Opt.LINKED)) { // store the order of linkedIds if (linkedOrder == null) linkedOrder = new ArrayList<>(); if (!linkedOrder.contains(linkedId)) linkedOrder.add(linkedId); } // store arg in the list of args used if (argList == null) argList = new ArrayList<>(); if (!argList.contains(a)) argList.add(a); } } } private String makeSubstitutions(String val) { if (!this.substitutions) return val; String subvals; String rest; if (val.indexOf('[') == 0 && val.indexOf(']') > 1) { int closeBracket = val.indexOf(']'); if (val.length() == closeBracket) return val; subvals = val.substring(0, closeBracket + 1); rest = val.substring(closeBracket + 1); } else { subvals = ""; rest = val; } rest.replace(AUTOCOUNTERLINKEDID, String.valueOf(idCounter)); rest.replace(INCREMENTAUTOCOUNTERLINKEDID, String.valueOf(++idCounter)); rest.replace("{}", String.valueOf(defaultLinkedIdCounter)); return new StringBuilder(subvals).append(rest).toString(); } /* * A helper method to take a list of String args where we're expecting * {"--previousargs", "--arg", "file1", "file2", "file3", "--otheroptionsornot"} * and the index of the globbed arg, here 1. It returns a * List {"file1", "file2", "file3"} * *and remove these from the original list object* so that processing * can continue from where it has left off, e.g. args has become * {"--previousargs", "--arg", "--otheroptionsornot"} * so the next increment carries on from the next --arg if available. */ protected static List getShellGlobbedFilenameValues(Arg a, List args, int i) { List vals = new ArrayList<>(); while (i < args.size() && !args.get(i).startsWith(DOUBLEDASH)) { vals.add(args.remove(i)); if (!a.hasOption(Opt.GLOB)) break; } return vals; } public boolean isSet(Arg a) { return a.hasOption(Opt.LINKED) ? isSet("", a) : isSet(null, a); } public boolean isSet(String linkedId, Arg a) { ArgValuesMap avm = linkedArgs.get(linkedId); return avm == null ? false : avm.containsArg(a); } public boolean getBool(Arg a) { if (!a.hasOption(Opt.BOOLEAN) && !a.hasOption(Opt.UNARY)) { Console.warn("Getting boolean from non boolean Arg '" + a.getName() + "'."); } return a.hasOption(Opt.LINKED) ? getBool("", a) : getBool(null, a); } public boolean getBool(String linkedId, Arg a) { ArgValuesMap avm = linkedArgs.get(linkedId); if (avm == null) return a.getDefaultBoolValue(); ArgValues avs = avm.getArgValues(a); return avs == null ? a.getDefaultBoolValue() : avs.getBoolean(); } public List linkedIds() { return linkedOrder; } public ArgValuesMap linkedArgs(String id) { return linkedArgs.get(id); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("UNLINKED\n"); sb.append(argValuesMapToString(linkedArgs.get(null))); if (linkedIds() != null) { sb.append("LINKED\n"); for (String id : linkedIds()) { // already listed these as UNLINKED args if (id == null) continue; ArgValuesMap avm = linkedArgs(id); sb.append("ID: '").append(id).append("'\n"); sb.append(argValuesMapToString(avm)); } } return sb.toString(); } private static String argValuesMapToString(ArgValuesMap avm) { if (avm == null) return null; StringBuilder sb = new StringBuilder(); for (Arg a : avm.getArgKeys()) { ArgValues v = avm.getArgValues(a); sb.append(v.toString()); sb.append("\n"); } return sb.toString(); } public static SubVals getSubVals(String item) { return new SubVals(item); } public static ArgParser parseArgFiles(List argFilenameGlobs) { List argFiles = new ArrayList<>(); for (String pattern : argFilenameGlobs) { // I don't think we want to dedup files, making life easier argFiles.addAll(FileUtils.getFilesFromGlob(pattern)); } return parseArgFileList(argFiles); } public static ArgParser parseArgFileList(List argFiles) { List argsList = new ArrayList<>(); for (File argFile : argFiles) { if (!argFile.exists()) { String message = DOUBLEDASH + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\"" + argFile.getPath() + "\": File does not exist."; Jalview.exit(message, 2); } try { argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath()))); } catch (IOException e) { String message = DOUBLEDASH + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\"" + argFile.getPath() + "\": File could not be read."; Jalview.exit(message, 3); } } return new ArgParser(argsList); } }