Getting a Tcl shell in Java

Any Java application can have an interactive Tcl shell added to it. This can be invaluable for probing and debugging the behavior of an existing Java program. Unfortunately, the tcl.lang.Shell class that comes with Jacl can only be used as a startup program, so here we'll show you a simple shell that we wrote, and along the way look at some internals of Jacl/Tcl Blend that we haven't yet covered.

The class that implements the Tcl shell is tutorial.tcltk98.TclShell. A TclShell is just an AWT widget (a subclass of java.awt.Panel, so it can be placed within any Java GUI. To illustrate, we wrote a simple program called OpenShell that creates a new TclShell in a top-level window each time you press a big fat button. To run it, enter

  > java tutorial.tcltk98.OpenShell

The code is in tutorial/tcltk98/OpenShell.java. The part that creates the Tcl shell is in an action listener attached to the button. The following code is executed when you press the button:

      TclShell tclShell = new TclShell();
      final Frame shellFrame = new Frame();
      shellFrame.pack();
      shellFrame.setSize(400,200);
      shellFrame.add(tclShell);
      shellFrame.show();

      // Make it closeable
      shellFrame.addWindowListener( new WindowAdapter() {
	public void windowClosing (WindowEvent e) {
	  shellFrame.dispose();
	}
      });
The only new thing here is the addition of a java.awt.event.WindowListener. The windowClosing method will be called when you do whatever it is that you normally do to a window to make it close. In this example, we make this method delete the window containing the Tcl Shell. You'll also notice the following code in a listener attached to the main window:
    mainFrame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
	System.exit(0);
      }
    });
This makes the whole program quit when we close that particular window.

The code for the Tcl shell itself is in tutorial/tcltk98/TclShell.java. This code sub-classes the AWT Panel class, and places an instance of the AWT TextArea class inside itself. It has a simple history that can be accessed with up- and down-arrow keys, and understands incomplete Tcl commands. The code is too long to reproduce here, but we'll look at the interesting parts.

The first interesting part is how we handle key events. In the constructor of the TclShell class, we placed this line of code:

  textArea.addKeyListener(new ShellKeyListener());
Whenever we type into the text pane, the key listener is called with key events. Here's a cut-down version of this class:
  class ShellKeyListener extends KeyAdapter {
    public void keyPressed (KeyEvent keyEvent) {
      switch (keyEvent.getKeyCode()) {
      case KeyEvent.VK_ENTER:
	evalCommand();
	keyEvent.consume();
	break;
      default:
	// TextArea will take care of displaying a regular character
      }
    }
  }
}
When we press a key, the keyPressed method is called with a key event object. We test the key code, and if it's a new-line, we call the evalCommand() method of the shell to process user input. Then we consume this event -- this prevents the TextArea from performing any further processing with this key event.

In the case of regular keys, we don't do anything at all. Because we don't consume the event, the TextArea will get the event and write the character into its text area at the cursor position.

When we process user input, we check if the command string we have so far is a complete Tcl command, using the Tcl interpreter's commandComplete method. This method is the same as the Tcl command info complete. If it is complete, we call the eval method to evaluate it, and the getResult method to get the result from the interpreter. The code looks like this:

   if (tclInterp.commandComplete(command)) {
      ...
      try {
	tclInterp.eval(command);
      }
      catch (TclException e) {
	// ignore
      }
      String result = tclInterp.getResult().toString();
      ...

The final interesting thing about this class is this piece of code in the constructor.

    try {
      tclInterp.setVar("tclShell",
         ReflectObject.newInstance(tclInterp, this), 0);
      tclInterp.eval(
         "proc puts {s} {global tclShell; $tclShell putText $s\\n}");
    }
    catch (TclException e) {}

The first line creates a variable named tclShell in the interpreter. We bound the variable to an instance of a tcl.lang.ReflectObject -- this is exactly the kind of object that is created by Jacl and Tcl Blend when you call java::new or a Java method from Tcl. Using this, you can create a Tcl shell that already gives Tcl access to key Java objects in your system. This can be very handy for debugging and probing the behavior of your Java code.

The second line evaluates a Tcl script that overwrites the default puts command to print output into the text pane of this shell, instead of to the console from which you started Java. It does this simply by calling the TclShell object with the string as argument! (The -nonewline option to puts is not supported by this simple procedure definition.)

Finally, note the use of the Java try-catch construct to catch exceptions from the Tcl interpreter. In this example, we ignore the exception.