package nxm.sys.lib;

import java.awt.GraphicsEnvironment;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteOrder;           // Used to determine byte ordering
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;

import nxm.sys.inc.CommandParent;
import nxm.sys.inc.Commandable;
import nxm.sys.inc.Debug;
import nxm.sys.inc.Installation;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.MidasReference;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.ShellParent;
import nxm.sys.inc.Verify;
import nxm.sys.lib.FileUtil.FnFilter;

/**
  A class to perform shell functions.  It is responsible for initializing
  the Midas environment and presenting a terminal for command line entry
  and text based output.

  It also contains methods for parsing command lines into Args tables.

  Upon exit, it can save the current session to a file.  Upon entry, the
  session can be initialized to any saved session file.

  @author Jeff Schoen
*/
public class Shell implements Runnable, MidasReference {

  private static final int    RUN_TO_COMPLETION = 0x1;               // Run Mode: Run to completion
  private static final int    RUN_DEFAULT       = 0x0;               // Default Run Mode
  private static final String cmdRunModeList    = "RunToCompletion"; // Run Mode List

  /** resource name to find this class in the ClassLoader. */
  private static final String CLASS_RESOURCE_NAME = "nxm/sys/lib/Shell.class"; // NOTE: resource name should NOT start with "/"

  /** The base string used by default for the main prompt */
  public static final String PROMPT_STRING = "nM";

  /** The extra prompt characters pipe mode */
  public static final String PROMPT_STRING_PIPE = "-Pipe";

  /** The extra prompt characters suspend mode */
  public static final String PROMPT_STRING_SUSPEND = "-Suspend";

  /** The location of the javadoc directory. */
  private static final String JAVADOC_PATH = getNmRoot() + "htdocs" + File.separator + "api" + File.separator;

  /** The Midas context / environment */
  @InternalUseOnly("Only meant for read only access")
  public Midas M = new Midas();

  /** The object that created the shell, or null for Shell.main */
  public ShellParent parent;

  private String  cmdline  = null;          // Current command line.
  private boolean configured = false;       // Has %nmstartup been run?
  private Command command;                  // Current command being run.
  private int     commands;                 // Number of commands run (executed) on CLI
  private Stack   history  = new Stack(64); // Command history.
  private boolean autosave = true;          // Save command history before running a command?

  /** @deprecated Since NeXtMidas 3.7.0, this was never set */
  @Deprecated protected static String javaVersion; // The version of the Java VM. TODO: remove in 3.9+
  protected static boolean  isOneShot;             // Indicates if the shell is in one-shot mode.
  protected static String   nmroot;                // The NMROOT path variable.
  protected static Shell    currentShell;          // The current shell.
  protected static Table    options = new Table(Table.SLHM); // The OPTS table.
  protected static int      privs   = 0;           // Current privileges.
  protected static boolean  noExternalCmds=false;  // Forces OS commands to run inside Shell.
  protected static Method   nmClassLoaderAddUrl;   // The NmClassLoader.addURL(URL) method
  protected static URLClassLoader nmClassLoader;   // The NmClassLoader being used.
  protected static ClassLoader    loader;          // The class loader.
  protected static Theme    theme;                 // The current theme.

  /** Info about the Java VM (rarely used).
   * @deprecated Since NeXtMidas 3.7.0, this was never set and will be removed in 3.9+
   */
  @Deprecated @InternalUseOnly("Since NeXtMidas 3.5.4, this was never set")
  public  static String vminfo; // TODO remove in 3.9+ since it was marked internal use in 3.5.x

  /** The OS byte representation ("IEEE"/"EEEI"/"VAX"). */
  public  static final String osrep;
  /** The OS byte representation id. */
  public  static final byte   rep;
  /** The full name of the operating system ("Linux"/etc). */
  public  static final String osname;
  /** The OS tag used to mark libraries ("lin"/"sol"/etc). */
  public  static final String ostag;
  /** The type of the operating system (often "UNIX"/"DOS"). */
  public  static final String ostype;
  /** The Java version from "java.version" system property. Since 3.7.0 */
          static final String javaversion;
  /** The Java major version number (i.e. 7, 8, 9, 10, 11) */
  private static final int    javaMajorVersion ;

  /** Is the operating system version RHEL/CentOS 7?
   * @since NeXtMidas 3.8.1
   */
  public static final boolean iscentos7; // NXM-3213
  /** The name of the class loader. */
  public  static String currentLoader;
  /** The NeXtMidas PID (unique per NeXtMidas session) */
  private static String nmpid;
  /** Should shell bootup build the classpath entries for all option trees in current PATH? */
  private boolean rebuildClasspath = false;
  /** Native Libraries already loaded from Gradle Jar
   *  Since all the native code for the option tree ends up in one .so, this only should be loaded once
   *  @since NeXtMidas 3.9.3 for NXM-3561
   */
  private static Set<String> loadedLibraries = new HashSet<>();

  private static String urlNeXtMidas = "";
  private static boolean gradleBuildCommand = false;

  /** Whether or not to enable fix for JVM hang with JSetUp */
  private static boolean jvmHangWorkaround = false;

  private static ClassLoader nxmLoader = Shell.class.getClassLoader();

  // since 3.7.0 - static initializer, moved here from Shell.boot()
  static {
    String osarch, osVersion, tmp;
    // All RIAs can retrieve the secure system properties used below.
    // See https://docs.oracle.com/javase/tutorial/deployment/doingMoreWithRIA/properties.html
    osarch = System.getProperty("os.arch", "unknown");
    osname = System.getProperty("os.name", "unknown");
    javaversion = System.getProperty("java.version", "0.0.0"); // since 3.7.0
    javaMajorVersion = calcJavaMajorVersion(); // since 4.0.1 - relies on javaversion, so that must be set first

    osVersion = System.getProperty("os.version","unknown");
    iscentos7 = osVersion.contains("el7"); // Check if it is RHEL/Centos 7 since 3.8.1 NXM-3213

    tmp = osname.toLowerCase();
    boolean isVMS = tmp.contains("vms");
    boolean isWin = tmp.contains("win");

    // set OS tag for native DLL / shared library (so) loading
    if      (isWin)                   ostag = "win";
    else if (isVMS)                   ostag = "vms";
    else if (osarch.contains("arm") ) ostag = "arm";
    else if (tmp.contains("linux")  ) ostag = "lin";
    else if (tmp.contains("solaris")) ostag = "sol";
    else if (tmp.contains("sun")    ) ostag = "sol";
    else if (tmp.contains("sgi")    ) ostag = "sgi";
    else if (tmp.contains("hp-ux")  ) ostag = "hpux";
    else if (tmp.contains("mac")    ) ostag = "osx";
    else if (tmp.contains("freebsd")) ostag = "bsd";
    else                              ostag = tmp.substring(0, 3);

    if      (isWin)  ostype = "DOS";
    else if (isVMS)  ostype = "VMS";
    else             ostype = "UNIX";

    // OS data representation (byte order)
    osrep = isVMS? "VAX" : (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN)? "IEEE": "EEEI";
    rep   = (byte) osrep.charAt(0);

    urlNeXtMidas = FileUtil.changePathSeparator(nxmLoader.getResource("nxm/sys/lib/NeXtMidas.class").toString(),"/");
    gradleBuildCommand = urlNeXtMidas.contains("build/classes/java/main/");
  }

  /**
   * Helper method to grab the java exectuable path.
   * @return java executable location
   */
  public static String getJavaHome() {
    return System.getenv("JAVAHOME") + File.separator + "bin" + File.separator + "java";
  }

  /** Creates a new instance. */
  public Shell () {
    classInit();
  }

  /** Creates a new instance with the given parent.
      @param parent The parent to use
   */
  public Shell (ShellParent parent) {
    this();
    this.parent = parent;
    currentShell = this;
  }

  /** Creates a new instance based from the specified Midas context.
      Since 3.3.1, this propagates the parent's history to the new instance.
      @param midas  The Midas context to initialize this shell from (Must NOT be null)
      @param attach Attach to/use the specified Midas context for this instance
      @since NeXtMidas 3.3.0
   */
   public Shell (Midas midas, boolean attach) {
     this();
     setAutosave(false);    // Don't want to save state since not the primary shell.
     Object previousShell = midas.registry.get("SHELL");  // since 3.3.1: Propagate the parent's history to new instance (BUG 1758)
     if (previousShell instanceof Shell) {
       Stack prevHistory = ((Shell) previousShell).getHistory();
       history = new Stack(prevHistory);
       parent = ((Shell) previousShell).parent; // since 3.3.4: propagate ShellParent reference for Web Start (BUG 2760)
     }
     // end since 3.3.1: Propagate the parent's history to new Shell instance (BUG 1758)
     if (!attach) {
       M = Macro.cloneContext(midas, false, 0); // clone Midas context (creating new RESULTS and PIPES/RAM Tables)
       M.registry.put("SHELL", this);           // add reference to this Shell instance
       M.registry.put("HANDLERS", Registry.handlers); // since 3.3.4: set REG.HANDLERS reference (Bug 2760)
       M.detach();
     } else {
       M = midas;           // attach to existing Midas context (uses same reference)
     }
   }

  /** Initialize the class. */
  private void classInit () {
    /* EXTERNAL OS COMMAND INIT */
    final String noExtCmdProp = Shell.getProperty("NOEXTERNAL");

    if (noExtCmdProp != null) {
      noExternalCmds = noExternalCmds || StringUtil.isTrue(noExtCmdProp);
    }

    /* CLASS LOADER INIT */
    ClassLoader classLoader = Shell.class.getClassLoader();
    Class<?>       clazz    = classLoader.getClass();

    if (clazz.getName().equals("NmClassLoader")) {
      try {
        nmClassLoaderAddUrl = clazz.getMethod("addURL", URL.class);
        nmClassLoader       = (URLClassLoader)classLoader;

      } catch (NoSuchMethodException e) {
        M.printStackTrace("ERROR: Can't initialize class loader in Shell.", e);
        nmClassLoader = null; // Prevent the use of the custom class loader.
      }
    }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Set a user environment privilege
      @param privilege integer privilege*/
  public static void setPriv (int privilege) { privs |= privilege; }

  /** Tests a user environment privilege
   *  @param privilege privilege number
   *  @return true if user has that privilege
   */
  public static boolean getPriv (int privilege) {
    return ((privs&privilege) != 0);
  }

  /** Return the history stack
      @return stack history*/
  public Stack getHistory () {
    return history;
  }

  /**
   * Utility method that lets program know if it was built by Gradle or not
   * @return if the jar was built by Gradle
   */
  @InternalUseOnly
  public static boolean isGradleBuild () {
    if(Boolean.valueOf(System.getenv("ISNEXTMIDASGRADLEBUILD"))) return true;
    Class<Shell> clazz = Shell.class;
    String classname = clazz.getSimpleName() + ".class";
    String classPath = clazz.getResource(classname).toString();
    if (!classPath.startsWith("jar")) return false;
    String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) + "/META-INF/MANIFEST.MF";
    Manifest manifest;
    try {
      manifest = new Manifest((new URI(manifestPath)).toURL().openStream());
    } catch (IOException | URISyntaxException e) {
      // no manifeset, can't be gradle build
      return false;
    }
    Attributes attr = manifest.getMainAttributes();
    String value = attr.getValue("Gradle-Build");
    return value!=null;
  }

  /**
   * Utility method that lets program know if it is currently being built by Gradle or not
   * @return if the program is currently being built by Gradle
   */
  @InternalUseOnly
  public static boolean isGradleBuildCommand () {
    return gradleBuildCommand;
  }

  /** Utility method that lets program know if it is currently being run from a jar file
   *  @return if the program is currently being run from a jar file
   */
  @InternalUseOnly
  public static boolean isRunningFromJarFile () {
    return nmroot.startsWith("jar:") || isWebStart() || nmroot.startsWith("classpath:");
  }

  /** Set if opt-in for fix for JVM hang with JSetUp is enabled
   *
   *  @param enabled true to enable opt-in for fix for JVM hang with JSetUp
   *  @since NeXtMidas 4.1.4
   */
  @ProvisionalUseOnly("API may change")
  public static void setJVMHangWorkaround (boolean enabled) {
    Shell.jvmHangWorkaround = enabled;
  }

