Merge branch 'develop' into features/JAL-4134_use_annotation_row_for_colours_and_groups
authorJames Procter <j.procter@dundee.ac.uk>
Thu, 1 Jun 2023 13:19:25 +0000 (14:19 +0100)
committerJames Procter <j.procter@dundee.ac.uk>
Thu, 1 Jun 2023 13:19:25 +0000 (14:19 +0100)
help/help/html/features/clarguments-basic.html
help/help/html/features/clarguments-reference.html
help/help/html/features/clarguments.html
src/jalview/bin/Jalview.java
src/jalview/bin/argparser/Arg.java
src/jalview/bin/argparser/BootstrapArgs.java
test/jalview/bin/argparser/ArgParserTest.java

index 626fb8c..7a87602 100644 (file)
   </pre>
   </p>
 
+  <p>
+  <em>Important!</em> If you use <code>--output</code> or any other argument that outputs a file, then it will be assumed you want to run Jalview in headless mode (as if you had specified <code>--headless</code>).  To use Jalview with <code>--output</code> and not assume headless mode, use the <code>--gui</code> or <code>--noheadless</code> argument (the order doesn't matter).
+  </p>
+
   <h3><a name="format"></a><code>--format</code></h3>
 
   <p>
index 6d65033..fc4f0a0 100644 (file)
     <tr valign="top"><td><code>&#8209;&#8209;help&#8209;all</code></td><td>Help for all arguments</td></tr>
 
     <tr valign="top">
-    <td><code>&#8209;&#8209;headless</code></td>
-    <td>Run Jalview in headless mode.  No GUI interface will be created and Jalview will quit after all arguments have been processed.</td>
+    <td><code>&#8209;&#8209;headless / &#8209;&#8209;noheadless</code></td>
+    <td>Run Jalview in headless (/ or not in headless) mode.  In headless mode, no GUI interface will be created and Jalview will quit after all arguments have been processed.
+    <br/>
+    If you use a command line argument to specify an output file of some kind (<code>--output</code>, <code>--image</code> or <code>--structureimage</code>) then <strong>headless mode will be assumed</strong>.  If you don't want this behaviour use <code>--noheadless</code> or <code>--gui</code>.
+    </td>
+    </tr>
+
+    <tr valign="top">
+    <td><code>&#8209;&#8209;gui</code></td>
+    <td>Force Jalview to run in graphical mode.  This can be used to counter the assumption of headless mode when an argument that creates an output file is used.  <code>--gui</code> takes precedence over <code>--headless</code>.</td>
     </tr>
 
     <tr valign="top">
 
     <tr valign="top">
     <td><code>&#8209;&#8209;news / &#8209;&#8209;nonews</code></td>
-    <td>Show (or don't show) the news feed.</td>
+    <td>Show (/ or don't show) the news feed.</td>
     </tr>
 
     <tr valign="top">
     <td><code>&#8209;&#8209;splash / &#8209;&#8209;nosplash</code></td>
-    <td>Show (or don't show) the About Jalview splash screen.</td>
+    <td>Show (/ or don't show) the About Jalview splash screen.</td>
     </tr>
 
     <tr valign="top">
     <td><code>&#8209;&#8209;questionnaire / &#8209;&#8209;noquestionnaire</code></td>
-    <td>Show (or don't show) the questionnaire if one is available.</td>
+    <td>Show (/ or don't show) the questionnaire if one is available.</td>
     </tr>
 
     <tr valign="top">
-    <td><code>&#8209;&#8209;usagestats / &#8209;&#8209;nousagestats</code></td>
-    <td>Send (or don't send) initial launch usage stats. <em>Note: usage stats are useful for future funding for Jalview!</em></td>
+    <td><code>&#8209;&#8209;nousagestats</code></td>
+    <td>Don't send usage stats via Google analytics for this session. <em>Note: usage stats are useful for future funding for Jalview!</em></td>
     </tr>
 
     <tr valign="top">
     <td><code>&#8209;&#8209;webservicediscovery / &#8209;&#8209;nowebservicediscovery</code></td>
-    <td>Attempt (or don't attempt) to connect to JABAWS web services.</td>
+    <td>Attempt (/ or don't attempt) to connect to JABAWS web services.</td>
     </tr>
 
     <tr valign="top">
     <td>Stop all output to STDOUT (after the Java Virtual Machine has started).  Use <code>&#8209;&#8209;quiet</code> a second time to stop all output to STDERR.</td>
     </tr>
 
+<!--
     <tr valign="top">
     <td><code>&#8209;&#8209;initsubstitutions / &#8209;&#8209;noinitsubstitutions</code></td>
     <td>Set <code>&#8209;&#8209;substitutions</code> to be initially enabled (or initially disabled).</td>
     </tr>
+-->
 
 <!--
     <tr valign="top">
index 20bcd10..b763a81 100644 (file)
@@ -32,6 +32,7 @@
   <ul>
   <li><a href="#introduction">Introduction</a></li>
   <li><a href="#syntax">Syntax</a></li>
+  <li><a href="#headlessmode">Headless mode</a></li>
   </ul>
 
   <h2><a name="introduction"></a>Introduction</h2>
         <br/>
         <code>--arg file1.fa otherfile.stk</code>
         <br/>
-        <code>--arg filename*.fa</code> <em>(expanded by shell)</em>
+        <code>--arg filename*.fa</code> <em>(filenames expanded by shell)</em>
         <br/>
-        <code>--arg=filename*.fa</code> <em>(expanded by Jalview)</em>
+        <code>--arg=filename*.fa</code> <em>(filenames expanded by Jalview)</em>
     </li>
     <li>
         For arguments that act as a switch, most can be negated by preceding the argument name with <code>no</code>.
   </p>
 
   <p>
-  This may sound complicated, but nearly everything can be done just with plain command line arguments, though in this case the ordering of the arguments is more important.
+  This may sound complicated, but nearly everything can be done just with plain command line arguments (see <a href="clarguments-basic.html">Command Line: basic usage</a>), though in this case the ordering of the arguments is more important.
   </p>
 
+
+  <h2><a name="headlessmode"></a>Headless mode</h2>
+
+  <p>
+  Jalview can be run in headless mode, i.e. without the usual graphical user interface (GUI), by specifying the <code>--headless</code> argument.  With command line arguments you can specify operations for Jalview to perform on one or more files and then stop running.  Most likely you will want to output another file, either an alignment for image file.
+  </p>
+  <p>
+  <strong>If you specify an argument for an output file</strong> (one or more of <code>--output</code>, <code>--image</code> or <code>--structureimage</code>) then it will be assumed that you wish to <strong>run in headless mode</strong>.
+  </p>
+  <p>
+  You can force Jalview to run in graphical mode using the <code>--gui</code> or <code>--noheadless</code> arguments.
+  </p>
+
+  <p>
+  </p>
+
+
   <hr/>
   Continue to <a href="clarguments-basic.html">Command Line: basic usage</a>.
   <br/>
index 7535f60..dc97549 100755 (executable)
@@ -396,7 +396,7 @@ public class Jalview
 
     // get bootstrap properties (mainly for the logger level)
     Properties bootstrapProperties = Cache
-            .bootstrapProperties(bootstrapArgs.get(Arg.PROPS));
+            .bootstrapProperties(bootstrapArgs.getValue(Arg.PROPS));
 
     // report Jalview version
     Cache.loadBuildProperties(
@@ -482,7 +482,7 @@ public class Jalview
     });
 
     String usrPropsFile = bootstrapArgs.contains(Arg.PROPS)
-            ? bootstrapArgs.get(Arg.PROPS)
+            ? bootstrapArgs.getValue(Arg.PROPS)
             : aparser.getValue("props");
     // if usrPropsFile == null, loadProperties will use the Channel
     // preferences.file
@@ -536,7 +536,7 @@ public class Jalview
       }
 
       // new CLI
-      headlessArg = isHeadless(bootstrapArgs);
+      headlessArg = bootstrapArgs.isHeadless();
       if (headlessArg)
       {
         System.setProperty("java.awt.headless", "true");
@@ -553,7 +553,7 @@ public class Jalview
       // allow https handshakes to download intermediate certs if necessary
       System.setProperty("com.sun.security.enableAIAcaIssuers", "true");
 
-      String jabawsUrl = bootstrapArgs.get(Arg.JABAWS);
+      String jabawsUrl = bootstrapArgs.getValue(Arg.JABAWS);
       if (jabawsUrl == null)
         jabawsUrl = aparser.getValue("jabaws");
       if (jabawsUrl != null)
@@ -645,7 +645,7 @@ public class Jalview
 
     if (!(headless || headlessArg))
     {
-      Desktop.nosplash = "false".equals(bootstrapArgs.get(Arg.SPLASH))
+      Desktop.nosplash = "false".equals(bootstrapArgs.getValue(Arg.SPLASH))
               || aparser.contains("nosplash")
               || Cache.getDefault("SPLASH", "true").equals("false");
       desktop = new Desktop();
@@ -774,8 +774,8 @@ public class Jalview
 
         if ((!aparser.contains("nonews")
                 && Cache.getProperty("NONEWS") == null
-                && !"false".equals(bootstrapArgs.get(Arg.NEWS)))
-                || "true".equals(bootstrapArgs.get(Arg.NEWS)))
+                && !"false".equals(bootstrapArgs.getValue(Arg.NEWS)))
+                || "true".equals(bootstrapArgs.getValue(Arg.NEWS)))
         {
           desktop.checkForNews();
         }
@@ -1886,26 +1886,4 @@ public class Jalview
     System.out.println("[TESTOUTPUT] arg "
             + (yes ? a.argString() : a.negateArgString()) + " was set");
   }
-
-  private static boolean isHeadless(BootstrapArgs bootstrapArgs)
-  {
-    if (bootstrapArgs == null)
-    {
-      return false;
-    }
-    boolean isHeadless = false;
-    if (bootstrapArgs.contains(Arg.GUI))
-    {
-      isHeadless = !bootstrapArgs.getBoolean(Arg.GUI);
-    }
-    else if (bootstrapArgs.contains(Arg.HEADLESS))
-    {
-      isHeadless = bootstrapArgs.getBoolean(Arg.HEADLESS);
-    }
-    else if (bootstrapArgs.argsHaveOption(Opt.OUTPUTFILE))
-    {
-      isHeadless = true;
-    }
-    return isHeadless;
-  }
 }
index c502eec..0d6e3b1 100644 (file)
@@ -287,57 +287,138 @@ 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)
-    OUTPUTFILE, // 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),
+    //
+    ;
+
+    private String description;
+
+    private Opt()
+    {
+      description = null;
+    }
+
+    private Opt(String description)
+    {
+      this.description = description;
+    }
+
+    public String description()
+    {
+      return description;
+    }
+
   }
 
   public static enum Type
@@ -627,7 +708,7 @@ public enum Arg
           }
         }
 
-        appendArgUsage(sb, a, maxArgLength);
+        appendArgUsage(sb, a, maxArgLength, Platform.consoleWidth());
 
         if (argsI.hasNext())
         {
@@ -639,69 +720,71 @@ public enum Arg
   }
 
   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))
