/* **************************************************************************
 %name: shell.java %
 %version: 14 %
 %date_modified: Thu Feb 19 15:51:29 1998 %

 Copyright (c) 1997-1999 Novell, Inc. All Rights Reserved.

 THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND TREATIES.
 USE AND REDISTRIBUTION OF THIS WORK IS SUBJECT TO THE LICENSE AGREEMENT
 ACCOMPANYING THE SOFTWARE DEVELOPMENT KIT (SDK) THAT CONTAINS THIS WORK.
 PURSUANT TO THE SDK LICENSE AGREEMENT, NOVELL HEREBY GRANTS TO DEVELOPER A
 ROYALTY-FREE, NON-EXCLUSIVE LICENSE TO INCLUDE NOVELL'S SAMPLE CODE IN ITS
 PRODUCT. NOVELL GRANTS DEVELOPER WORLDWIDE DISTRIBUTION RIGHTS TO MARKET,
 DISTRIBUTE, OR SELL NOVELL'S SAMPLE CODE AS A COMPONENT OF DEVELOPER'S
 PRODUCTS. NOVELL SHALL HAVE NO OBLIGATIONS TO DEVELOPER OR DEVELOPER'S
 CUSTOMERS WITH RESPECT TO THIS CODE.
****************************************************************************/

import java.io.*;
import java.util.*;

import javax.naming.*;
import javax.naming.directory.*;
import javax.naming.spi.*;

/**
 * Sets up a useful environment in which the JNDIShell class can operate.
 *
 * <p>The main method interprets any strings passed to it as
 * command line parameters.
 *
 * <p>The runShell method runs the shell given a set of minimal
 * options and the necessary input and output streams.
 *
 * @author John Sumsion, Novell, Inc.
 * @version 1.1.5
 */

public class shell
{
   /**
    * Program identification for the command line.
    */
   public static final String helpStr = "JNDI Shell (c) 1997-1999 " +
         "Novell, Inc.";
   public static final String jndiInitialContextKey =
         "java.naming.factory.initial";

   /**
    * Takes parameters from the command line and spawns a JNDIShell
    * according to the options specified in the command line parameters.
    * To get a list of valid parameters run this class with the -? or
    * -h command line options. The main method contains all the 
    * System.out.println calls, and any special policies for loading
    * a startup file.
    *
    * <p>The shell is actually spawned by the runShell() method.
    * Therefore, if you wanted to use this shell in a browser or
    * other environment where System.in and System.out and File
    * don't mean much, then set everything up and call the runShell()
    * method with the appropriate parameters.
    *
    * @param argv  The command line parameters as a string array.
    */
   public static void main (String[] argv)
   {
      System.out.println (shell.helpStr);

      boolean continueWStdIn = false;
      boolean verbose = false;
      boolean startDS = false;
      int startArgs = 0;  // used to mark last non-option cmd. line param.

      /* Cycle through the command line.
       */
      for (int a = 0; a < argv.length; a++)
      {
         /* If it's not an option, then break this loop and assume
          * no more options are in the command line.
          */
         if (!argv[a].startsWith ("-"))
         {
            startArgs = a;
            break;
         }
         else
         {
            /* Get the option minus the '-' char.
             */
            String option = argv[a].substring (1);

            /* Increment up the non-option pointer.
             */
            startArgs = a + 1;

            /* The verbose option when set to TRUE means that a stack
             * dump is printed for any exceptions.
             */
            if (option.equals ("verbose"))
               verbose = true;
            /* Start with InitialDirContext instead of InitialContext
             */
            else if (option.equals ("ds"))
               startDS = true;
            /* Display help for -? and -h options.
             */
            else if (option.toUpperCase ().equals ("H") ||
                option.equals ("?"))
            {
               String[] helpStrs =
               {
                  "usage: java shell [-verbose] [-ds] [<input> [output]]",
                  "",
                  "  -verbose    stack trace printed on all exceptions",
                  "  -ds         start with InitialDSContext",
                  "  input       input file from which to read commands",
                  "  output      output file to which to write results",
                  "",
                  "If an input file is specified, but no output file,",
                  "then control is returned to the terminal after",
                  "executing all input commands.",
                  "",
                  "'#' and ';' are comment characters."
               };

               for (int x = 0; x < helpStrs.length; x++)
                  System.out.println (helpStrs[x]);

               return;
            }
            else
            {
               System.out.println ("error: invalid option: " + option);
               return;
            }
         }
      }

      Properties props = new Properties ();
      File file = new File ("shell.ini");  // startup file, contains props

      /* Look for the startup file.
       */
      if (file.exists ())
      {
         try
         {
            /* Load any properties specified in the startup file.
             */
            props.load (new FileInputStream (file));
         }
         catch (IOException e)
         {
            System.out.println ("error: reading shell startup properties");
         }
      }

      /* Look for the initial context factory property, which is necessary
       * for the class java.naming.InitialContext to function correctly.
       * InitialContext gets this system property and treats the contents
       * as a fully distinguished class name, which should implement
       * java.naming.spi.InitialContextFactory.
       */
      if (!props.containsKey (Context.INITIAL_CONTEXT_FACTORY))
      {
         /* The runShell method sets the default initialContext
          * to be COM.novell.jndi.nw.NetWareInitialContextFactory.
          */
         System.out.println ("warning: no '" + jndiInitialContextKey +
               "' property, using NetWareInitialContextFactory");
      }

      InputStream in = System.in;
      OutputStream out = System.out;

      try
      {
         /* Get the first non-option parameter and open the input stream.
          */
         if (argv.length - startArgs > 0)
         {
            in = new FileInputStream (new File (argv[startArgs]));

            /* Tell the shell to return to System.in for input
             * after the last input command. This allows for a setup
             * script to be run to get you in a known state so that
             * you can test further.
             */
            continueWStdIn = true;
         }

         /* Get the second non-option parameter and open the output stream.
          */
         if (argv.length - startArgs > 1)
         {
            out = new FileOutputStream (new File (argv[startArgs + 1]));

            // tell the shell to terminate after last input command
            continueWStdIn = false;
         }

         /* Load and run the shell with the default command bindings.
          */
         runShell (in, out, props, continueWStdIn, verbose, startDS);
      }
      catch (Throwable t)
      {
         if (verbose)
         {
            PrintWriter err = new PrintWriter (new OutputStreamWriter (
                  System.out), true);

            JNDIShell.printException ("shell", err, t, verbose);
         }
         else
            System.out.println ("shell: " + t.toString ());
      }

      System.gc ();
      System.exit (0);
   }