  /** Get if opt-in for fix for JVM hang with JSetUp is enabled
   *
   *  @return true if opt-in for fix for JVM hang with JSetUp is enabled
   *  @since NeXtMidas 4.1.4
   */
  @ProvisionalUseOnly("API may change")
  public static boolean getJVMHangWorkaround () {
    return Shell.jvmHangWorkaround;
  }

  /** Get if need to call removeNotify(), based on the following criteria:
   *    1. if opt-in for fix for JVM hang with JSetUp is enabled
   *    2. if running from a jar file
   *
   *  @return true if need to call removeNotify()
   *  @since NeXtMidas 4.1.4
   */
  @InternalUseOnly
  public static boolean needRemoveNotifyToAvoidJVMHang () {
    return Shell.jvmHangWorkaround && isRunningFromJarFile();
  }

  /** Get the NeXtMidas root (codebase). When NeXtMidas is run as a stand-alone application on
      Unix this will usually be the same as <code>$NMROOT</code>.
      @return The NeXtMidas root.
   */
  public static String getNmRoot () {
    if (nmroot==null) nmroot=getProperty("NMROOT");
    return nmroot;
  }

  /** Run the shell as a standalone executable.
      NOTE: this calls <code>System.exit(0)</code> when done to make sure it exits JVM after it is done.
      @param args Any command-line arguments.
   */
  public static void main (String[] args) {
    if ("Mac OS X".equalsIgnoreCase(osname)) {
      initForMacOsX(); // OS X Fixes
    }

    String arglist="";
    if (args.length>0) arglist=args[0];
    for (int i=1; i<args.length; i++) arglist += " "+args[i];

    isOneShot = arglist.length() > 0;

    int exitCode = 0;
    try {
      Shell.runNew (null,arglist,false);
    } catch (Throwable t) { // BUG 2818
      exitCode = 1; // Flag error exit status
      t.printStackTrace();
    } finally {
      System.exit(exitCode); // make sure to exit application even if stuff are backgrounded
    }
  }

  /** starts a shell to run one command or the command line interpreter
   *  @param parent  parent for new Shell
   *  @param cmdline the one command to run
   *  @param bg      true if to run in background
   *  @return        the new Shell
   */
  public static Shell runNew (ShellParent parent, String cmdline, boolean bg) {
    currentShell = new Shell();
    Midas shellM = currentShell.M;
    if (shellM.isAutoDetectingBehavior()) shellM.setIfJavaBehavior(false); // NXM-4097
    currentShell.run(parent,cmdline,bg);
    return currentShell;
  }

  /** run
   *
   *  @param parent  parent
   *  @param cmdline Command line (as String)
   *  @param bg      true if should run in background
   */
  public void run (ShellParent parent, String cmdline, boolean bg) {
    this.parent = parent;
    this.cmdline = cmdline;
    if (bg) {
      Thread thread = new Thread(this);
      thread.start();
    }
    else run();
  }

  @Override
  @SuppressWarnings("deprecation")
  public void run () {
    // handle security access
    setPriv(Privileges.PRIV_ALL);

    open();
    process();
    close();
  }

  /** Try to figure out (detect) NMROOT based on where nxm.sys.lib.Shell was loaded from.
      @return NMROOT based on where nxm.sys.lib.Shell was loaded from or null
                     if it cannot figure this out.
      @since NeXtMidas 2.9.0
   */
  private String detectNMROOT () {
    if (loader == null) {
      loader = this.getClass().getClassLoader();
      if (loader == null) {
        M.warning("Cannot get ClassLoader for Shell.class! Returning null for detected NMROOT.");
        return null;
      }
    }
    URL url = loader.getResource(CLASS_RESOURCE_NAME);
    String detectedNMROOT;
    if (url != null) {
      String urlstr = url.toExternalForm(); // use this to get "jar:..." when load via Web Start or jar file
      int index = urlstr.lastIndexOf(CLASS_RESOURCE_NAME);
      String nmrootURL = urlstr.substring(0,index);
      if (FileUtil.isLocalFileName(nmrootURL)) {
        detectedNMROOT = FileUtil.toLocalFileName( nmrootURL ); // strip off "file:" or "file:/C:/" for NMROOT on local file system
      } else {
        detectedNMROOT = nmrootURL;
      }
      M.info("NMROOT not defined. Setting it to: "+detectedNMROOT);
    }
    else {
      detectedNMROOT = null; // cannot find Shell class from ClassLoader
    }
    return detectedNMROOT;
  }

  /** open */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public void open () {
    boolean gotState;

    loader = this.getClass().getClassLoader();

    if (parent!=null) nmroot = parent.getNmRoot();
    else nmroot = Shell.getProperty("NMROOT");

    if (nmroot==null) {  // BUG 2207: if NMROOT is not defined, try to figure it out
      nmroot = detectNMROOT();
    }

    boot();

    if ((parent!=null) && (parent.getTerminal()!=null)) { // since 3.3.0: use ShellParent.getTerminal()
        M.terminal = parent.getTerminal();
        M.terminal.M = M; // update Terminal's Midas context to Shell's Midas context
        gotState = false;
    }
    else {
      // attach a native terminal for CL recall
      if (Native.useIO) {
        try {
          NTerminal nterminal = new NTerminal (M);
          M.terminal = (Terminal)nterminal;
        } catch (Exception e) {
          M.printStackTrace("Error loading native terminal, using Java version.", e);
          M.terminal = new Terminal (M);
        }
      }
      gotState = loadState();
    }
    if (!gotState) configure();
    else if (rebuildClasspath) addPathToClasspath(options.getS("PATH",""));  // Since 2.9.3 for BUG 1795

    String[] cpEntries = FileUtil.splitEntries(options.getS("CLASSPATH._APPEND_")); // 2011-11-15: to support both comma and OS path-sep separated list
    addToClasspath(cpEntries); // Since 2.9.3 for BUG 1795

    commands=0;
  }

  /** The command line interpreter loop */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public void process () {
    boolean oneShot   = (cmdline!=null && cmdline.length()>0);
    boolean configure = StringUtil.isTrue(getProperty("CONFIGURE"));

    // Special case for "nm make ..." (i.e. from bootstrap build) to run:
    //   1. USER's %nmstartup b/c during bootstrap build it is "normally" skipped in configure()
    //   2. run one-shot command (i.e. make all)
    //   3. then exit
    if (!configured && configure) {
      // Don't have to run SYS %nmstartup (since it ran already) -NTN
      runStartupMacro("nmstartup", "HOME", "AUX", "NMSTARTUP.USER");
      if (oneShot) runCommand(cmdline);    // handle one-shot command
      return;
    }

    // handle one-shot command (normal case)
    if (oneShot) {
      runCommand (cmdline);
      return;
    }

    boolean suspended = (M.macro!=null && M.macro.suspend!=0);

    // handle command-line interface (CLI) loop
    while (true) {
      if (M.status == Commandable.EXIT) break;
      // Since 3.9.1 for jdk 11 modularity
      try {
        Class<?> exitClass = Class.forName("nxm.sys.intr.Exit");
        if(exitClass.isInstance(command)) {
          break;
        }
      } catch (ClassNotFoundException e1) {
        // It can't be an instance of Exit if that class can't be found. Lets move on.
      }

      if (suspended && M.macro.suspend<=0) break;
      if (M.terminal.getInterrupt()!=0) {
        exit();
        break;
      } // NTN: Added to exit Shell when user presses CTRL-C
      command = null; // since 3.3.0 (be tidy and NOT keep reference to previous executed command)
      String prompt = PROMPT_STRING;
      if (M.pipeMode!=Midas.POFF) prompt += PROMPT_STRING_PIPE;
      if (suspended)  prompt += PROMPT_STRING_SUSPEND;
      prompt += "> ";
      cmdline = getNextCommand(prompt,"");
      if (cmdline == null) return;  // done nothing
      if (cmdline.length()==0) continue;
      if (autosave) saveState();        // JC4: Added 01JUN06 (Bug 90 - Environment Settings Lost)
      try {
        runCommand(cmdline, true); // this is an nM> prompt command
        System.gc();  // be tidy (since 3.3.2: moved from runCommand(String) to here
      }
      catch (Exception e) { M.printStackTrace(e); }
      commands++;
    } // end while loop for handling CLI
    command = null; // JGS: For clean re-entry
    cmdline = null;
  }

  /** close */
  public void close () {
    M.registry.clear();
    saveState();
  }

  /** put EXIT message on command's message QUIT and wait for it to process
      and change the command's state to DONE. Wait up to one second, then return.
   */
  public void exit () {
    if (command!=null && command.MQ!=null) {
      command.MQ.put("EXIT",0,null);
      for (int i=0; i<10 && command.state!=Command.DONE; i++) Time.sleep(0.1);
    }
  }

  /** Get the next command from the terminal
   *  @param prompt termminal prompt
   *  @param seed   initial text to display on terminal
   *  @return the next command
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public String getNextCommand (String prompt, String seed) {
    int popcode;
    Object peekobj  = null;
    String previous = null; // Previous input for continued lines
    while (true) {
      if (previous == null) {
        cmdline = M.terminal.getInputTabComplete(prompt,seed);
      } else {
        cmdline = previous + M.terminal.getInput("--> ", seed, Terminal.INPUT_NO_ESC);
      }
      seed = "";
      if (cmdline == null) break;    // done nothing
      cmdline = cmdline.trim();
      if (cmdline.length() == 0) break;    // do nothing
      else if (cmdline.startsWith("!")) {  // recall command
        popcode = 0; seed = null;
             if (cmdline.equals("!LAST")) popcode = -1;
        else if (cmdline.equals("!NEXT")) popcode =  1;
        else if (cmdline.equals("!LASTREF")) popcode = -2;
        else if (cmdline.equals("!NEXTREF")) popcode =  2;
        else seed = recall (cmdline.substring(1));
        if (seed == null) seed = "";
        if (popcode != 0) {
          if (popcode==1 && peekobj==null) popcode=2;
          peekobj = history.peek(popcode);
          if (peekobj != null) seed = (String)peekobj;
          else seed = "";
        }
      }
      else if (cmdline.endsWith("?")) {  // complete command
        seed = query (cmdline.substring(0,cmdline.length()-1));
        if (seed == null) seed = "";
      }
      else if (cmdline.endsWith("&") && !cmdline.endsWith("&&")) { // Continued on next line
        previous = cmdline.substring(0,cmdline.length()-1);
      }
      else {  // run command
        final String origCmdline = cmdline; // 2011-03-11 BUG 2363
        if (cmdline.endsWith("&&")) { // Convert ending "&&" to "&" for OS commands (matches X-Midas)
          cmdline = cmdline.substring(0,cmdline.length()-1);
        }
        String lastcmd = (String)history.peek();
        if (!origCmdline.equals(lastcmd)) {
          history.push(origCmdline); // 2011-03-11 put in original command line into history (BUG 2363)
        }
        break;
      }
    }
    return cmdline;
  }

  /** Sets the command state
   *  @param state This should be one of the Command Sequence Directives
   */
  public void setState (int state) {
    if (command!=null) command.setState(state);
  }

  /** Turns on/off automatic saving of state before a command is run.
      @param save autosave state true=onn, false=off*/
  public void setAutosave (boolean save) {
    autosave = save;
  }

  /** Indicates if automatic saving of state before a command is run is on/off.
      @return true if autosave is on */
  public boolean getAutosave () {
    return autosave;
  }

  /** Save the current state of the object to the file specified by the ENV.MSF property.
      @return True if the state was successfully saved, false otherwise.
      @see #loadState()
   */
  public boolean saveState () {
    Results MR = M.results;
    String msf = MR.getString("ENV.MSF");
    if (msf==null || msf.equals("NULL")) return false;
    MR.put("ENV.TIMER",Convert.x2o(M.timer),Results.FORCE);
    MR.put("ENV.ELAPSE",Convert.x2o(M.elapse),Results.FORCE);
    try {
      FileOutputStream fos = new FileOutputStream(msf+".msf");
      ObjectOutputStream oos = new ObjectOutputStream(fos);
      // NOTE: updates to this serialization may require updates to tools.NmSaveFile.readResultsTable(..)
      oos.writeObject(Table.getDefaultModeName());
      oos.writeObject(Table.getDefaultFlags());
      //MR.remove("REG");  // don't want this in saved state - refs are stale
      MR.put("REG", new Table()); // don't want this in saved state - refs are stale
      oos.writeObject(MR.toBytes(true)); // replace any dots in Table key names
      MR.put("REG",M.registry.table); // restore in current context
      oos.writeObject(Arrays.copyOfRange(M.dictionary.entry, 0, M.dictionary.size)); // truncate to dictionary size (BUG 2804)
      oos.writeObject(history);
      oos.writeObject(Registry.handlers.toBytes());
      oos.flush();
      fos.close();
      return true;
    } catch (Exception e) {
      Shell.printStackTrace("Error saving state",e);
    }
    return false;
  }