+    for (Opt o : EnumSet.allOf(Opt.class))
     {
-      options.add("allows file globs");
-    }
-
-    if (a.hasOption(Opt.ALLOWSUBSTITUTIONS))
-    {
-      options.add("allows substitutions");
-    }
-
-    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 (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());
     }
   }
 
index a6bad24..e1ad1d7 100644 (file)
@@ -25,6 +25,8 @@ public class BootstrapArgs
 
   private Set<Opt> argsOptions = new HashSet<>();
 
+  private Set<Type> argsTypes = new HashSet<>();
+
   public static BootstrapArgs getBootstrapArgs(String[] args)
   {
     List<String> argList = new ArrayList<>(Arrays.asList(args));
@@ -122,6 +124,11 @@ public class BootstrapArgs
               argsOptions.add(opt);
             }
           }
+          Type t = a.getType();
+          if (!argsTypes.contains(t))
+          {
+            argsTypes.add(t);
+          }
         }
 
         if (a == null || !a.hasOption(Opt.BOOTSTRAP))
@@ -275,7 +282,7 @@ public class BootstrapArgs
    * Retrieves the first value even if MULTI.
    * A convenience for non-MULTI args.
    */
-  public String get(Arg a)
+  public String getValue(Arg a)
   {
     if (!bootstrapArgMap.containsKey(a))
       return null;
@@ -287,7 +294,7 @@ public class BootstrapArgs
   {
     if (!bootstrapArgMap.containsKey(a))
       return d;
-    return Boolean.parseBoolean(get(a));
+    return Boolean.parseBoolean(getValue(a));
   }
 
   public boolean getBoolean(Arg a)
@@ -298,7 +305,7 @@ public class BootstrapArgs
     }
     if (bootstrapArgMap.containsKey(a))
     {
-      return Boolean.parseBoolean(get(a));
+      return Boolean.parseBoolean(getValue(a));
     }
     else
     {
@@ -310,4 +317,41 @@ public class BootstrapArgs
   {
     return argsOptions.contains(opt);
   }
+
+  public boolean argsHaveType(Type type)
+  {
+    return argsTypes.contains(type);
+  }
+
+  public boolean isHeadless()
+  {
+    boolean isHeadless = false;
+    if (this.argsHaveType(Type.HELP))
+    {
+      // --help, --help-all, ... specified => headless
+      isHeadless = true;
+    }
+    else if (this.contains(Arg.VERSION))
+    {
+      // --version specified => headless
+      isHeadless = true;
+    }
+    else if (this.contains(Arg.GUI))
+    {
+      // --gui specified => forced NOT headless
+      isHeadless = !this.getBoolean(Arg.GUI);
+    }
+    else if (this.contains(Arg.HEADLESS))
+    {
+      // --headless, --noheadless specified => use value
+      isHeadless = this.getBoolean(Arg.HEADLESS);
+    }
+    else if (this.argsHaveOption(Opt.OUTPUTFILE))
+    {
+      // --output file.fa, --image pic.png, --structureimage struct.png =>
+      // assume headless unless above has been already specified
+      isHeadless = true;
+    }
+    return isHeadless;
+  }
 }
