JAL-629 Added Arg.Types and additional --help-type args to be --help with a type...
[jalview.git] / src / jalview / bin / argparser / Arg.java
index a9cacbd..958569e 100644 (file)
@@ -10,15 +10,16 @@ import java.util.List;
 import java.util.Locale;
 import java.util.stream.Collectors;
 
-import jalview.bin.Cache;
+import jalview.bin.argparser.Arg.Opt;
 import jalview.util.ChannelProperties;
+import jalview.util.Platform;
 
 public enum Arg
 {
 
   // Initialising arguments (BOOTSTRAP)
-  HELP(Type.HELP, "h", "Display this help statement", Opt.UNARY,
-          Opt.BOOTSTRAP, Opt.HASTYPE, Opt.MULTI),
+  HELP(Type.HELP, "h", "Display basic help", Opt.UNARY, Opt.BOOTSTRAP,
+          Opt.HASTYPE, Opt.MULTI),
   /*
    * Other --help-type Args will be added by the static block.
    */
@@ -248,11 +249,11 @@ public enum Arg
   JVMMEMPC(Type.CONFIG,
           "Limit maximum heap size (memory) to PERCENT% of total physical memory detected. This defaults to 90 if total physical memory can be detected.\n"
                   + "The equals sign (\"=\") separator must be used with no spaces.",
-          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING),
+          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING, Opt.LAST),
   JVMMEMMAX(Type.CONFIG,
           "Limit maximum heap size (memory) to MAXMEMORY. MAXMEMORY can be specified in bytes, kilobytes(k), megabytes(m), gigabytes(g) or if you're lucky enough, terabytes(t). This defaults to 32g if total physical memory can be detected, or to 8g if total physical memory cannot be detected.\n"
                   + "The equals sign (\"=\") separator must be used with no spaces.",
-          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING),
+          Opt.NOACTION, Opt.BOOTSTRAP, Opt.STRING, Opt.LAST),
 
   ;
 
@@ -306,26 +307,33 @@ public enum Arg
     HELP, // This Arg is a --help type arg
     PRIMARY, // This Arg is the main Arg for its type
     HASTYPE, // This Arg can have an Arg.Type assigned to it (and no value)
+    FIRST, // Move this arg to the first in usage statement (within type)
+    LAST, // Move this arg to the end in usage statement (within type)
   }
 
   public static enum Type
   {
     // Type restricts argument to certain usage output
-    CONFIG("Arguments user to configure "
+    HELP, // --help
+    CONFIG("arguments used to configure "
             + ChannelProperties.getProperty("app_name") + " from startup"),
-    OPENING("Arguments used to open and format alignments"),
-    STRUCTURE("Arguments used to add and format 3D structure data"),
-    PROCESS("Arguments used to process an alignment once opened"),
-    OUTPUT("Arguments used to save data from a processed alignment"),
-    IMAGE("Arguments used to export an image of an alignment"),
-    FLOW("Arguments that control processing of the other arguments"),
-    HELP("Arguments to provide help text"), // --help
-    ALL("All arguments"), // mostly just a place-holder for --help-all
-    NONE("No specific arguments"), // mostly a place-holder for --help
-    INVALID("This type of argument doesn't exist");
+    OPENING("arguments used to open and format alignments"),
+    STRUCTURE("arguments used to add and format 3D structure data"),
+    PROCESS("arguments used to process an alignment once opened"),
+    OUTPUT("arguments used to save data from a processed alignment"),
+    IMAGE("arguments used to export an image of an alignment"),
+    FLOW("arguments that control processing of the other arguments"), //
+    ALL("all arguments"), // mostly just a place-holder for --help-all
+    NONE, // mostly a place-holder for --help
+    INVALID;
 
     private String description;
 
+    private Type()
+    {
+      description = null;
+    }
+
     private Type(String description)
     {
       this.description = description;
@@ -337,15 +345,6 @@ public enum Arg
     }
   }
 
-  static
-  {
-    for (Type t : EnumSet.allOf(Type.class))
-    {
-      String type = t.name();
-
-    }
-  }
-
   private final String[] argNames;
 
   private Opt[] argOptions;
@@ -501,130 +500,240 @@ public enum Arg
     return usage(null);
   }
 