  /** Restore the state of the object from a previously saved state file. The current
      state of the object will be overwritten by the contents of the state file.
      @return true if the state was successfully loaded, false otherwise.
      @see #saveState()
   */
  public boolean loadState () {
    String msf = M.results.getString("ENV.MSF");
    if (StringUtil.isNull(msf)) return false;
    try {
      // Read from input stream
      FileInputStream   fis = new FileInputStream(msf+".msf");
      ObjectInputStream ois = new ObjectInputStream(fis);
      String   tableMode  = (String)ois.readObject();
      String   tableFlags = (String)ois.readObject();
      byte[]   res        = (byte[])ois.readObject();
      String[] dict       = (String[])ois.readObject();
      Stack    hist       = (Stack)ois.readObject();
      byte[]   hand       = (byte[])ois.readObject();
      fis.close();

      // init based on input
      Table.setDefaultMode(tableMode);
      Table.setDefaultFlags(tableFlags);
      M.results.setMode(tableMode);   // <-- Update existing results table to match default mode
      M.results.setFlags(tableFlags); // <-- Update existing results table to match default flags
      M.results.fromBytes(res, 0, res.length); // <-- Must use eager convert so we find errors (Bug 1394)
      M.dictionary.fromArray(dict);
      Registry.handlers = new Table(hand);
      history = hist;
      options = M.results.getTable("OPT");
      options.setMode(Table.SLHM); // keep order of inserted entries for the OPT table

      options.setFlags(Table.READONLY); // Protect the OPT table (since 2.9.3 for BUG 1795)
      Table cpTable = options.getTable("CLASSPATH");
      cpTable.setFlags(Table.READONLY); // Protect the OPT.CLASSPATH table

      String rebuildCP = Shell.getProperty("REBUILDCLASSPATH");
      if ((rebuildCP!=null) && (rebuildCP.length()>0)) rebuildClasspath = StringUtil.isTrue(rebuildCP);
      if (!rebuildClasspath) {
        String[] keys = cpTable.getKeys();
        for (String key : keys) {      // add entries in OPT.CLASSPATH table to the classpath
          if (key.indexOf('_') != 0) { // ignore entries starting with underscores
            addToClasspath(cpTable.getS(key));
          }
        }
      }

      // Setup the pointers that are needed
      Results MR = M.results;
      MR.put("REG",M.registry.table);
      MR.put("REG.SHELL",this);
      MR.put("REG.HANDLERS",Registry.handlers);
      MR.put("RAM",M.pipes);
      MR.put("AUX.CWD",getPathProperty("CWD","user.dir","",true));
      if (MR.getState("AUX._TRACKCWD_")) MR.put("AUX.HOMEPATH", MR.get("AUX.CWD"));  // since 3.1.1 (Bug 2481)
      M.debug  = Parser.mask(Debug.debugList,MR.getS("ENV.DEBUG"),0);
      M.verify = Parser.mask(Verify.verifyList,MR.getS("ENV.VERIFY"),0);
      M.timer  = MR.getX("ENV.TIMER");
      M.elapse = MR.getX("ENV.ELAPSE");
      Native.setUse(MR.getS("ENV.USENATIVE"));
      Theme.setOptions(MR.getS("ENV.GRAPHICS"));
      Theme.updateUIManager(MR.getTable("ENV.UIMANAGER")); // since 2.7.3
      M.io.setOptions(MR.getS("ENV.IOOPTIONS"));           // since 2.9.0
      MR.getTable("ENV").setFlags(Table.READONLY);
      return true;
    }
    catch (FileNotFoundException e) {
      // ignore, the error, no state saved yet
    }
    catch (Exception e) {
      M.warning("Error loading state: "+e.getMessage());
      M.warning("Restarting NeXtMidas session.");
      boot();
    }
    return false;
  }

  /** recall the last command starting with the given string
   *  @param text start of command to recall
   *  @return the last command starting with the given string
   */
  public String recall (String text) {
    text = text.toUpperCase();
    Object peekobj = history.peek(-1);
    String peekstr;
    while (peekobj != null) {
      peekstr = (String)peekobj;
      if (peekstr.toUpperCase().startsWith(text)) return peekstr;
      peekobj = history.peek(-1);
    }
    return null;
  }

  /** query the command for completion of arguments
   *  @param text The NeXtMidas command in string form (not null or empty string).
   *  @return the original command line if args not found, otherwise completed command with arguments
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public String query (String text) {
    Args args = parseCommand(M,text);
    if (args==null) return text;
    for (int i=1; i<=args.numberOf(); i++) {
      String key = args.getKey(i);
      String value = args.getValue(i);
      value = M.terminal.getInput (key+" = ",value,0);
      args.put(key,value);
    }
    return args.toString().substring(2); // strip off type char and ':'
  }

  /** The Shell boot-strap loader */
  public void boot () {
    String username, userhome, hostname, hostaddr, editor;
    username = userhome = hostname = hostaddr = "unknown";

    String msf = "NULL";
    boolean qos = getPriv(Privileges.PRIV_QUERYOS);
    if (qos) {
      try {
        java.net.InetAddress ia = java.net.InetAddress.getLocalHost();
        hostname = ia.getHostName();
        hostaddr = ia.getHostAddress();
      } catch (Exception e) {
        M.printStackTrace("Error in host lookup",e);
      }
      nmpid = Shell.getProperty("NMPID");
      msf = Shell.getProperty("NMSF");
    }
    if (nmpid==null || nmpid.length()<1) nmpid = "0000";

    if      (osname.contains("Win")) editor = "WORDPAD";
    else if (osname.contains("VMS")) editor = "edit/edt";
    else                             editor = "xterm -e vi";

    if (qos) {
      // user is not a path, so term==false to not add the terminator
      username = getPathProperty("USER","user.name","unknown",false);
      userhome = getPathProperty("HOME","user.home","unknown",true);
    }

    // determine whether it is possible to run native code
    Native.initUse();

    // initialize the registry
    M.results.put("REG",M.registry.table);
    M.results.put("REG.SHELL",this);
    M.results.put("REG.HANDLERS",Registry.handlers);
    M.results.put("RAM",M.pipes);

    M.results.put("ENV",new Table());
    M.results.put("ENV.NMROOT",nmroot);
    M.results.put("ENV.USER",username);
    M.results.put("ENV.HOME",userhome);
    M.results.put("ENV.HOSTNAME",hostname);
    M.results.put("ENV.HOSTADDR",hostaddr);
    M.results.put("ENV.OSTYPE",ostype);
    M.results.put("ENV.OSNAME",osname);
    M.results.put("ENV.OSREP",osrep);
    M.results.put("ENV.EDITOR",editor);
    M.results.put("ENV.VIEW","TEXT");
    M.results.put("ENV.THEME",Theme.defaultTheme);
    M.results.put("ENV.STYLE",Theme.defaultTheme);
    M.results.put("ENV.GRAPHICS",Theme.getOptions());
    M.results.put("ENV.PID",nmpid);
    M.results.put("ENV.MSF",msf);
    M.results.put("ENV.NMVERSION", Installation.NM_VERSION);      // since 2.1.0
    M.results.put("ENV.JVMVERSION", getJavaVersion());            // since 2.1.0
    M.results.put("ENV.NMPRERELEASE",Installation.NM_PRE_RELEASE? "TRUE":"FALSE"); // since 2.1.0
    M.results.put("ENV.PAUSE",M.pause);
    M.results.put("ENV.VERIFY","|");
    M.results.put("ENV.DEBUG","|");
    M.results.put("ENV.IOOPTIONS",M.io.getOptions()); // since 2.9.0
    M.results.put("ENV.USENATIVE",Native.getUse());
    M.results.getTable("ENV").setFlags(Table.READONLY);

    // initialize the OPT table
    M.results.put("OPT",options);
    M.results.put("OPT.PATH","SYS");

    // new OPT.CLASSPATH structure since 2.9.3 for BUG 1795
    Table classpathTbl = new Table(Table.SLHM, Table.READONLY);
    String preappendCP = Shell.getProperty("CLASSPATHPREPEND");
    String afterSysCP  = Shell.getProperty("CLASSPATHAFTERSYS");
    String appendCP    = Shell.getProperty("CLASSPATH");
    if ((preappendCP!=null) && (preappendCP.length()>0)) classpathTbl.put("_PREPEND_", preappendCP, Table.FORCE); // informational at this point
    if ((afterSysCP !=null) && (afterSysCP.length() >0)) classpathTbl.put("_AFTERSYS_",afterSysCP,  Table.FORCE); // informational at this point
    if ((appendCP   !=null) && (appendCP.length()   >0)) classpathTbl.put("_APPEND_",  appendCP,    Table.FORCE); // applied later at end of open()
    updateFromClassLoader(classpathTbl, null); // set OPT.CLASSPATH._CURRENT_ for informational purposes
    M.results.put("OPT.CLASSPATH", classpathTbl);
    // end of new OPT.CLASSPATH structure since 2.9.3 for BUG 1795

    // initialize the AUX table
    M.results.put("AUX",new Table());
    M.results.put("AUX.WRITE","HOME");
    M.results.put("AUX.READ","HOME|RAM|DAT");
    String cwd = nmroot;
    if (qos) cwd = getPathProperty("CWD","user.dir","",true);
    M.results.put("AUX.CWD",cwd);
    if (userhome.startsWith("http")) userhome = nmroot;
    M.results.put("AUX.HOME",userhome);
    M.results.put("AUX.HOMEPATH",userhome);
    M.results.put("AUX.DAT",getOptPath("SYS","DAT"));
    M.results.put("AUX.RAM","ram:");

    // initialize the dictionary
    M.dictionary.append("CONFIGURE;SYS:4,I,2,FUNC=,OPT=SYS");
    M.dictionary.append("EXIT;SYS:4,I,1,FILE=NXMSHELL,");
    M.dictionary.append("HELP;SYS:4,I,3,CAT=MAINHELP,ITEM=,OPT=SYS");
    M.dictionary.append("MENU;SYS:4,I,4,ACT=LIST,NAME=,SUP=ALL,OPT");
    M.dictionary.genAlpha();
  }

  /** Run specified startup macro applying a few rules for reserved values like:
      null, "default", blank, and "none".
      These rules are similar to X-Midas's use of $XMSTARTUP_MACRO.
      @param macro     macro to run on startup (null to use default of "nmstartup" and allow property override).
      @param areaOrAux area of specified option tree or AUX name if opt is "AUX"
      @param opt       option tree or "AUX" to use AUX instead of area
      @param overridePropName name of property that can override the macro BUT only if it is null
      @since NeXtMidas 3.1.3
   */
  private void runStartupMacro (String macro, String areaOrAux, String opt, String overridePropName) {
    configured = true;
    macro = getProperty(overridePropName, macro); // allow specified system property to override

    // next two checks are rules that are similar to X-Midas's use of $XMSTARTUP_MACRO
    if (macro == null || macro.equalsIgnoreCase("default")) macro = "nmstartup";
    else if (macro.length() == 0 || macro.equalsIgnoreCase("none")) return; // SKIP if given blank or "none"

    if (areaOrAux != null || macro != null) {
      if (areaOrAux == null) areaOrAux = "HOME"; // special case
      // save off any arguments to startup macro
      macro = macro.trim();
      int iSpace = macro.indexOf(' ');
      String arguments ="";
      if (iSpace > 0) {
        arguments = macro.substring(iSpace);
        macro = macro.substring(0, iSpace);
      } // end of argument stripping / saving
      int iSemiColon = macro.indexOf(';');
      if (iSemiColon == 0) throw new MidasException("invalid macro ["+macro+"] specified.");
      else if (iSemiColon > 0) { // input macro is formatted as macro;opt
        areaOrAux = "mcr";       // use "mcr" as area in this scenario (matches X-Midas behavior)
        opt       = macro.substring(iSemiColon + 1);
        macro     = macro.substring(0, iSemiColon);
      }
      FileName fn = new FileName(macro,true);
      String macroName = fn.getRoot();
      String fname = macro;
      if (fn.getPath().length() == 0) {                 // if no path specified
        fname = M.io.name(areaOrAux, macro, "mm", opt); // then get full filename path to macro
      }
      if (M.io.exists(fname)) {
        runCommand("%"+macroName+",/MFN="+fname+arguments); // allow arguments to startup macro
      } else if (!macro.equalsIgnoreCase("nmstartup")) {
        M.warning("Could not find %"+macro + (macro.equals(fname)? "" : " at "+fname));
      }
    }
  }

