JAL-629 allow comments in argfiles '#'
[jalview.git] / src / jalview / bin / argparser / ArgParser.java
index 54f57fe..5f0cad6 100644 (file)
@@ -29,7 +29,6 @@ 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;
@@ -41,6 +40,8 @@ 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)
@@ -49,21 +50,44 @@ public class ArgParser
   // 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 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 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}";
+  // 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;
@@ -76,6 +100,8 @@ public class ArgParser
 
   protected List<Arg> argList;
 
+  private static final char ARGFILECOMMENT = '#';
+
   static
   {
     argMap = new HashMap<>();
@@ -86,7 +112,8 @@ public class ArgParser
         if (argMap.containsKey(argName))
         {
           Console.warn("Trying to add argument name multiple times: '"
-                  + argName + "'"); // RESTORE THIS WHEN MERGED
+                  + argName + "'"); // RESTORE THIS WHEN
+          // MERGED
           if (argMap.get(argName) != a)
           {
             Console.error(
@@ -104,16 +131,27 @@ public class ArgParser
 
   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)));
+    this(new ArrayList<>(Arrays.asList(args)), initsubstitutions);
   }
 
-  public ArgParser(List<String> args)
+  public ArgParser(List<String> args, boolean initsubstitutions)
+  {
+    this(args, initsubstitutions, false);
+  }
+
+  public ArgParser(List<String> args, boolean initsubstitutions,
+          boolean allowPrivate)
   {
     // do nothing if there are no "--" args and some "-" args
     boolean d = false;
@@ -133,14 +171,16 @@ public class ArgParser
     if (d && !dd)
     {
       // leave it to the old style -- parse an empty list
-      parse(new ArrayList<String>());
+      parse(new ArrayList<String>(), false, false);
       return;
     }
-    parse(args);
+    parse(args, initsubstitutions, allowPrivate);
   }
 