-  public static final String usage(List<Arg> helpArgs)
+  public static final void appendUsageGeneral(StringBuilder sb,
+          int maxArgLength)
   {
-    StringBuilder sb = new StringBuilder();
-
-    sb.append(ChannelProperties.getProperty("app_name"));
-    String version = Cache.getDefault("VERSION", null);
-    if (version != null)
+    for (Type t : EnumSet.allOf(Type.class))
     {
-      sb.append(" version ");
-      sb.append(Cache.getDefault("VERSION", "unknown"));
+      if (t.description() != null)
+      {
+        StringBuilder argSb = new StringBuilder();
+        argSb.append(Arg.HELP.argString()).append(ArgParser.SINGLEDASH)
+                .append(t.name().toLowerCase(Locale.ROOT));
+        appendArgAndDescription(sb, argSb.toString(),
+                "Help for " + t.description(), null, maxArgLength);
+        sb.append(System.lineSeparator());
+      }
     }
-    sb.append(System.lineSeparator());
-    sb.append("Usage: jalview [files...] [args]");
+  }
+
+  public static final String usage(List<Type> types)
+  {
+    StringBuilder sb = new StringBuilder();
+
+    sb.append("usage: jalview [" + Arg.HEADLESS.argString() + "] ["
+            + Arg.OPEN.argString() + "/" + Arg.APPEND.argString()
+            + " file(s)] [args]");
     sb.append(System.lineSeparator());
     sb.append(System.lineSeparator());
 
-    // PROGRAM THIS!
-    Type type = Type.HELP;
-    Iterator<Arg> typeArgs = Arg.getAllOfType(type);
-
-    int maxArgLength = 0;
-    for (Arg a : EnumSet.allOf(Arg.class))
+    if (types == null || types.contains(null))
     {
-      if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET))
-        continue;
-
-      StringBuilder argSb = new StringBuilder();
-      argSb.append(a.hasOption(Opt.BOOLEAN) ? booleanArgString(a)
-              : a.argString());
-      if (a.hasOption(Opt.STRING))
-        argSb.append("=value");
-      if (argSb.length() > maxArgLength)
-        maxArgLength = argSb.length();
-    }
+      // always show --help
+      appendArgAndDescription(sb, null, "Display this basic help", Arg.HELP,
+              DESCRIPTIONINDENT);
+      sb.append(System.lineSeparator());
 
-    // might want to sort these
-    for (Arg a : EnumSet.allOf(Arg.class))
+      appendUsageGeneral(sb, DESCRIPTIONINDENT);
+    }
+    else
     {
-      if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET))
-        continue;
-      StringBuilder argSb = new StringBuilder();
-      argSb.append(a.hasOption(Opt.BOOLEAN) ? booleanArgString(a)
-              : a.argString());
-      if (a.hasOption(Opt.STRING))
-        argSb.append("=value");
-      Iterator<String> descLines = null;
-      if (a.getDescription() != null)
-      {
-        descLines = Arrays.stream(a.getDescription().split("\\n"))
-                .iterator();
-      }
-      sb.append(String.format("%-" + maxArgLength + "s", argSb.toString()));
-      boolean first = true;
-      if (descLines != null)
+      List<Arg> args = argsSortedForDisplay(types);
+
+      /*
+       * just use a set maxArgLength of DESCRIPTIONINDENT
+       
+      int maxArgLength = 0;
+      for (Arg a : args)
       {
-        while (descLines.hasNext())
-        {
-          if (first)
-            sb.append(" - ");
-          else
-            sb.append(" ".repeat(maxArgLength + 3));
-          sb.append(descLines.next());
-          sb.append(System.lineSeparator());
-          first = false;
-        }
+        if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET))
+          continue;
+      
+        String argS = argDisplayString(a);
+        if (argS.length() > maxArgLength)
+          maxArgLength = argS.length();
       }
+      */
+      int maxArgLength = DESCRIPTIONINDENT;
 
-      List<String> options = new ArrayList<>();
+      // always show --help
+      appendArgAndDescription(sb, null, null, Arg.HELP, maxArgLength);
+      sb.append(System.lineSeparator());
 
-      if (a.hasOption(Opt.BOOLEAN))
+      if ((args.contains(Arg.HELP) && types.contains(Type.ALL)))
       {
-        options.add("default " + (a.getDefaultBoolValue() ? a.argString()
-                : a.negateArgString()));
+        appendUsageGeneral(sb, maxArgLength);
       }
 
-      if (a.hasOption(Opt.MULTI))
+      Iterator<Arg> argsI = args.iterator();
+      while (argsI.hasNext())
       {
-        options.add("multiple");
-      }
+        Arg a = argsI.next();
 
