Merge branch 'patch/JAL-4195_cli_eps_export_npe' into develop
[jalview.git] / src / jalview / bin / argparser / Arg.java
index 2a080fc..2f25978 100644 (file)
@@ -28,7 +28,11 @@ public enum Arg
                   + ChannelProperties.getProperty("app_name"),
           Opt.UNARY, Opt.BOOTSTRAP),
   HEADLESS(Type.CONFIG,
-          "Run Jalview in headless mode. No GUI interface will be created and Jalview will quit after all arguments have been processed.",
+          "Run Jalview in headless mode. No GUI interface will be created and Jalview will quit after all arguments have been processed. "
+                  + "Headless mode is assumed if an output file is to be generated, this can be overridden with --noheadless or --gui.",
+          Opt.BOOLEAN, Opt.BOOTSTRAP),
+  GUI(Type.CONFIG,
+          "Do not run Jalview in headless mode.  This overrides the assumption of headless mode when an output file is to be generated.",
           Opt.UNARY, Opt.BOOTSTRAP),
   JABAWS(Type.CONFIG, "Set a different URL to connect to a JABAWS server.",
           Opt.STRING, Opt.BOOTSTRAP),
@@ -40,9 +44,10 @@ public enum Arg
   QUESTIONNAIRE(Type.CONFIG,
           "Show (or don't show) the questionnaire if one is available.",
           true, Opt.BOOLEAN, Opt.BOOTSTRAP),
-  USAGESTATS(Type.CONFIG,
-          "Send (or don't send) initial launch usage stats.", true,
-          Opt.BOOLEAN, Opt.BOOTSTRAP),
+  NOUSAGESTATS(Type.CONFIG, "Don't send initial launch usage stats.",
+          Opt.UNARY, Opt.BOOTSTRAP),
+  NOSTARTUPFILE(Type.CONFIG, "Don't show the default startup file.",
+          Opt.UNARY, Opt.BOOTSTRAP),
   WEBSERVICEDISCOVERY(Type.CONFIG,
           "Attempt (or don't attempt) to connect to JABAWS web services.",
           true, Opt.BOOLEAN, Opt.BOOTSTRAP),
@@ -61,6 +66,9 @@ public enum Arg
   INITSUBSTITUTIONS(Type.CONFIG,
           "Set ‑‑substitutions to be initially enabled (or initially disabled).",
           true, Opt.BOOLEAN, Opt.BOOTSTRAP, Opt.NOACTION, Opt.SECRET),
+  P(Type.CONFIG, "Set a Jalview preference value for this session.",
+          Opt.PREFIXKEV, Opt.PRESERVECASE, Opt.STRING, Opt.BOOTSTRAP,
+          Opt.MULTI, Opt.NOACTION, Opt.SECRET), // keep this secret for now.
 
   // Opening an alignment
   OPEN(Type.OPENING,
@@ -76,7 +84,7 @@ public enum Arg
           "Specifies the title for the open alignment window as string.",
           Opt.STRING, Opt.LINKED),
   COLOUR(Type.OPENING, "color", // being a bit soft on the Americans!
-          "Applies the colour scheme to the open alignment window. Valid values are:\n"
+          "Applies the colour scheme to the open alignment window. Valid values include:\n"
                   + "clustal,\n" + "blosum62,\n" + "pc-identity,\n"
                   + "zappo,\n" + "taylor,\n" + "gecos-flower,\n"
                   + "gecos-blossom,\n" + "gecos-sunset,\n"
@@ -85,7 +93,11 @@ public enum Arg
                   + "turn-propensity,\n" + "buried-index,\n"
                   + "nucleotide,\n" + "nucleotide-ambiguity,\n"
                   + "purine-pyrimidine,\n" + "rna-helices,\n"
-                  + "t-coffee-scores,\n" + "sequence-id.",
+                  + "t-coffee-scores,\n" + "sequence-id.\n"
+                  +"\n"
+                  + "Names of user defined colourschemes will also work,\n"
+                 +"and jalview colourscheme specifications like\n"
+                  +"--colour=\"D,E=red; K,R,H=0022FF; C,c=yellow\"",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
   FEATURES(Type.OPENING, "Add a feature file or URL to the open alignment.",
           Opt.STRING, Opt.LINKED, Opt.MULTI, Opt.ALLOWSUBSTITUTIONS),
@@ -128,14 +140,10 @@ public enum Arg
                   + "none,\n" + "jmol,\n" + "chimera,\n" + "chimerax,\n"
                   + "pymol.",
           Opt.STRING, Opt.LINKED, Opt.MULTI),
