Merge branch 'develop' into spike/JAL-4047/JAL-4048_columns_in_sequenceID
[jalview.git] / src / jalview / bin / Jalview.java
index c792a96..eabad91 100755 (executable)
@@ -26,7 +26,9 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.OutputStream;
 import java.io.OutputStreamWriter;
+import java.io.PrintStream;
 import java.io.PrintWriter;
 import java.net.MalformedURLException;
 import java.net.URI;
@@ -37,7 +39,9 @@ import java.security.CodeSource;
 import java.security.PermissionCollection;
 import java.security.Permissions;
 import java.security.Policy;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
@@ -45,9 +49,11 @@ import java.util.Vector;
 import java.util.logging.ConsoleHandler;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import java.util.stream.Collectors;
 
 import javax.swing.JDialog;
 import javax.swing.JFrame;
+import javax.swing.JInternalFrame;
 import javax.swing.JOptionPane;
 import javax.swing.SwingUtilities;
 import javax.swing.UIManager;
@@ -62,13 +68,18 @@ import com.threerings.getdown.util.LaunchUtil;
 //import edu.stanford.ejalbert.launching.IBrowserLaunching;
 import groovy.lang.Binding;
 import groovy.util.GroovyScriptEngine;
-import jalview.bin.ArgParser.Arg;
+import jalview.bin.argparser.Arg;
+import jalview.bin.argparser.Arg.Opt;
+import jalview.bin.argparser.Arg.Type;
+import jalview.bin.argparser.ArgParser;
+import jalview.bin.argparser.BootstrapArgs;
 import jalview.ext.so.SequenceOntology;
 import jalview.gui.AlignFrame;
 import jalview.gui.Desktop;
 import jalview.gui.PromptUserConfig;
 import jalview.gui.QuitHandler;
 import jalview.gui.QuitHandler.QResponse;
+import jalview.gui.StructureViewerBase;
 import jalview.io.AppletFormatAdapter;
 import jalview.io.BioJsHTMLOutput;
 import jalview.io.DataSourceType;
@@ -80,6 +91,7 @@ import jalview.io.FileLoader;
 import jalview.io.HtmlSvgOutput;
 import jalview.io.IdentifyFile;
 import jalview.io.NewickFile;
+import jalview.io.exceptions.ImageOutputException;
 import jalview.io.gff.SequenceOntologyFactory;
 import jalview.schemes.ColourSchemeI;
 import jalview.schemes.ColourSchemeProperty;
@@ -123,8 +135,21 @@ public class Jalview
 
   private Desktop desktop;
 
+  protected Commands cmds;
+
   public static AlignFrame currentAlignFrame;
 