-      if (a.hasOption(Opt.LINKED))
-      {
-        options.add("can be linked");
-      }
+        if (a.hasOption(Opt.PRIVATE) || a.hasOption(Opt.SECRET)
+                || a == Arg.HELP)
+        {
+          continue;
+        }
 
-      if (a.hasOption(Opt.GLOB))
-      {
-        options.add("allows file globs");
-      }
+        appendArgUsage(sb, a, maxArgLength);
 
-      if (a.hasOption(Opt.ALLOWSUBSTITUTIONS))
-      {
-        options.add("allows substitutions");
+        if (argsI.hasNext())
+          sb.append(System.lineSeparator());
       }
+    }
+    return sb.toString();
+  }
+
+  private static void appendArgUsage(StringBuilder sb, Arg a,
+          int maxArgLength)
+  {
+    boolean first = appendArgAndDescription(sb, null, null, a,
+            maxArgLength);
+    List<String> options = new ArrayList<>();
+
+    if (a.hasOption(Opt.BOOLEAN))
+    {
+      options.add("default " + (a.getDefaultBoolValue() ? a.argString()
+              : a.negateArgString()));
+    }
+
+    if (a.hasOption(Opt.MULTI))
+    {
+      options.add("multiple");
+    }
+
+    if (a.hasOption(Opt.LINKED))
+    {
+      options.add("can be linked");
+    }
+
+    if (a.hasOption(Opt.GLOB))
+    {
+      options.add("allows file globs");
+    }
+
+    if (a.hasOption(Opt.ALLOWSUBSTITUTIONS))
+    {
+      options.add("allows substitutions");
+    }
 
-      if (a.hasOption(Opt.ALLOWALL))
+    if (a.hasOption(Opt.ALLOWALL))
+    {
+      options.add("can be applied to all linked arguments");
+    }
+
+    if (a.hasOption(Opt.PRIVATE))
+    {
+      options.add("for internal use only");
+    }
+
+    if (a.hasOption(Opt.SECRET))
+    {
+      options.add("for development use only");
+    }
+
+    if (options.size() > 0)
+    {
+      if (first)
       {
-        options.add("can be applied to all linked arguments");
+        sb.append(ARGDESCRIPTIONSEPARATOR);
       }
-
-      if (a.hasOption(Opt.PRIVATE))
+      else
       {
-        options.add("for internal use only");
+        sb.append(String.format("%-"
+                + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
+                ""));
       }
+      sb.append("(");
+      sb.append(String.join("; ", options));
+      sb.append(')');
+      sb.append(System.lineSeparator());
+    }
+  }
+
+  public static String argDisplayString(Arg a)
+  {
+    StringBuilder argSb = new StringBuilder();
+    argSb.append(
+            a.hasOption(Opt.BOOLEAN) ? booleanArgString(a) : a.argString());
+    if (a.hasOption(Opt.STRING))
+      argSb.append("=value");
+    return argSb.toString();
+  }
+
+  public static boolean appendArgAndDescription(StringBuilder sb,
+          String aString, String description, Arg a, int maxArgLength)
+  {
+    return appendArgAndDescription(sb, aString, description, a,
+            maxArgLength, Platform.consoleWidth());
+  }
+
+  public static boolean appendArgAndDescription(StringBuilder sb,
+          String aString, String description, Arg a, int maxArgLength,
+          int maxLength)
+  {
+    if (aString == null && a != null)
+    {
+      aString = argDisplayString(a);
+    }
+    if (description == null && a != null)
+    {
+      description = a.getDescription();
+    }
+    sb.append(String.format("%-" + maxArgLength + "s", aString));
+    if (aString.length() > maxArgLength)
+    {
+      sb.append(System.lineSeparator());
+      sb.append(String.format("%-" + maxArgLength + "s", ""));
+    }
 
-      if (a.hasOption(Opt.SECRET))
+    int descLength = maxLength - maxArgLength
+            - ARGDESCRIPTIONSEPARATOR.length();
+    // reformat the descriptions lines to the right width
+    Iterator<String> descLines = null;
+    if (description != null)
+    {
+      descLines = Arrays.stream(description.split("\\n")).iterator();
+    }
+    List<String> splitDescLinesList = new ArrayList<>();
+    while (descLines != null && descLines.hasNext())
+    {
+      String line = descLines.next();
+      while (line.length() > descLength)
       {
-        options.add("for development use only");
+        int splitIndex = line.lastIndexOf(" ", descLength);
+        if (splitIndex > descLength)
+        {
+          break;
+        }
+        else
+        {
+          splitDescLinesList.add(line.substring(0, splitIndex));
+          line = line.substring(splitIndex + 1);
+        }
       }
+      splitDescLinesList.add(line);
+    }
 
-      if (options.size() > 0)
+    Iterator<String> splitDescLines = splitDescLinesList.iterator();
+    boolean first = true;
+    if (splitDescLines != null)
+    {
+      while (splitDescLines.hasNext())
       {
         if (first)
-          sb.append(" - ");
+          sb.append(ARGDESCRIPTIONSEPARATOR);
         else
-          sb.append(" ".repeat(maxArgLength + 3));
-        sb.append("(");
-        sb.append(String.join("; ", options));
-        sb.append(')');
+          sb.append(String.format("%-"
+                  + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
+                  ""));
+        sb.append(splitDescLines.next());
         sb.append(System.lineSeparator());
+        first = false;
       }
-      sb.append(System.lineSeparator());
     }
-    return sb.toString();
+    return first;
   }
 
   protected static Iterator<Arg> getAllOfType(Type type)