-  STRUCTUREIMAGE(Type.STRUCTURE,
-          "Export an image of a 3D structure opened in JMOL", Opt.STRING,
-          Opt.LINKED, Opt.MULTI),
   NOTEMPFAC(Type.STRUCTURE,
           "Do not show the temperature factor annotation for the preceding --structure.",
           Opt.UNARY, Opt.LINKED, Opt.ALLOWALL, Opt.SECRET), // keep this secret
-                                                            // until it
-  // works!
+                                                            // until it works!
   SHOWSSANNOTATIONS(Type.STRUCTURE, null, Opt.BOOLEAN, Opt.LINKED,
           Opt.ALLOWALL),
 
@@ -144,13 +152,13 @@ public enum Arg
           "Output an image of the open alignment window. Format is specified by the subval modifier, a following --type argument or guessed from the file extension. Valid formats/extensions are:\n"
                   + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
           Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS, Opt.ALLOWALL,
-          Opt.REQUIREINPUT, Opt.OUTPUT, Opt.PRIMARY),
+          Opt.REQUIREINPUT, Opt.OUTPUTFILE, Opt.PRIMARY),
   TYPE(Type.IMAGE,
-          "Set the image format for the preceding --image to name. Valid values for name are: svg,\n"
-                  + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
+          "Set the image format for the preceding --image. Valid values are:\n"
+                  + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
   TEXTRENDERER(Type.IMAGE,
-          "Sets whether text in a vector image format (SVG, HTML, EPS) should be rendered as text or vector line-art. Possible values for name are:\n"
+          "Sets whether text in a vector image format (SVG, HTML, EPS) should be rendered as text or vector line-art. Possible values are:\n"
                   + "text,\n" + "lineart.",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
   SCALE(Type.IMAGE,
@@ -162,6 +170,27 @@ public enum Arg
   HEIGHT(Type.IMAGE,
           "Sets a height for bitmap image format (PNG) with the width maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --scale and --width then the smallest scaling will be used (scale, width and height provide bounds for the image).",
           Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGE(Type.STRUCTUREIMAGE,
+          "Export an image of a 3D structure opened in JMOL", Opt.STRING,
+          Opt.LINKED, Opt.MULTI, Opt.OUTPUTFILE),
+  STRUCTUREIMAGETYPE(Type.STRUCTUREIMAGE,
+          "Set the structure image format for the preceding --structureimage. Valid values are:\n"
+                  + "svg,\n" + "png,\n" + "eps,\n" + "html,\n" + "biojs.",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGETEXTRENDERER(Type.STRUCTUREIMAGE,
+          "Sets whether text in a vector structure image format (SVG, EPS) should be rendered as text or vector line-art. Possible values are:\n"
+                  + "text,\n" + "lineart.",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGESCALE(Type.STRUCTUREIMAGE,
+          "Sets a scaling for bitmap structure image format (PNG). Should be given as a floating point number. If used in conjunction with --structureimagewidth and --structureimageheight then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGEWIDTH(Type.STRUCTUREIMAGE,
+          "Sets a width for bitmap structure image format (PNG) with the height maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --structureimagescale and --structureimageheight then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+  STRUCTUREIMAGEHEIGHT(Type.STRUCTUREIMAGE,
+          "Sets a height for bitmap structure image format (PNG) with the width maintaining the aspect ratio. Should be given as a positive integer. If used in conjunction with --structureimagescale and --structureimagewidth then the smallest scaling will be used (structureimagescale, structureimagewidth and structureimageheight provide bounds for the structure image).",
+          Opt.STRING, Opt.LINKED, Opt.ALLOWALL),
+
   OUTPUT(Type.OUTPUT,
           "Export the open alignment to file filename. The format name is specified by the subval modifier format=name, a following --format name argument or guessed from the file extension. Valid format names (and file extensions) are:\n"
                   + "fasta (fa, fasta, mfa, fastq),\n" + "pfam (pfam),\n"
@@ -171,7 +200,7 @@ public enum Arg
                   + "clustal (aln),\n" + "phylip (phy),\n"
                   + "jalview (jvp, jar).",
           Opt.STRING, Opt.LINKED, Opt.ALLOWSUBSTITUTIONS, Opt.ALLOWALL,
-          Opt.REQUIREINPUT, Opt.OUTPUT, Opt.PRIMARY),
+          Opt.REQUIREINPUT, Opt.OUTPUTFILE, Opt.PRIMARY),
   FORMAT(Type.OUTPUT,
           "Sets the format for the preceding --output file. Valid formats are:\n"
                   + "fasta,\n" + "pfam,\n" + "stockholm,\n" + "pir,\n"
@@ -197,7 +226,8 @@ public enum Arg
           "Move on to a new alignment window. This will ensure --append will start a new alignment window and other linked arguments will apply to the new alignment window.",
           Opt.UNARY, Opt.MULTI, Opt.NOACTION, Opt.INCREMENTDEFAULTCOUNTER),
   SUBSTITUTIONS(Type.FLOW,
-          "The following argument values allow (or don't allow) subsituting filename parts. This is initially true. Valid substitutions are {basename} - the filename-without-extension of the currently --opened file (or first --appended file),\n"
+          "The following argument values allow (or don't allow) subsituting filename parts. This is initially true. Valid substitutions are:\n"
+                  + "{basename} - the filename-without-extension of the currently --opened file (or first --appended file),\n"
                   + "{dirname} - the directory (folder) name of the currently --opened file (or first --appended file),\n"
                   + "{argfilebasename} - the filename-without-extension of the current --argfile,\n"
                   + "{argfiledirname} - the directory (folder) name of the current --argfile,\n"
@@ -223,6 +253,12 @@ public enum Arg
   QUIT(Type.FLOW,
           "After all files have been opened, appended and output, quit Jalview. In ‑‑headless mode this already happens.",
           Opt.UNARY),
+  NOQUIT(Type.FLOW,
+          "Secret arg to not quit after --headless mode for tests",
+          Opt.UNARY, Opt.SECRET),
+  ALLSTRUCTURES(Type.FLOW,
+          "Apply the following 3D structure formatting arguments to all structures within the open alignment.",
+          Opt.BOOLEAN, Opt.MULTI, Opt.NOACTION),
 
   // secret options
   TESTOUTPUT(Type.CONFIG,
@@ -259,56 +295,146 @@ public enum Arg
 
   public static enum Opt
   {
-    BOOLEAN, // This Arg can be specified as --arg or --noarg to give true or
-             // false. A default can be given with setOptions(bool, Opt....).
-             // Use ArgParser.isSet(Arg) to see if this arg was not specified.
-    STRING, // This Arg can accept a value either through --arg=value or --arg
-            // value.
-    UNARY, // This Arg is a boolean value, true if present, false if not. Like
-           // BOOLEAN but without the --noarg option.
-    MULTI, // This Arg can be specified multiple times. Multiple values are
-           // stored in the ArgValuesMap (along with their positional index) for
-           // each linkedId.
-    LINKED, // This Arg can be linked to others through a --arg[linkedId] or
-            // --arg[linkedId]=value. If no linkedId is specified then the
-            // current default linkedId will be used.
-    NODUPLICATEVALUES, // This Arg can only have one value (per linkedId). The
-                       // first value will be used and subsequent values ignored
-                       // with a warning.
-    BOOTSTRAP, // This Arg value(s) can be determined at an earlier stage than
-               // non-BOOTSTRAP Args. Substitutions do not happen in BOOTSTRAP
-               // Args and they cannot be linked or contain SubVals. See
-               // jalview.bin.argparser.BootstrapArgs.
-    GLOB, // This Arg can expand wildcard filename "globs" (e.g.
-          // path/*/filename*). If the Arg value is given as --arg filename*
-          // then the shell will have expanded the glob already, but if
-          // specified as --arg=filename* then the Java glob expansion method
-          // will be used (see FileUtils.getFilenamesFromGlob()). Note that this
-          // might be different from the shell expansion rules.
-    NOACTION, // This Arg does not perform a data task, usually used to control
-              // flow in ArgParser.parse(args).
-    ALLOWSUBSTITUTIONS, // This Arg allows substitutions in its linkedId,
-                        // SubVals and values.
-    PRIVATE, // This Arg is used internally, and cannot be specified by the
-             // user.
-    SECRET, // This Arg is used by development processes and although it can be
-            // set by the user, it is not displayed to the user.
-    ALLOWALL, // This Arg can use the '*' linkedId to apply to all known
-              // linkedIds
-    INCREMENTDEFAULTCOUNTER, // If an Arg has this option and the default
-                             // linkedId is used, the defaultLinkedIdCounter is
-                             // incremented *first*.
-    INPUT, // This Arg counts as an input for REQUIREINPUT
-    REQUIREINPUT, // This Arg can only be applied via --all if there is an
-                  // input (i.e. --open or --append)
-    OUTPUT, // This Arg provides an output filename. With Opt.ALLOWALL *.ext is
-            // shorthand for --all --output={basename}.ext
-    STORED, // This Arg resets and creates a new set of "opened" linkedIds
-    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)
+    /*
+     * A BOOLEAN Arg can be specified as --arg or --noarg to give true or false.
+     * A default can be given with setOptions(bool, Opt....).
+     * Use ArgParser.isSet(Arg) to see if this arg was not specified.
+     */
+    BOOLEAN("can be negated with " + ArgParser.DOUBLEDASH
+            + ArgParser.NEGATESTRING + "..."),
+
+    /*
+     * A STRING Arg will take a value either through --arg=value or --arg value.
+     */
+    STRING("expects a value"),
+    /*
+     * A UNARY Arg is a boolean value, true if present, false if not.
+     * Like BOOLEAN but without the --noarg option.
+     */
+    UNARY(null),
+    /*
+     * A MULTI Arg can be specified multiple times.
+     * Multiple values are stored in the ArgValuesMap (along with their positional index) for each linkedId.
+     */
+    MULTI("can be specified multiple times"),
+    /*
+     * A Linked Arg can be linked to others through a --arg[linkedId] or --arg[linkedId]=value.
+     * If no linkedId is specified then the current default linkedId will be used.
+     */
+    LINKED("is linked to an alignment"),
+    /*
+     * A NODUPLICATES Arg can only have one value (per linkedId).
+     * The first value will be used and subsequent values ignored with a warning.
+     */
+    NODUPLICATEVALUES("cannot have the same value more than once"),
+    /*
+     * A BOOTSTRAP Arg value(s) can be determined at an earlier stage than non-BOOTSTRAP Args.
+     * Substitutions do not happen in BOOTSTRAP Args and they cannot be linked or contain SubVals.
+     * See jalview.bin.argparser.BootstrapArgs.
+     */
+    BOOTSTRAP("a configuration argument"),
+    /*
+     * A GLOB Arg can expand wildcard filename "globs" (e.g. path/* /filename*).
+     * If the Arg value is given as --arg filename* then the shell will have expanded the glob already,
+     * but if specified as --arg=filename* then the Java glob expansion method will be used
+     * (see FileUtils.getFilenamesFromGlob()).
+     * Note that this might be different from the shell expansion rules.
+     */
+    GLOB("can take multiple filenames with wildcards"),
+    /*
+     * A NOACTION Arg does not perform a data task,
+     * usually used to control flow in ArgParser.parse(args).
+     */
+    NOACTION(null),
+    /*
+     * An ALLOWSUBSTITUTIONS Arg allows substitutions in its linkedId,
+     * SubVals and values.
+     */
+    ALLOWSUBSTITUTIONS("values can use substitutions"),
+    /*
+     * A PRIVATE Arg is used internally, and cannot be specified by the user.
+     */
+    PRIVATE(null),
+    /*
+     * A SECRET Arg is used by development processes and although it can be set by the user,
+     * it is not displayed to the user.
+     */
+    SECRET(null),
+    /*
+     * An ALLOWALL Arg can use the '*' linkedId to apply to all known linkedIds
+     */
+    ALLOWALL("can be used with " + ArgParser.DOUBLEDASH + "all"),
+    /*
+     * If an Arg has the INCREMENTDEFAULTCOUNTER option and the default linkedId is used,
+     * the defaultLinkedIdCounter is incremented *first*.
+     */
+    INCREMENTDEFAULTCOUNTER("starts a new default alignment"),
+    /*
+     * An INPUT Arg counts as an input for REQUIREINPUT
+     */
+    INPUT(null),
+    /*
+     * A REQUIREINPUT Arg can only be applied via --all if there is an input
+     * (i.e. --open or --append)
+     */
+    REQUIREINPUT(null),
+    /*
+     * An OUTPUTFILE Arg provides an output filename. With Opt.ALLOWALL *.ext is shorthand for
+     * --all --output={basename}.ext
+     */
+    OUTPUTFILE("output file --headless will be assumed unless --gui used"),
+    /*
+     * A STORED Arg resets and creates a new set of "opened" linkedIds
+     */
+    STORED(null),
+    /*
+     * A HELP Arg is a --help type arg
+     */
+    HELP("provides a help statement"),
+    /*
+     * A PRIMARY Arg is the main Arg for its type
+     */
+    PRIMARY("is a primary argument for its type"),
+    /*
+     * A HASTYPE Arg can have an Arg.Type assigned to its ArgValue
+     */
+    HASTYPE(null),
+    /*
+     * A FIRST arg gets moved to appear first in the usage statement (within type)
+     */
+    FIRST(null),
+    /*
+     * A LAST arg gets moved to appear last in the usage statement (within type)
+     */
+    LAST(null),
+    /*
+     * After other args are checked, the following args can prefix a KEY=VALUE argument
+     */
+    PREFIXKEV("prefixes key=value"),
+    /*
+     * do not lowercase the name when getting the arg name or arg string
+     */
+    PRESERVECASE(null),
+    //
+    ;
+
+    private String description;
+
+    private Opt()
+    {
+      description = null;
+    }
+
+    private Opt(String description)
+    {
+      this.description = description;
+    }
+
+    public String description()
+    {
+      return description;
+    }
+
   }
 
   public static enum Type
@@ -322,6 +448,7 @@ public enum Arg
     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"),
+    STRUCTUREIMAGE("arguments used to export an image of an structure"),
     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
@@ -375,15 +502,15 @@ public enum Arg
   private Arg(Type type, String alternativeName, String description,
           boolean defaultBoolean, Opt... options)
   {
+    this.type = type;
+    this.description = description;
+    this.defaultBoolValue = defaultBoolean;
+    this.setOptions(options);
     this.argNames = alternativeName != null
             ? new String[]
             { this.getName(), alternativeName }
             : new String[]
             { this.getName() };
-    this.type = type;
-    this.description = description;
-    this.defaultBoolValue = defaultBoolean;
-    this.setOptions(options);
   }
 
   public String argString()
@@ -433,7 +560,9 @@ public enum Arg
 
   public String getName()
   {
-    return this.name().toLowerCase(Locale.ROOT).replace('_', '-');
+    String name = hasOption(Opt.PRESERVECASE) ? this.name()
+            : this.name().toLowerCase(Locale.ROOT);
+    return name.replace('_', '-');
   }
 
   @Override
@@ -464,6 +593,11 @@ public enum Arg
     return true;
   }
 
+  protected Opt[] getOptions()
+  {
+    return argOptions;
+  }
+
   protected void setOptions(Opt... options)
   {
     this.argOptions = options;
@@ -521,9 +655,9 @@ public enum Arg
   {
     StringBuilder sb = new StringBuilder();
 
-    sb.append("usage: jalview [" + Arg.HEADLESS.argString() + "] ["
+    sb.append("usage: jalview [" + Arg.HEADLESS.argString() + "] [["
             + Arg.OPEN.argString() + "/" + Arg.APPEND.argString()
-            + " file(s)] [args]");
+            + "] file(s)] [args]");
     sb.append(System.lineSeparator());
     sb.append(System.lineSeparator());
 
@@ -566,6 +700,7 @@ public enum Arg
       }
 
       Iterator<Arg> argsI = args.iterator();
+      Type typeSection = null;
       while (argsI.hasNext())
       {
         Arg a = argsI.next();
@@ -576,79 +711,98 @@ public enum Arg
           continue;
         }
 
-        appendArgUsage(sb, a, maxArgLength);
+        if (a.getType() != typeSection)
+        {
+          typeSection = a.getType();
+          String typeDescription = a.getType().description();
+          if (typeDescription != null && typeDescription.length() > 0)
+          {
+            // typeDescription = typeDescription.substring(0,
+            // 1).toUpperCase(Locale.ROOT) + typeDescription.substring(1);
+            typeDescription = typeDescription.toUpperCase(Locale.ROOT);
+            sb.append(typeDescription);
+            sb.append(System.lineSeparator());
+            sb.append(System.lineSeparator());
+          }
+        }
+
+        appendArgUsage(sb, a, maxArgLength, Platform.consoleWidth());
 
         if (argsI.hasNext())
+        {
           sb.append(System.lineSeparator());
+        }
       }
     }
     return sb.toString();
   }
 
   private static void appendArgUsage(StringBuilder sb, Arg a,
-          int maxArgLength)
+          int maxArgLength, int maxWidth)
   {
     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))
+    for (Opt o : EnumSet.allOf(Opt.class))
     {
-      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 (a.hasOption(o) && o.description() != null)
+      {
+        options.add(o.description());
+      }
     }
 
+    final String optDisplaySeparator = "; ";
     if (options.size() > 0)
     {
+      int linelength = 0;
+      String spacing = String.format("%-"
+              + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
+              "");
       if (first)
       {
         sb.append(ARGDESCRIPTIONSEPARATOR);
+        linelength += maxArgLength + ARGDESCRIPTIONSEPARATOR.length();
       }
       else
       {
-        sb.append(String.format("%-"
-                + (maxArgLength + ARGDESCRIPTIONSEPARATOR.length()) + "s",
-                ""));
+        sb.append(spacing);
+        linelength += spacing.length();
+      }
+      if (options.size() > 0)
+      {
+        boolean optFirst = true;
+        Iterator<String> optionsI = options.listIterator();
+        while (optionsI.hasNext())
+        {
+          String desc = optionsI.next();
+          if (optFirst)
+          {
+            sb.append("(");
+            linelength += 1;
+          }
+          int descLength = desc.length()
+                  + (optionsI.hasNext() ? optDisplaySeparator.length() : 0);
+          if (linelength + descLength > maxWidth)
+          {
+            sb.append(System.lineSeparator());
+            linelength = 0;
+            sb.append(spacing);
+            linelength += spacing.length();
+          }
+          // sb.append(linelength + "+" + desc.length() + " ");
+          sb.append(desc);
+          linelength += desc.length();
+          if (optionsI.hasNext())
+          {
+            sb.append(optDisplaySeparator);
+            linelength += optDisplaySeparator.length();
+          }
+          optFirst = false;
+        }
+        sb.append(')');
+        sb.append(System.lineSeparator());
       }
-      sb.append("(");
-      sb.append(String.join("; ", options));
-      sb.append(')');
-      sb.append(System.lineSeparator());
     }
   }
 
@@ -658,7 +812,16 @@ public enum Arg
     argSb.append(
             a.hasOption(Opt.BOOLEAN) ? booleanArgString(a) : a.argString());
     if (a.hasOption(Opt.STRING))
-      argSb.append("=value");
+    {
+      if (a.hasOption(Opt.PREFIXKEV))
+      {
+        argSb.append("key=value");
+      }
+      else
+      {
+        argSb.append("=value");
+      }
+    }
     return argSb.toString();
   }