+  private ArgParser argparser = null;
+
+  private BootstrapArgs bootstrapArgs = null;
+
+  private boolean QUIET = false;
+
+  public static boolean quiet()
+  {
+    return Jalview.getInstance() != null && Jalview.getInstance().QUIET;
+  }
+
   static
   {
     if (!Platform.isJS())
@@ -249,7 +274,7 @@ public class Jalview
      * 
      */
     {
-      System.out.println("not in js");
+      Console.outPrintln("not in js");
     }
 
     // BH - for event debugging in JavaScript (Java mode only)
@@ -275,37 +300,114 @@ public class Jalview
    */
   void doMain(String[] args)
   {
-
     if (!Platform.isJS())
     {
       System.setSecurityManager(null);
     }
 
+    if (args == null || args.length == 0 || (args.length == 1
+            && (args[0] == null || args[0].length() == 0)))
+    {
+      args = new String[] {};
+    }
+
     // get args needed before proper ArgParser
-    Map<ArgParser.Arg, String> bootstrapArgs = ArgParser
-            .bootstrapArgs(args);
+    bootstrapArgs = BootstrapArgs.getBootstrapArgs(args);
 
-    System.out
-            .println("Java version: " + System.getProperty("java.version"));
-    System.out.println("Java Home: " + System.getProperty("java.home"));
-    System.out.println(System.getProperty("os.arch") + " "
-            + System.getProperty("os.name") + " "
-            + System.getProperty("os.version"));
+    if (!Platform.isJS())
+    {
+      // are we being --quiet ?
+      if (bootstrapArgs.contains(Arg.QUIET))
+      {
+        QUIET = true;
+        OutputStream devNull = new OutputStream()
+        {
 
-    String val = System.getProperty("sys.install4jVersion");
-    if (val != null)
+          @Override
+          public void write(int b)
+          {
+            // DO NOTHING
+          }
+        };
+        System.setOut(new PrintStream(devNull));
+        // redirecting stderr not working
+        if (bootstrapArgs.getList(Arg.QUIET).size() > 1)
+        {
+          System.setErr(new PrintStream(devNull));
+        }
+      }
+
+      if (bootstrapArgs.contains(Arg.HELP)
+              || bootstrapArgs.contains(Arg.VERSION))
+      {
+        QUIET = true;
+      }
+    }
+
+    // set individual session preferences
+    if (bootstrapArgs.contains(Arg.P))
     {
-      System.out.println("Install4j version: " + val);
+      for (String kev : bootstrapArgs.getValueList(Arg.P))
+      {
+        if (kev == null)
+        {
+          continue;
+        }
+        int equalsIndex = kev.indexOf(ArgParser.EQUALS);
+        if (equalsIndex > -1)
+        {
+          String key = kev.substring(0, equalsIndex);
+          String val = kev.substring(equalsIndex + 1);
+          Cache.setSessionProperty(key, val);
+        }
+      }
     }
-    val = System.getProperty("installer_template_version");
-    if (val != null)
+
+    // Move any new getdown-launcher-new.jar into place over old
+    // getdown-launcher.jar
+    String appdirString = System.getProperty("getdownappdir");
+    if (appdirString != null && appdirString.length() > 0)
     {
-      System.out.println("Install4j template version: " + val);
+      final File appdir = new File(appdirString);
+      new Thread()
+      {
+
+        @Override
+        public void run()
+        {
+          LaunchUtil.upgradeGetdown(
+                  new File(appdir, "getdown-launcher-old.jar"),
+                  new File(appdir, "getdown-launcher.jar"),
+                  new File(appdir, "getdown-launcher-new.jar"));
+        }
+      }.start();
     }
-    val = System.getProperty("launcher_version");
-    if (val != null)
+
+    if (!quiet() || !bootstrapArgs.outputToStdout()
+            || bootstrapArgs.contains(Arg.VERSION))
     {
-      System.out.println("Launcher version: " + val);
+      Console.outPrintln(
+              "Java version: " + System.getProperty("java.version"));
+      Console.outPrintln("Java home: " + System.getProperty("java.home"));
+      Console.outPrintln("Java arch: " + System.getProperty("os.arch") + " "
+              + System.getProperty("os.name") + " "
+              + System.getProperty("os.version"));
+
+      String val = System.getProperty("sys.install4jVersion");
+      if (val != null)
+      {
+        Console.outPrintln("Install4j version: " + val);
+      }
+      val = System.getProperty("installer_template_version");
+      if (val != null)
+      {
+        Console.outPrintln("Install4j template version: " + val);
+      }
+      val = System.getProperty("launcher_version");
+      if (val != null)
+      {
+        Console.outPrintln("Launcher version: " + val);
+      }
     }
 
     if (Platform.isLinux() && LaunchUtils.getJavaVersion() < 11)
@@ -315,10 +417,17 @@ 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(true);
+    Cache.loadBuildProperties(
+            !quiet() || bootstrapArgs.contains(Arg.VERSION));
+
+    // stop now if only after --version
+    if (bootstrapArgs.contains(Arg.VERSION))
+    {
+      Jalview.exit(null, 0);
+    }
 
     // old ArgsParser
     ArgsParser aparser = new ArgsParser(args);
@@ -330,8 +439,15 @@ public class Jalview
 
     try
     {
-      String logLevel = bootstrapArgs.containsKey(Arg.DEBUG) ? "DEBUG"
-              : null;
+      String logLevel = null;
+      if (bootstrapArgs.contains(Arg.TRACE))
+      {
+        logLevel = "TRACE";
+      }
+      else if (bootstrapArgs.contains(Arg.DEBUG))
+      {
+        logLevel = "DEBUG";
+      }
       if (logLevel == null && !(bootstrapProperties == null))
       {
         logLevel = bootstrapProperties.getProperty(Cache.JALVIEWLOGLEVEL);
@@ -340,22 +456,41 @@ public class Jalview
     } catch (NoClassDefFoundError error)
     {
       error.printStackTrace();
-      System.out.println("\nEssential logging libraries not found."
-              + "\nUse: java -classpath \"$PATH_TO_LIB$/*:$PATH_TO_CLASSES$\" jalview.bin.Jalview");
-      System.exit(0);
+      String message = "\nEssential logging libraries not found."
+              + "\nUse: java -classpath \"$PATH_TO_LIB$/*:$PATH_TO_CLASSES$\" jalview.bin.Jalview";
+      Jalview.exit(message, 0);
     }
 
     // register SIGTERM listener
     Runtime.getRuntime().addShutdownHook(new Thread()
     {
+      @Override
       public void run()
       {
         Console.debug("Running shutdown hook");
+        QuitHandler.startForceQuit();
+        boolean closeExternal = Cache
+                .getDefault("DEFAULT_CLOSE_EXTERNAL_VIEWERS", false)
+                || Cache.getDefault("ALWAYS_CLOSE_EXTERNAL_VIEWERS", false);
+        StructureViewerBase.setQuitClose(closeExternal);
+        if (desktop != null)
+        {
+          for (JInternalFrame frame : Desktop.desktop.getAllFrames())
+          {
+            if (frame instanceof StructureViewerBase)
+            {
+              ((StructureViewerBase) frame).closeViewer(closeExternal);
+            }
+          }
+        }
+
         if (QuitHandler.gotQuitResponse() == QResponse.CANCEL_QUIT)
         {
           // Got to here by a SIGTERM signal.
           // Note we will not actually cancel the quit from here -- it's too
-          // late -- but we can wait for saving files.
+          // late -- but we can wait for saving files and close external viewers
+          // if configured.
+          // Close viewers/Leave viewers open
           Console.debug("Checking for saving files");
           QuitHandler.getQuitResponse(false);
         }
@@ -368,26 +503,33 @@ public class Jalview
       }
     });
 
-    String usrPropsFile = bootstrapArgs.containsKey(Arg.PROPS)
-            ? bootstrapArgs.get(Arg.PROPS)
+    String usrPropsFile = bootstrapArgs.contains(Arg.PROPS)
+            ? bootstrapArgs.getValue(Arg.PROPS)
             : aparser.getValue("props");
+    // if usrPropsFile == null, loadProperties will use the Channel
+    // preferences.file
     Cache.loadProperties(usrPropsFile);
     if (usrPropsFile != null)
     {
-      System.out.println(
+      Console.outPrintln(
               "CMD [-props " + usrPropsFile + "] executed successfully!");
+      testoutput(bootstrapArgs, Arg.PROPS,
+              "test/jalview/bin/testProps.jvprops", usrPropsFile);
     }
 
-    // new ArgParser
-    ArgParser argparser;
     // --argfile=... -- OVERRIDES ALL NON-BOOTSTRAP ARGS
-    if (bootstrapArgs.containsKey(Arg.ARGFILE))
+    if (bootstrapArgs.contains(Arg.ARGFILE))
     {
-      argparser = ArgParser.parseArgFile(bootstrapArgs.get(Arg.ARGFILE));
+      argparser = ArgParser.parseArgFiles(
+              bootstrapArgs.getValueList(Arg.ARGFILE),
+              bootstrapArgs.getBoolean(Arg.INITSUBSTITUTIONS),
+              bootstrapArgs);
     }
     else
     {
-      argparser = new ArgParser(args);
+      argparser = new ArgParser(args,
+              bootstrapArgs.getBoolean(Arg.INITSUBSTITUTIONS),
+              bootstrapArgs);
     }
 
     if (!Platform.isJS())
@@ -397,24 +539,35 @@ public class Jalview
      * @j2sIgnore
      */
     {
-      if (aparser.contains("help") || aparser.contains("h")
-              || argparser.getBool(Arg.HELP))
+      if (bootstrapArgs.contains(Arg.HELP))
       {
+        List<Map.Entry<Type, String>> helpArgs = bootstrapArgs
+                .getList(Arg.HELP);
+        Console.outPrintln(Arg.usage(helpArgs.stream().map(e -> e.getKey())
+                .collect(Collectors.toList())));
+        Jalview.exit(null, 0);
+      }
+      if (aparser.contains("help") || aparser.contains("h"))
+      {
+        /*
+         * Now using new usage statement.
         showUsage();
-        System.exit(0);
+        */
+        Console.outPrintln(Arg.usage());
+        Jalview.exit(null, 0);
       }
 
-      if (argparser.isSet(Arg.HEADLESS))
+      // new CLI
+      headlessArg = bootstrapArgs.isHeadless();
+      if (headlessArg)
       {
         System.setProperty("java.awt.headless", "true");
-        // new
-        headlessArg = argparser.getBool(Arg.HEADLESS);
       }
+      // old CLI
       if (aparser.contains("nodisplay") || aparser.contains("nogui")
               || aparser.contains("headless"))
       {
         System.setProperty("java.awt.headless", "true");
-        // old
         headless = true;
       }
       // anything else!
@@ -422,50 +575,68 @@ public class Jalview
       // allow https handshakes to download intermediate certs if necessary
       System.setProperty("com.sun.security.enableAIAcaIssuers", "true");
 
-      final String jabawsUrl = aparser.getValue("jabaws");
+      String jabawsUrl = bootstrapArgs.getValue(Arg.JABAWS);
+      if (jabawsUrl == null)
+        jabawsUrl = aparser.getValue("jabaws");
       if (jabawsUrl != null)
       {
         try
         {
           Jws2Discoverer.getDiscoverer().setPreferredUrl(jabawsUrl);
-          System.out.println(
+          Console.outPrintln(
                   "CMD [-jabaws " + jabawsUrl + "] executed successfully!");
+          testoutput(bootstrapArgs, Arg.JABAWS,
+                  "http://www.compbio.dundee.ac.uk/jabaws", jabawsUrl);
         } catch (MalformedURLException e)
         {
-          System.err.println(
+          jalview.bin.Console.errPrintln(
                   "Invalid jabaws parameter: " + jabawsUrl + " ignored");
         }
       }
     }
 
-    String defs = aparser.getValue("setprop");
-    while (defs != null)
+    List<String> setprops = new ArrayList<>();
+    if (bootstrapArgs.contains(Arg.SETPROP))
     {
-      int p = defs.indexOf('=');
+      setprops = bootstrapArgs.getValueList(Arg.SETPROP);
+    }
+    else
+    {
+      String sp = aparser.getValue("setprop");
+      while (sp != null)
+      {
+        setprops.add(sp);
+        sp = aparser.getValue("setprop");
+      }
+    }
+    for (String setprop : setprops)
+    {
+      int p = setprop.indexOf('=');
       if (p == -1)
       {
-        System.err.println("Ignoring invalid setprop argument : " + defs);
+        System.err
+                .println("Ignoring invalid setprop argument : " + setprop);
       }
       else
       {
-        System.out.println("Executing setprop argument: " + defs);
+        jalview.bin.Console
+                .errPrintln("Executing setprop argument: " + setprop);
         if (Platform.isJS())
         {
-          Cache.setProperty(defs.substring(0, p), defs.substring(p + 1));
+          Cache.setProperty(setprop.substring(0, p),
+                  setprop.substring(p + 1));
         }
         // DISABLED FOR SECURITY REASONS
         // TODO: add a property to allow properties to be overriden by cli args
-        // Cache.setProperty(defs.substring(0,p), defs.substring(p+1));
+        // Cache.setProperty(setprop.substring(0,p), setprop.substring(p+1));
       }
-      defs = aparser.getValue("setprop");
     }
     if (System.getProperty("java.awt.headless") != null
             && System.getProperty("java.awt.headless").equals("true"))
     {
       headless = true;
     }
-    System.setProperty("http.agent",
-            "Jalview Desktop/" + Cache.getDefault("VERSION", "Unknown"));
+    System.setProperty("http.agent", HttpUtils.getUserAgent());
 
     try
     {
@@ -475,9 +646,9 @@ public class Jalview
     NoClassDefFoundError error)
     {
       error.printStackTrace();
-      System.out.println("\nEssential logging libraries not found."
-              + "\nUse: java -classpath \"$PATH_TO_LIB$/*:$PATH_TO_CLASSES$\" jalview.bin.Jalview");
-      System.exit(0);
+      String message = "\nEssential logging libraries not found."
+              + "\nUse: java -classpath \"$PATH_TO_LIB$/*:$PATH_TO_CLASSES$\" jalview.bin.Jalview";
+      Jalview.exit(message, 0);
     }
     desktop = null;
 
@@ -496,7 +667,9 @@ public class Jalview
 
     if (!(headless || headlessArg))
     {
-      Desktop.nosplash = aparser.contains("nosplash");
+      Desktop.nosplash = "false".equals(bootstrapArgs.getValue(Arg.SPLASH))
+              || aparser.contains("nosplash")
+              || Cache.getDefault("SPLASH", "true").equals("false");
       desktop = new Desktop();
       desktop.setInBatchMode(true); // indicate we are starting up
 
@@ -556,20 +729,37 @@ public class Jalview
           }
         }
 
-        if (!aparser.contains("nowebservicediscovery"))
+        boolean webservicediscovery = bootstrapArgs
+                .getBoolean(Arg.WEBSERVICEDISCOVERY);
+        if (aparser.contains("nowebservicediscovery"))
+          webservicediscovery = false;
+        if (webservicediscovery)
         {
           desktop.startServiceDiscovery();
         }
-        if (!aparser.contains("nousagestats"))
+        else
+        {
+          testoutput(argparser, Arg.WEBSERVICEDISCOVERY);
+        }
+
+        boolean usagestats = !bootstrapArgs.getBoolean(Arg.NOUSAGESTATS);
+        if (aparser.contains("nousagestats"))
+          usagestats = false;
+        if (usagestats)
         {
           startUsageStats(desktop);
+          testoutput(argparser, Arg.NOUSAGESTATS);
         }
         else
         {
-          System.err.println("CMD [-nousagestats] executed successfully!");
+          Console.outPrintln("CMD [-nousagestats] executed successfully!");
+          testoutput(argparser, Arg.NOUSAGESTATS);
         }
 
-        if (!aparser.contains("noquestionnaire"))
+        boolean questionnaire = bootstrapArgs.getBoolean(Arg.QUESTIONNAIRE);
+        if (aparser.contains("noquestionnaire"))
+          questionnaire = false;
+        if (questionnaire)
         {
           String url = aparser.getValue("questionnaire");
           if (url != null)
@@ -578,7 +768,7 @@ public class Jalview
             // questionnaire
             Console.debug("Starting questionnaire url at " + url);
             desktop.checkForQuestionnaire(url);
-            System.out.println("CMD questionnaire[-" + url
+            Console.outPrintln("CMD questionnaire[-" + url
                     + "] executed successfully!");
           }
           else
@@ -599,36 +789,56 @@ public class Jalview
         }
         else
         {
-          System.err
-                  .println("CMD [-noquestionnaire] executed successfully!");
+          Console.outPrintln(
+                  "CMD [-noquestionnaire] executed successfully!");
+          testoutput(argparser, Arg.QUESTIONNAIRE);
         }
 
-        if (!aparser.contains("nonews")
-                || Cache.getProperty("NONEWS") == null)
+        if ((!aparser.contains("nonews")
+                && Cache.getProperty("NONEWS") == null
+                && !"false".equals(bootstrapArgs.getValue(Arg.NEWS)))
+                || "true".equals(bootstrapArgs.getValue(Arg.NEWS)))
         {
           desktop.checkForNews();
         }
 
         if (!aparser.contains("nohtmltemplates")
-                || Cache.getProperty("NOHTMLTEMPLATES") == null)
+                && Cache.getProperty("NOHTMLTEMPLATES") == null)
         {
           BioJsHTMLOutput.updateBioJS();
         }
       }
     }
     // Run Commands from cli
-    boolean commandsSuccess = Commands.processArgs(argparser, headlessArg);
+    cmds = new Commands(argparser, headlessArg);
+    cmds.processArgs();
+    boolean commandsSuccess = cmds.argsWereParsed();
+
     if (commandsSuccess)
     {
-      Console.info("Successfully completed commands");
       if (headlessArg)
-        System.exit(0);
+      {
+        if (argparser.getBoolean(Arg.NOQUIT))
+        {
+          Console.warn(
+                  "Completed " + Arg.HEADLESS.getName() + " commands, but "
+                          + Arg.NOQUIT + " is set so not quitting!");
+        }
+        else
+        {
+          Jalview.exit("Successfully completed commands in headless mode",
+                  0);
+        }
+      }
+      Console.info("Successfully completed commands");
     }
     else
     {
-      Console.warn("Error when running commands");
       if (headlessArg)
-        System.exit(1);
+      {
+        Jalview.exit("Error when running Commands in headless mode", 1);
+      }
+      Console.warn("Error when running commands");
     }
 
     // Check if JVM and compile version might cause problems and log if it
@@ -641,25 +851,6 @@ public class Jalview
               + LaunchUtils.getJavaCompileVersion() + ".");
     }
 