  /** The Shell configuration loader (called after {@link #boot()} and {@link #loadState()})
      if previous state was NOT restored.
   */
  public void configure () {
    String tmp;

    // load the handlers table
    tmp = M.io.name ("CFG","HANDLERS","TBL","SYS");
    TextFile tf = new TextFile(M,tmp);
    final int tabFlags = Table.getDefaultFlagsInt() & ~Table.ADD; // ensure ADD is NOT in flags to prevent duplicates entries)
    if (tf.find(IOResource.DIR_INPUT)) Registry.handlers.fromTextFile(tf, tabFlags);

    // configure the SYS command dictionary.
    runCommand("CONFIGURE,KC");

    // system specific startup
    if (parent==null) {
      runCommand ("VERSION/ALL SYS");
      if ((cmdline != null) && (cmdline.length() == 0)) { // Normal case for "nmstart"
        runStartupMacro("nmstartup", "cfg",  "SYS", "NMSTARTUP.SYS");
        runStartupMacro("nmstartup", "HOME", "AUX", "NMSTARTUP.USER");
        cmdline = "EXIT"; // Can't make it "" since then it wou't run as one-shot.
      } else { // Special case for "nm make ..." (from bootstrap build) AND -INDEPENDENT run modes
        runStartupMacro("nmstartup", "cfg", "SYS", "NMSTARTUP.SYS");
        runStartupMacro("none",     "HOME", "AUX", "NMSTARTUP.USER"); // "none" to skip unless overridden by system property
      }
    }

    if ( parent != null ) {
      if ((tmp=parent.getCWD()) != null) M.results.put ("AUX.CWD", tmp);
      if ((tmp=parent.getAux()) != null) runCommand ("AUX "+tmp);
      if ((tmp=parent.getHome()) != null) {
        M.results.put ("AUX.HOME", tmp);
        M.results.put ("AUX.HOMEPATH", tmp);
      }
      if ((tmp=parent.getOpts()) != null) configureOpts(tmp, parent.getServer());
      if ((tmp=parent.getNMS()) != null) {
        // NOTE1: this needs to be checked after configureOpts(..) above, otherwise custom startup
        //        macros in non SYS option tree will NOT be found. -NTN
        // NOTE2: SYS startup is not included b/c Web Start, etc. run modes are executed
        //        on client machines which probably DO NOT have the same disk configuration as
        //        the server machine.
        runStartupMacro(tmp, "HOME", "AUX", "NMSTARTUP.USER");
      }
      // set default graphics theme/style
      if ((tmp=parent.getTheme()) != null) {
        M.results.put("ENV.THEME", tmp, Table.FORCE);
        try { Theme.setGlobalTheme(tmp, M); }
        catch (Exception e) { M.warning("Unable to set theme to '"+tmp+"': "+e); }
      }
      if ((tmp=parent.getStyle()) != null) M.results.put("ENV.STYLE", tmp, Table.FORCE);
    }

    initCompletionExcludedAuxes();  // since 3.1.1 for BUG 2482

    M.status = 0;    // mark booted
  }

  /** Configure the specified option trees. When this command returns each
      option tree will be added to the path and the classpath will be
      modified. Each option tree in the opt list will be configured by running
      the <code>option</code> and <code>path,set</code> commands on the option
      tree. In order for these commands to have been run, the SYS tree must
      have first been configured. Trees get added in reverse order.
      @param opts The option tree names to configure. This should be a list of
                  option trees separated by the | character.
      @param server optional HTTP server name where the option tree directories
                    should exist.
  */
  protected void configureOpts (String opts, String server) {
    if (opts == null) return;
    Parser p = new Parser(opts,',');
    String path = "SYS";
    for (int i=p.elements(); i>=1; i--) {
      String opt = p.get(i);
      String dir = null;
      int j = opt.indexOf('=');
      if (j>0) { dir = opt.substring(j+1); opt = opt.substring(0,j); }
      if (dir!=null && dir.indexOf(':')<0 && server!=null) dir = "http://"+server+dir;
      opt = opt.toUpperCase();
      if (dir != null) M.results.put ("OPT."+opt, dir);
      runCommand ("CONFIGURE,C,"+opt);
      path = opt+","+path;
    }
    runCommand("PATH/RERUN/VERBOSE=FALSE,SET,"+path);  // rerun switch added to force call of optStart macros (BUG 2283)
  }

  /**
    returns a path obtained from the property list, optionally
    searching two names and providing a default if neither of
    the two names was found, ensuring that the correct path separator
    for the destination architecture is used.
    @param name first property name to look for
    @param name2 property name to look for (if first was not found)
    @param def default property definition if name and name2 not found
    @param term if true, then terminate result with path/directory terminator
    @return path terminated with the appropriate directory separator
  */
  private String getPathProperty (String name, String name2, String def, boolean term) {
    String tmp = Shell.getProperty(name);
    if (tmp==null) tmp = Shell.getProperty(name2);
    if (tmp==null) tmp = def;
    // Fix the path to end with a path terminator if necessary
    if (term) tmp = FileName.terminatePath(tmp);
    return tmp;
  }

  /** Runs one command line.
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread).
      @param cmdline the command to run
      @return reference to the Command that is executed (null if cmdline is empty)
   */
  public Command runCommand (String cmdline) {
    Command command = runCommand(M, cmdline, null);
    this.command = command; // TODO: stop using (this.command) for next major NeXtMidas release (3.9/4.0)?
    // since 3.3.2: we no longer call the garbage collector here, so that it does not slow down
    // other callers of this public method. instead it has been moved to the CLI loop in process().
    // System.gc();  // be tidy (prior to 3.3.2)
    return command;
  }

  // matches runCommand (String cmdline) except allows specification that it is nM> command
  private Command runCommand (String cmdline, boolean nmPromptCmd) {
    Command command = runCommand(M, cmdline, null, null, nmPromptCmd);
    this.command = command; // TODO: stop using (this.command) for next major NeXtMidas release (3.9/4.0)?
    return command;
  }

  /** Runs one command line and embed graphical commands on specified MidasDisplay panel.
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread).
      @param cmdline   The command line.
      @param mdPanel   graphical panel to embed graphical commands (change to JPanel in 3.9.1 for Java 11 modularity)
      @return reference to the Command that is executed (null if cmdline is empty)
      @since NeXtMidas 3.3.0
  */
  public Command runCommand (String cmdline, JPanel mdPanel) {
    return runCommand(M, cmdline, null, mdPanel);
  }

  /** Runs a command in the given Midas context. This is identical to
      <code>runCommand(M,cmdline,null)</code>.
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread).
      @param M       The Midas context.
      @param cmdline The command line.
      @return reference to the Command that is executed (null if cmdline is empty)
   */
  public static Command runCommand (Midas M, String cmdline) {
    return runCommand(M,cmdline,null);
  }

  /** Runs a command in the given Midas context.
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread).
      @param M         The Midas context.
      @param cmdline   The command line.
      @param cmdParent The command parent to use (use <code>null</code> when not applicable).
      @return reference to the Command that is executed (null if cmdline is empty)
      @since NeXtMidas 2.1.0
   */
  public static Command runCommand (Midas M, String cmdline, CommandParent cmdParent) {
    return runCommand(M, cmdline, cmdParent, null);
  }

  /** incrementing number use to get unique panel names for /WIN= in {runCommand(Midas, String, CommandParent, MidasDisplay)} */
  private static AtomicInteger panelCounter = new AtomicInteger(1); // since 3.3.0

  /** Runs a command in the given Midas context.
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread).
      @param M         The Midas context.
      @param cmdline   The command line.
      @param cmdParent The command parent to use (use <code>null</code> when not applicable).
      @param mdPanel   graphical panel to embed graphical commands
      @return reference to the Command that is executed (null if cmdline is empty)
      @since NeXtMidas 3.3.0
   */
  public static Command runCommand (Midas M, String cmdline, CommandParent cmdParent, JPanel mdPanel) {
    return runCommand(M, cmdline, cmdParent, mdPanel, false);
  }

  private static Command runCommand (Midas M, String cmdline, CommandParent cmdParent, JPanel mdPanel, boolean nmPromptCmd) {
    if (cmdline.isEmpty()) return null;  // since 3.3.0 (to match original Command runCommand (String cmdline) method
    String uniqueRegWinName = null;
    if (mdPanel != null) {                // since 3.3.0: MidasDisplay panel to embed graphical commands on (Bug 2485)
      // TODO: in future, maybe we should update nxm.sys.lib.Args so that it can hold Objects vs "Strings"
      //       so that we could do something like <code>args.put("/WIN", mdPanel)</code>
      //       (without using a temp registry entry like in the below code):
      uniqueRegWinName = "_MDISPLAY"+panelCounter.getAndIncrement()+"_"; // use unique name for /WIN=
      Table win = (Table) M.registry.get("WIN");
      if (win == null) {                  // create WIN table in registry
        win = new Table();
        M.registry.put("WIN", win);
      }
      if (win.containsKey(uniqueRegWinName)) { // this should NOT happen, but give user warning if it does
        M.warning("Overriding existing REG.WIN."+uniqueRegWinName+" panel entry ["+win.getO(uniqueRegWinName)+"] with "+mdPanel);
      }
      win.put(uniqueRegWinName, mdPanel); // put mdPanel into REG.WIN.<cmd_id>
      // args.put("/WIN", uniqueRegWinName); // add /WIN= switch for this command
      cmdline += " /WIN="+uniqueRegWinName;   // add /WIN= switch for this command
    }
    Args args = parseCommand(M,cmdline);
    if (args==null) return null;
    Command command = loadCommand(M,args,cmdParent);
    if (command==null) {
      M.warning("couldn't find command : " + cmdline);
      return null;
    }
    command.setNmPromptCmd(nmPromptCmd);
    if (M.isAutoDetectingBehavior() && M.isJavaBehavior()) { // NXM-4097 Java vs Macro defaults
      M.setIfJavaBehavior(!nmPromptCmd);
      if (Midas.isJavaVsMidasDebug()) {
        M.info("#JvsM Shell.runCommand setting Macro/Midas usage to true");
      }
    }

    execCommand(M,command);               // <== execute/run the command

    if (mdPanel != null) {                // remove temporary REG.WIN.<uniqueRegWinName> panel entry
      M.registry.remove("WIN."+uniqueRegWinName);
    }
    return command;
  }

  /** Parse a command line into an Args.
   *  @param M       The Midas context to use.
   *  @param cmdline The NeXtMidas command line in string form (not null or empty string).
   *  @return The parsed args instance or null if command is not found.
   */
  public static Args parseCommand (Midas M, String cmdline) {
    return Args.parseCommand(M, cmdline);
  }

  /** Load one command object. This is identical to <code>loadCommand(M,args,null)</code>.
      @param M    The Midas context to run the command in.
      @param args The command line arguments.
      @return the command
   */
  public static Command loadCommand (Midas M, Args args) {
    return loadCommand(M,args,null);
  }