   /**
    * Sets up a useful environment in which JNDIShell can operate.
    *
    * <p>Binds the following commands to the shell: load, unload,
    * dir, cd, cda, cds, cdcdef, cdadef, cdsdef, attr, ren, bind,
    * rebind, unbind, del, shell, exec, v, exit, quit, q, help, h,
    * and ?.
    * 
    * With the cd, cda, dir, show, bind, and rebind commands, a
    * relative name may include '..', meaning the parent context.
    * '...' means '../..' and so forth. With other commands, this
    * may not be used, because a backwards-relative name cannot be
    * passed directly to JNDI.
    *
    * <p>If the properties that are provided to this method do not
    * contain the "java.naming.factory.initial" property, this
    * property is added with the default value of
    * "com.novell.service.nw.NetWareInitialContextFactory".
    *
    * @param in             The input stream from which to read
    *                       commands.
    * @param out            The output stream to which to write output
    *                       (can be NULL for no output).
    * @param props          The properties that contains any custom
    *                       values for the property
    *                       "java.naming.factory.initial" and other
    *                       properties needed by JNDI and the providers.
    * @param continueWStdIn Set to TRUE if control should be returned
    *                       to System.in after all commands have been
    *                       executed as read from the input stream.
    * @param verbose        Set to TRUE if stack dumps should be done
    *                       when exceptions occur.
    */
   public static void runShell (InputStream in, OutputStream out,
         Properties props, boolean continueWStdIn, boolean verbose,
         boolean startDS)
      throws NamingException
   {
      /* Set the default value to "jndi.initialContextFactory" if
       * not present.
       */
      if (!props.containsKey (jndiInitialContextKey))
      {
         props.put (jndiInitialContextKey,
               "com.novell.service.nw.NetWareInitialContextFactory");
      }

      JNDIShell shell;
      Context ctx;
      
      if (startDS)
         ctx = new InitialDirContext (props);
      else
         ctx = new InitialContext (props);

      shell = new JNDIShell (ctx, in, out, continueWStdIn, verbose);

      /* Sets up the default command bindings. Because of load and
       * unload, these can then be changed during the course of the
       * execution of the shell.
       */
      shell.addCommand ("load",   new LoadCommand ());
      shell.addCommand ("unload", new UnloadCommand ());
      shell.addCommand ("dir",    new DirCommand ());
      shell.addCommand ("cd",     new CdCommand ());
      shell.addCommand ("cda",    new CdAtomicCommand ());
      shell.addCommand ("cds",    new CdSchemaRootCommand ());
      shell.addCommand ("cdcdef", new CdSchemaClassDefCommand ());
      shell.addCommand ("cdadef", new CdSchemaAttributeDefCommand ());
      shell.addCommand ("cdsdef", new CdSchemaSyntaxDefCommand ());
      shell.addCommand ("attr",   new AttrCommand ());
      shell.addCommand ("show",   new ShowCommand ());
      shell.addCommand ("ren",    new RenameCommand ());
      shell.addCommand ("bind",   new BindCommand ());
      shell.addCommand ("rebind", new RebindCommand ());
      shell.addCommand ("unbind", new UnbindCommand ());
      shell.addCommand ("create", new CreateCommand ());
      shell.addCommand ("del",    new DestroyCommand ());
      shell.addCommand ("env",    new EnvCommand ());
      shell.addCommand ("shell",  new ShellCommand ());
      shell.addCommand ("exec",   new ExecCommand ());
      shell.addCommand ("v",      new VerboseToggleCommand ());
      shell.addCommand ("exit",   new ExitCommand ());
      shell.addCommand ("quit",   new ExitCommand ());
      shell.addCommand ("q",      new ExitCommand ());
      shell.addCommand ("help",   new HelpCommand ());
      shell.addCommand ("h",      new HelpCommand ());
      shell.addCommand ("?",      new HelpCommand ());

      shell.run ();
   }
}

/**
 * Adds a command class to the shell under a command name.
 *
 * Help is provided by implementing the CommandWithHelp interface.
 */
class LoadCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length < 2)
         throw new ShellException ("error: command name not specified");
      if (argv.length < 1)
         throw new ShellException ("error: command class not specified");
      if (argv.length > 2)
         throw new ShellException ("error: too many arguments");

      Object obj = null;

      /* Try to load the class specified in first parameter.
       */
      try
      {
         obj = Class.forName (argv[0]).newInstance ();
      }
      catch (ClassNotFoundException e)
      {
         throw new ShellException ("error: class not found: " + argv[0]);
      }
      catch (Exception e)
      {
         throw new ShellException ("error: could not instantiate class: " +
               argv[0]);
      }

      /* The class must implement the Command interface.
       */
      if (!(obj instanceof Command))
      {
         throw new ShellException ("error: class is not a Command: " +
               argv[0]);
      }

      /* Bind the command to the shell using the name in second parameter.
       */
      shell.addCommand (argv[1], (Command) obj);

      PrintWriter out = new PrintWriter (shell.getOutputStream (), true);

      out.println ("Command sucessfully loaded and bound to '" +
            argv[1] + "'");
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <cmdclass> <cmdname>",
         "  cmdname: overwrites any command already bound to shell",
         "Notes:",
         "- Adds a command to the shell under the specified command name.",
         "- The command class must implement the 'Command' interface.",
      };

      return (helpStrs);
   }
}

/**
 * Removes a command from the shell. Note that UnloadCommand can remove
 * itself, so be careful. Help is provided by implementing the
 * CommandWithHelp interface.
 */
class UnloadCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length < 1)
         throw new ShellException ("error: command name not specified");
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");

      Command oldCommand = shell.removeCommand (argv[0]);

      if (oldCommand == null)
         throw new ShellException ("error: no command bound to: " + argv[0]);

      PrintWriter out = new PrintWriter (shell.getOutputStream (), true);

      out.println ("Command sucessfully unbound");
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <cmdname>",
         "Notes:",
         "- Removes a command from the shell."
      };

      return (helpStrs);
   }
}

/**
 * Lists bindings of the current context or a relative context. Help
 * is provided by implementing the CommandWithHelp interface.
 */
class DirCommand implements CommandWithHelp
{
   PrintWriter out = null;

   /**
    * Perfoms a possibly recursive list on a context.
    *
    * @param ctx     The Context for which to list bindings.
    * @param path    The literal path for listing. Allows the list
    *                method to be called directly with a certain string
    *                name.
    * @param level   The level of recursion (used to print indented levels.
    * @param recurse A boolean that determines whether or not to print the
    *                contents of any bindings that are also Contexts.
    * @param objects A boolean set to TRUE if listBindings() will be called,
    *                otherwise set to FALSE if list() will be called.
    *
    * @return        The number of elements listed.
    *
    * @exception NamingException When there is a naming error.
    */
   private int listContext (Context ctx, String path, int level,
         boolean recurse, boolean nns, boolean objects)
      throws NamingException
   {
      /* Get a list of names and classes of all objects bound to the
       * context.
       */
      Enumeration enum;
      int count = 0;

      if (path == null)
         path = "";

      if (objects)
         enum = ctx.listBindings (path);
      else
         enum = ctx.list (path);

      while (enum.hasMoreElements ())
      {
         Object element = null;
         boolean elementExists = true;

         try
         {
            element = enum.nextElement ();
         }
         catch (NoSuchElementException e)
         {
            elementExists = false;
         }

         /* Print any indent spaces based on the level.
          */
         for (int i = 0; i < level; i++)
            out.print ("  ");

         /* Always print out the item.
          */
         if (elementExists)
            out.print (element);
         else
            out.print ("*** No such element ***");

         count++;

         /* If we're doing recursion, then go in here.
          */
         if (recurse)
         {
            Object obj = null;
            boolean resolvable = true;

            /* The resolvable boolean variable will be set to FALSE if:
             * 1) We are doing a name class pair list, and:
             *   a) Context.lookup() returns NULL
             *   b) Context.lookup() throws NamingException
             * 2) We are doing a binding list, and:
             *   a) BindingEnumerator.next() returns NULL
             *   b) BindingEnumerator.next() throws NamingException
             *   c) Binding.getObject() returns NULL
             *
             * Lookup the bound object, so we can determine if it's a
             * Context.
             */
            try
            {
               if (objects)
                  obj = ((Binding) element).getObject ();
               else
                  obj = ctx.lookup (((NameClassPair) element).getName ());

               if (obj == null)
                  resolvable = false;
            }
            catch (NamingException e)
            {
               resolvable = false;
            }

            if (!resolvable)
            {
               // if the object is listed, but cannot be resolved,
               //   then tell the user so
               out.println (" (not resolvable)");
               continue;
            }

            out.println ();

            /* If we got a context back, try a list on it (level + 1).
             */
            if (obj instanceof Context)
            {
               /* Do not use literal path on a sub-list.
                */
               count += listContext ((Context) obj, null, level + 1, recurse,
                     nns, objects);
            }
         }
         else
            out.println ();
      }

      /* If nothing showed up then try the next naming system
       * The next naming system is indicated by an empty component
       * on the end of a name (eg. Trees/NOVELL_INC/ could resolve
       * to the root NDS context, not a tree context) even if recurse
       * is FALSE, this code will execute, thereby allowing the user
       * to see the NNS.
       */
      if (nns && count == 0)
      {
         Object obj = null;

         /* The nns pointer is not counted in the total of items listed.
          */
         try
         {
            obj = ctx.lookup ("/");
         }
         catch (NamingException e)
         {
            /* If and exception is thrown, obj will still be NULL so
             * the test 'obj instanceof Context' will fail.
             */
         }

         if (obj instanceof Context)
         {
            for (int i = 0; i < level; i++)
               out.print ("  ");

            out.println ("--> implicit next naming system (NNS)");

            /* Do not use a literal path on a sub-list.
             */
            count += listContext ((Context) obj, null, level + 1, recurse,
                  nns, objects);
         }
      }

      return (count);
   }

  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      Hashtable args = new Hashtable ();
      String path = "";