-    // Move any new getdown-launcher-new.jar into place over old
-    // getdown-launcher.jar
-    String appdirString = System.getProperty("getdownappdir");
-    if (appdirString != null && appdirString.length() > 0)
-    {
-      final File appdir = new File(appdirString);
-      new Thread()
-      {
-        @Override
-        public void run()
-        {
-          LaunchUtil.upgradeGetdown(
-                  new File(appdir, "getdown-launcher-old.jar"),
-                  new File(appdir, "getdown-launcher.jar"),
-                  new File(appdir, "getdown-launcher-new.jar"));
-        }
-      }.start();
-    }
-
     String file = null, data = null;
 
     FileFormatI format = null;
@@ -674,10 +865,9 @@ public class Jalview
     groovyscript = aparser.getValue("groovy", true);
     file = aparser.getValue("open", true);
 
-    if (file == null && desktop == null)
+    if (file == null && desktop == null && !commandsSuccess)
     {
-      System.out.println("No files to open!");
-      System.exit(1);
+      Jalview.exit("No files to open!", 1);
     }
 
     long progress = -1;
@@ -691,7 +881,7 @@ public class Jalview
                         .getString("status.processing_commandline_args"),
                 progress = System.currentTimeMillis());
       }
-      System.out.println("CMD [-open " + file + "] executed successfully!");
+      Console.outPrintln("CMD [-open " + file + "] executed successfully!");
 
       if (!Platform.isJS())
       /**
@@ -704,11 +894,12 @@ public class Jalview
         {
           if (!(new File(file)).exists())
           {
-            System.out.println("Can't find " + file);
             if (headless)
             {
-              System.exit(1);
+              Jalview.exit(
+                      "Can't find file '" + file + "' in headless mode", 1);
             }
+            Console.warn("Can't find file'" + file + "'");
           }
         }
       }
@@ -727,7 +918,7 @@ public class Jalview
               format);
       if (af == null)
       {
-        System.out.println("error");
+        Console.outPrintln("error");
       }
       else
       {
@@ -742,7 +933,7 @@ public class Jalview
 
           if (cs != null)
           {
-            System.out.println(
+            Console.outPrintln(
                     "CMD [-colour " + data + "] executed successfully!");
           }
           af.changeColour(cs);
@@ -754,8 +945,8 @@ public class Jalview
         {
           af.parseFeaturesFile(data,
                   AppletFormatAdapter.checkProtocol(data));
-          // System.out.println("Added " + data);
-          System.out.println(
+          // Console.outPrintln("Added " + data);
+          Console.outPrintln(
                   "CMD groups[-" + data + "]  executed successfully!");
         }
         data = aparser.getValue("features", true);
@@ -763,8 +954,8 @@ public class Jalview
         {
           af.parseFeaturesFile(data,
                   AppletFormatAdapter.checkProtocol(data));
-          // System.out.println("Added " + data);
-          System.out.println(
+          // Console.outPrintln("Added " + data);
+          Console.outPrintln(
                   "CMD [-features " + data + "]  executed successfully!");
         }
 
@@ -772,8 +963,8 @@ public class Jalview
         if (data != null)
         {
           af.loadJalviewDataFile(data, null, null, null);
-          // System.out.println("Added " + data);
-          System.out.println(
+          // Console.outPrintln("Added " + data);
+          Console.outPrintln(
                   "CMD [-annotations " + data + "] executed successfully!");
         }
         // set or clear the sortbytree flag.
@@ -782,7 +973,7 @@ public class Jalview
           af.getViewport().setSortByTree(true);
           if (af.getViewport().getSortByTree())
           {
-            System.out.println("CMD [-sortbytree] executed successfully!");
+            Console.outPrintln("CMD [-sortbytree] executed successfully!");
           }
         }
         if (aparser.contains("no-annotation"))
@@ -790,7 +981,7 @@ public class Jalview
           af.getViewport().setShowAnnotation(false);
           if (!af.getViewport().isShowAnnotation())
           {
-            System.out.println("CMD no-annotation executed successfully!");
+            Console.outPrintln("CMD no-annotation executed successfully!");
           }
         }
         if (aparser.contains("nosortbytree"))
@@ -798,8 +989,8 @@ public class Jalview
           af.getViewport().setSortByTree(false);
           if (!af.getViewport().getSortByTree())
           {
-            System.out
-                    .println("CMD [-nosortbytree] executed successfully!");
+            Console.outPrintln(
+                    "CMD [-nosortbytree] executed successfully!");
           }
         }
         data = aparser.getValue("tree", true);
@@ -807,7 +998,7 @@ public class Jalview
         {
           try
           {
-            System.out.println(
+            Console.outPrintln(
                     "CMD [-tree " + data + "] executed successfully!");
             NewickFile nf = new NewickFile(data,
                     AppletFormatAdapter.checkProtocol(data));
@@ -815,127 +1006,135 @@ public class Jalview
                     .setCurrentTree(af.showNewickTree(nf, data).getTree());
           } catch (IOException ex)
           {
-            System.err.println("Couldn't add tree " + data);
+            jalview.bin.Console.errPrintln("Couldn't add tree " + data);
             ex.printStackTrace(System.err);
           }
         }
-        // TODO - load PDB structure(s) to alignment JAL-629
-        // (associate with identical sequence in alignment, or a specified
-        // sequence)
+
         if (groovyscript != null)
         {
           // Execute the groovy script after we've done all the rendering stuff
           // and before any images or figures are generated.
-          System.out.println("Executing script " + groovyscript);
+          Console.outPrintln("Executing script " + groovyscript);
           executeGroovyScript(groovyscript, af);
-          System.out.println("CMD groovy[" + groovyscript
+          Console.outPrintln("CMD groovy[" + groovyscript
                   + "] executed successfully!");
           groovyscript = null;
         }
         String imageName = "unnamed.png";
         while (aparser.getSize() > 1)
         {
-          String outputFormat = aparser.nextValue();
-          file = aparser.nextValue();
-
-          if (outputFormat.equalsIgnoreCase("png"))
-          {
-            af.createPNG(new File(file));
-            imageName = (new File(file)).getName();
-            System.out.println("Creating PNG image: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("svg"))
-          {
-            File imageFile = new File(file);
-            imageName = imageFile.getName();
-            af.createSVG(imageFile);
-            System.out.println("Creating SVG image: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("html"))
+          try
           {
-            File imageFile = new File(file);
-            imageName = imageFile.getName();
-            HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
-            htmlSVG.exportHTML(file);
+            String outputFormat = aparser.nextValue();
+            file = aparser.nextValue();
 
-            System.out.println("Creating HTML image: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("biojsmsa"))
-          {
-            if (file == null)
+            if (outputFormat.equalsIgnoreCase("png"))
             {
-              System.err.println("The output html file must not be null");
-              return;
+              Console.outPrintln("Creating PNG image: " + file);
+              af.createPNG(new File(file));
+              imageName = (new File(file)).getName();
+              continue;
             }
-            try
+            else if (outputFormat.equalsIgnoreCase("svg"))
+            {
+              Console.outPrintln("Creating SVG image: " + file);
+              File imageFile = new File(file);
+              imageName = imageFile.getName();
+              af.createSVG(imageFile);
+              continue;
+            }
+            else if (outputFormat.equalsIgnoreCase("html"))
             {
-              BioJsHTMLOutput.refreshVersionInfo(
-                      BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
-            } catch (URISyntaxException e)
+              File imageFile = new File(file);
+              imageName = imageFile.getName();
+              HtmlSvgOutput htmlSVG = new HtmlSvgOutput(af.alignPanel);
+
+              Console.outPrintln("Creating HTML image: " + file);
+              htmlSVG.exportHTML(file);
+              continue;
+            }
+            else if (outputFormat.equalsIgnoreCase("biojsmsa"))
             {
-              e.printStackTrace();
+              if (file == null)
+              {
+                jalview.bin.Console.errPrintln(
+                        "The output html file must not be null");
+                return;
+              }
+              try
+              {
+                BioJsHTMLOutput.refreshVersionInfo(
+                        BioJsHTMLOutput.BJS_TEMPLATES_LOCAL_DIRECTORY);
+              } catch (URISyntaxException e)
+              {
+                e.printStackTrace();
+              }
+              BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
+              Console.outPrintln(
+                      "Creating BioJS MSA Viwer HTML file: " + file);
+              bjs.exportHTML(file);
+              continue;
             }
-            BioJsHTMLOutput bjs = new BioJsHTMLOutput(af.alignPanel);
-            bjs.exportHTML(file);
-            System.out
-                    .println("Creating BioJS MSA Viwer HTML file: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("imgMap"))
-          {
-            af.createImageMap(new File(file), imageName);
-            System.out.println("Creating image map: " + file);
-            continue;
-          }
-          else if (outputFormat.equalsIgnoreCase("eps"))
-          {
-            File outputFile = new File(file);
-            System.out.println(
-                    "Creating EPS file: " + outputFile.getAbsolutePath());
-            af.createEPS(outputFile);
-            continue;
-          }
-          FileFormatI outFormat = null;
-          try
-          {
-            outFormat = FileFormats.getInstance().forName(outputFormat);
-          } catch (Exception formatP)
-          {
-            System.out.println("Couldn't parse " + outFormat
-                    + " as a valid Jalview format string.");
-          }
-          if (outFormat != null)
-          {
-            if (!outFormat.isWritable())
+            else if (outputFormat.equalsIgnoreCase("imgMap"))
+            {
+              Console.outPrintln("Creating image map: " + file);
+              af.createImageMap(new File(file), imageName);
+              continue;
+            }
+            else if (outputFormat.equalsIgnoreCase("eps"))
+            {
+              File outputFile = new File(file);
+              Console.outPrintln(
+                      "Creating EPS file: " + outputFile.getAbsolutePath());
+              af.createEPS(outputFile);
+              continue;
+            }
+
+            FileFormatI outFormat = null;
+            try
             {
-              System.out.println(
-                      "This version of Jalview does not support alignment export as "
-                              + outputFormat);
+              outFormat = FileFormats.getInstance().forName(outputFormat);
+            } catch (Exception formatP)
+            {
+              Console.outPrintln("Couldn't parse " + outFormat
+                      + " as a valid Jalview format string.");
             }
-            else
+            if (outFormat != null)
             {
-              af.saveAlignment(file, outFormat);
-              if (af.isSaveAlignmentSuccessful())
+              if (!outFormat.isWritable())
               {
-                System.out.println("Written alignment in "
-                        + outFormat.getName() + " format to " + file);
+                Console.outPrintln(
+                        "This version of Jalview does not support alignment export as "
+                                + outputFormat);
               }
               else
               {
-                System.out.println("Error writing file " + file + " in "
-                        + outFormat.getName() + " format!!");
+                af.saveAlignment(file, outFormat);
+                if (af.isSaveAlignmentSuccessful())
+                {
+                  Console.outPrintln("Written alignment in "
+                          + outFormat.getName() + " format to " + file);
+                }
+                else
+                {
+                  Console.outPrintln("Error writing file " + file + " in "
+                          + outFormat.getName() + " format!!");
+                }
               }
             }
+          } catch (ImageOutputException ioexc)
+          {
+            Console.outPrintln(
+                    "Unexpected error whilst exporting image to " + file);
+            ioexc.printStackTrace();
           }
 
         }
 
         while (aparser.getSize() > 0)
         {
-          System.out.println("Unknown arg: " + aparser.nextValue());
+          Console.outPrintln("Unknown arg: " + aparser.nextValue());
         }
       }
     }
@@ -947,7 +1146,8 @@ public class Jalview
 
     if (!Platform.isJS() && !headless && file == null
             && Cache.getDefault("SHOW_STARTUP_FILE", true)
-            && !Commands.commandArgsProvided())
+            && !cmds.commandArgsProvided()
+            && !bootstrapArgs.getBoolean(Arg.NOSTARTUPFILE))
     // don't open the startup file if command line args have been processed
     // (&& !Commands.commandArgsProvided())
     /**
@@ -1002,12 +1202,12 @@ public class Jalview
     {
       if (Cache.groovyJarsPresent())
       {
-        System.out.println("Executing script " + groovyscript);
+        Console.outPrintln("Executing script " + groovyscript);
         executeGroovyScript(groovyscript, startUpAlframe);
       }
       else
       {
-        System.err.println(
+        jalview.bin.Console.errPrintln(
                 "Sorry. Groovy Support is not available, so ignoring the provided groovy script "
                         + groovyscript);
       }
@@ -1025,88 +1225,102 @@ public class Jalview
 
   private static void setLookAndFeel()
   {
-    // property laf = "crossplatform", "system", "gtk", "metal", "nimbus",
-    // "mac" or "flat"
-    // If not set (or chosen laf fails), use the normal SystemLaF and if on Mac,
-    // try Quaqua/Vaqua.
-    String lafProp = System.getProperty("laf");
-    String lafSetting = Cache.getDefault("PREFERRED_LAF", null);
-    String laf = "none";
-    if (lafProp != null)
-    {
-      laf = lafProp;
-    }
-    else if (lafSetting != null)
-    {
-      laf = lafSetting;
-    }
-    boolean lafSet = false;
-    switch (laf)
+    if (!Platform.isJS())
+    /**
+     * Java only
+     * 
+     * @j2sIgnore
+     */
     {
-    case "crossplatform":
-      lafSet = setCrossPlatformLookAndFeel();
-      if (!lafSet)
+      // property laf = "crossplatform", "system", "gtk", "metal", "nimbus",
+      // "mac" or "flat"
+      // If not set (or chosen laf fails), use the normal SystemLaF and if on
+      // Mac,
+      // try Quaqua/Vaqua.
+      String lafProp = System.getProperty("laf");
+      String lafSetting = Cache.getDefault("PREFERRED_LAF", null);
+      String laf = "none";
+      if (lafProp != null)
       {
-        Console.error("Could not set requested laf=" + laf);
+        laf = lafProp;
       }
-      break;
-    case "system":
-      lafSet = setSystemLookAndFeel();
-      if (!lafSet)
+      else if (lafSetting != null)
       {
-        Console.error("Could not set requested laf=" + laf);
+        laf = lafSetting;
       }
-      break;
-    case "gtk":
-      lafSet = setGtkLookAndFeel();
-      if (!lafSet)
+      boolean lafSet = false;
+      switch (laf)
       {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "metal":
-      lafSet = setMetalLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "nimbus":
-      lafSet = setNimbusLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "flat":
-      lafSet = setFlatLookAndFeel();
-      if (!lafSet)
-      {
-        Console.error("Could not set requested laf=" + laf);
+      case "crossplatform":
+        lafSet = setCrossPlatformLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "system":
+        lafSet = setSystemLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "gtk":
+        lafSet = setGtkLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "metal":
+        lafSet = setMetalLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "nimbus":
+        lafSet = setNimbusLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "flat":
+        lafSet = setFlatLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "mac":
+        lafSet = setMacLookAndFeel();
+        if (!lafSet)
+        {
+          Console.error("Could not set requested laf=" + laf);
+        }
+        break;
+      case "none":
+        break;
+      default:
+        Console.error("Requested laf=" + laf + " not implemented");
       }
-      break;
-    case "mac":
-      lafSet = setMacLookAndFeel();
       if (!lafSet)
       {
-        Console.error("Could not set requested laf=" + laf);
-      }
-      break;
-    case "none":
-      break;
-    default:
-      Console.error("Requested laf=" + laf + " not implemented");
-    }
-    if (!lafSet)
-    {
-      setSystemLookAndFeel();
-      if (Platform.isLinux())
-      {
-        setLinuxLookAndFeel();
-      }
-      if (Platform.isMac())
-      {
-        setMacLookAndFeel();
+        // Flatlaf default for everyone!
+        lafSet = setFlatLookAndFeel();
+        if (!lafSet)
+        {
+          setSystemLookAndFeel();
+        }
+        if (Platform.isLinux())
+        {
+          setLinuxLookAndFeel();
+        }
+        if (Platform.isMac())
+        {
+          setMacLookAndFeel();
+        }
       }
     }
   }
