From: <ls...@us...> - 2007-10-26 21:30:15
|
Revision: 3571 http://jnode.svn.sourceforge.net/jnode/?rev=3571&view=rev Author: lsantha Date: 2007-10-26 14:30:12 -0700 (Fri, 26 Oct 2007) Log Message: ----------- Improved support for pipes, command completion for piped commands, introduced jnode.interpreter, jnode.debug, jnode.invoker and jnode.history system properties for controlling the shell command interpreter, detailed exception reporting of shell commands, shell command invoker and shell command history, by crawley. Modified Paths: -------------- trunk/distr/src/apps/org/jnode/apps/charvabsh/CharvaBsh.java trunk/fs/src/fs/org/jnode/fs/command/CatCommand.java trunk/shell/src/shell/org/jnode/shell/AsyncCommandInvoker.java trunk/shell/src/shell/org/jnode/shell/CommandInterpreter.java trunk/shell/src/shell/org/jnode/shell/CommandInvoker.java trunk/shell/src/shell/org/jnode/shell/CommandLine.java trunk/shell/src/shell/org/jnode/shell/CommandShell.java trunk/shell/src/shell/org/jnode/shell/CommandThread.java trunk/shell/src/shell/org/jnode/shell/DefaultCommandInvoker.java trunk/shell/src/shell/org/jnode/shell/NullInputStream.java trunk/shell/src/shell/org/jnode/shell/ProcletCommandInvoker.java trunk/shell/src/shell/org/jnode/shell/ShellManager.java trunk/shell/src/shell/org/jnode/shell/ShellUtils.java trunk/shell/src/shell/org/jnode/shell/ThreadCommandInvoker.java trunk/shell/src/shell/org/jnode/shell/command/test/SuiteCommand.java trunk/shell/src/shell/org/jnode/shell/def/DefaultShellManager.java trunk/shell/src/shell/org/jnode/shell/help/Argument.java trunk/shell/src/shell/org/jnode/shell/help/Help.java trunk/shell/src/shell/org/jnode/shell/help/Parameter.java trunk/shell/src/shell/org/jnode/shell/help/Syntax.java trunk/shell/src/shell/org/jnode/shell/help/argument/StringArgument.java Added Paths: ----------- trunk/shell/src/shell/org/jnode/shell/DefaultInterpreter.java trunk/shell/src/shell/org/jnode/shell/RedirectingInterpreter.java Modified: trunk/distr/src/apps/org/jnode/apps/charvabsh/CharvaBsh.java =================================================================== --- trunk/distr/src/apps/org/jnode/apps/charvabsh/CharvaBsh.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/distr/src/apps/org/jnode/apps/charvabsh/CharvaBsh.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -42,6 +42,7 @@ import javax.naming.NameNotFoundException; import org.jnode.shell.CommandShell; import org.jnode.shell.Shell; +import org.jnode.shell.ShellException; import org.jnode.shell.ShellUtils; /** @@ -414,8 +415,13 @@ public void invoke( String command ) { if( shell != null ) { if( shell instanceof CommandShell ) { - CommandShell cs = (CommandShell)shell; - cs.getDefaultCommandInvoker().invoke( command ); + CommandShell cs = (CommandShell) shell; + try { + cs.invokeCommand( command ); + } + catch (ShellException ex) { + System.err.println( "Command invocation failed: " + ex.getMessage() ); + } } else { System.err.println( "Shell wasn't a CommandShell: " + shell.getClass() ); Modified: trunk/fs/src/fs/org/jnode/fs/command/CatCommand.java =================================================================== --- trunk/fs/src/fs/org/jnode/fs/command/CatCommand.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/fs/src/fs/org/jnode/fs/command/CatCommand.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -67,7 +67,7 @@ String[] fileNames = ARG_FILE.getValues(cmdLine); boolean ok = true; try { - if (fileNames.length == 0) { + if (fileNames == null || fileNames.length == 0) { process(in, out); } else { Modified: trunk/shell/src/shell/org/jnode/shell/AsyncCommandInvoker.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/AsyncCommandInvoker.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/shell/src/shell/org/jnode/shell/AsyncCommandInvoker.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -21,29 +21,19 @@ package org.jnode.shell; -import gnu.java.security.action.InvokeAction; import java.awt.event.KeyEvent; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; +import java.io.Closeable; import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintStream; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.security.AccessController; import java.security.PrivilegedAction; -import java.security.PrivilegedActionException; import org.jnode.driver.input.KeyboardEvent; import org.jnode.driver.input.KeyboardListener; -import org.jnode.shell.help.Help; -import org.jnode.shell.help.HelpException; -import org.jnode.shell.help.SyntaxErrorException; -import org.jnode.vm.VmExit; /** * User: Sam Reid Date: Dec 20, 2003 Time: 1:20:33 AM Copyright (c) Dec 20, 2003 @@ -72,11 +62,10 @@ boolean blocking; Thread blockingThread; - Thread threadProcess = null; - String cmdName; + public AsyncCommandInvoker(CommandShell commandShell) { this.commandShell = commandShell; this.err = commandShell.getErrorStream(); @@ -84,153 +73,147 @@ // ctrl-c } - public void invoke(String cmdLineStr) { - commandShell.addCommandToHistory(cmdLineStr); + public int invoke(CommandLine cmdLine) throws ShellException { + CommandInfo cmdInfo = lookupCommand(cmdLine); + if (cmdInfo == null) { + return 0; + } + Runnable cr = setup(cmdLine, cmdInfo); + return runIt(cmdLine, cmdInfo, cr); + } - InputStream inputStream = commandShell.getInputStream(); - InputStream nextInputStream = null; - PrintStream errStream = commandShell.getErrorStream(); - PrintStream outputStream = null; - boolean mustCloseOutputStream = false; + public CommandThread invokeAsynchronous(CommandLine cmdLine) + throws ShellException { + CommandInfo cmdInfo = lookupCommand(cmdLine); + if (cmdInfo == null) { + return null; + } + Runnable cr = setup(cmdLine, cmdInfo); + return forkIt(cmdLine, cmdInfo, cr); + } - CommandLine cmdLine; - Method method; - Runnable cr; - CommandInfo cmdInfo; - - String[] commands = cmdLineStr.split("\\|"); - String command; - ByteArrayOutputStream byteArrayOutputStream = null; - - for (int i = 0; i < commands.length; i++) { - command = commands[i].trim(); - cmdLine = new CommandLine(command); - - if (!cmdLine.hasNext()) - continue; - - cmdName = cmdLine.next(); - - try { - cmdInfo = commandShell.getCommandClass(cmdName); - - if (cmdLine.sendToOutFile()) { - File file = new File(cmdLine.getOutFileName()); - + private CommandInfo lookupCommand(CommandLine cmdLine) throws ShellInvocationException { + String cmdName = cmdLine.getCommandName(); + if (cmdName == null) { + return null; + } try { - FileOutputStream fileOutputStream = new FileOutputStream( - file); - outputStream = new PrintStream(fileOutputStream); - mustCloseOutputStream = true; - } catch (SecurityException e) { - e.printStackTrace(); - return; // FIXME - } catch (FileNotFoundException e) { - e.printStackTrace(); - return; // FIXME - } - } else if (i + 1 < commands.length) { - byteArrayOutputStream = new ByteArrayOutputStream(); - outputStream = new PrintStream(byteArrayOutputStream); - } else { - outputStream = commandShell.getOutputStream(); + return commandShell.getCommandClass(cmdName); + } + catch (ClassNotFoundException ex) { + throw new ShellInvocationException("Cannot resolve command '" + cmdName + "'"); } - - if (byteArrayOutputStream != null) { - nextInputStream = new ByteArrayInputStream( - byteArrayOutputStream.toByteArray()); } - if (nextInputStream != null) - inputStream = nextInputStream; + private Runnable setup(CommandLine cmdLine, CommandInfo cmdInfo) throws ShellException { + Method method; + Runnable cr = null; - CommandLine commandLine = null; - - if (inputStream.available() > 0) { - // FIXME we shouldn't do this. It consumes keyboard typeahead - // that should be delivered to the command's standard input!! - commandLine = new CommandLine(inputStream); - } else { - commandLine = cmdLine.getRemainder(); + Closeable[] streams = cmdLine.getStreams(); + InputStream in; + PrintStream out, err; + try { + in = (InputStream) ((streams[0] == null) ? commandShell.getInputStream() : streams[0]); + OutputStream tmp = (OutputStream) + ((streams[1] == null) ? commandShell.getOutputStream() : streams[1]); + out = (tmp instanceof PrintStream) ? (PrintStream) tmp : new PrintStream(tmp); + tmp = (OutputStream) ((streams[2] == null) ? commandShell.getErrorStream() : streams[2]); + err = (tmp instanceof PrintStream) ? (PrintStream) tmp : new PrintStream(tmp); + } + catch (ClassCastException ex) { + throw new ShellFailureException("streams array broken", ex); } - - commandLine.setOutFileName(cmdLine.getOutFileName()); try { method = cmdInfo.getCommandClass().getMethod( EXECUTE_METHOD, EXECUTE_ARG_TYPES); - + if ((method.getModifiers() & Modifier.STATIC) == 0) { cr = createRunner(cmdInfo.getCommandClass(), method, - new Object[] { commandLine, inputStream, - outputStream, errStream }, - inputStream, outputStream, errStream); + new Object[]{cmdLine, in, out, err}, + in, out, err); + } } catch (NoSuchMethodException e) { - method = cmdInfo.getCommandClass().getMethod(MAIN_METHOD, - MAIN_ARG_TYPES); + // continue; + } + if (cr == null) { + try { + method = cmdInfo.getCommandClass().getMethod(MAIN_METHOD, MAIN_ARG_TYPES); + if ((method.getModifiers() & Modifier.STATIC) != 0) { + if (streams[0] != null || streams[1] != null || streams[2] != null) { + throw new ShellInvocationException( + "Entry point method for " + cmdInfo.getCommandClass() + + " does not allow redirection or pipelining"); + } cr = createRunner(cmdInfo.getCommandClass(), method, - new Object[] { cmdLine.getRemainder().toStringArray() }, - commandShell.getInputStream(), commandShell.getOutputStream(), - commandShell.getErrorStream()); + new Object[]{cmdLine.getArguments()}, + in, out, err); } + } catch (NoSuchMethodException e) { + // continue; + } + } + if (cr == null) { + throw new ShellInvocationException( + "No suitable entry point method for " + cmdInfo.getCommandClass()); + } + if (streams == null) { + cmdLine.setStreams(new Closeable[]{in, out, err}); + } + return cr; + } + + public int runIt(CommandLine cmdLine, CommandInfo cmdInfo, Runnable cr) + throws ShellInvocationException { try { if (cmdInfo.isInternal()) { cr.run(); } else { - threadProcess = createThread(cr, inputStream, - outputStream, errStream); + try { + threadProcess = createThread(cmdLine, cr); + } catch (Exception ex) { + throw new ShellInvocationException("Exception while creating command thread", ex); + } threadProcess.start(); this.blocking = true; this.blockingThread = Thread.currentThread(); - while (blocking) { + this.cmdName = cmdLine.getCommandName(); + while (this.blocking) { try { Thread.sleep(6000); } catch (InterruptedException interrupted) { if (!blocking) { // interruption was okay, break normally. } else { - // abnormal interruption - interrupted.printStackTrace(); - return; // FIXME + throw new ShellFailureException("unexpected interrupt", interrupted); } } } } + return 0; } catch (Exception ex) { - err.println("Exception in command"); - ex.printStackTrace(err); - return; // FIXME + throw new ShellInvocationException("Uncaught Exception in command", ex); } catch (Error ex) { - err.println("Fatal error in command"); - ex.printStackTrace(err); - return; // FIXME - } - } catch (NoSuchMethodException ex) { - err.println("Alias class has no main method " + cmdName); - return; // FIXME - } catch (ClassNotFoundException ex) { - err.println("Unknown alias class " + ex.getMessage()); - return; // FIXME - } catch (ClassCastException ex) { - err.println("Invalid command " + cmdName); - return; // FIXME - } catch (Exception ex) { - err.println("Unknown error: " + ex.getMessage()); - ex.printStackTrace(err); - return; // FIXME + throw new ShellInvocationException("Fatal Error in command", ex); } finally { - if (mustCloseOutputStream) { - outputStream.close(); - mustCloseOutputStream = false; - } + this.blockingThread = null; + this.blocking = false; } } - nextInputStream = null; + public CommandThread forkIt(CommandLine cmdLine, CommandInfo cmdInfo, Runnable cr) + throws ShellInvocationException { + if (cmdInfo.isInternal()) { + throw new ShellFailureException("unexpected internal command"); + } + try { + return createThread(cmdLine, cr); + } catch (Exception ex) { + throw new ShellInvocationException("Exception while creating command thread", ex); + } } - abstract Thread createThread(Runnable cr, InputStream inputStream, - PrintStream outputStream, PrintStream errStream); + abstract CommandThread createThread(CommandLine cmdLine, Runnable cr); public void keyPressed(KeyboardEvent ke) { //disabling Ctrl-C since currently we have no safe method for killing a thread @@ -247,7 +230,7 @@ } private void doCtrlZ() { - if(blockingThread != null && blockingThread.isAlive()) { + if (blockingThread != null && blockingThread.isAlive()) { System.err.println("ctrl-z: Returning focus to console. (" + cmdName + " is still running)"); unblock(); @@ -260,7 +243,6 @@ blockingThread != null && blockingThread.isAlive()) { System.err.println("ctrl-c: Returning focus to console. (" + cmdName + " has been killed)"); - unblock(); AccessController.doPrivileged(new PrivilegedAction<Void>(){ @@ -286,4 +268,18 @@ abstract Runnable createRunner(Class cx, Method method, Object[] args, InputStream commandIn, PrintStream commandOut, PrintStream commandErr); + + abstract class CommandRunner implements Runnable { + + boolean isDebugEnabled() { + return commandShell.isDebugEnabled(); + } + + void stackTrace(Throwable ex) { + if (ex != null && isDebugEnabled()) { + ex.printStackTrace(err); + } + } + + } } Modified: trunk/shell/src/shell/org/jnode/shell/CommandInterpreter.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/CommandInterpreter.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/shell/src/shell/org/jnode/shell/CommandInterpreter.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -27,6 +27,12 @@ * @author cr...@jn... */ public interface CommandInterpreter { + + public interface Factory { + CommandInterpreter create(); + String getName(); + } + /** * Parse and execute a command line, and return the resulting return code. * Modified: trunk/shell/src/shell/org/jnode/shell/CommandInvoker.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/CommandInvoker.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/shell/src/shell/org/jnode/shell/CommandInvoker.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -21,12 +21,54 @@ package org.jnode.shell; -/** +/* * User: Sam Reid * Date: Dec 20, 2003 * Time: 1:18:31 AM * Copyright (c) Dec 20, 2003 by Sam Reid */ + +/** + * This is the common API for the various mechanisms for running 'commands'. + * + * @author Sam Reid + * @author cr...@jn... + */ public interface CommandInvoker { - void invoke(String cmdLineStr); + + public interface Factory { + CommandInvoker create(CommandShell shell); + String getName(); + } + + /** + * Run a command synchronously, passing back the resulting return code. + * + * @param commandLine this provides the command name (alias), the command arguments and + * (where relevant) the command's i/o stream context. + * @return an integer return code, with zero indicating command success, non-zero indicating + * command failure. + * @throws ShellException if there was some problem launching the command. + */ + int invoke(CommandLine commandLine) throws ShellException; + + /** + * Create a thread for running a command asynchronously. This can be used for running + * and for assembling command pipelines. + * + * @param commandLine this provides the command name (alias), the command arguments and + * (where relevant) the command's i/o stream context. + * @return the thread for the command. Calling {@link java.lang.Thread.start()} will + * cause the command to execute. + * @throws ShellException if there was some problem launching the command. + */ + CommandThread invokeAsynchronous(CommandLine commandLine) + throws ShellException; + + + /** + * Get the invoker's name. + * @return the name. + */ + String getName(); } Modified: trunk/shell/src/shell/org/jnode/shell/CommandLine.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/CommandLine.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/shell/src/shell/org/jnode/shell/CommandLine.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -21,21 +21,29 @@ package org.jnode.shell; -import java.util.ArrayList; -import java.util.List; +import java.io.Closeable; +import java.util.Iterator; import java.util.NoSuchElementException; -import java.io.InputStream; -import java.io.IOException; +import org.jnode.driver.console.CompletionInfo; +import org.jnode.shell.help.Argument; +import org.jnode.shell.help.CompletionException; +import org.jnode.shell.help.Help; +import org.jnode.shell.help.HelpException; +import org.jnode.shell.help.Parameter; +import org.jnode.shell.help.argument.AliasArgument; +import org.jnode.shell.help.argument.FileArgument; + /** - * This class represents the command line as an iterator. A trailing space leads - * to an empty token to be appended. + * This class represents the command line as command name and a sequence + * of argument strings. It also can carry the i/o stream environment for + * launching the command. * - * @author epr - * @author qades - * @author Martin Husted Hartvig (ha...@jn...) + * TODO This class needs to be "syntax agnostic". + * + * @author cr...@jn... */ -public class CommandLine { +public class CommandLine implements Completable, Iterator<String> { public static final int LITERAL = 0; @@ -43,6 +51,8 @@ public static final int CLOSED = 2; + public static final int SPECIAL = 4; + public static final char ESCAPE_CHAR = '\\'; public static final char FULL_ESCAPE_CHAR = '\''; @@ -71,76 +81,73 @@ private static final char T = 't'; - private String s; + private static final String[] NO_ARGS = new String[0]; - private int pos = 0; + private final Help.Info defaultParameter = new Help.Info("file", + "default parameter for command line completion", + new Parameter(new FileArgument("file", "a file", Argument.MULTI), Parameter.OPTIONAL) + ); - private int type = LITERAL; + private final Argument defaultArg = new AliasArgument("command", + "the command to be called"); + + private String commandName; - private static final char linebreak = "\n".charAt(0); + private String[] arguments; - // private boolean inEscape = false; - private boolean inFullEscape = false; + private Closeable[] streams; - private boolean inQuote = false; + private int pos = 0; - private String outFileName = null; + private int type = LITERAL; - private void setCommandLine(String s) { - int send_to_file = s.indexOf(SEND_OUTPUT_TO_CHAR); + private boolean argumentAnticipated = false; - if (send_to_file != -1) { - if (send_to_file < s.length()) { - setOutFileName((s.substring(send_to_file + 1)).trim()); - this.s = s.substring(0, send_to_file); - } else { - this.s = s.substring(send_to_file); + /** + * Create a new instance encapsulating a command name, argument list and io stream array. + * If 'arguments' is <code>null</code>, a zero length String array is substituted. + * If 'streams' is <code>null</code> , an array of length 3 is substituted. A non-null + * 'streams' argument must have a length of at least 3. + * + * @param commandName the command name or <code>null</code>. + * @param arguments the argument list or <code>null</code>. + * @param streams the io stream array or <code>null</code>. + */ + public CommandLine(String commandName, String[] arguments, Closeable[] streams) { + this.commandName = commandName; + this.arguments = (arguments == null || arguments.length == 0) ? NO_ARGS : arguments.clone(); + if (streams == null) { + this.streams = new Closeable[3]; + } + else if (streams.length < 3) { + throw new IllegalArgumentException("streams.length < 3"); } - } else { - this.s = s; + else { + this.streams = streams.clone(); } } /** - * Creates a new instance. + * Create a new instance. Equivalent to CommandLine(commandName, arguments, null); + * + * @param commandName the command name or <code>null</code>. + * @param arguments the argument list or <code>null</code>. */ - public CommandLine(String s) { - setCommandLine(s); + public CommandLine(String commandName, String[] arguments) { + this(commandName, arguments, null); } /** - * Create a new instance. + * Create a new instance. Equivalent to CommandLine(null, arguments, null); * - * @param args + * @param arguments the argument list or <code>null</code>. + * @deprecated It is a bad idea to leave out the command name. */ - public CommandLine(String[] args) { - this(escape(args)); + public CommandLine(String[] arguments) { + this(null, arguments, null); } - public CommandLine(InputStream in) { - try { - int avaliable = in.available(); - StringBuilder stringBuilder = new StringBuilder(avaliable); - - if (avaliable > 0) { - int data = in.read(); - char ch; - while (data > -1) { - ch = (char) data; - if (ch != linebreak) - stringBuilder.append(ch); - - data = in.read(); - } - } - - setCommandLine(stringBuilder.toString()); - } catch (IOException e) { - e.printStackTrace(); - } - } - /** * Reset, so a call to nextToken will retreive the first token. */ @@ -149,13 +156,13 @@ } /** - * Returns if there is another token on the command list. + * Returns if there is another argument on the command list. * * @return <code>true</code> if there is another token; <code>false</code> * otherwise */ public boolean hasNext() { - return pos < s.length(); + return pos < arguments.length; } /** @@ -176,79 +183,12 @@ * @return the next token */ public String next() throws NoSuchElementException { - if (!hasNext()) + if (!hasNext()) { throw new NoSuchElementException(); - - type = LITERAL; - - StringBuilder token = new StringBuilder(5); - char currentChar; - - boolean finished = false; - - while (!finished && pos < s.length()) { - currentChar = s.charAt(pos++); - - switch (currentChar) { - case ESCAPE_CHAR: - if(pos >= s.length()) - { - throw new IllegalArgumentException("escape char ('\\') not followed by a character"); } - token.append(CommandLine.unescape(s.charAt(pos++))); - break; - - case FULL_ESCAPE_CHAR: - if (inQuote) { - token.append(currentChar); - } else { - inFullEscape = !inFullEscape; // just a toggle - type = STRING; - if (!inFullEscape) - type |= CLOSED; - } - break; - case QUOTE_CHAR: - if (inFullEscape) { - token.append(currentChar); - } else { - inQuote = !inQuote; - type = STRING; - if (!inQuote) - type |= CLOSED; - } - break; - case SPACE_CHAR: - if (inFullEscape || inQuote) { - token.append(currentChar); - } else { - if (token.length() != 0) { // don't return an empty token - finished = true; - pos--; // to return trailing space as empty last token - } - } - break; - case COMMENT_CHAR: - if (inFullEscape || inQuote) { - token.append(currentChar); - } else { - finished = true; - pos = s.length(); // ignore EVERYTHING - } - break; - /* - * case SEND_OUTPUT_TO_CHAR: next(); break; - */ - default: - token.append(currentChar); - } - } - - String collectedToken = token.toString(); - token = null; - - return collectedToken; + type = LITERAL; + return arguments[pos++]; } /** @@ -261,29 +201,31 @@ } /** - * Get the remaining CommandLine. + * Get the command name * - * @return the remainder + * @return the command name */ - public CommandLine getRemainder() { - return new CommandLine(s.substring(pos)); + public String getCommandName() { + return commandName; } /** - * Get the entire command line as String[]. + * Get the arguments as String[]. * - * @return the command line as String[] + * @return the arguments as String[] */ - public String[] toStringArray() { - final List<String> res = new ArrayList<String>(); - CommandLine line = new CommandLine(s); - - while (line.hasNext()) { - res.add(line.next()); + public String[] getArguments() { + return arguments.clone(); } - String[] result = (String[]) res.toArray(new String[res.size()]); - return result; + /** + * Get the arguments as String[]. + * + * @return the arguments as String[] + * @deprecated this method name is wrong. + */ + public String[] toStringArray() { + return arguments.clone(); } /** @@ -292,7 +234,12 @@ * @return the entire command line */ public String toString() { - return escape(toStringArray()); // perform all possible conversions + StringBuilder sb = new StringBuilder(escape(commandName)); + for (String argument : arguments) { + sb.append(' '); + sb.append(escape(argument)); + } + return sb.toString(); } /** @@ -304,7 +251,7 @@ if (!hasNext()) { return ""; } - return getRemainder().next(); + return arguments[pos]; } /** @@ -313,16 +260,15 @@ * @return the remaining number of parts */ public int getLength() { - if (!hasNext()) - return 0; + return arguments.length; + } - CommandLine remainder = getRemainder(); - int result = 0; - while (remainder.hasNext()) { - result++; - remainder.next(); + public boolean isArgumentAnticipated() { + return argumentAnticipated; } - return result; + + public void setArgumentAnticipated(boolean newValue) { + argumentAnticipated = newValue; } public static class Token { @@ -359,8 +305,8 @@ * the unescaped argument * @return the escaped argument */ - public static String escape(String arg) { - return escape(arg, false); // don't force quotation + public String escape(String arg) { + return doEscape(arg, false); // don't force quotation } /** @@ -372,96 +318,110 @@ * if <code>true</code>, forces the argument to be returned in * quotes even if not necessary * @return the escaped argument + * @deprecated This method does not belong here. Escaping is an interpretter + * concern, and this class needs to be interpretter specific. */ - public static String escape(String arg, boolean forceQuote) { - String s = null; - StringBuilder stringBuilder = new StringBuilder(arg.length() > 0 ? 5 - : 0); + public static String doEscape(String arg, boolean forceQuote) { + int length = arg.length(); + if (length == 0) { + return "" + QUOTE_CHAR + QUOTE_CHAR; + } + StringBuilder sb = new StringBuilder(length); - // one-character escapes + // insert escape sequences for (int i = 0; i < arg.length(); i++) { char c = arg.charAt(i); - for (int j = 0; j < escapes.length; j++) + for (int j = 0; j < escapes.length; j++) { if (escapes[j].plain == c) { - stringBuilder.append(ESCAPE_CHAR); + sb.append(ESCAPE_CHAR); c = escapes[j].escaped; break; } - stringBuilder.append(c); } - - s = stringBuilder.toString(); - - if (s.indexOf(QUOTE_CHAR) != -1) { // full escape needed - stringBuilder.insert(0, FULL_ESCAPE_CHAR); - stringBuilder.append(FULL_ESCAPE_CHAR); - s = stringBuilder.toString(); - } else if (forceQuote || (s.indexOf(SPACE_CHAR) != -1)) { // normal - // quote if - // needed or - // forced - stringBuilder.insert(0, QUOTE_CHAR); - stringBuilder.append(QUOTE_CHAR); - s = stringBuilder.toString(); + forceQuote |= (c == SPACE_CHAR || c == QUOTE_CHAR); + sb.append(c); } - // debug output do show how escapes are translated - // System.out.println(); - // System.out.println("escaped \"" + arg + "\" as \"" + s + "\""); - return s; - } - /** - * Escape a command line for the Shell. - * - * @param args - * the unescaped command line - * @return the escaped argument - */ - public static String escape(String[] args) { - StringBuilder stringBuilder = new StringBuilder(args.length > 0 ? 5 : 0); - - for (int i = 0; i < args.length; i++) { - stringBuilder.append(escape(args[i])); // escape the argument - if (i != args.length - 1) - stringBuilder.append(SPACE_CHAR); // escape the argument + if (forceQuote) { + sb.insert(0, FULL_ESCAPE_CHAR); + sb.append(FULL_ESCAPE_CHAR); } - return stringBuilder.toString(); + return sb.toString(); } - public boolean sendToOutFile() { - return outFileName != null; + public String escape(String arg, boolean forceQuote) { + return CommandLine.doEscape(arg, forceQuote); } - public String getOutFileName() { - return outFileName; - } + private static class Escape { + final char plain; - void setOutFileName(String outFileName) { - this.outFileName = outFileName; + final char escaped; + + Escape(char plain, char escaped) { + this.plain = plain; + this.escaped = escaped; + } } /** - * Unescape a single character + * Get the IO stream context for executing the command. The result + * is guaranteed to be non-null and to have at least 3 entries, but + * these entries may be <code>null</code>. The implied meaning of + * <code>null</code> is: + * <ul> + * <li> offset 0: the invoking context's 'standard input' stream + * <li> offset 1: the invoking context's 'standard output' stream + * <li> offset 2: the invoking context's 'standard error' stream + * </ul> + * + * @return stream context as described above. */ - private static char unescape(char arg) { - // one-character escapes - for (int i = 0; i < escapes.length; i++) { - Escape e = escapes[i]; - if (e.escaped == arg) - return e.plain; + public Closeable[] getStreams() { + return streams.clone(); } - return arg; + + /** + * Set the IO stream context for executing the command. + * + * @param the new tream context. + */ + public void setStreams(Closeable[] streams) { + this.streams = streams.clone(); } - private static class Escape { - final char plain; + public void complete(CompletionInfo completion, CommandShell shell) throws CompletionException { + String cmd = (commandName == null) ? "" : commandName.trim(); + String result = null; + if (!cmd.equals("") && (arguments.length > 0 || argumentAnticipated)) { + try { + // get command's help info + CommandInfo cmdClass = shell.getCommandClass(cmd); - final char escaped; + Help.Info info; + try { + info = Help.getInfo(cmdClass.getCommandClass()); + } catch (HelpException ex) { + //assuming default syntax; i.e. multiple file arguments + info = defaultParameter; + } - Escape(char plain, char escaped) { - this.plain = plain; - this.escaped = escaped; + // perform completion of the command arguments based on the command's + // help info / syntax ... if any. + result = info.complete(this); + + } catch (ClassNotFoundException ex) { + throw new CompletionException("Command class not found", ex); + } } + else { + // do completion on the command name + result = defaultArg.complete(cmd); + } + completion.setCompleted(result); } + public void remove() { + throw new UnsupportedOperationException("remove not supported"); + } } Modified: trunk/shell/src/shell/org/jnode/shell/CommandShell.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/CommandShell.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/shell/src/shell/org/jnode/shell/CommandShell.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -21,9 +21,6 @@ package org.jnode.shell; -import gnu.java.security.action.GetPropertyAction; -import gnu.java.security.action.SetPropertyAction; - import java.io.File; import java.io.FilterInputStream; import java.io.InputStream; @@ -53,13 +50,7 @@ import org.jnode.naming.InitialNaming; import org.jnode.shell.alias.AliasManager; import org.jnode.shell.alias.NoSuchAliasException; -import org.jnode.shell.help.Argument; import org.jnode.shell.help.CompletionException; -import org.jnode.shell.help.Help; -import org.jnode.shell.help.HelpException; -import org.jnode.shell.help.Parameter; -import org.jnode.shell.help.argument.AliasArgument; -import org.jnode.shell.help.argument.FileArgument; import org.jnode.util.SystemInputStream; import org.jnode.vm.VmSystem; @@ -71,7 +62,23 @@ public class CommandShell implements Runnable, Shell, ConsoleListener { public static final String PROMPT_PROPERTY_NAME = "jnode.prompt"; + public static final String INTERPRETER_PROPERTY_NAME = "jnode.interpreter"; + public static final String INVOKER_PROPERTY_NAME = "jnode.invoker"; + public static final String CMDLINE_PROPERTY_NAME = "jnode.cmdline"; + public static final String DEBUG_PROPERTY_NAME = "jnode.debug"; + public static final String HISTORY_PROPERTY_NAME = "jnode.history"; + + public static final String HOME_PROPERTY_NAME = "user.home"; + public static final String DIRECTORY_PROPERTY_NAME = "user.dir"; + public static final String INITIAL_INVOKER = "thread"; + public static final String INITIAL_INTERPRETER = "redirecting"; + public static final String FALLBACK_INVOKER = "default"; + public static final String FALLBACK_INTERPRETER = "default"; + + private static String DEFAULT_PROMPT = "JNode $P$G"; + private static final String COMMAND_KEY = "cmd="; + /** * My logger */ @@ -119,27 +126,23 @@ */ private volatile boolean threadSuspended = false; - private static String DEFAULT_PROMPT = "JNode $P$G"; + private CommandInvoker invoker; + private String invokerName; - private static final String command = "cmd="; + private CommandInterpreter interpreter; + private String interpreterName; - // private static final Class[] MAIN_ARG_TYPES = new Class[] { - // String[].class }; - - private CommandInvoker commandInvoker; - - private ThreadCommandInvoker threadCommandInvoker; - - private DefaultCommandInvoker defaultCommandInvoker; + private CompletionInfo completion; - private ProcletCommandInvoker procletCommandInvoker; + private boolean historyEnabled; - private boolean historyEnabled = true; + private boolean debugEnabled; private boolean exitted = false; private Thread ownThread; + public TextConsole getConsole() { return console; } @@ -149,27 +152,6 @@ shell.run(); } - public void setThreadCommandInvoker() { - if (this.commandInvoker != threadCommandInvoker) { - err.println("Switched to thread invoker"); - this.commandInvoker = threadCommandInvoker; - } - } - - public void setDefaultCommandInvoker() { - if (this.commandInvoker != defaultCommandInvoker) { - err.println("Switched to default invoker"); - this.commandInvoker = defaultCommandInvoker; - } - } - - public void setProcletCommandInvoker() { - if (this.commandInvoker != procletCommandInvoker) { - err.println("Switched to proclet invoker"); - this.commandInvoker = procletCommandInvoker; - } - } - /** * Create a new instance * @@ -189,16 +171,11 @@ SystemInputStream.getInstance().initialize(this.in); cons.setCompleter(this); - defaultCommandInvoker = new DefaultCommandInvoker(this); - threadCommandInvoker = new ThreadCommandInvoker(this); - procletCommandInvoker = new ProcletCommandInvoker(this); - setThreadCommandInvoker(); // default to separate this.console.addConsoleListener(this); // threads for commands. aliasMgr = ((AliasManager) InitialNaming.lookup(AliasManager.NAME)) .createAliasManager(); - AccessController.doPrivileged(new SetPropertyAction( - PROMPT_PROPERTY_NAME, DEFAULT_PROMPT)); + System.setProperty(PROMPT_PROPERTY_NAME, DEFAULT_PROMPT); // ShellUtils.getShellManager().registerShell(this); } catch (NameNotFoundException ex) { throw new ShellException("Cannot find required resource", ex); @@ -214,62 +191,70 @@ * @see java.lang.Runnable#run() */ public void run() { - ownThread = Thread.currentThread(); // Here, we are running in the CommandShell (main) Thread // so, we can register ourself as the current shell // (it will also be the current shell for all children Thread) + + // FIXME - At one point, the 'current shell' had something to do with + // dispatching keyboard input to the right application. Now this is + // handled by the console layer. Is 'current shell' a meaningful / + // useful concept anymore? try { ShellUtils.getShellManager().registerShell(this); + + ShellUtils.registerCommandInvoker(DefaultCommandInvoker.FACTORY); + ShellUtils.registerCommandInvoker(ThreadCommandInvoker.FACTORY); + ShellUtils.registerCommandInvoker(ProcletCommandInvoker.FACTORY); + ShellUtils.registerCommandInterpreter(DefaultInterpreter.FACTORY); + ShellUtils.registerCommandInterpreter(RedirectingInterpreter.FACTORY); } catch (NameNotFoundException e1) { e1.printStackTrace(); } + // Configure the shell based on Syetsm properties. + setupFromProperties(); + + // Now become interactive + ownThread = Thread.currentThread(); + // Run commands from the JNode commandline first - final String cmdLine = (String) AccessController - .doPrivileged(new GetPropertyAction("jnode.cmdline", "")); + final String cmdLine = System.getProperty(CMDLINE_PROPERTY_NAME, ""); final StringTokenizer tok = new StringTokenizer(cmdLine); while (tok.hasMoreTokens()) { final String e = tok.nextToken(); try { - if (e.startsWith(command)) { - final String cmd = e.substring(command.length()); + if (e.startsWith(COMMAND_KEY)) { + final String cmd = e.substring(COMMAND_KEY.length()); out.println(prompt() + cmd); processCommand(cmd, false); } } catch (Throwable ex) { - ex.printStackTrace(err); + err.println("Error while processing bootarg commands: " + ex.getMessage()); + stackTrace(ex); } } - final String user_home = (String) AccessController.doPrivileged(new GetPropertyAction("user.home", "")); + final String user_home = System.getProperty(HOME_PROPERTY_NAME, ""); + AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { + final File shell_ini = new File(user_home + "/shell.ini"); try { - final File shell_ini = new File(user_home + "/shell.ini"); - if(shell_ini.exists()) + if (shell_ini.exists()) { executeFile(shell_ini); - } catch(IOException ioe){ - ioe.printStackTrace(); + } + } catch (IOException ex) { + err.println("Error while reading " + shell_ini + ": " + ex.getMessage()); + stackTrace(ex); } return null; } }); - // Now become interactive while (!isExitted()) { try { - // Temporary mechanism for switching invokers - String invokerName = System.getProperty("jnode.invoker", ""); - if (invokerName.equalsIgnoreCase("default")) { - setDefaultCommandInvoker(); - } - else if (invokerName.equalsIgnoreCase("thread")) { - setThreadCommandInvoker(); - } - else if (invokerName.equalsIgnoreCase("proclet")) { - setProcletCommandInvoker(); - } + refreshFromProperties(); clearEof(); out.print(prompt()); @@ -283,11 +268,76 @@ exitted = true; } } catch (Throwable ex) { - ex.printStackTrace(err); + err.println("Uncaught exception while processing command(s): " + ex.getMessage()); + stackTrace(ex); } } } + private void setupFromProperties() { + debugEnabled = Boolean.parseBoolean(System.getProperty(DEBUG_PROPERTY_NAME, "true")); + historyEnabled = Boolean.parseBoolean(System.getProperty(HISTORY_PROPERTY_NAME, "true")); + try { + setCommandInvoker(System.getProperty(INVOKER_PROPERTY_NAME, INITIAL_INVOKER)); + } catch (Exception ex) { + err.println(ex.getMessage()); + stackTrace(ex); + setCommandInvoker(FALLBACK_INVOKER); // fallback to default + } + try { + setCommandInterpreter(System.getProperty(INTERPRETER_PROPERTY_NAME, INITIAL_INTERPRETER)); + } catch (Exception ex) { + err.println(ex.getMessage()); + stackTrace(ex); + setCommandInterpreter(FALLBACK_INTERPRETER); // fallback to default + } + } + + private void refreshFromProperties() { + debugEnabled = Boolean.parseBoolean(System.getProperty(DEBUG_PROPERTY_NAME, "true")); + historyEnabled = Boolean.parseBoolean(System.getProperty(HISTORY_PROPERTY_NAME, "true")); + try { + setCommandInterpreter(System.getProperty(INTERPRETER_PROPERTY_NAME, "")); + } + catch (Exception ex) { + err.println(ex.getMessage()); + stackTrace(ex); + } + try { + setCommandInvoker(System.getProperty(INVOKER_PROPERTY_NAME, "")); + } + catch (Exception ex) { + err.println(ex.getMessage()); + stackTrace(ex); + } + } + + public synchronized void setCommandInvoker(String name) + throws IllegalArgumentException { + if (!name.equals(this.invokerName)) { + this.invoker = ShellUtils.createInvoker(name, this); + err.println("Switched to " + name + " invoker"); + this.invokerName = name; + System.setProperty(INVOKER_PROPERTY_NAME, name); + } + } + + public synchronized void setCommandInterpreter(String name) + throws IllegalArgumentException { + if (!name.equals(this.interpreterName)) { + this.interpreter = ShellUtils.createInterpreter(name); + err.println("Switched to " + name + " interpreter"); + this.interpreterName = name; + System.setProperty(INTERPRETER_PROPERTY_NAME, name); + } + } + + private void stackTrace(Throwable ex) { + if (this.debugEnabled) { + ex.printStackTrace(err); + } + } + private String readInputLine() throws IOException { StringBuffer sb = new StringBuffer(40); Reader r = new InputStreamReader(in); @@ -314,16 +364,55 @@ // for input completion applicationHistory.set(new InputHistory()); } - commandInvoker.invoke(cmdLineStr); + try { + interpreter.interpret(this, cmdLineStr); + } + catch (ShellException ex) { + err.println("Shell exception: " + ex.getMessage()); + stackTrace(ex); + } + if (interactive) { applicationHistory.set(null); } } - public void invokeCommand(String command) { + /** + * Parse and run a command line using the CommandShell's current interpreter. + * @param command the command line. + * @throws ShellException + */ + public void invokeCommand(String command) throws ShellException { processCommand(command, false); } + /** + * Run a command encoded as a CommandLine object. The command line + * will give the command name (alias), the argument list and the + * IO stream. The command is run using the CommandShell's current invoker. + * + * @param cmdLine the CommandLine object. + * @return the command's return code + * @throws ShellException + */ + public int invoke(CommandLine cmdLine) throws ShellException { + return this.invoker.invoke(cmdLine); + } + + /** + * Prepare a CommandThread to run a command encoded as a CommandLine object. + * When the thread's "start" method is called, the command will be executed + * using the CommandShell's current (now) invoker. + * + * @param cmdLine the CommandLine object. + * @return the command's return code + * @throws ShellException + */ + public CommandThread invokeAsynchronous(CommandLine cmdLine) + throws ShellException { + return this.invoker.invokeAsynchronous(cmdLine); + } + protected CommandInfo getCommandClass(String cmd) throws ClassNotFoundException { try { @@ -336,6 +425,10 @@ } } + boolean isDebugEnabled() { + return debugEnabled; + } + /** * Gets the alias manager of this shell */ @@ -366,8 +459,7 @@ * Gets the expanded prompt */ protected String prompt() { - String prompt = (String) AccessController.doPrivileged( - new GetPropertyAction(PROMPT_PROPERTY_NAME)); + String prompt = System.getProperty(PROMPT_PROPERTY_NAME, DEFAULT_PROMPT); final StringBuffer result = new StringBuffer(); boolean commandMode = false; try { @@ -378,16 +470,15 @@ if (commandMode) { switch (c) { case 'P': - result.append(new File((String) AccessController - .doPrivileged(new GetPropertyAction("user.dir")))); + result.append(new File( + System.getProperty(DIRECTORY_PROPERTY_NAME, ""))); break; case 'G': result.append("> "); break; case 'D': final Date now = new Date(); - DateFormat.getDateTimeInstance().format(now, result, - null); + DateFormat.getDateTimeInstance().format(now, result, null); break; default: result.append(c); @@ -410,15 +501,10 @@ return result.toString(); } - private final Help.Info defaultParameter = new Help.Info("file", - "default parameter for command line completion", - new Parameter(new FileArgument("file", "a file", Argument.MULTI), Parameter.OPTIONAL) - ); - private final Argument defaultArg = new AliasArgument("command", - "the command to be called"); + public Completable parseCommandLine(String cmdLineStr) throws ShellSyntaxException { + return interpreter.parsePartial(this, cmdLineStr); + } - private CompletionInfo completion; - public CompletionInfo complete(String partial) { if (!readingCommand) { // dummy completion behavior for application input. @@ -435,65 +521,47 @@ } // do command completion - String result = null; completion = new CompletionInfo(); + boolean success = false; try { - CommandLine cl = new CommandLine(partial); - String cmd = ""; - if (cl.hasNext()) { - cmd = cl.next(); - if (!cmd.trim().equals("") && cl.hasNext()) - try { - // get command's help info - CommandInfo cmdClass = getCommandClass(cmd); - - Help.Info info = defaultParameter; - try { - info = Help.getInfo(cmdClass - .getCommandClass()); - }catch (HelpException ex) { - /*ex.printStackTrace(); - throw new CompletionException("Command class not found");*/ - - //assuming default completion which is multiple files - } - - // perform completion - result = cmd + " " + info.complete(cl); // prepend - completion.setCompleted(result); - // command - // name and - // space - // again - } catch (ClassNotFoundException ex) { - throw new CompletionException("Command class not found"); - } - } - if (result == null) // assume this is the alias to be called - result = defaultArg.complete(cmd); - - if (!partial.equals(result) && !completion.hasItems()) { - // performed direct - // completion without listing - completion.setCompleted(result); + Completable cl = parseCommandLine(partial); + if (cl != null) { + cl.complete(completion, this); + if (!partial.equals(completion.getCompleted()) && !completion.hasItems()) { + // we performed direct completion without listing completion.setNewPrompt(false); } + success = true; + } + } catch (ShellSyntaxException ex) { + out.println(); // next line + err.println("Cannot parse: " + ex.getMessage()); // print the error (optional) + } catch (CompletionException ex) { out.println(); // next line - err.println(ex.getMessage()); // print the error (optional) - // this debug output is to trace where the Exception came from - // ex.printStackTrace(err); - // restore old value and need a new prompt + err.println("Problem in completer: " + ex.getMessage()); // print the error (optional) + } + + if (!success) { + // Make sure the caller knows to repaint the prompt completion.setCompleted(partial); completion.setNewPrompt(true); } - return completion; + // Make sure that the shell's completion context gets nulled. + CompletionInfo myCompletion = completion; + completion = null; + return myCompletion; } public void list(String[] items) { + if (completion == null) { + throw new ShellFailureException("list called when no completion is in progress"); + } + else { completion.setItems(items); } + } public void addCommandToHistory(String cmdLineStr) { // Add this command to the command history. @@ -525,6 +593,10 @@ } } + /** + * This class subtypes FilterInputStream to capture console input to an + * application in the application input history. + */ private class HistoryInputStream extends FilterInputStream { // TODO - revisit for support of multi-byte character encodings. private StringBuilder line = new StringBuilder(); @@ -579,50 +651,54 @@ return err; } - public DefaultCommandInvoker getDefaultCommandInvoker() { - return defaultCommandInvoker; + public CommandInvoker getDefaultCommandInvoker() { + return ShellUtils.createInvoker("default", this); } public void executeFile(File file) throws IOException { - if(!file.exists()){ - System.err.println( "File does not exist: " + file); + if (!file.exists()) { + err.println( "File does not exist: " + file); return; } - try{ + try { setHistoryEnabled(false); final BufferedReader br = new BufferedReader(new FileReader(file)); - for(String line = br.readLine(); line != null; line = br.readLine()){ + for (String line = br.readLine(); line != null; line = br.readLine()) { line = line.trim(); - if(line.startsWith("#") || line.equals("")) + if (line.startsWith("#") || line.equals("")) { continue; - + } + try { invokeCommand(line); } + catch (ShellException ex) { + err.println("Shell exception: " + ex.getMessage()); + stackTrace(ex); + } + } br.close(); - }finally{ + } finally { setHistoryEnabled(true); } } public void exit(){ - exit0(); console.close(); - } public void consoleClosed(ConsoleEvent event) { - if(!exitted) - if(Thread.currentThread() == ownThread){ + if (!exitted) { + if (Thread.currentThread() == ownThread){ exit0(); } else { - synchronized(this){ + synchronized(this) { exit0(); notifyAll(); } } - + } } private void exit0() { @@ -630,7 +706,7 @@ threadSuspended = false; } - private synchronized boolean isExitted(){ + private synchronized boolean isExitted() { return exitted; } Modified: trunk/shell/src/shell/org/jnode/shell/CommandThread.java =================================================================== --- trunk/shell/src/shell/org/jnode/shell/CommandThread.java 2007-10-26 21:17:14 UTC (rev 3570) +++ trunk/shell/src/shell/org/jnode/shell/CommandThread.java 2007-10-26 21:30:12 UTC (rev 3571) @@ -63,11 +63,15 @@ @Override public void run() { + try { super.run(); + } + finally { if (listener != null) { listener.notifyThreadExitted(this);... [truncated message content] |