  /** Load one command object, but does not execute it.
      @param M         The Midas context to run the command in.
      @param args      The command line arguments.
      @param cmdParent The command parent to use (use <code>null</code> when not applicable).
      @return The command.
      @throws MidasException If command could not be loaded.
      @since NeXtMidas 2.1.0
   */
  public static Command loadCommand (Midas M, Args args, CommandParent cmdParent) {
    Command command=null;
    switch (args.support) {
      case 'I': command = getCommandForClassName(M, args);            break; // Java Intrinsic
      case 'P': command = getCommandForClassName(M, args);            break; // Java Primitive
      case 'H': command = getCommandForClassName(M, args);            break; // Host Primitive
      case 'M': command = new Macro();                                break; // Macro
      case 'O': command = getCommandForeign(M, args);                 break; // OS
      case 'U': command = getCommandForeign(M, args);                 break; // OS
      case 'D': command = getCommandDeprecated(M, args);              break; // Deprecated
      default:  M.error("Undefined command support: "+args.support);  break; // Error
    }
    if (command!=null) {
      command.setCmdParent(cmdParent); // JC4 Added 8/2005 for XMJVM support
      command.setArgs(args);
      if (command.checkin(M) != Command.INIT) {
        M.error("loading command "+command.name);
      }
      if (args.support=='U' && args.getState("/NOEXEC")) {
        M.error("Cannot execute command "+command.name+" from inside shell");
      }
    }
    return command;
  }

  /** set loader with getClassLoader() */
  public void setLoader () {
    loader = this.getClass().getClassLoader();
  }

  /** Gets a new instance of Deprecated for the given Midas context. */
  private static Command getCommandDeprecated (Midas M, Args args) {
    Parser pconf = M.dictionary.findParser(args.getName());
    CommandLine cl = new CommandLine (pconf.getAll());
    String str = args.getName()+" command deprecated";
    if (args.numberOf() > 0) str += ", "+cl.getDefault(1);
    M.deprecate (str);

    return null; // Always returns null for now.
  }

  /** Gets a new instance of Foreign for the given Midas context. */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  private static Command getCommandForeign(Midas M, Args args) {
    return new Foreign(M.terminal.isExtCmdSupported() && !noExternalCmds);
  }

  /** Instantiates a command when given the appropriate class name. This is used ONLY by loadCommand(..).
      @param M    The midas context to use.
      @param args The Args to get the Command Class Name from (via {@link Args#getCmdClassName()})
      @return The instantiated command.
   */
  private static Command getCommandForClassName (Midas M, Args args) {
    String className = args.getCmdClassName();
    try {
      Class<?> clazz = loader.loadClass(className);

      if (!Modifier.isPublic(clazz.getModifiers())) {
        throw new MidasException("The class "+className+" is not public.");
      }

      Constructor<?> cnst = clazz.getConstructor();
      return (Command)cnst.newInstance(new Object[0]);

    } catch (ClassNotFoundException e) {
      String fileName = "" + loader.getResource(className.replace('.', '/') + ".class");
      String srcName  = "" + loader.getResource(className.replace('.', '/') + ".java");

      if (M.io.fexists(srcName) && (!M.io.fexists(fileName))) {
        throw new MidasException("The class "+className+" has not been compiled.");
      } else {
        throw new MidasException("Can not find the class "+className+".", e);
      }
    } catch (IllegalAccessException e) {
      throw new MidasException("Could not find a public no-argument constructor for "+className+".");
    } catch (NoSuchMethodException e) {
      throw new MidasException("Could not find a public no-argument constructor for "+className+".");
    } catch (ClassCastException e) {
      throw new MidasException("The class "+className+" does not extend nxm.sys.lib.Command.", e);
    } catch (MidasException e) {
      throw e;
    } catch (Exception e) {
      throw new MidasException("Error loading "+className+": "+e.getMessage(), e);
    }
  }

  /** Instantiates a class.
      @param className The name of the class to instantiate.
      @return The new class instance.
   */
  public static Object getInstanceForClassName (String className) {
    try {
      Class<?> c = loader.loadClass(className);
      return c.getConstructor().newInstance(); // Updated in 3.9.0 to use default constructor
    } catch (Exception e) {
      throw new MidasException("Error loading class '"+className+"': "+e, e);
    }
  }

  /** Execute a loaded command within the given Midas context. <br>
      <br>
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread)
      as it can cause running a PLOT command to lockup since PLOT depends on initializing its
      graphics panels on the EDT. But if you must, then you may want to add the /CMDOPENTIMEOUT
      switch to specify a timeout as a crude workaround.
      @param M       The midas context to use (not null).
      @param command The command to run (not null).
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public static void execCommand (Midas M, Command command) {
    int     runMode = Parser.mask(cmdRunModeList, command.MA.getS("/CMDRUNMODE"), RUN_DEFAULT); // since NeXtMidas 3.5.4
    boolean bg      = command.MA.getState("/BG");
    boolean isPiped = ((runMode&RUN_TO_COMPLETION)==0) && command.isPipable() && (M.pipeMode==Midas.PINIT || (bg && M.pipeMode != Midas.POFF));
    if (command.MA.getState("/NOPIPE")) isPiped=false;
    Object  engine  = (isPiped && !bg)? command.MA.getO("/ENGINE") : null;
    command.state   = Command.INIT;
    command.isPiped = isPiped;

    if (engine != null && command.processReady()!=-1) {
      M.macro.addCommand(command);
    }
    else if (bg || isPiped) {
      // piped and backgrounded commands need to finish their open() before we continue so that
      // any files/pipes/resources that it setups can be available for subsequent commands
      final double SLEEP_INTERVAL = 0.01;
      MidasThread thread = new MidasThread(M,command);
      command.thread = thread;
      thread.start();

      // following variables (cmdOpenTimeout, numLoops) are for Bug 2056
      double cmdOpenTimeout = command.MA.getD("/CMDOPENTIMEOUT", -1); // -1 for infinite (default); in seconds; for Bug 2056
      int numLoops = Integer.MAX_VALUE; // Integer.MAX_VALUE is effectively infinity
      if (cmdOpenTimeout >=0 ) numLoops = (int)(cmdOpenTimeout/SLEEP_INTERVAL); // for Bug 2056

      // wait for command to finish opening, unless it timeout
      while ((command.state==Command.INIT || command.state==Command.OPEN) && (numLoops-- > 0)) {
        Time.sleep(SLEEP_INTERVAL);
      }
    }
    else {
      command.thread =  Thread.currentThread();
      command.run();
    }
    checkAndSetInterrupt(M);

  }

  /** Convert a command string to a background Command and execute it<br>
      <br>
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread)
      as it can cause running a PLOT command to lockup since PLOT depends on initializing its
      graphics panels on the EDT. But if you must, then you may want to add the /CMDOPENTIMEOUT
      switch to specify a timeout as a crude workaround.
      @param midas         The midas context to use
      @param commandString The String form of the command
      @return the Command instance
      @since NeXtMidas 3.9.1
   */
  public static Command execBackgroundCommand (Midas midas, String commandString) {
    if (midas == null) midas = getSharedMidasContext();
    String cmdline = commandString+" /BG";
    Args args = Args.parseCommand(midas, cmdline);
    Command command = Shell.loadCommand(midas, args);
    Shell.execCommand(midas, command);
    return command;
  }

  /** Convert a command string to a Command and execute it. This block until complete<br>
      <br>
      WARNING: This SHOULD NOT be run on the EventDispatchThread (EDT) (i.e. the AWT/Swing thread)
      as it can cause running a PLOT command to lockup since PLOT depends on initializing its
      graphics panels on the EDT. But if you must, then you may want to add the /CMDOPENTIMEOUT
      switch to specify a timeout as a crude workaround.
      @param midas         The midas context to use
      @param commandString The String form of the command
      @return the Command instance
      @since NeXtMidas 3.9.2
   */
  public static Command execCommandAndWait (Midas midas, String commandString) {
    if (midas == null) midas = getSharedMidasContext();
    String cmdline = commandString;
    Args args = Args.parseCommand(midas, cmdline);
    Command command = Shell.loadCommand(midas, args);
    Shell.execCommand(midas, command);
    return command;
  }

  /** Gets the default machine architecture type (32-bit, 64-bit) to use.
      This is a best guest for current machine architecture type and
      defaults to "32" for unknown machine architecture types.
      It first uses the SUN System property "sun.arch.data.model" and if
      that is not available, then tries the "os.arch" property with our
      own mapping for os.arch string to "32" or "64".
      @return "64" for 64-bit machine architecture or
              "32" for 32-bit and unknown machine architectures
      @since NeXtMidas 2.9.2 (moved from nxm.sys.lib.Build.getDefArchType)
   */
  static String getDefArchType () {
    // The following property is likely to exist with non-Sun/Oracle versions of Java
    String bits = Shell.getProperty("sun.arch.data.model");

    if (bits == null || bits.length() == 0 || bits.equalsIgnoreCase("unknown")) {
      String arch = Shell.getProperty("os.arch");
      // may add more to this list in the future
      if      (arch.equals("i386"  )) bits = "32";
      else if (arch.equals("x86"   )) bits = "32";
      else if (arch.equals("ppc"   )) bits = "32";
      else if (arch.equals("arm"   )) bits = "32";
      else if (arch.equals("sparc" )) bits = "64";
      else if (arch.equals("amd64" )) bits = "64";
      else if (arch.equals("alpha" )) bits = "64"; // remove this under Java 5
      else if (arch.equals("x86_64")) bits = "64"; // since 3.1.2 for BUG 2589
      else {
        warning("Could not determine if '"+arch+"' was 32Bit or 64Bit, assuming 32Bit.");
        bits = "32";
      }
    }
    return bits;
  }

  /** Gets the library suffix used for native libraries. Intended for internal usage only!
      Only used in loadLibrary(..), Build.java and HPrimitive.java.
      Since NeXtMidas 2.9.2: includes the machine architectural bits (i.e. 32 or 64) in the returned suffix,
      unless a NATIVELIBSUFFIX system property overrides it. E.g. <code>setenv NM_USER_JVM_FLAGS -DNATIVELIBSUFFIX="_lin"</code>
      for library suffix on Linux machines matching versions prior to 2.9.2 <br>
      OR -DNATIVELIBSUFFIX="_rhel5-32bit" to specify both Linux OS type and machine architecture.
      @return the suffix to append to shared libraries (NATIVELIBSUFFIX system property or "_lin32",
              "_lin64", "_sol32", etc.) without extension.
      @see #getLibraryExt()
   */
  public static String getLibrarySuffix () {
    String libSuffix = Shell.getProperty("NATIVELIBSUFFIX");
    if (libSuffix != null) {
      return libSuffix;
    }
    return "_" + ostag + getDefArchType();
  }

  /** Gets the library extension (including dot) used for native libraries.
      Intended for internal usage only! Only used in loadLibrary(..), Build.java and HPrimitive.java.
      @return the library extension (including dot) used for native libraries
   */
  public static String getLibraryExt () {
         if (ostag.equals("win")) return ".dll";
    else if (ostag.equals("vms")) return ".exe";
    else                          return ".so";
  }

  /** Gets the library prefix used for native libraries.
      Intended for internal usage only! Only used in loadLibrary(..), Build.java and HPrimitive.java.
      @return library prefix used for native libraries.
  */
  public static String getLibraryPrefix () {
       if (ostag.equals("win") || ostag.equals("vms")) return "";
       else  return "lib";
  }

  /** Gets the suffix added onto shared libraries.
      Same as <code>getLibrarySuffix() + getLibraryExt()</code>.
      In most cases this is equal to:
      <pre>
        Linux 32-bit  : "_lin32.so"
        Linux 64-bit  : "_lin64.so"
        Solaris 32-bit: "_sol32.so"
        Windows 32-bit: "_win32.dll"
      </pre>
      @return the suffix added onto shared libraries
      @see #getLibrarySuffix()
      @see #getLibraryExt()
   */
  public static String getSharedObjExt () {
    return getLibrarySuffix() + getLibraryExt();
  }

  /** Attempt to load the specified library.  Return true if the load was
      successful, false otherwise.  One should not call this routine
      without first checking Native.useXXX flags.
      @param path path to library
      @param name name of library
      @return true if the load was successful
   */
  public static boolean loadLibrary (String path, String name) {
    try {
      return loadLibraryUnsafe(path,name);
    }
    catch (SecurityException e) {
      Shell.printStackTrace("Shell.loadLibrary Security Error: path="+path+" name="+name, e);
    }
    catch (UnsatisfiedLinkError e) {
      Shell.printStackTrace("Shell.loadLibrary Error: path="+path+" name="+name, e);
    }
    return false;
  }