-  private void parse(List<String> args)
+  private void parse(List<String> args, boolean initsubstitutions,
+          boolean allowPrivate)
   {
+    this.substitutions = initsubstitutions;
     int argIndex = 0;
     boolean openEachInitialFilenames = true;
     for (int i = 0; i < args.size(); i++)
@@ -155,7 +195,7 @@ public class ArgParser
       if (openEachInitialFilenames && !arg.startsWith(DOUBLEDASH)
               && !arg.startsWith("-") && new File(arg).exists())
       {
-        arg = DOUBLEDASH + Arg.OPENNEW.getName();
+        arg = Arg.OPENNEW.argString();
       }
       else
       {
@@ -169,7 +209,7 @@ public class ArgParser
       String linkedId = null;
       if (arg.startsWith(DOUBLEDASH))
       {
-        int equalPos = arg.indexOf('=');
+        int equalPos = arg.indexOf(EQUALS);
         if (equalPos > -1)
         {
           argName = arg.substring(DOUBLEDASH.length(), equalPos);
@@ -206,17 +246,23 @@ public class ArgParser
           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 '--" + NEGATESTRING + argName
+          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 '--" + argName
+          Console.error("Argument '" + a.argString()
                   + "' does not expect a value (given as '" + arg
                   + "').  Ignoring.");
           continue;
@@ -224,7 +270,7 @@ public class ArgParser
         if (!a.hasOption(Opt.LINKED) && linkedId != null)
         {
           // set --argname[linkedId] when arg does not use linkedIds
-          Console.error("Argument '--" + argName
+          Console.error("Argument '" + a.argString()
                   + "' does not expect a linked id (given as '" + arg
                   + "'). Ignoring.");
           continue;
@@ -238,9 +284,11 @@ public class ArgParser
             if (a.hasOption(Opt.GLOB))
             {
               // strip off and save the SubVals to be added individually later
-              globSubVals = ArgParser.getSubVals(val);
-              globVals = FileUtils
-                      .getFilenamesFromGlob(globSubVals.getContent());
+              globSubVals = new SubVals(val);
+              // make substitutions before looking for files
+              String fileGlob = makeSubstitutions(globSubVals.getContent(),
+                      linkedId);
+              globVals = FileUtils.getFilenamesFromGlob(fileGlob);
             }
             else
             {
@@ -252,7 +300,7 @@ public class ArgParser
           {
             // There is no "=" so value is next arg or args (possibly shell
             // glob-expanded)
-            if (i + 1 >= args.size())
+            if ((openEachInitialFilenames ? i : i + 1) >= args.size())
             {
               // no value to take for arg, which wants a value
               Console.error("Argument '" + a.getName()
@@ -291,6 +339,14 @@ public class ArgParser
         {
           substitutions = !negated;
         }
+        else if (a == Arg.SETARGFILE)
+        {
+          argFile = val;
+        }
+        else if (a == Arg.UNSETARGFILE)
+        {
+          argFile = null;
+        }
 
         String autoCounterString = null;
         boolean usingAutoCounterLinkedId = false;
@@ -319,21 +375,21 @@ public class ArgParser
                       + arg);
             }
           }
-          else if (linkedId.contains(AUTOCOUNTERLINKEDID))
+          else if (linkedId.contains(LINKEDIDAUTOCOUNTER))
           {
             // turn {n} to the autoCounter
             autoCounterString = Integer.toString(linkedIdAutoCounter);
-            linkedId = linkedId.replace(AUTOCOUNTERLINKEDID,
+            linkedId = linkedId.replace(LINKEDIDAUTOCOUNTER,
                     autoCounterString);
             usingAutoCounterLinkedId = true;
             Console.debug(
                     "Changing linkedId to '" + linkedId + "' from " + arg);
           }
-          else if (linkedId.contains(INCREMENTAUTOCOUNTERLINKEDID))
+          else if (linkedId.contains(INCREMENTLINKEDIDAUTOCOUNTER))
           {
             // turn {++n} to the incremented autoCounter
             autoCounterString = Integer.toString(++linkedIdAutoCounter);
-            linkedId = linkedId.replace(INCREMENTAUTOCOUNTERLINKEDID,
+            linkedId = linkedId.replace(INCREMENTLINKEDIDAUTOCOUNTER,
                     autoCounterString);
             usingAutoCounterLinkedId = true;
             Console.debug(
@@ -353,19 +409,19 @@ public class ArgParser
         // not dealing with both NODUPLICATEVALUES and GLOB
         if (a.hasOption(Opt.NODUPLICATEVALUES) && avm.hasValue(a, val))
         {
-          Console.error("Argument '--" + argName
+          Console.error("Argument '" + a.argString()
                   + "' cannot contain a duplicate value ('" + val
                   + "'). Ignoring this and subsequent occurrences.");
           continue;
         }
 
         // check for unique id
-        SubVals idsv = ArgParser.getSubVals(val);
+        SubVals idsv = new SubVals(val);
         String id = idsv.get(ArgValues.ID);
         if (id != null && avm.hasId(a, id))
         {
-          Console.error("Argument '--" + argName + "' has a duplicate id ('"
-                  + id + "'). Ignoring.");
+          Console.error("Argument '" + a.argString()
+                  + "' has a duplicate id ('" + id + "'). Ignoring.");
           continue;
         }
 
@@ -380,7 +436,7 @@ public class ArgParser
           {
             for (String v : globVals)
             {
-              v = makeSubstitutions(v);
+              v = makeSubstitutions(v, linkedId);
               SubVals vsv = new SubVals(globSubVals, v);
               avs.addValue(vsv, v, argIndex++);
               argIndexIncremented = true;
@@ -388,7 +444,7 @@ public class ArgParser
           }
           else
           {
-            avs.addValue(makeSubstitutions(val), argIndex);
+            avs.addValue(makeSubstitutions(val, linkedId), argIndex);
           }
         }
         else if (a.hasOption(Opt.BOOLEAN))
@@ -424,7 +480,7 @@ public class ArgParser
     }
   }
 
-  private String makeSubstitutions(String val)
+  private String makeSubstitutions(String val, String linkedId)
   {
     if (!this.substitutions)
       return val;
@@ -444,14 +500,40 @@ public class ArgParser
       subvals = "";
       rest = val;
     }
-    if ((rest.contains(AUTOCOUNTERLINKEDID)))
-      rest = rest.replace(AUTOCOUNTERLINKEDID,
+    if (rest.contains(LINKEDIDAUTOCOUNTER))
+      rest = rest.replace(LINKEDIDAUTOCOUNTER,
               String.valueOf(linkedIdAutoCounter));
-    if ((rest.contains(INCREMENTAUTOCOUNTERLINKEDID)))
-      rest = rest.replace(INCREMENTAUTOCOUNTERLINKEDID,
+    if (rest.contains(INCREMENTLINKEDIDAUTOCOUNTER))
+      rest = rest.replace(INCREMENTLINKEDIDAUTOCOUNTER,
               String.valueOf(++linkedIdAutoCounter));
-    if ((rest.contains("{}")))
-      rest = rest.replace("{}", String.valueOf(defaultLinkedIdCounter));
+    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();
   }
@@ -459,12 +541,11 @@ public class ArgParser
   /*
    * 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<String> {"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.
+   * and the index of the globbed arg, here 1. It returns a List<String> {"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<String> getShellGlobbedFilenameValues(Arg a,
           List<String> args, int i)
@@ -556,12 +637,8 @@ public class ArgParser
     return sb.toString();
   }
 
-  public static SubVals getSubVals(String item)
-  {
-    return new SubVals(item);
-  }
-
-  public static ArgParser parseArgFiles(List<String> argFilenameGlobs)
+  public static ArgParser parseArgFiles(List<String> argFilenameGlobs,
+          boolean initsubstitutions)
   {
     List<File> argFiles = new ArrayList<>();
 
@@ -571,33 +648,63 @@ public class ArgParser
       argFiles.addAll(FileUtils.getFilesFromGlob(pattern));
     }
 
-    return parseArgFileList(argFiles);
+    return parseArgFileList(argFiles, initsubstitutions);
   }
 
-  public static ArgParser parseArgFileList(List<File> argFiles)
+  public static ArgParser parseArgFileList(List<File> argFiles,
+          boolean initsubstitutions)
   {
     List<String> argsList = new ArrayList<>();
     for (File argFile : argFiles)
     {
       if (!argFile.exists())
       {
-        String message = DOUBLEDASH
-                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
+        String message = Arg.ARGFILE.argString() + EQUALS + "\""
                 + argFile.getPath() + "\": File does not exist.";
         Jalview.exit(message, 2);
       }
       try
       {
-        argsList.addAll(Files.readAllLines(Paths.get(argFile.getPath())));
+        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<String> readArgFile(File argFile)
+  {
+    List<String> 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 = DOUBLEDASH
-                + Arg.ARGFILE.name().toLowerCase(Locale.ROOT) + "=\""
-                + argFile.getPath() + "\": File could not be read.";
+        String message = Arg.ARGFILE.argString() + "=\"" + argFile.getPath()
+                + "\": File could not be read.";
+        Console.debug(message, e);
         Jalview.exit(message, 3);
       }
     }
-    return new ArgParser(argsList);
+    return args;
   }
 
 }
\ No newline at end of file