/* * 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.Cache; 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 char EQUALS = '='; 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 substitution string used to use the defaultLinkedIdCounter private static final String DEFAULTLINKEDIDCOUNTER = "{}"; // the counter added to the default linked id prefix. NOW using // linkedIdAutoCounter // private int openLinkedIdCounter = 0; // the linked id prefix used for --open files. NOW the same as DEFAULT protected static final String OPENLINKEDIDPREFIX = DEFAULTLINKEDIDPREFIX; // the counter used for {n} substitutions private int linkedIdAutoCounter = 0; // the linked id substitution string used to increment the idCounter (and use // the incremented value) private static final String INCREMENTLINKEDIDAUTOCOUNTER = "{++n}"; // the linked id substitution string used to use the idCounter private static final String LINKEDIDAUTOCOUNTER = "{n}"; // the linked id substitution string used to use the base filename of --append // or --open private static final String LINKEDIDBASENAME = "{basename}"; // the linked id substitution string used to use the dir path of --append // or --open private static final String LINKEDIDDIRNAME = "{dirname}"; // the current argfile private String argFile = null; // the linked id substitution string used to use the dir path of the latest // --argfile name private static final String ARGFILEBASENAME = "{argfilebasename}"; // the linked id substitution string used to use the dir path of the latest // --argfile name private static final String ARGFILEDIRNAME = "{argfiledirname}"; // 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; private static final char ARGFILECOMMENT = '#'; 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) { this(args, false); } public ArgParser(String[] args, boolean initsubstitutions) { // 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 --append/--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)), initsubstitutions); } public ArgParser(List args, boolean initsubstitutions) { this(args, initsubstitutions, false); } public ArgParser(List args, boolean initsubstitutions, boolean allowPrivate) { // do nothing if there are no "--" args and (some "-" args || >0 arg is // "open") boolean d = false; boolean dd = false; for (String arg : args) { if (arg.startsWith(DOUBLEDASH)) { dd = true; break; } else if (arg.startsWith("-") || arg.equals("open")) { d = true; } } if (d && !dd) { // leave it to the old style -- parse an empty list parse(new ArrayList(), false, false); return; } parse(args, initsubstitutions, allowPrivate); } private void parse(List args, boolean initsubstitutions, boolean allowPrivate) { this.substitutions = initsubstitutions; int argIndex = 0; boolean openEachInitialFilenames = true; for (int i = 0; i < args.size(); i++) { String arg = args.get(i); // 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 = Arg.OPEN.argString(); } else { openEachInitialFilenames = false; } String argName = null; String val = null; List globVals = null; // for Opt.GLOB only SubVals globSubVals = null; // also for use by Opt.GLOB only String linkedId = null; if (arg.startsWith(DOUBLEDASH)) { int equalPos = arg.indexOf(EQUALS); 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.PRIVATE) && !allowPrivate) { Console.error( "Argument '" + a.argString() + "' is private. Ignoring."); continue; } if (!a.hasOption(Opt.BOOLEAN) && negated) { // used "no" with a non-boolean option Console.error("Argument '" + DOUBLEDASH + 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 '" + a.argString() + "' 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 '" + a.argString() + "' does not expect a linked id (given as '" + arg + "'). Ignoring."); continue; } // String value(s) if (a.hasOption(Opt.STRING)) { if (equalPos >= 0) { if (a.hasOption(Opt.GLOB)) { // strip off and save the SubVals to be added individually later globSubVals = new SubVals(val); // make substitutions before looking for files String fileGlob = makeSubstitutions(globSubVals.getContent(), linkedId); globVals = FileUtils.getFilenamesFromGlob(fileGlob); } else { // val is already set -- will be saved in the ArgValue later in // the normal way } } else { // There is no "=" so value is next arg or args (possibly shell // glob-expanded) if ((openEachInitialFilenames ? i : 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 globVals = 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) { linkedIdAutoCounter++; } else if (a == Arg.SUBSTITUTIONS) { substitutions = !negated; } else if (a == Arg.SETARGFILE) { argFile = val; } else if (a == Arg.UNSETARGFILE) { argFile = null; } String autoCounterString = null; boolean usingAutoCounterLinkedId = false; String defaultLinkedId = new StringBuilder(DEFAULTLINKEDIDPREFIX) .append(Integer.toString(defaultLinkedIdCounter)) .toString(); boolean usingDefaultLinkedId = false; if (a.hasOption(Opt.LINKED)) { if (linkedId == null) { if (a == Arg.OPEN) { // use the next default prefixed OPENLINKEDID // NOW using the linkedIdAutoCounter defaultLinkedIdCounter++; linkedId = new StringBuilder(OPENLINKEDIDPREFIX) .append(Integer.toString(defaultLinkedIdCounter)) .toString(); } else { // use default linkedId for linked arguments linkedId = defaultLinkedId; usingDefaultLinkedId = true; Console.debug("Changing linkedId to '" + linkedId + "' from " + arg); } } else if (linkedId.contains(LINKEDIDAUTOCOUNTER)) { // turn {n} to the autoCounter autoCounterString = Integer.toString(linkedIdAutoCounter); linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER, autoCounterString); usingAutoCounterLinkedId = true; Console.debug( "Changing linkedId to '" + linkedId + "' from " + arg); } else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER)) { // turn {++n} to the incremented autoCounter autoCounterString = Integer.toString(++linkedIdAutoCounter); linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER, 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 '" + a.argString() + "' cannot contain a duplicate value ('" + val + "'). Ignoring this and subsequent occurrences."); continue; } // check for unique id SubVals idsv = new SubVals(val); String id = idsv.get(ArgValues.ID); if (id != null && avm.hasId(a, id)) { Console.error("Argument '" + a.argString() + "' has a duplicate id ('" + id + "'). Ignoring."); continue; } boolean argIndexIncremented = false; ArgValues avs = avm.getOrCreateArgValues(a); // store appropriate String value(s) if (a.hasOption(Opt.STRING)) { if (a.hasOption(Opt.GLOB) && globVals != null && globVals.size() > 0) { for (String v : globVals) { v = makeSubstitutions(v, linkedId); SubVals vsv = new SubVals(globSubVals, v); avs.addValue(vsv, v, argIndex++); argIndexIncremented = true; } } else { avs.addValue(makeSubstitutions(val, linkedId), 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); } } } public String makeSubstitutions(String val, String linkedId) { if (!this.substitutions || val == null) 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; } if (rest.contains(LINKEDIDAUTOCOUNTER)) rest = rest.replace(LINKEDIDAUTOCOUNTER, String.valueOf(linkedIdAutoCounter)); if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER)) rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER, String.valueOf(++linkedIdAutoCounter)); if (rest.contains(DEFAULTLINKEDIDCOUNTER)) rest = rest.replace(DEFAULTLINKEDIDCOUNTER, String.valueOf(defaultLinkedIdCounter)); ArgValuesMap avm = linkedArgs.get(linkedId); if (avm != null) { if (rest.contains(LINKEDIDBASENAME)) { rest = rest.replace(LINKEDIDBASENAME, avm.getBasename()); } if (rest.contains(LINKEDIDDIRNAME)) { rest = rest.replace(LINKEDIDDIRNAME, avm.getDirname()); } } if (argFile != null) { if (rest.contains(ARGFILEBASENAME)) { rest = rest.replace(ARGFILEBASENAME, FileUtils.getBasename(new File(argFile))); } if (rest.contains(ARGFILEDIRNAME)) { rest = rest.replace(ARGFILEDIRNAME, FileUtils.getDirname(new File(argFile))); } } 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(FileUtils.substituteHomeDir(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 ArgParser parseArgFiles(List argFilenameGlobs, boolean initsubstitutions) { 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, initsubstitutions); } public static ArgParser parseArgFileList(List argFiles, boolean initsubstitutions) { List argsList = new ArrayList<>(); for (File argFile : argFiles) { if (!argFile.exists()) { String message = Arg.ARGFILE.argString() + EQUALS + "\"" + argFile.getPath() + "\": File does not exist."; Jalview.exit(message, 2); } try { String setargfile = new StringBuilder(Arg.SETARGFILE.argString()) .append(EQUALS).append(argFile.getCanonicalPath()) .toString(); argsList.add(setargfile); argsList.addAll(readArgFile(argFile)); argsList.add(Arg.UNSETARGFILE.argString()); } catch (IOException e) { String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath() + "\": File could not be read."; Jalview.exit(message, 3); } } // Third param "true" uses Opt.PRIVATE args --setargile=argfile and // --unsetargfile return new ArgParser(argsList, initsubstitutions, true); } protected static List readArgFile(File argFile) { List args = new ArrayList<>(); if (argFile != null && argFile.exists()) { try { for (String line : Files.readAllLines(Paths.get(argFile.getPath()))) { if (line != null && line.length() > 0 && line.charAt(0) != ARGFILECOMMENT) args.add(line); } } catch (IOException e) { String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath() + "\": File could not be read."; Console.debug(message, e); Jalview.exit(message, 3); } } return args; } public static enum Position { FIRST, BEFORE, AFTER } public static String getValueFromSubValOrArg(ArgValuesMap avm, Arg a, SubVals sv) { return getFromSubValArgOrPref(avm, a, sv, null, null, null); } public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a, SubVals sv, String key, String pref, String def) { return getFromSubValArgOrPref(avm, a, Position.FIRST, null, sv, key, pref, def); } public static String getFromSubValArgOrPref(ArgValuesMap avm, Arg a, Position pos, ArgValue av, SubVals sv, String key, String pref, String def) { if (key == null) key = a.getName(); if (sv != null && sv.has(key) && sv.get(key) != null) return sv.get(key); if (avm != null && avm.containsArg(a)) { String val = null; if (pos == Position.FIRST && avm.getValue(a) != null) return avm.getValue(a); else if (pos == Position.BEFORE && avm.getClosestPreviousArgValueOfArg(av, a) != null) return avm.getClosestPreviousArgValueOfArg(av, a).getValue(); else if (pos == Position.AFTER && avm.getClosestNextArgValueOfArg(av, a) != null) return avm.getClosestNextArgValueOfArg(av, a).getValue(); } return pref != null ? Cache.getDefault(pref, def) : def; } public static boolean getBoolFromSubValOrArg(ArgValuesMap avm, Arg a, SubVals sv) { return getFromSubValArgOrPref(avm, a, sv, null, null, false); } public static boolean getFromSubValArgOrPref(ArgValuesMap avm, Arg a, SubVals sv, String key, String pref, boolean def) { if (key == null) key = a.getName(); if (sv != null && sv.has(key) && sv.get(key) != null) return sv.get(key).toLowerCase(Locale.ROOT).equals("true"); if (avm != null && avm.containsArg(a)) return avm.getBoolean(a); return pref != null ? Cache.getDefault(pref, def) : def; } }