      /* Extract the options from command.
       */
      for (int i = 0; i < argv.length; i++)
      {
         if (argv[i].charAt (0) == '-')
            args.put (argv[i].substring (1), argv[i].substring(1));
         else if (!path.equals (""))
            throw new ShellException ("error: too many arguments");
         else
            path = argv[i];
      }

      Object obj;
      boolean isLiteral = (args.get("literal") != null);

      /* The -literal option means to pass the context name directly
       * to list() or listBindings() with no pre-resolution of the name.
       */

      if (!isLiteral)
      {
         /* Use the current context if no subordinate is specified.
          */
         if (path.equals (""))
            obj = shell.getCurrCtx ().getContext ();
         /* Resolve the subordinate context.
          */
         else
         {
            obj = shell.resolveRelative (shell.getCurrCtx (), path).
                  getObject();
         }

         /* Set 'path' to null to signal listContext() to not use a path.
          */
         path = null;
      }
      /* If isLiteral is TRUE, and no path is specified, path will be "".
       */
      else
         obj = shell.getCurrCtx ().getContext ();

      /* Any subordinate name must resolve to a Context.
       */
      if (!(obj instanceof Context))
         throw new ShellException ("error: path specified is not a Context");
      else
      {
         PrintWriter out = new PrintWriter (shell.getOutputStream (), true);
         boolean recurse = (args.get ("s") != null);
         boolean nns = (args.get ("nns") != null);
         boolean objects = (args.get ("b") != null);
         int count;

         /* The -s option means a recursive list.
          * The -nns option means lookup nns if no bound objects.
          * The -b option means a bound object list.
          */

         this.out = out;
         count = listContext ((Context) obj, path, 0, recurse, nns, objects);

         if (count == 0)
            out.println ("No elements found to list!");
         else
            out.println (count + " element(s) listed.");
      }
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [-s] [-nns] [-b] [-literal] [context]",
         "  s:   lists recursively any binding that is a Context",
         "  nns: looks for NNS on contexts that have no bindings",
         "  b:   calls listBindings() to obtain list",
         "  literal: passes context directly to list(),listBindings()",
         "  context: '..' directives valid unless -literal is used",
         "Notes:",
         "- Lists bindings of the current or a relative context."
      };

      return (helpStrs);
   }
}

/**
 * Changes the current context to a relative context. Help is
 * provided by implementing CommandWithHelp.
 */
class CdCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");

      if (argv.length == 0)
      {
         Context initCtx = shell.getCurrCtx ().getInitialContext ();

         shell.setCurrCtx (new NamedContext (new CompositeName (),
               initCtx, initCtx));

         shell.setPrompt ("");
      }
      else
      {
         NamedObject obj;

         /* Resolve the relative name.
          */
         obj = shell.resolveRelative (shell.getCurrCtx (), argv[0]);

         /* The name must resolve to a Context.
          */
         if (!(obj.getObject () instanceof Context))
         {
            throw new ShellException ("error: path specified is not " +
                  "a Context");
         }

         /* Set the current shell context.
          */
         shell.setCurrCtx (new NamedContext (obj.getName (),
            (Context) obj.getObject (), shell.getCurrCtx ().
            getInitialContext ()));

         /* Reset the prompt to the absolute name of the resolved
          * context.
          */
         shell.setPrompt (obj.getName ().toString ());
      }
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [context]",
         "  context: '..' directives are allowed unless -literal used",
         "Notes:",
         "- Changes current context to a relative context, tracking the",
         "  name using the CompositeName naming convention.",
         "- If no '..' directives are used, the name is passed directly to",
         "  the lookup() method of the current context.",
         "- If no relative context is specified, then the current context",
         "  is reset to the initial context.",
      };

      return (helpStrs);
   }
}

/**
 * Changes the current context to a literal relative context.
 * The name is tracked using the current context's name parser.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class CdAtomicCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");

      if (argv.length == 0)
         throw new ShellException ("error: atomic name must be specified");

      NamedObject obj;

      /* Resolve the atomic name.
       */
      obj = shell.resolveRelativeAtomic (shell.getCurrCtx (), argv[0]);
      if (!(obj.getObject () instanceof Context))
      {
         throw new ShellException ("error: path specified is not " +
               "a Context");
      }

      /* Set the current shell context.
       */
      shell.setCurrCtx (new NamedContext (obj.getName (),
            (Context) obj.getObject (), shell.getCurrCtx ().
            getInitialContext ()));

      /* Reset the prompt to the absolute name of the resolved atomic
       * context.
       */
      shell.setPrompt (obj.getName ().toString ());
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [context]",
         "  context: NO '..' directives allowed",
         "Notes:",
         "- Changes current context to a literal relative context, tracking",
         "  the name using the current context's name parser.",
         "- The target context must support the getNameParser() method."
      };

      return (helpStrs);
   }
}

/**
 * Starts a new shell with the initial context at the schema root of
 * the specified, relative DirContext. Help is provided by
 * implementing the CommandWithHelp interface.
 */
class CdSchemaRootCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");

      Object objForSchema = shell.getCurrCtx ().getContext ();

      if (!(objForSchema instanceof DirContext))
         throw new ShellException ("error: current context is not a " +
               "DirContext");

      String relName = argv.length > 0 ? argv[0] : "";
      DirContext schemaCtx = ((DirContext) objForSchema).getSchema (relName);
      JNDIShell child = new JNDIShell (shell, schemaCtx, null, null);

      child.run ();
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [dircontext]",
         "  dircontext: no '..' directives allowed",
         "Notes:",
         "- Starts a child shell with the initial context at the context",
         "  returned by calling getSchema() with the specified name."
      };

      return (helpStrs);
   }
}

/**
 * Starts a new shell with the initial context at the schema class
 * definition of the specified relative DirContext. Help is
 * provided by implementing the CommandWithHelp interface.
 */
class CdSchemaClassDefCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");

      Object objForSchema = shell.getCurrCtx ().getContext ();

      if (!(objForSchema instanceof DirContext))
         throw new ShellException ("error: current context is not a " +
               "DirContext");

      String relName = argv.length > 0 ? argv[0] : "";
      DirContext schemaCtx = ((DirContext) objForSchema).
            getSchemaClassDefinition (relName);

      JNDIShell child = new JNDIShell (shell, schemaCtx, null, null);

      child.run ();
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [dircontext]",
         "  dircontext: no '..' directives allowed",
         "Notes:",
         "- Starts a child shell with the initial context at the context",
         "  returned by calling getSchemaClassDefinition() with the",
         "  specified name."
      };

      return (helpStrs);
   }
}

/**
 * Starts a new shell with the initial context at the schema attribute
 * definition of the specified attribute of a relative DirContext.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class CdSchemaAttributeDefCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length == 0)
         throw new ShellException ("error: attribute id must be specified");

      if (argv.length > 2)
         throw new ShellException ("error: too many arguments");

      Object obj = shell.getCurrCtx ().getContext ();

      if (!(obj instanceof DirContext))
         throw new ShellException ("error: path specified is not a " +
               "DirContext");

      String relName;
      String attrID;

      if (argv.length == 1)
      {
         relName = "";
         attrID = argv[0];
      }
      else
      {
         relName = argv[0];
         attrID = argv[1];
      }

      String[] arr = { attrID };
      Attributes attrs = ((DirContext) obj).getAttributes (relName, arr);
      Attribute attr = attrs.get (attrID);

      if (attr == null)
         throw new ShellException ("error: " + attrID +
               ": undefined attribute");

      DirContext schemaCtx = attr.getAttributeDefinition ();

      if (schemaCtx == null)
         throw new ShellException ("error: null attribute definition");

      JNDIShell child = new JNDIShell (shell, schemaCtx, null, null);

      child.run ();
   }

  /**
   * Returns the Help strings as a String array.
   *
   * @return The help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [dircontext] <attrid>",
         "  attrid: attribute ID of attribute definition to return",
         "  dircontext: no '..' directives allowed",
         "Notes:",
         "- Starts a child shell with the initial context at the context",
         "  returned by calling getAttributeDefinition() on the Attribute",
         "  returned by getAttributes() called with the specified name."
      };

      return (helpStrs);
   }
}

/**
 * Starts a new shell with the initial context at the schema syntax
 * definition of the specified attribute of a relative DirContext.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class CdSchemaSyntaxDefCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length == 0)
         throw new ShellException ("error: attribute id must be specified");

      if (argv.length > 2)
         throw new ShellException ("error: too many arguments");

      Object obj = shell.getCurrCtx ().getContext ();

      if (!(obj instanceof DirContext))
         throw new ShellException ("error: path specified is not a " +
               "DirContext");

      String relName;
      String attrID;

      if (argv.length == 1)
      {
         relName = "";
         attrID = argv[0];
      }
      else
      {
         relName = argv[0];
         attrID = argv[1];
      }

      String[] arr = { attrID };
      Attributes attrs = ((DirContext) obj).getAttributes (relName, arr);
      Attribute attr = attrs.get (attrID);

      if (attr == null)
         throw new ShellException ("error: " + attrID +
               ": undefined attribute");

      DirContext schemaCtx = attr.getAttributeSyntaxDefinition ();

      if (schemaCtx == null)
         throw new ShellException ("error: null attribute syntax " +
               "definition");

      JNDIShell child = new JNDIShell (shell, schemaCtx, null, null);

      child.run ();
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [dircontext] <attrid>",
         "  attrid: attribute ID of syntax definition to return",
         "  dircontext: no '..' directives allowed",
         "Notes:",
         "- Starts a child shell with the initial context at the context",
         "  returned by calling getAttributeSyntaxDefinition() on the",
         "  Attribute returned by getAttributes() called with the",
         "  specified name."
      };

      return (helpStrs);
   }
}

/**
 * Dumps all or selected attributes of a DirContext. Help is
 * provided by implementing the CommandWithHelp interface.
 */
class AttrCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      String name = null;
      Vector attrIDVector = new Vector ();
      boolean ids = false;
      boolean verbose = false;

      for (int i = 0; i < argv.length; i++)
      {
         String arg = argv[i];

         /* Check for bracketed arg so that we can pull the attr. IDs
          * out.
          */
         if (arg.startsWith ("["))
         {
            arg = arg.trim ();

            if (!arg.endsWith ("]"))
            {
               throw new ShellException ("error: attr. ID list missing " +
                     "ending bracket");
            }

            /* Get the attr. IDs without the braces.
             */
            arg = arg.substring (1, arg.length () - 1);

            int commaPos = 0;

            /* Collect attr. IDs until there are no more commas.
             */
            while (commaPos >= 0)
            {
               int savePos = commaPos;

               commaPos = arg.indexOf (',', commaPos);

               /* If encounter the last ID.
                */
               if (commaPos == -1)
                  attrIDVector.addElement (arg.substring (savePos));
               else
               {
                  attrIDVector.addElement (arg.substring (savePos,commaPos));
                  commaPos++;
               }
            }
         }
         else if (arg.equals ("-ids"))
            ids = true;
         else if (arg.equals ("-verbose"))
            verbose = true;
         else if (name == null)
            name = arg;
         else
            throw new ShellException ("error: too many arguments");
      }

      String[] attrIDs;

      /* Flag whether a specific attribute set was requested.
       */
      if (attrIDVector.size () == 0)
         attrIDs = null;
      else
      {
         attrIDs = new String[attrIDVector.size ()];

         for (int i = 0; i < attrIDs.length; i++)
            attrIDs[i] = (String) attrIDVector.elementAt (i);
      }

      /* If no name is given, choose "", as the name for the current
       * context.
       */
      if (name == null)
         name = "";

      /* Make sure that we have a DirContext, otherwise exit.
       */
      if (!(shell.getCurrCtx ().getContext () instanceof DirContext))
      {
         throw new ShellException ("error: current context is not " +
               "a DirContext");
      }

      PrintWriter out = new PrintWriter (shell.getOutputStream (), true);
      DirContext ctx = (DirContext) shell.getCurrCtx ().getContext ();
      Attributes attrs;
      NamingEnumeration attrEnum;

      /* A full attribute set is requested if attrIDs is set to NULL,
       * otherwise a specified attribute set is requested.
       */
      if (attrIDs == null)
         attrs = ctx.getAttributes (name);
      else
         attrs = ctx.getAttributes (name, attrIDs);

      if (attrs == null)
         throw new ShellException ("error: attribute set null for this " +
               "DirContext");

      // check for an empty set
      if (attrs.size () == 0)
         throw new ShellException ("attribute set empty for this " +
               "DirContext");

      if (ids)
      {
         /* Enumerate all attributes in the attribute set.
          */
         attrEnum = attrs.getIDs ();

         if (attrEnum == null)
            throw new ShellException ("error: attribute set returned null " +
                  "attribute ID enumeration");

         /* Print every attribute ID in the set.
          */
         while (attrEnum.hasMoreElements ())
            out.println (attrEnum.next () + ":");
      }
      else
      {
         /* Enumerate all attributes in the attribute set.
          */
         attrEnum = attrs.getAll ();

         if (attrEnum == null)
            throw new ShellException ("error: attribute set returned null " +
                  "attribute enumeration");

         /* Print every attribute in the set.
          */
         while (attrEnum.hasMoreElements ())
            out.println (attrEnum.next ());
      }

      /* Print diagnostic information on the actual attribute set.
       */
      if (verbose)
      {
         out.println ("verbose: size: " + attrs.size ());
         out.println ("verbose: case ingnored: " + attrs.isCaseIgnored ());
      }
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [-ids] [-verbose] [[attrid,...]] [dircontext]",
         "  ids: print just the IDs returned by Attribute.getIDs()",
         "  verbose: print info on Attributes instance returned",
         "  [attrid,...]: specify limited set of string attr. IDs",
         "  dircontext: no '..' directives allowed",
         "Notes:",
         "- Starts a child shell with the initial context at the context",
         "  returned by calling getAttributeSyntaxDefinition() on the",
         "  Attribute returned by getAttributes() called with the",
         "  specified name."
      };

      return (helpStrs);
   }
}

/**
 * Prints the toString() representation of a bound object. Help is
 * provided by implementing the CommandWithHelp interface.
 */
class ShowCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");

      Object obj;
      String path;

      // get the object
      if (argv.length == 0)
      {
         obj = shell.getCurrCtx ().getContext ();
         path = ".";
      }
      else
      {
         obj = shell.resolveRelative (shell.getCurrCtx (), argv[0]).
               getObject ();

         path = argv[0];
      }

      PrintWriter out = new PrintWriter (shell.getOutputStream (), true);

      // print the object
      out.println (path + ": " + obj);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [name]",
         "  name: name of any bound object, '..' directives allowed",
         "Notes:",
         "- The object returned by calling lookup() is printed.",
         "- lookup() is not called if no name is specified."
      };

      return (helpStrs);
   }
}

/**
 * Renames a bound object. Help is provided by implementing the
 * CommandWithHelp interface.
 */
class RenameCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 2)
         throw new ShellException ("error: too many arguments");
      if (argv.length < 2)
         throw new ShellException ("error: not enough arguments");

      // get the current context
      Context ctx = shell.getCurrCtx ().getContext ();

      ctx.rename (argv[0], argv[1]);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <oldname> <newname>",
         "  oldname: name of object; passed directly to rename()",
         "  newname: new name of object"
      };

      return (helpStrs);
   }
}