  /** Attempt to load the specified library. Return true if the load was successful, false
      otherwise. One should not call this routine without first checking Native.useXXX flags.
      This method throws an exception if the library could not be loaded by the Java VM; it
      simply returns false when native library access is turned off. This method differs from
      {@link #loadLibrary(String,String)} in that it allows the caller to handle the different
      types of exceptions in different ways.
      @param path The directory that the library is typically in.
      @param name The name of the library.
      @return true on success, false on failure
      @throws SecurityException    If the security manager does not allow the library to be loaded.
      @throws UnsatisfiedLinkError If the library does not exist or the library is missing a method.
      @since NeXtMidas 2.5.0
  */


  public static boolean loadLibraryUnsafe (String path, String name)
                          throws SecurityException, UnsatisfiedLinkError {
    if (!Native.canUse) return false;
    String libName = name+getLibrarySuffix();
    if (currentShell.parent != null    &&    // This applies to Web Start, nxm.sys.lib.NeXtMidas, etc.
             IOResource.isURLResource(path) &&    // AND if path is a URL
             !FileUtil.isLocalFileName(path)  ) { // BUT NOT an local file URL
      try {
        System.loadLibrary(libName);
        // Note System.loadLibrary(..) uses system specific libname and uses system.library.path.
        // e.g. libName gets map to lib<libname>.so on UNIX/Linux/Solaris
      }
      catch (UnsatisfiedLinkError e) {
        try {
          // If library is not found, try searching for the library file, and if found,
          // extracting from the .jar to load it
          System.load(extractLibFromJar(libName, e));
        }
        catch (UnsatisfiedLinkError e2) {
          // If the library was not found in the jar file or by the library name, try the new Gradle style shared object naming.
          String extractionFileName = extractLibFromGradleJar(libName, path, e);
          if (extractionFileName != null) {
            System.load(extractionFileName);
          }
        }
      }
    }
    else {
      libName = path+libName+getLibraryExt();
      int i = libName.indexOf(".jar");
      if (i>0) libName = "file:"+libName.substring(0,i+4)+"!"+libName.substring(i+4);
      System.load(libName);
    }
    return true;
  }

  /** Load library by className
   *  @param className library's Class name in dot syntax nxm.opt.area.className
   *  @return true if successfully loaded
   */
  public static boolean loadLibrary (String className) {
    if (!className.startsWith("nxm.")) return false;
    String name = className.substring(4);
    int i=name.indexOf('.');
    String option = name.substring(0,i);
    name = name.substring(i+1);
    i=name.indexOf('.');
    String area = name.substring(0,i);
    name = name.substring(i+1);
    return loadLibrary( getOptPath(option,area), name);
  }

  private static String getOptNameFromPath(String path) {
    String[] temp = path.split("/");
    return temp[temp.length-2];
  }

  /** Load library by software tree
   *  @param option option tree
   *  @param area   area in the option tree
   *  @param name   library name
   *  @return true if loaded
   */
  public static boolean loadLibrary (String option, String area, String name) {
    return loadLibrary( getOptPath(option,area), name);
  }

  /**
   * Searches for the library file and, if found, extracts it from the
   * jar file and places it in a temporary file.
   * (Called after library is not initially found - UnsatisfiedLinkError)
   *
   * @param libName - full name of library (including prefix and extension)
   * @param eOrig - original error from library not being found
   * @return String - name of temporary file holding copy of library file
   * @throws original Error if the library is not found
   */
  private static String extractLibFromJar(String libName, Error eOrig) {
    // get full name of library (OS based)
    String fullFileName = getLibraryPrefix()+libName+getLibraryExt();
    return loadFromJar(libName, fullFileName, eOrig);  // return full name & path of temporary file
  }

  // Gradle will have the native code in the directory path within the jar file. Extract it and use
  private static String extractLibFromGradleJar (String libName, String path, Error eOrig) {
    String gradleNaming = "nxm-"+getOptNameFromPath(path)+getLibrarySuffix();
    String fullFileName = "main/shared/lib"+ gradleNaming + getLibraryExt();
    if (loadedLibraries.contains(fullFileName)) {
      // we have already loaded the .so, trying to load it again can cause load issues
      // since 3.9.3 for NXM-3561
      return null;
    }
    else {
      loadedLibraries.add(fullFileName); // save off the name, so we only try to load it once
      return loadFromJar(gradleNaming, fullFileName, eOrig);
    }
  }

  private static String loadFromJar(String libName, String fullFileName, Error eOrig) {
    String libFileName = null;
    try {
      URL url = Shell.class.getClassLoader().getResource(fullFileName);

      if (url == null) throw eOrig; // not found

      // create temporary file to hold copy of library file
      File tempLibFile = File.createTempFile(getLibraryPrefix() + libName, getLibraryExt());
      tempLibFile.deleteOnExit(); // clean up file when JVM exits
      libFileName = tempLibFile.toString();

      // extract from .jar and place copy in the temporary file
      FileUtil.copyBaseFile(currentShell.M, url.toString(), libFileName);
    }
    catch (Throwable t) {
      throw eOrig; // throw top-level error
    }

    return libFileName;
  }

  /** Creates an option class filename
   *  @param name dot separated style NeXtMidas Class filename
   *  @return path
   */
  public String getOptionClassFileName (String name) {
    if (!name.startsWith("nxm.")) return null;
    int i1 = name.indexOf('.',4);
    String opt = name.substring(4,i1);
    int i2 = name.indexOf('.',i1+1);
    String area = name.substring(i1+1,i2);
    String path = getOptPath(opt,area);
    path += name.substring(i2+1);
    path += ".class";
    return path;
  }

  /** Creates an option tree path in a system independent manner.
      <br>
      WARNING: cannot call this BEFORE {@link Shell#open()}!
      @param opt  The option tree name.
      @param area The area in the option tree. (Set this to <code>null</code> to get the option
                  tree's root.)
      @return The path to the given area in the given option tree.
              This will be a path name if it is for a local file and a URL if it is not.
              If the given area is not <code>null</code> is not found then <code>getOptPath(opt,null)+area+"/"</code> will be  returned.
              If the given option is not found then <code>getNmRoot()+area+"/"</code> will be returned.
   */
  public static String getOptPath (String opt, String area) {
    // Look for the path to the option tree.
    String path = (String)options.getKey(opt.toUpperCase());
    // If not found, assume that the path is the nmroot.
    if (path == null) path = nmroot;
    // Form path based on separators gleaned from root path
    return FileName.getOptionPath (path, opt, area);
  }

  /** get AUX path
   *
   *  @param aux AUX
   *  @return AUX path
   */
  public static String getAuxPath (String aux) {
    return currentShell.M.io.getAuxPath(aux.toUpperCase());
  }

  /** Gets the OS type. <br>
      @return The OS type.
   */
  public String getOSType () { return ostype; }

  /** Gets the OS name. <br>
      @return The OS type.
   */
  public String getOSName () { return osname; }

  /** Gets the OS rep. <br>
      @return The OS rep.
   */
  public String getOSRep () { return osrep; }

  /** Get the NeXtMidas PID (ENV.PID). This should be unique for each
      NeXtMidas session. Note: this is NOT your current process ID.
      @return NeXtMidas PID (ENV.PID)
      @since NeXtMidas 2.7.1
   */
  public static String getNmPID() { return nmpid; }

  /** Gets the current Shell
      @return current Shell */
  public static Shell getCurrent () { return currentShell; }

  /** Return a default Midas context object. The returned context has the NMROOT and HOME set to
      the default values. It returns the Midas context from the first shell created in this JVM.
      If no shell exists, one is created.
      @return Midas context or null if context cannot be created
      deprecated since 3.5.0 use {@link #getSharedMidasContext()}
  */
  @InternalUseOnly("deprecated Since NeXtMidas 3.5.0, use getSharedMidasContext()") // TODO: add deprecated annotation in 3.7+
  public static Midas getMidasContext () {
    return getSharedMidasContext();
  }

  /** Return a default Midas context object. The returned context has the NMROOT and HOME set to
      the default values. It returns the Midas context from the first shell created in this JVM.
      If no shell exists, one is created.
      @return Shared Midas context
      @since NeXtMidas 3.5.0
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public static Midas getSharedMidasContext () {
    if (currentShell==null) {
      Shell shell = new Shell();
      currentShell = shell; // 2010-07-02 BUG 2123 & 2207: moved this before shell.open() to prevent
      // potential "infinite" loop when starting up, since currentShell
      // would still be null and Shell.getMidasContext() could be call again
      // and again, which will eventually case a StackOverflowError
      shell.open();
      shell.M.terminal = new Terminal(shell.M);
    }
    return currentShell.M;
  }

  /** Issue a message to terminal.
      @param str message
      @since NeXtMidas 2.7.0
   */
  public static void writeln (CharSequence str) {
    getSharedMidasContext().type(str);
  }

  /** Issue an informational message.
      @param str message
      @since NeXtMidas 2.7.1
  */
  public static void info (CharSequence str) {
    getSharedMidasContext().info(str);
  }

  /** issue a warning message.
      @param str message
      @since NeXtMidas 2.7.1
   */
  public static void warning (CharSequence str) {
    // TODO: look at current thread,
    // 1. if Thread instance of MidasThread it can have a Midas context, use it (e.g. in Macro, Primitive or Macro's Subroutine)
    // 2. inject Midas context when starting new MidasThread via thread local variable via java.lang.ThreadLocal or InheritableThreadLocal (1.2 API)
    // getThreadMidasContext()
    getSharedMidasContext().warning(str);
  }

  /** Issue an informational message to indicate that a method is deprecated.
      @param str message
      @since NeXtMidas 2.7.1
  */
  public static void deprecate (CharSequence str) {
    getSharedMidasContext().deprecate(str);
  }

  /** Prints out a stack trace as an ERROR. This is the same as <code>printStackTrace(msg,throwable,false)</code>.
    @param msg  An additional explanation message about the error (ignored if null).
    @param err  The error/exception.
    @since NeXtMidas 2.7.1
  */
  public static void printStackTrace (CharSequence msg, Throwable err) {
    getSharedMidasContext().printStackTrace(msg, err, false);
  }

  /** Prints out a stack trace as an ERROR. This is the same as <code>printStackTrace(msg,throwable,false)</code>.
    @param err  The error/exception.
    @since NeXtMidas 2.7.0
  */
  public static void printStackTrace (Throwable err) {
    getSharedMidasContext().printStackTrace(err, false);
  }

  /** Prints out a stack trace as a WARNING.
   * @param msg  An additional explanation message about the exception (ignored if null).
   * @param throwable The error/exception
   * @since NeXtMidas 3.7.0
   */
  public static void warnStackTrace (CharSequence msg, Throwable throwable) {
    getSharedMidasContext().warnStackTrace(msg, throwable);
  }

  /** returns a Midas Context for using classes outside of framework,
    allowing servlets,etc to pass in the required environment information.
    @param homepath user home path
    @param nmRoot path containing the root of the NeXtMidas baseline
    @return Midas context or null if context cannot be created
  */
  @InternalUseOnly("Since NeXtMidas 3.5.1")
  public static Midas getMidasContext (String homepath, String nmRoot) {
    System.setProperty("HOME",homepath);
    System.setProperty("NMROOT",nmRoot);
    return getSharedMidasContext();
  }

  /** Return the url String to the given resource. URLs are more useful to us than
      paths since the resource may lie within a jar file, on a web site, or within a file.
      @param path path to resource
      @param root resource root name
      @param ext  resource extension
      @return String representation of URL
   */
  public static String getResourceUrl (String path, String root, String ext) {
    // If the path begins with a / get rid of it and then form the true path
    // by adding a datafile extension if there isn't one.
    if (path.startsWith("/")) path = path.substring(1);
    if (ext.length()==0) ext = "prm";
    String resPath = path + root+ "." + ext;

    Object res = loader.getResource ( resPath );
    if (res!=null) return res.toString();
    else return null;
  }

  /** Emits an audio beep. Unlike {@link Toolkit#beep()} this falls back to using a system beep
      (via the ASCII bell character) on headless systems.
      @since 2.5.5
      @since NeXtMidas 3.9.1 - created to removed dependency on graphics module for Java 11 modularity
   */
  public static void beep () {
    Toolkit toolkit;
    try {
      toolkit = Toolkit.getDefaultToolkit(); // <-- Throws Exception or Error in Java 1.4.2 if headless
    } catch (Throwable e) {
      toolkit = null;
    }
    if ((toolkit != null) && !GraphicsEnvironment.isHeadless()) {
      toolkit.beep();
    }
    else {
      // simulate a beep by printing the ASCII bell character to the terminal
      System.out.print("\u0007");
      System.out.flush();
    }
  }