@@ -1288,7 +1502,7 @@ public class Jalview
       UIManager.put("TabbedPane.tabType", "card");
       UIManager.put("TabbedPane.showTabSeparators", true);
       UIManager.put("TabbedPane.showContentSeparator", true);
-      UIManager.put("TabbedPane.tabSeparatorsFullHeight", true);
+      // UIManager.put("TabbedPane.tabSeparatorsFullHeight", true);
       UIManager.put("TabbedPane.tabsOverlapBorder", true);
       UIManager.put("TabbedPane.hasFullBorder", true);
       UIManager.put("TabbedPane.tabLayoutPolicy", "scroll");
@@ -1296,6 +1510,8 @@ public class Jalview
       UIManager.put("TabbedPane.smoothScrolling", true);
       UIManager.put("TabbedPane.tabWidthMode", "compact");
       UIManager.put("TabbedPane.selectedBackground", Color.white);
+      UIManager.put("TabbedPane.background", new Color(236, 236, 236));
+      UIManager.put("TabbedPane.hoverColor", Color.lightGray);
     }
 
     Desktop.setLiveDragMode(Cache.getDefault("FLAT_LIVE_DRAG_MODE", true));
@@ -1333,9 +1549,10 @@ public class Jalview
     return set;
   }
 
+  /*
   private static void showUsage()
   {
-    System.out.println(
+    jalview.bin.Console.outPrintln(
             "Usage: jalview -open [FILE] [OUTPUT_FORMAT] [OUTPUT_FILE]\n\n"
                     + "-nodisplay\tRun Jalview without User Interface.\n"
                     + "-props FILE\tUse the given Jalview properties file instead of users default.\n"
@@ -1361,7 +1578,7 @@ public class Jalview
                     + "-questionnaire URL\tQueries the given URL for information about any Jalview user questionnaires.\n"
                     + "-noquestionnaire\tTurn off questionnaire check.\n"
                     + "-nonews\tTurn off check for Jalview news.\n"
-                    + "-nousagestats\tTurn off google analytics tracking for this session.\n"
+                    + "-nousagestats\tTurn off analytics tracking for this session.\n"
                     + "-sortbytree OR -nosortbytree\tEnable or disable sorting of the given alignment by the given tree\n"
                     // +
                     // "-setprop PROPERTY=VALUE\tSet the given Jalview property,
@@ -1375,6 +1592,7 @@ public class Jalview
                     + "-jvmmemmax=MAXMEMORY\tOnly available with standalone executable jar or jalview.bin.Launcher. 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. See https://www.jalview.org/help/html/memory.html for more details.\n"
                     + "\n~Read documentation in Application or visit https://www.jalview.org for description of Features and Annotations file~\n\n");
   }
+  */
 
   private static void startUsageStats(final Desktop desktop)
   {
@@ -1382,18 +1600,16 @@ public class Jalview
      * start a User Config prompt asking if we can log usage statistics.
      */
     PromptUserConfig prompter = new PromptUserConfig(Desktop.desktop,
-            "USAGESTATS", "Jalview Usage Statistics",
-            "Do you want to help make Jalview better by enabling "
-                    + "the collection of usage statistics with Google Analytics ?"
-                    + "\n\n(you can enable or disable usage tracking in the preferences)",
+            "USAGESTATS",
+            MessageManager.getString("prompt.plausible_analytics_title"),
+            MessageManager.getString("prompt.plausible_analytics"),
             new Runnable()
             {
               @Override
               public void run()
               {
-                Console.debug(
-                        "Initialising googletracker for usage stats.");
-                Cache.initGoogleTracker();
+                Console.debug("Initialising analytics for usage stats.");
+                Cache.initAnalytics();
                 Console.debug("Tracking enabled.");
               }
             }, new Runnable()
@@ -1401,7 +1617,7 @@ public class Jalview
               @Override
               public void run()
               {
-                Console.debug("Not enabling Google Tracking.");
+                Console.debug("Not enabling analytics.");
               }
             }, null, true);
     desktop.addDialogThread(prompter);