index ca63e44..9beba17 100644 (file)
@@ -90,7 +90,7 @@ public class ArgParserTest
     Assert.assertTrue(b.contains(a));
     if (a == Arg.PROPS)
     {
-      Properties bP = Cache.bootstrapProperties(b.get(Arg.PROPS));
+      Properties bP = Cache.bootstrapProperties(b.getValue(Arg.PROPS));
       Assert.assertNotNull(bP);
       Assert.assertTrue(other.equals(bP.get(Cache.BOOTSTRAP_TEST)));
       Assert.assertFalse(bP.contains("NOT" + Cache.BOOTSTRAP_TEST));
@@ -130,7 +130,7 @@ public class ArgParserTest
     Assert.assertFalse(b.contains(Arg.APPEND));
     if (a == Arg.PROPS)
     {
-      Properties bP = Cache.bootstrapProperties(b.get(Arg.PROPS));
+      Properties bP = Cache.bootstrapProperties(b.getValue(Arg.PROPS));
       Assert.assertTrue("true".equals(bP.get(Cache.BOOTSTRAP_TEST)));
     }
   }
@@ -340,4 +340,78 @@ public class ArgParserTest
         //
     };
   }
+
+  @Test(groups = "Functional", dataProvider = "bootstrapArgsData")
+  public void bootstrapArgsValuesAndHeadlessModeTest(String commandLineArgs,
+          Arg a, String valS, boolean valB, boolean headlessValue)
+  {
+    String[] args = commandLineArgs.split("\\s+");
+    BootstrapArgs bsa = BootstrapArgs.getBootstrapArgs(args);
+    if (a != null)
+    {
+      if (valS != null)
+      {
+        Assert.assertEquals(bsa.getValue(a), valS,
+                "BootstrapArg " + a.argString()
+                        + " value does not match expected '" + valS + "'");
+      }
+      else
+      {
+        Assert.assertEquals(bsa.getBoolean(a), valB,
+                "Boolean/Unary value of BootstrapArg " + a.argString()
+                        + "' is not the expected '" + valB + "'");
+      }
+    }
+
+    boolean isHeadless = bsa.isHeadless();
+    Assert.assertEquals(isHeadless, headlessValue,
+            "Assumed headless setting '" + isHeadless + "' is wrong.");
+  }
+
+  @DataProvider(name = "bootstrapArgsData")
+  public Object[][] bootstrapArgsData()
+  {
+    return new Object[][] {
+        /*
+         * cmdline args
+         * Arg (null if only testing headless)
+         * String value if there is one (null otherwise)
+         * boolean value if String value is null
+         * expected value of isHeadless()
+         */
+        /*
+        */
+        { "--open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
+            Arg.JABAWS, "https://forwardsandbackwards.com/", false, true },
+        { "--help-all --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
+            Arg.HELP, null, true, true },
+        { "--help-all --nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
+            Arg.NEWS, null, false, true },
+        { "--help --nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
+            Arg.NEWS, null, false, true },
+        { "--help-opening --nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
+            Arg.NEWS, null, false, true },
+        { "--nonews --open thisway.fa --output thatway.fa --jabaws https://forwardsandbackwards.com/",
+            Arg.NEWS, null, false, true },
+        { "--open thisway.fa --image thatway.png", null, null, false,
+            true },
+        { "--open thisway.fa --output thatway.png", null, null, false,
+            true },
+        { "--open thisway.fa --image thatway.png --noheadless", null, null,
+            false, false },
+        { "--open thisway.fa --output thatway.png --noheadless", null, null,
+            false, false },
+        { "--open thisway.fa --image thatway.png --gui", null, null, false,
+            false },
+        { "--open thisway.fa --output thatway.png --gui", null, null, false,
+            false },
+        // --gui takes precedence
+        { "--open thisway.fa --image thatway.png --gui --headless", null,
+            null, false, false },
+        { "--open thisway.fa --output thatway.png --gui --headless", null,
+            null, false, false },
+        //
+    };
+  }
+
 }