/**
 * Binds a relative object to the current context under a specified name.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class BindCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 2)
         throw new ShellException ("error: too many arguments");
      if (argv.length < 2)
         throw new ShellException ("error: object and binding name" +
               "must be specified");

      Object obj;
      
      if (argv[0].startsWith ("~"))
      {
         obj = Class.forName (argv[0].substring (1)).newInstance ();
      }
      else
      {
         obj = shell.resolveRelative (shell.getCurrCtx (), argv[0]).
               getObject ();  
      }

      Context ctx = shell.getCurrCtx ().getContext ();

      ctx.bind (argv[1], obj);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <[relname]|[~class]> <name>",
         "  relname: name of a bound object, '..' directives allowed",
         "  class: name of a loadable class with null constructor",
         "  name: atomic name to which to bind the specified object",
         "Notes:",
         "- Most providers will bind objects that implement Referenceable."
      };

      return (helpStrs);
   }
}

/**
 * Rebinds a relative object to the current context under a specified name.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class RebindCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 2)
         throw new ShellException ("error: too many arguments");
      if (argv.length < 2)
         throw new ShellException ("error: object and binding name" +
               "must be specified");

      Object obj;
      
      if (argv[0].startsWith ("~"))
      {
         obj = Class.forName (argv[0].substring (1)).newInstance ();
      }
      else
      {
         obj = shell.resolveRelative (shell.getCurrCtx (), argv[0]).
               getObject ();  
      }

      Context ctx = shell.getCurrCtx ().getContext ();

      ctx.rebind (argv[1], obj);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <[relname]|[~class]> <name>",
         "  relname: name of a bound object, '..' directives allowed",
         "  class: name of a loadable class with null constructor",
         "  name: atomic name to which to bind the specified object",
         "Notes:",
         "- Most providers will bind objects that implement Referenceable."
      };

      return (helpStrs);
   }
}

/**
 * Unbinds a relative object to the current context under a specified name.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class UnbindCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");
      if (argv.length == 0)
         throw new ShellException ("error: binding name must be specified");

      Context ctx = shell.getCurrCtx ().getContext ();

      ctx.unbind (argv[0]);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <name>",
         "  name: passed directly to unbind()"
      };

      return (helpStrs);
   }
}

/**
 * Creates a subcontext relative to the current context.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class CreateCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");
      if (argv.length == 0)
         throw new ShellException ("error: context name must be specified");

      Context ctx = shell.getCurrCtx ().getContext ();

      ctx = ctx.createSubcontext (argv[0]);

      PrintWriter out = new PrintWriter (shell.getOutputStream (), true);

      out.println ("new context: " + ctx);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <name>",
         "  name: passed directly to Context.createSubcontext()",
         "Notes:",
         "- Some providers do not support a simple createSubcontext() call;",
         "  it is necessary to call DirContext.createSubcontext(Attributes)."
      };

      return (helpStrs);
   }
}

/**
 * Destroys a subcontext relative to the current context.
 * Help is provided by implementing the CommandWithHelp interface.
 */
class DestroyCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");
      if (argv.length == 0)
         throw new ShellException ("error: context name must be specified");

      Context ctx = shell.getCurrCtx ().getContext ();

      ctx.destroySubcontext (argv[0]);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <name>",
         "  name: passed directly to destroySubcontext()"
      };

      return (helpStrs);
   }
}

/**
 * Displays the environment of the current context, allowing
 * modifications. Help is provided by implementing the
 * CommandWithHelp interface.
 */
class EnvCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 2 || argv.length > 1 && !argv[0].equals ("-d"))
         throw new ShellException ("error: too many arguments");

      Context ctx = shell.getCurrCtx ().getContext ();
      PrintWriter out = new PrintWriter (shell.getOutputStream (), true);

      if (argv.length == 0)
      {
         Hashtable env = ctx.getEnvironment ();
         Enumeration enum = env.keys ();

         while (enum.hasMoreElements ())
         {
            String name = (String) enum.nextElement ();
            Object value = env.get (name);

            out.println (name + ": " + value);
         }
      }
      else
      {
         if (argv.length == 1)
         {
            String arg = argv[0];
            int equalPos = arg.indexOf ('=');

            if (equalPos == -1)
               out.println (arg + ": " + ctx.getEnvironment ().get (arg));
            else
            {
               String argName = arg.substring (0, equalPos);
               String argValue = arg.substring (equalPos + 1, arg.length());

               ctx.addToEnvironment (argName, argValue);

               out.println ("'" + argName + "' set to '" + argValue + "' " +
                     "in environment");
            }
         }
         else if (argv.length == 2)
         {
            ctx.removeFromEnvironment (argv[1]);

            out.println ("'" + argv[1] + "' removed from environment");
         }
      }
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <[prop=[value]]|[-d prop]>",
         "  prop: name of an environment property",
         "Notes:",
         "- Although the Context interface allows setting of Object",
         "  properties, this shell command only allows string settings.",
         "- [prop=value] can have no spaces; quote when necessary.",
         "- [-d prop] removes a property from the environment."
      };

      return (helpStrs);
   }
}

/**
 * Executes a shell script. Help is provided by implementing the
 * CommandWithHelp interface.
 */
class ShellCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");
      if (argv.length < 1)
         throw new ShellException ("error: cmd. script must be specified");

      InputStream in = null;

      // open the shell script
      try
      {
         in = new FileInputStream (argv[0]);
      }
      catch (FileNotFoundException e)
      {
         throw new ShellException ("error: could not open file: " + argv[0]);
      }

      JNDIShell child = new JNDIShell (shell, null, in, null);

      child.run ();

      shell.clearCommands ();

      String[] cmdNames = child.getCommandNames ();

      for (int i = 0; i < cmdNames.length; i++)
         shell.addCommand (cmdNames[i], child.getCommand (cmdNames[i]));
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <script>",
         "Notes:",
         "- Reads shell commands from the specified script file.",
         "- The currently executing shell's environment is duplicated and",
         "  passed to a child shell.",
         "- Output of child shell goes to output of current shell.",
         "- Parent shell's command table set to child's at exit."
      };

      return (helpStrs);
   }
}

/**
 * Executes an OS-specific command. Help is provided by implementing the
 * CommandWithHelp interface.
 */
class ExecCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      Process p = Runtime.getRuntime ().exec (argv);
      PrintWriter out = new PrintWriter (shell.getOutputStream (), true);
      int exitCode = p.waitFor ();

      out.println ("External process exited: " + exitCode);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c <cmd>",
         "Notes:",
         "- All parameters passed to this command are passed to the local",
         "  OS as a command line parameter."
      };

      return (helpStrs);
   }
}

/**
 * Toggles the verbose flag in the shell. Help is provided by implementing
 * the CommandWithHelp interface.
 */
class VerboseToggleCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      if (argv.length > 0)
      {
         PrintWriter out = new PrintWriter (shell.getOutputStream (), true);

         out.println ("error: too many arguments");
      }

      shell.setVerbose (!shell.getVerbose ());
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c",
         "Notes:",
         "- Toggles the verbose flag in the current shell."
      };

      return (helpStrs);
   }
}

/**
 * Exits the shell. Help is provided by implementing the
 * CommandWithHelp interface.
 */
class ExitCommand implements CommandWithHelp
{
  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell shell, String[] argv)
      throws Exception
   {
      throw new ShellException (true);
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c",
         "Notes:",
         "- Exits the current shell; any parent shell resumes control."
      };

      return (helpStrs);
   }
}

/**
 * Prints help for all of the default bound commands. Help is provided
 * by implementing the CommandWithHelp interface. This help will not
 * be accurate if the command bindings are changed.
 */
class HelpCommand implements CommandWithHelp
{
   String substituteCommandName (String cmdName, String syntax)
   {
      int cmdPos;

      if ((cmdPos = syntax.indexOf ("%c")) != -1)
      {
         return (syntax.substring (0, cmdPos) + cmdName +
               syntax.substring (cmdPos + 2));
      }
      else
         return (syntax);
   }

   void sortStringArr (String[] arr)
   {
      for (int i = arr.length; i > 0; i--)
      {
         for (int j = 0; j < i; j++)
         {
            if (arr[i-1].compareTo (arr[j]) < 0)
            {
               String temp = arr[j];

               arr[j] = arr[i-1];
               arr[i-1] = temp;
            }
         }
      }
   }

  /**
   * Loads the JNDIShell class specified in the shell parameter,
   * and binds the Command to the shell using the name in argv
   * parameter. The class must implement the CommandWithHelp
   * or the Command interface.
   *
   * @param shell The JNDIShell that is to be executed.
   * @param argv  A string array containing the command that
   *              is to be bound to the shell.
   *
   * @exception ShellException When the command name or the
   *            class are not specified or there are too many
   *            argument in the argv parameter.
   */
   public void execute (JNDIShell s, String[] argv)
      throws Exception
   {
      if (argv.length > 1)
         throw new ShellException ("error: too many arguments");

      PrintWriter out = new PrintWriter (s.getOutputStream (), true);

      if (argv.length == 0)
      {
         out.println (shell.helpStr);

         String[] cmdNames = s.getCommandNames ();
         String[] helpStrs;
         Command cmd;

         sortStringArr (cmdNames);

         for (int i = 0; i < cmdNames.length; i++)
         {
            out.print (cmdNames[i] + ": ");
            cmd = s.getCommand (cmdNames[i]);

            if (cmd instanceof CommandWithHelp)
            {
               CommandWithHelp hcmd = (CommandWithHelp) cmd;
               helpStrs = hcmd.getHelpStrs ();

               if (helpStrs.length > 0)
               {
                  out.print (substituteCommandName (cmdNames[i],
                        helpStrs[0]));
               }
            }
            else
            {
               out.print ("no help available (" +
                     cmd.getClass ().getName () + ")");
            }

            out.println ();
         }
      }
      else if (argv.length == 1)
      {
         Command cmd = s.getCommand (argv[0]);

         if (cmd == null)
            throw new ShellException ("error: command undefined: "+ argv[0]);
         if (!(cmd instanceof CommandWithHelp))
            throw new ShellException ("error: command does not support " +
                  "help: " + argv[0]);

         CommandWithHelp hcmd = (CommandWithHelp) cmd;
         String[] helpStrs = hcmd.getHelpStrs();

         for (int i = 0; i < helpStrs.length; i++)
            out.println (substituteCommandName (argv[0], helpStrs[i]));
      }
   }

  /**
   * Returns the Help strings as string arrays.
   *
   * @return The Help strings.
   */
   public String[] getHelpStrs ()
   {
      String[] helpStrs =
      {
         "Syntax: %c [cmdname]",
         "  cmdname: name of command for which to display help",
         "Notes:",
         "- If executed with no parameters, displays syntax of all commands."
      };

      return (helpStrs);
   }
}