@@ -647,11 +756,11 @@ public enum Arg
     }).iterator();
   }
 
-  private static List<Arg> sortForDisplay(Type... types)
+  private static List<Arg> argsSortedForDisplay(List<Type> types)
   {
     List<Arg> argsToSort;
     // if no types provided, do all
-    if (types == null || types.length == 0)
+    if (types == null || types.size() == 0 || types.contains(Type.ALL))
     {
       argsToSort = Arrays
               .asList(EnumSet.allOf(Arg.class).toArray(new Arg[] {}));
@@ -661,42 +770,59 @@ public enum Arg
       argsToSort = new ArrayList<>();
       for (Type type : types)
       {
+        if (type == null)
+          continue;
         Arg.getAllOfType(type).forEachRemaining(a -> argsToSort.add(a));
       }
     }
 
-    Collections.sort(argsToSort, new ArgComparator());
+    Collections.sort(argsToSort, new ArgDisplayComparator());
     return argsToSort;
   }
 
-  protected int compareForDisplay(Arg other)
+  private static final String ARGDESCRIPTIONSEPARATOR = " - ";
+
+  private static final int DESCRIPTIONINDENT = 20;
+
+}
+
+class ArgDisplayComparator implements Comparator<Arg>
+{
+  private int compareArgOpts(Arg a, Arg b, Opt o)
   {
-    if (other == null)
-      return 1;
-    // first compare types (in order of appearance)
-    int i = this.getType().compareTo(other.getType());
-    if (i == 0)
-    {
-      // next prioritise primary arguments
-      i = this.hasOption(Opt.PRIMARY)
-              ? (other.hasOption(Opt.PRIMARY) ? 0 : 1)
-              : (other.hasOption(Opt.PRIMARY) ? -1 : 0);
-      if (i == 0)
-      {
-        // finally order of appearance in enum declarations
-        i = this.compareTo(other);
-      }
-    }
+    int i = a.hasOption(o) ? (b.hasOption(o) ? 0 : -1)
+            : (b.hasOption(o) ? 1 : 0);
     return i;
   }
 
-}
+  private int compareForDisplay(Arg a, Arg b)
+  {
+    if (b == null)
+      return -1;
+    // first compare types (in enum order)
+    int i = a.getType().compareTo(b.getType());
+    if (i != 0)
+      return i;
+    // do Opt.LAST next (oddly). Reversed args important!
+    i = compareArgOpts(b, a, Opt.LAST);
+    if (i != 0)
+      return i;
+    // priority order
+    Opt[] optOrder = { Opt.HELP, Opt.FIRST, Opt.PRIMARY, Opt.STRING,
+        Opt.BOOLEAN };
+    for (Opt o : optOrder)
+    {
+      i = compareArgOpts(a, b, o);
+      if (i != 0)
+        return i;
+    }
+    // finally order of appearance in enum declarations
+    return a.compareTo(b);
+  }
 
-class ArgComparator implements Comparator<Arg>
-{
   @Override
   public int compare(Arg a, Arg b)
   {
-    return a.compareForDisplay(b);
+    return compareForDisplay(a, b);
   }
 }
\ No newline at end of file