  /** Gets the version of the currently running Java Virtual Machine (JVM). <br>
      <br>
      Note that since Java 5.0 (1.5.0) the Java versioning has dropped the "1." so
      that "1.5.0" becomes "5.0" however this method will continue to use the internal (old)
      versioning method (thus it will return "1.5.0" for Java 5.0).

      @return The JVM version in the form <code>"1.4.2"</code>.
      @since NeXtMidas 1.9.2: This replaces the deprecated method <code>getJvmVersion()</code>.
   */
  public static String getJavaVersion () {
    /* Get the raw Java version.
       The version may have an underscore (e.g. "1.4.2_04") or a dash (e.g. "1.4.2-p6") for non-GA/non-FCS/beta release
       that represents the update/patch number. In this case, remove the update/patch number.
     */
//  String version = Shell.getProperty("java.version").replace('-', '_'); // e.g. "1.4.2_04" (prior to 3.7.0)
    String version = javaversion.replace('-', '_'); // e.g. "1.4.2_04"
    int index = version.indexOf('_');

    if (index > 0) {
      version = version.substring(0,index);
    }

    return version;
  }

  /** Gets the Java major version number (i.e. 7, 8, 9, 10, 11)
   *  @return The Java major version
   *  @since NeXtMidas 4.0.1
   */
  @InternalUseOnly("Since added in NeXtMidas 4.0.1")
  public static int getJavaMajorVersion () {
    return javaMajorVersion;
  }

  /** Calculates the Java major version number (i.e. 7, 8, 9, 10, 11)
   *  @return The Java major version
   *  @since NeXtMidas 4.0.1
   */
  private static int calcJavaMajorVersion () {
    int _javaMajorVersion = Integer.parseInt(getJavaVersion().split("[.]")[0]);
    if (_javaMajorVersion == 1) _javaMajorVersion = Integer.parseInt(getJavaVersion().split("[.]")[1]);

    return _javaMajorVersion;
  }

  /** Get Java's update version
   *  @return Java's update version (e.g. for 1.8.0_31, it returns 31), if unable to parse/determine, this returns 0. */
  @InternalUseOnly("Since added in NeXtMidas 3.5.3")
  public static int getJavaUpdateVersion () {
    int updateVer = 0;
    String javaVersion = javaversion;
    int index = javaVersion.indexOf('_');              // first look for underscore (for GA or FCS release)
    if (index == -1) index = javaVersion.indexOf('-'); // else look for dash (for non-GA/non-FCS/beta release)
    if(index > 0 && (index + 1) < javaVersion.length()) {
      String str = javaVersion.substring(index + 1);   // move pass underscore/dash
      try {
        updateVer = Integer.parseInt(str);
      } catch (NumberFormatException nfe) {
        // ignore, will use default of zero
      }
    }
    return updateVer;
  }

  /** Return the path to the temp directory. This defaults to the value of the Java property
      <code>"java.io.tmpdir"</code>.
      @return The path of the temp directory.
   */
  public static String getTempDir () {
    return Shell.getProperty("java.io.tmpdir");
  }

  /** Gets a system property. Unlike the normal Java {@link System#getProperty(String)} method, this
   *  supports a number of special cases:
   *  <pre>
   *      {@code System.getProperty(name)}; if it returns a non-null value, that value will be used.
   *    - If {@code System.getProperty(name)} returns a null value, it will then be called using
   *      {@code System.getProperty("jnlp."+name)}. This is because any names that do not have a
   *      "jnlp" prefix are considered insecure when running from Java Web Start in
   *      Java 7u45 and later causing the lookup to fail unless using a signed JAR
   *      (which causes issues for some dynamic applications). [Since NeXtMidas 3.6]
   *    - If an exception is thrown, null will be returned.
   *  </pre>
   *  @param name The name of the property.
   *  @return The value of the property of null if unavailable.
   */
  @SuppressWarnings("deprecation")
  public static String getProperty (String name) {
    String value = null;

    if (value == null) { value = System.getProperty(name); }

    // The following is for handling changes made to java 7u45 where jnlp properties
    // are considered insecure if they do not have a jnlp prefix.  This should only be
    // happening to untrusted applications but we are not sure why our signed jars no
    // longer work. Until this is resolved, we will prepend jnlp. to the properties.
    // The following code will look for these and strip the prefix. BUG 2799
    if (value == null) { value = System.getProperty("jnlp."+name); }
    return value;
  }

  /** Gets a system property with option for default value. Unlike the normal Java
   *  {@link System#getProperty(String)} method, this supports a number of special cases, see
   *  {@link Shell#getProperty(String)} for more details.
   *  @param name         The name of the property.
   *  @param defaultValue a default value.
   *  @return The string value of the property or the default value if there is no
   *          property with that key.
   *  @see #getProperty(String)
   *  @since NeXtMidas 3.1.3
   */
  public static String getProperty (String name, String defaultValue) {
    String val = getProperty(name);

    return (val == null) ? defaultValue : val;
  }

  /** Indicates if the shell is running in one-shot mode.
   *  @return true If one-shot mode, false otherwise.
   *  @since NeXtMidas 1.9.2
   */
  public static boolean isOneShot () {
    return isOneShot;
  }

  /** Indicates if the shell is running in headless mode. If the shell is running in headless mode
      then any features that require graphical capabilities should be disabled by default. This
      indicates if the shell is "structurally" in headless mode (e.g. no monitor is detected, or
      running on Linux without access to an active X11 session) and may return <code>false</code>
      when the shell is asked to run in headless mode, but it is still capable of running
      non-headless. <br>
      <br>
      To prevent Java from trying to auto-detect if it is in headless mode, use the following
      defines when starting NeXtMidas:
      <pre>
        -DHEADLESS=TRUE    (is headless)
        -DHEADLESS=FALSE   (is not headless)
      </pre>

      @return <code>true</code> if in headless mode, <code>false</code> if not.

      @since NeXtMidas 1.9.5
   */
  public static boolean isHeadless () {
    return Theme.isHeadless();
  }

  /** Indicates if the shell has been initialized.
      @return <code>true</code> if a shell has been initialized, <code>false</code> otherwise.
   */
  public static boolean isShellInit () {
    return (currentShell != null);
  }

  /** Get the full path of the option tree jar file
   *  @param opt The option tree
   *  @return the full path of the option tree jar file
   *  @since NeXtMidas 4.1.1 Initially getPathToCallingJar(),
   *                         changed to getPathToOptJar(String opt) in 4.1.4
   */
  @InternalUseOnly
  public static String getPathToOptJar(String opt) {
    if (nmroot.startsWith("classpath:") && !isGradleBuildCommand()) {
      String safeFileToFind = "nxm/"+opt+"/version.txt";
      String pathFile = loader.getResource(safeFileToFind).toString();
      int idxFile = pathFile.indexOf("!/"+safeFileToFind);
      pathFile = pathFile.substring(9,idxFile);
      if (isWindows()) pathFile = pathFile.replace('/', '\\').replace("\\C:", "C:");
      return pathFile;
    }
    else {
      String nmrootRaw = nmroot;
      if (isGradleBuildCommand()) {
        String safeFileToFind = "nxm/"+opt+"/version.txt";
        String pathFile = loader.getResource(safeFileToFind).toString();
        int idxFile = pathFile.indexOf(opt+"/build/resources");
        nmrootRaw = pathFile.substring(5,idxFile);
      }
      File dir  = new File(nmrootRaw+"jars");
      FnFilter filt = FileUtil.createFnFilter(null,"*"+opt+"*.jar",null);
      File[] files = FileUtil.findFiles(dir,filt);
      for (File file : files) {
        String filePath = file.toString();
        String fileName = file.getName();
        if (!fileName.contains("-native") && !fileName.contains("-sources") &&
            !fileName.contains("-test") && !fileName.contains("-demo")) {
          return filePath;
        }
      }
      return null;
    }
  }

  /** Sets a number of system properties required when using Mac OS X. */
  private static void initForMacOsX () {
    System.setProperty("com.apple.mrj.application.apple.menu.about.name", "NeXtMidas");
    System.setProperty("com.apple.mrj.application.growbox.intrudes",      "false"    );
    System.setProperty("apple.awt.graphics.OptimizedShapes",              "false"    );
    System.setProperty("apple.awt.graphics.EnableLazyDrawing",            "false"    );
    System.setProperty("apple.awt.showGrowBox",                           "false"    );
    System.setProperty("apple.awt.brushMetalLook",                        "true"     );
  }

  /** Indicates if running from X-Midas.
      @return true if running from X-Midas
      @throws NullPointerException if current shell has NOT been initialized
   */
  @InternalUseOnly("This has always been might for internal use only.")
  public static boolean isNeXtOpt () {
    ShellParent parent = currentShell.parent;
    return (parent != null) && parent.getClass().getName().equals("nxm.sys.libxm.XmShellParent");
  }

  /** Indicates if running from Java Web Start via Webstart class.
   *  @return true if
   *  @since NeXtMidas 3.6.0 running from Java Web Start via Webstart class.
   */
  @InternalUseOnly("Since initial version")
  public static boolean isWebStart () {
    return (currentShell != null && currentShell.parent instanceof Webstart);
  }