@@ -1416,7 +1632,7 @@ public class Jalview
    *          the Jalview Desktop object passed in to the groovy binding as the
    *          'Jalview' object.
    */
-  private void executeGroovyScript(String groovyscript, AlignFrame af)
+  protected void executeGroovyScript(String groovyscript, AlignFrame af)
   {
     /**
      * for scripts contained in files
@@ -1447,9 +1663,10 @@ public class Jalview
 
       } catch (Exception ex)
       {
-        System.err.println("Failed to read from STDIN into tempfile "
-                + ((tfile == null) ? "(tempfile wasn't created)"
-                        : tfile.toString()));
+        jalview.bin.Console
+                .errPrintln("Failed to read from STDIN into tempfile "
+                        + ((tfile == null) ? "(tempfile wasn't created)"
+                                : tfile.toString()));
         ex.printStackTrace();
         return;
       }
@@ -1458,7 +1675,7 @@ public class Jalview
         sfile = tfile.toURI().toURL();
       } catch (Exception x)
       {
-        System.err.println(
+        jalview.bin.Console.errPrintln(
                 "Unexpected Malformed URL Exception for temporary file created from STDIN: "
                         + tfile.toURI());
         x.printStackTrace();
@@ -1475,17 +1692,20 @@ public class Jalview
         tfile = new File(groovyscript);
         if (!tfile.exists())
         {
-          System.err.println("File '" + groovyscript + "' does not exist.");
+          jalview.bin.Console.errPrintln(
+                  "File '" + groovyscript + "' does not exist.");
           return;
         }
         if (!tfile.canRead())
         {
-          System.err.println("File '" + groovyscript + "' cannot be read.");
+          jalview.bin.Console.errPrintln(
+                  "File '" + groovyscript + "' cannot be read.");
           return;
         }
         if (tfile.length() < 1)
         {
-          System.err.println("File '" + groovyscript + "' is empty.");
+          jalview.bin.Console
+                  .errPrintln("File '" + groovyscript + "' is empty.");
           return;
         }
         try
@@ -1493,7 +1713,7 @@ public class Jalview
           sfile = tfile.getAbsoluteFile().toURI().toURL();
         } catch (Exception ex)
         {
-          System.err.println("Failed to create a file URL for "
+          jalview.bin.Console.errPrintln("Failed to create a file URL for "
                   + tfile.getAbsoluteFile());
           return;
         }
@@ -1518,8 +1738,9 @@ public class Jalview
       }
     } catch (Exception e)
     {
-      System.err.println("Exception Whilst trying to execute file " + sfile
-              + " as a groovy script.");
+      jalview.bin.Console
+              .errPrintln("Exception Whilst trying to execute file " + sfile
+                      + " as a groovy script.");
       e.printStackTrace(System.err);
 
     }
@@ -1548,7 +1769,7 @@ public class Jalview
   public void quit()
   {
     // System.exit will run the shutdownHook first
-    System.exit(0);
+    Jalview.exit("Quitting now. Bye!", 0);
   }
 
   public static AlignFrame getCurrentAlignFrame()
@@ -1560,4 +1781,206 @@ public class Jalview
   {
     Jalview.currentAlignFrame = currentAlignFrame;
   }
+
+  public Commands getCommands()
+  {
+    return cmds;
+  }
+
+  public static void exit(String message, int exitcode)
+  {
+    if (Console.log == null)
+    {
+      // Don't start the logger just to exit!
+      if (message != null)
+      {
+        if (exitcode == 0)
+        {
+          Console.outPrintln(message);
+        }
+        else
+        {
+          jalview.bin.Console.errPrintln(message);
+        }
+      }
+    }
+    else
+    {
+      Console.debug("Using Jalview.exit");
+      if (message != null)
+      {
+        if (exitcode == 0)
+        {
+          Console.info(message);
+        }
+        else
+        {
+          Console.error(message);
+        }
+      }
+    }
+    if (exitcode > -1)
+    {
+      System.exit(exitcode);
+    }
+  }
+
+  /******************************
+   * 
+   * TEST OUTPUT METHODS
+   * 
+   * these operate only when Arg.TESTOUTPUT has been passed, and variously check
+   * if an expected value / arg was set and report it to the test framework.
+   * 
+   ******************************/
+  /**
+   * report string values parsed/processed during tests When the Bootstrap
+   * argument Arg.TESTOUTPUT is present - reports on debug if given s1 is not
+   * null and not equals s2, warns if given argument is not set, and calls
+   * testoutput(true,a,s1,s2) to report processing progress.
+   * 
+   * @param ap
+   *          - ArgParser handling parsing
+   * @param a
+   *          - Arg currently being processed
+   * @param s1
+   *          - expected 
+   * @param s2
+   */
+  protected static void testoutput(ArgParser ap, Arg a, String s1,
+          String s2)
+  {
+    BootstrapArgs bsa = ap.getBootstrapArgs();
+    if (!bsa.getBoolean(Arg.TESTOUTPUT))
+      return;
+    if (!((s1 == null && s2 == null) || (s1 != null && s1.equals(s2))))
+    {
+      Console.debug("testoutput with unmatching values '" + s1 + "' and '"
+              + s2 + "' for arg " + a.argString());
+      return;
+    }
+    boolean isset = a.hasOption(Opt.BOOTSTRAP) ? bsa.contains(a)
+            : ap.isSet(a);
+    if (!isset)
+    {
+      Console.warn("Arg '" + a.getName() + "' not set at all");
+      return;
+    }
+    testoutput(true, a, s1, s2);
+  }
+
+  /**
+   * report values passed via bootstrap arguments
+   * 
+   * TODO: significant code duplication with testouput(Argparser...) - move it
+   */
+
+  protected static void testoutput(BootstrapArgs bsa, Arg a, String s1,
+          String s2)
+  {
+    if (!bsa.getBoolean(Arg.TESTOUTPUT))
+      return;
+    if (!((s1 == null && s2 == null) || (s1 != null && s1.equals(s2))))
+    {
+      Console.debug("testoutput with unmatching values '" + s1 + "' and '"
+              + s2 + "' for arg " + a.argString());
+      return;
+    }
+    if (!a.hasOption(Opt.BOOTSTRAP))
+    {
+      Console.error("Non-bootstrap Arg '" + a.getName()
+              + "' given to testoutput(BootstrapArgs bsa, Arg a, String s1, String s2) with only BootstrapArgs");
+    }
+    if (!bsa.contains(a))
+    {
+      Console.warn("Arg '" + a.getName() + "' not set at all");
+      return;
+    }
+    testoutput(true, a, s1, s2);
+  }
+
+  /**
+   * conditionally (on @param yes) report that expected value s1 was set during CommandsTest tests
+   */
+  private static void testoutput(boolean yes, Arg a, String s1, String s2)
+  {
+    if (yes && ((s1 == null && s2 == null)
+            || (s1 != null && s1.equals(s2))))
+    {
+      Console.outPrintln("[TESTOUTPUT] arg " + a.argString() + "='" + s1
+              + "' was set");
+    }
+  }
+
+  /*
+   * testoutput for boolean and unary values
+   */
+  protected static void testoutput(ArgParser ap, Arg a)
+  {
+    if (ap == null)
+      return;
+    BootstrapArgs bsa = ap.getBootstrapArgs();
+    if (bsa == null)
+      return;
+    if (!bsa.getBoolean(Arg.TESTOUTPUT))
+      return;
+    boolean val = a.hasOption(Opt.BOOTSTRAP) ? bsa.getBoolean(a)
+            : ap.getBoolean(a);
+    boolean isset = a.hasOption(Opt.BOOTSTRAP) ? bsa.contains(a)
+            : ap.isSet(a);
+    if (!isset)
+    {
+      Console.warn("Arg '" + a.getName() + "' not set at all");
+      return;
+    }
+    testoutput(val, a);
+  }
+
+  protected static void testoutput(BootstrapArgs bsa, Arg a)
+  {
+    if (!bsa.getBoolean(Arg.TESTOUTPUT))
+      return;
+    if (!a.hasOption(Opt.BOOTSTRAP))
+    {
+      Console.warn("Non-bootstrap Arg '" + a.getName()
+              + "' given to testoutput(BootstrapArgs bsa, Arg a) with only BootstrapArgs");
+
+    }
+    if (!bsa.contains(a))
+    {
+      Console.warn("Arg '" + a.getName() + "' not set at all");
+      return;
+    }
+    testoutput(bsa.getBoolean(a), a);
+  }
+
+  private static void testoutput(boolean yes, Arg a)
+  {
+    String message = null;
+    if (a.hasOption(Opt.BOOLEAN))
+    {
+      message = (yes ? a.argString() : a.negateArgString()) + " was set";
+    }
+    else if (a.hasOption(Opt.UNARY))
+    {
+      message = a.argString() + (yes ? " was set" : " was not set");
+    }
+    Console.outPrintln("[TESTOUTPUT] arg " + message);
+  }
+
+  public ArgParser getArgParser()
+  {
+    return argparser;
+  }
+
+  public BootstrapArgs getBootstrapArgs()
+  {
+    return bootstrapArgs;
+  }
+
+  public static boolean isBatchMode()
+  {
+    return getInstance()!=null && (getInstance().desktop == null || getInstance().desktop.isInBatchMode());
+  }
+
 }