/*
* 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.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 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
private int opennewLinkedIdCounter = 0;
// the linked id prefix used for --opennew files
protected static final String OPENNEWLINKEDIDPREFIX = "OPENNEW:";
// 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 --open
// or --opennew
private static final String LINKEDIDBASENAME = "{basename}";
// the linked id substitution string used to use the dir path of --open
// or --opennew
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 --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)), 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
boolean d = false;
boolean dd = false;
for (String arg : args)
{
if (arg.startsWith(DOUBLEDASH))
{
dd = true;
break;
}
else if (arg.startsWith("-"))
{
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.OPENNEW.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.OPENNEW)
{
// use the next default prefixed OPENNEWLINKEDID
linkedId = new StringBuilder(OPENNEWLINKEDIDPREFIX)
.append(Integer.toString(opennewLinkedIdCounter))
.toString();
opennewLinkedIdCounter++;
}
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);
}
}
}
private String makeSubstitutions(String val, String linkedId)
{
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;
}
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;
}
}