  /** Indicates if running on Windows platform.
   * This implementation determines this by looking for "Windows" in "os.name" system property.
   * @return true if running on Windows, false otherwise
   * @since NeXtMidas 3.7.0
   */
  @ProvisionalUseOnly("Added in NeXtMidas 3.7.0")
  public static boolean isWindows () {
    return osname.contains("Windows");
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Methods for modifying the class path and class loading while the Java VM is running.
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Indicates if using a custom NeXtMidas class loader is being used.
      @return <code>true</code> if the custom NeXtMidas class loader (that supports additions to
              the class path and class path aliases) is being used, <code>false</code> if it is not.
      @see #addToClasspath(String)
      @see #addOptToClasspath(String)
   */
  public static boolean isUsingNmClassLoader () {
    return (nmClassLoader != null);
  }

  /** Update the OPTS.CLASSPATH._CURRENT_ entry with the classpath from the current class
      loader, only if it it is an instance of URLClassLoader (via getULRs() method), otherwise
      just append to that entry.
      @param cpTable The OPTS.CLASSPATH Table to update the current classpath entry
      @param url     (optional) If current class loader is NOT instanceof URLClassLoader,
                     then append this URL (if not null) to the current classpath entry.
      @since 2.9.3
   */
  static void updateFromClassLoader (Table cpTable, URL url) {
    String newClassloaderCP = null; // comma-separated
    if (loader instanceof URLClassLoader) {
      newClassloaderCP = StringUtil.join(((URLClassLoader)loader).getURLs(), ",");
    } else if (url != null) {
      newClassloaderCP = cpTable.getS("_CURRENT_", "") + "," + url;
    }

    if (newClassloaderCP != null) {
      cpTable.put("_CURRENT_", newClassloaderCP, Table.FORCE);
    }
  }

  /** Adds (appends) an element to the CLASSPATH. Note that this has no affect if
      {@link #isUsingNmClassLoader()} returns <code>false</code>.
      @param cp  The classpath element to add (may map to a directory or a JAR). This may be a comma-separated list
                 of items to add. If a URL is included then it **MUST** end with a slash ('/') if it is a directory.
      @see #isUsingNmClassLoader()
      @see #addToClasspath(File)
      @see #addToClasspath(URL)
      @see #addToClasspath(String[])
      @see #addOptToClasspath(String)
   */
  public static void addToClasspath (String cp) {
    if (cp == null) return; // ignore null input
    String[] urls = StringUtil.COMMA_REGEX.split(cp); // this may be a comma separated list

    addToClasspath(urls);
  }

  /** Adds (appends) an element to the CLASSPATH. Note that this has no affect if
      {@link #isUsingNmClassLoader()} returns <code>false</code>.
      @param cpEntries  The array of classpath element to add (may map to a directory or a JAR).
             If a URL is included then it **MUST** end with a slash ('/') if it is a directory.
      @see #addToClasspath(String)
      @since NeXtMidas 3.0.1
   */
  public static void addToClasspath (String[] cpEntries) {
    if (cpEntries == null) return;                  // ignore null input

    for (String urlstr : cpEntries) {
      if (StringUtil.isEmpty(urlstr)) { continue; } // ignore null/empty entries
      urlstr = urlstr.trim();
      if (urlstr.length() == 0) { continue; }       // ignore blank strings with length > 0

      try {
        // If a path name, convert it to "file:" (note: Use '>1' not '>=0' since Windows may have drive letter)
        if (urlstr.indexOf(':') > 1) addToClasspath((new URI(urlstr)).toURL());
        else                         addToClasspath(new File(urlstr));
      }
      catch (MalformedURLException | URISyntaxException e) {
        throw new MidasException("ERROR: Unable to add url='" + urlstr + "' to classpath.", e);
      }
    }
  }

  /** Adds an element to the class path. This is the same as
      <code>addToClasspath(file.toURI().toURL())</code>
      @param file The classpath element to add (may map to a directory or a JAR/Zip file).
      @see #addToClasspath(URL)
   */
  public static void addToClasspath (File file) {
    try {
      // Use File.toURI().toURL() to make a URL for the file since it will properly map
      // directories with a trailing '/' and will properly handle escaping "illegal characters".
      addToClasspath(file.toURI().toURL());

    } catch (MalformedURLException e) {
      throw new MidasException("ERROR: Unable to add file='" + file + "' to classpath.", e);
    }
  }

  /** Adds (APPENDS) an element to the class path.
      Note that this has no affect if {@link #isUsingNmClassLoader()} returns <code>false</code>.
      @param url The classpath element to add (may map to a directory or a JAR/Zip file). If this
                 represents a directory it **MUST** end with a slash ('/').
      @see #isUsingNmClassLoader()
      @see #addToClasspath(String)
   */
  public static void addToClasspath (URL url) {
    if (isUsingNmClassLoader()) {
      try {
        nmClassLoaderAddUrl.invoke(nmClassLoader, new Object[] { url });

        if (options != null) {
          updateFromClassLoader(options.getTable("CLASSPATH"), url);
        }
      } catch (Exception e) {
        throw new MidasException("ERROR: Unable to add '" + url + "' to classpath.", e);
      }
    }
    else {
      updateFromClassLoader(options.getTable("CLASSPATH"), url);
    }
  }

  /** This will add an option tree to the classpath. This adds the basic option tree root
      (stored in <code>OPT.[opt]</code>, if not <code>NMROOT</code>) and adds all the JARS
      in the <code>nxm/[opt]/jars/</code> directory. Note that this has no affect if
      {@link #isUsingNmClassLoader()} returns <code>false</code>.
      @param opt The name of the option tree.
      @see #isUsingNmClassLoader()
      @see #addToClasspath(String)
   */
  public static void addOptToClasspath (String opt) {
    addOptToClasspath(opt, false);
  }

  /** This will add an option tree to the classpath. This adds the basic option tree root
      (stored in <code>OPT.[opt]</code>, if not <code>NMROOT</code>) and adds all the JARS
      in the <code>nxm/[opt]/jars/</code> directory. Note that this has no affect if
      {@link #isUsingNmClassLoader()} returns <code>false</code>.
      @param opt  The name of the option tree (should not be null and should be in UPPER-CASE)
      @param save persist underneath OPT.CLASSPATH Table (Only for internal usage by Configure command)
      @see #isUsingNmClassLoader()
      @see #addToClasspath(String)
      @since NeXtMidas 2.9.3
   */
  public static void addOptToClasspath (String opt, boolean save) {
    opt = opt.toUpperCase();
    String[] elements = getClasspathFor(opt);
    Table cpTable = options.getTable("CLASSPATH");
    int ii = 0;

    for (String element : elements) {
      addToClasspath(element);
      if (save) {
        String key = opt+"_"+Integer.toString(ii);
        cpTable.put(key, element, Table.FORCE);
        ii++;
      }
    }

    if (ii>0) {
      String optPath = options.getS(opt);
      if (optPath != null) { // only track add option trees NOT under $NMROOT
        OptionTree.addedToPath(opt, optPath);
      }
    }
  }

  /** Add option trees (and it's jars) in specified path to the classpath and
      save them to the OPT.CLASSPATH Table.
      @param pathstr NeXtMidas PATH (i.e. from OPT.PATH)
      @see #addOptToClasspath(String, boolean)
      @since NeXtMidas 2.9.3
   */
  void addPathToClasspath (String pathstr) {
    String[] path = pathstr.split(",");

    for (String opt : path) {
      opt = opt.trim();
      if (opt.length() == 0) { continue; } // Ignore ",,"
      addOptToClasspath(opt, true);
    }
  }

  /** Gets the classpath elements for a given option tree. This functionality was initially used
      ONLY by Shell, but was found that Build also needs it when building option trees not on the
      PATH. No other classes should call this method.
      @param opt The option tree name.
      @return The path elements for the option tree.
   */
  public static String[] getClasspathFor (String opt) {
    ArrayList<String> elements = new ArrayList<>(); // The class path elements
    String   root     = options.getString(opt.toUpperCase());
    String[] jars     = OptionTree.getJars(opt);

    if ((root != null) && !root.equals(getNmRoot())) {
      elements.add(root.toString());
    }

    if (jars != null) {
      elements.addAll(Arrays.asList(jars));
    }

    return elements.toArray(new String[0]);
  }

  /** Get current global / default theme
   *  @return current global / default theme
   */
  public Theme getTheme() {
    if (theme!=null) return theme;
    String name = (String)M.results.get("ENV.THEME");
    if (name==null) name = Theme.defaultTheme;
    theme = Theme.getTheme(name);
    return theme;
  }

  /** Check the COMPLETIONAUXEXCLUDE system property for any AUX's which should be skipped when
      finding files via the FILES command or tab-completion. If found, then it seeds/sets
      AUX._COMPLETIONAUXEXCLUDE_  with the values from that pipe-separated list.
      @since NeXtMidas 3.1.0
  */
  private void initCompletionExcludedAuxes () {
    // This method added for Bug 2482
    String excludeList = Shell.getProperty("COMPLETIONAUXEXCLUDE");
    setCompletionExcludedAuxes(excludeList);
  }

  /** Set the list of AUXes which will not be searched when using the FILES command
      with wildcards or with command line tab-completion
      @param excludeList List of pipe-separated AUXes (e.g. "RAM|99")
      @since NeXtMidas 3.1.1
   */
  public void setCompletionExcludedAuxes (String excludeList) {
    // Method added for Bug 2482
    if (excludeList != null) {
      excludeList    = StringUtil.stripQuotes(excludeList);   // need to remove quotes from Windows ENV variable
      StringBuilder sb = new StringBuilder("|");
      Parser parsedList = new Parser(excludeList, '|', true); // clean=true
      for (String aux : parsedList.getArray(0)) {
        if (aux!=null && aux.length()>0) sb.append(aux).append('|');
      }
      M.results.put("AUX._COMPLETIONAUXEXCLUDE_", sb, Table.FORCE);
    }
  }

  /** Gets the values in list of AUXes to exclude when searching for files via the FILES command or tab completion.
      @return pipe-separated String if AUX has been set, or else null.
      @since NeXtMidas 3.1.0
  */
  public String getExcludedAuxes () {
    // This method added for Bug 2482
    return M.results.getS("AUX._COMPLETIONAUXEXCLUDE_");
  }

  /** Whether or not a given AUX is to be excluded when searching for files via the FILES command or tab-completion.
      @param auxName name of AUX to check against excluded list
      @return true if it is on the excluded list
      @since NeXtMidas 3.1.0
   */
  public boolean isExcludedAux (String auxName) {
    // This method added for Bug 2482
    String excludeList = M.results.getS("AUX._COMPLETIONAUXEXCLUDE_",null);
    if (excludeList == null || excludeList.length() == 0) {
      return false;
    }

    auxName = auxName.toUpperCase();
    Parser parsedList = new Parser(excludeList, '|', true);
    for (String aux : parsedList.getArray(0)) {
      if (aux.equals(auxName)) return true;
    }
    return false;
  }

  /** @since NeXtMidas 3.5.1 */
  @Override
  public Midas getMidas () {
    return this.M;
  }

  /** replace global Shell.currentShell (static class variable) with specified Shell.
      @param shell used to replace global Shell.currentShell
      @return previous global Shell.currentShell
    */
  @InternalUseOnly("Since added in NeXtMidas 3.5.2")
  static Shell setCurrentShell (Shell shell) {
    Shell prevShell = Shell.currentShell;
    Shell.currentShell = shell;
    return prevShell;
  }

  /** Checks for an interrupt in the Midas object. If there is interrupt, and it has a macro,
   *  abort. Reset interrupt to 0.
   *
   *  @param M Midas object checking inside of.
   *  @since NeXtMidas 3.7.1
   */
  @SuppressWarnings("deprecation") // for M.terminal - since 3.7.1 for NXM-2813
  public static void checkAndSetInterrupt(Midas M){
    if(M.terminal.getInterrupt()>0) {
      if(M.macro!=null) {
        M.macro.state=Commandable.ABORT;
      }
      M.terminal.setInterrupt(0);  // reset the interrupt (BUG 2150)
    }
  }

  /** This will get a list of all the option trees that have had their documents
      compiled at some point in the past. <br>
      <br>
      This works by listing all of the directories (each representing an option
      tree) that are in the <code>$NMROOT/htdocs/api</code> directory.
      @return The list of option trees that have had their documents compiled.
              All values in this array will be capitalized.
      @since NeXtMidas 1.8.0
   */
  public static String[] getKnownOptionTrees () {
    String[] options = null;
    try {
      /* This will filter out all non-directories. */
      final FilenameFilter filter = new FilenameFilter() {
        public boolean accept (java.io.File dir, String name) {
          return (new java.io.File(dir, name)).isDirectory();
        }
      };
      options = (new java.io.File(JAVADOC_PATH)).list(filter);
    } catch (Exception exception) {
      // ignore and help below when options is null
    }
    if (options == null) {
      options = StringUtil.EMPTY_STRING_ARRAY; // since 3.3.4: prevent NPE when $NMROOT/htdocs/api has not been created
                                               // yet
    }
    /* Capitalize the names. */
    for (int i = 0; i < options.length; i++) {
      options[i] = options[i].toUpperCase();
    }
    return options;
  }

  /** Prompts the user for the password for the private key
      @param message The message to display to the user.
      @return The password entered by the user.
      @since NeXtMidas 3.9.1 - created to removed dependency on graphics module for Java 11 modularity
   */
  public static char[] passwdFromGUI (String message) {
    if(Shell.isHeadless()) return null;
    int messageType = JOptionPane.QUESTION_MESSAGE;
    JPanel jp = new JPanel(); // Panel to hold label and Password field
    jp.setLayout(new GridLayout(2, 0)); // Set layout to have label on top row and password field on bottom row
    JLabel jl = new JLabel(message);// Create jlabel using provided message
    JPasswordField pf = new JPasswordField(); // create password field
    jp.add(jl); // add label
    jp.add(pf); // add password field
    // display the panel and get result - the int value of the button pressed
    int result = JOptionPane.showConfirmDialog(null , jp, "Password Requested", JOptionPane.OK_CANCEL_OPTION, messageType);
    String userPwd = null;
    if(result == JOptionPane.OK_OPTION) {
      // if ok pressed, use the password provided
      userPwd = new String(pf.getPassword());
    }
    return (userPwd == null) ? null : userPwd.toCharArray();
  }

  /** Create Popup with Selections
      @param messageType - int representation of the type of message - static options can be found in {@link JOptionPane}
      @param title - String representation of the title of the popup window
      @param message -String message to describe the window
      @param choices - Array of objects to
      @return The selection made from the choices
      @since NeXtMidas 3.9.2 - created to removed dependency on graphics module for Java 11 modularity
   */
  public static Object createPopupWithSelections (int messageType, String title, String message, Object[] choices) {
    if(Shell.isHeadless()) return null; // Don't try to do a popup if we are not doing any graphics
    return JOptionPane.showInputDialog(null, message, title, messageType, null, choices, 0);
  }

}
