/*
* $Id$
*
* Copyright (C) 2003-2014 JNode.org
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
* License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library; If not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.jnode.shell;
import java.util.NoSuchElementException;
import org.jnode.driver.console.CompletionInfo;
import org.jnode.shell.help.CompletionException;
import org.jnode.shell.io.CommandIO;
import org.jnode.shell.io.CommandIOMarker;
import org.jnode.shell.syntax.AliasArgument;
import org.jnode.shell.syntax.Argument;
import org.jnode.shell.syntax.ArgumentBundle;
import org.jnode.shell.syntax.ArgumentSyntax;
import org.jnode.shell.syntax.CommandSyntaxException;
import org.jnode.shell.syntax.FileArgument;
import org.jnode.shell.syntax.RepeatSyntax;
import org.jnode.shell.syntax.Syntax;
import org.jnode.shell.syntax.SyntaxBundle;
/**
* 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.
* <p/>
* TODO This class needs to be fully "shell and command syntax agnostic".
* TODO Get rid of API methods using a String argument representation.
*
* @author crawley@jnode.org
*/
public class CommandLine implements Completable, Iterable<String> {
public static final CommandIO DEFAULT_STDIN = new CommandIOMarker("STDIN");
public static final CommandIO DEFAULT_STDOUT = new CommandIOMarker("STDOUT");
public static final CommandIO DEFAULT_STDERR = new CommandIOMarker("STDERR");
public static final CommandIO DEVNULL = new CommandIOMarker("DEVNULL");
public static final int LITERAL = 0;
public static final int STRING = 1;
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 = '\'';
public static final char QUOTE_CHAR = '"';
public static final char SPACE_CHAR = ' ';
public static final char SEND_OUTPUT_TO_CHAR = '>';
public static final char COMMENT_CHAR = '#';
private static final char ESCAPE_B = '\b';
private static final char B = 'b';
private static final char ESCAPE_N = '\n';
private static final char N = 'n';
private static final char ESCAPE_R = '\r';
private static final char R = 'r';
private static final char ESCAPE_T = '\t';
private static final char T = 't';
private static final String[] NO_ARGS = new String[0];
private static final Token[] NO_TOKENS = new Token[0];
private final Token commandToken;
private final Token[] argumentTokens;
private CommandIO[] ios;
private CommandInfo commandInfo;
private boolean argumentAnticipated = false;
/**
* Create a new instance using Tokens instead of Strings.
*
* @param commandToken the command name token or <code>null</code>.
* @param argumentTokens the argument token list or <code>null</code>.
* @param ios the io stream array or <code>null</code>.
*/
public CommandLine(Token commandToken, Token[] argumentTokens,
CommandIO[] ios) {
this.commandToken = commandToken;
this.argumentTokens = (argumentTokens == null || argumentTokens.length == 0) ? NO_TOKENS
: argumentTokens.clone();
this.ios = setupStreams(ios);
}
/**
* 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 4 is substituted. A non-null 'streams' argument must have
* a length of at least 4.
*
* @param commandName the command name or <code>null</code>.
* @param arguments the argument list or <code>null</code>.
* @param ios the io stream array or <code>null</code>.
*/
public CommandLine(String commandName, String[] arguments, CommandIO[] ios) {
this.commandToken = commandName == null ? null : new Token(commandName);
if (arguments == null || arguments.length == 0) {
this.argumentTokens = NO_TOKENS;
} else {
int len = arguments.length;
argumentTokens = new Token[len];
for (int i = 0; i < len; i++) {
this.argumentTokens[i] = new Token(arguments[i]);
}
}
this.ios = setupStreams(ios);
}
/**
* 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 commandName, String[] arguments) {
this(commandName, arguments, null);
}
/**
* Create a new instance. Equivalent to CommandLine(null, arguments, null);
*
* @param arguments the argument list or <code>null</code>.
* @deprecated It is a bad idea to leave out the command name.
*/
public CommandLine(String[] arguments) {
this(null, arguments, null /* FIXME */);
}
private CommandIO[] setupStreams(CommandIO[] ios) {
if (ios == null) {
ios = new CommandIO[4];
ios[Command.STD_IN] = DEFAULT_STDIN;
ios[Command.STD_OUT] = DEFAULT_STDOUT;
ios[Command.STD_ERR] = DEFAULT_STDERR;
ios[Command.SHELL_ERR] = DEFAULT_STDERR;
return ios;
} else if (ios.length < 4) {
throw new IllegalArgumentException("streams.length < 4");
} else {
return ios.clone();
}
}
/**
* This method returns an Iterator for the arguments represented as Strings.
*
* @deprecated
*/
public SymbolSource<String> iterator() {
final boolean whitespaceAfterLast = this.argumentAnticipated;
return new SymbolSource<String>() {
private int pos = 0;
public boolean hasNext() {
return pos < argumentTokens.length;
}
public String next() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException();
}
return argumentTokens[pos++].text;
}
public String peek() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException();
}
return argumentTokens[pos].text;
}
public String last() throws NoSuchElementException {
if (pos <= 0) {
throw new NoSuchElementException();
}
return argumentTokens[pos - 1].text;
}
public void remove() {
throw new UnsupportedOperationException();
}
public void seek(int pos) throws NoSuchElementException {
if (pos >= 0 && pos <= argumentTokens.length) {
this.pos = pos;
} else {
throw new NoSuchElementException("pos out of range");
}
}
public int tell() {
return pos;
}
public boolean whitespaceAfterLast() {
return whitespaceAfterLast;
}
};
}
/**
* This method returns an Iterator for the arguments represented as Tokens
*/
public SymbolSource<Token> tokenIterator() throws NoTokensAvailableException {
if (argumentTokens == null) {
throw new NoTokensAvailableException("No tokens available in the CommandLine");
}
final boolean whitespaceAfterLast = this.argumentAnticipated;
return new SymbolSource<Token>() {
private int pos = 0;
public boolean hasNext() {
return pos < argumentTokens.length;
}
public Token next() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException();
}
return argumentTokens[pos++];
}
public Token peek() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException();
}
return argumentTokens[pos];
}
public Token last() throws NoSuchElementException {
if (pos <= 0) {
throw new NoSuchElementException();
}
return argumentTokens[pos - 1];
}
public void remove() {
throw new UnsupportedOperationException();
}
public void seek(int pos) throws NoSuchElementException {
if (pos >= 0 && pos <= argumentTokens.length) {
this.pos = pos;
} else {
throw new NoSuchElementException("pos out of range");
}
}
public int tell() {
return pos;
}
public boolean whitespaceAfterLast() {
return whitespaceAfterLast;
}
};
}
/**
* Get the command name
*
* @return the command name
*/
public String getCommandName() {
return commandToken == null ? null : commandToken.text;
}
/**
* Get the command name in token form
*
* @return the command token
*/
public Token getCommandToken() {
return commandToken;
}
/**
* Get the arguments as String[].
*
* @return the arguments as String[]
*/
public String[] getArguments() {
int len = argumentTokens.length;
if (len == 0) {
return NO_ARGS;
}
String[] arguments = new String[len];
for (int i = 0; i < len; i++) {
arguments[i] = argumentTokens[i].text;
}
return arguments;
}
public Token[] getArgumentTokens() {
return argumentTokens;
}
/**
* Get the arguments as String[].
*
* @return the arguments as String[]
* @deprecated this method name is wrong.
*/
public String[] toStringArray() {
return getArguments();
}
/**
* Returns the entire command line as a string.
*
* @return the entire command line
*/
public String toString() {
StringBuilder sb = new StringBuilder(escape(commandToken.text));
for (Token arg : argumentTokens) {
sb.append(' ');
sb.append(escape(arg.text));
}
return sb.toString();
}
/**
* Gets the remaining number of parts
*
* @return the remaining number of parts
*/
public int getLength() {
return argumentTokens.length;
}
public boolean isArgumentAnticipated() {
return argumentAnticipated;
}
public void setArgumentAnticipated(boolean newValue) {
argumentAnticipated = newValue;
}
/**
* The Token class is a light-weight representation for tokens that make up
* a command line.
*/
public static class Token {
/**
* This field holds the "cooked" representation of command line token.
* By the time we reach the CommandLine, all shell meta-characters
* should have been processed so that the value of the field represents
* a command name or argument.
*/
public final String text;
/**
* This field represents the type of the token. The meaning is
* interpreter specific. The value -1 indicates that no token type is
* available.
*/
public final int tokenType;
/**
* This field denotes the character offset of the first character of
* this token in the source character sequence passed to the
* interpreter. The value -1 indicates that no source start position is
* available.
*/
public final int start;
/**
* This field denotes the character offset + 1 for the last character of
* this token in the source character sequence passed to the
* interpreter. The value -1 indicates that no source end position is
* available.
*/
public final int end;
public Token(String value, int type, int start, int end) {
this.text = value;
this.tokenType = type;
this.start = start;
this.end = end;
}
public Token(String token) {
this(token, -1, -1, -1);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + end;
result = prime * result + start;
result = prime * result + ((text == null) ? 0 : text.hashCode());
result = prime * result + tokenType;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Token other = (Token) obj;
if (end != other.end)
return false;
if (start != other.start)
return false;
if (text == null) {
if (other.text != null)
return false;
} else if (!text.equals(other.text))
return false;
if (tokenType != other.tokenType)
return false;
return true;
}
public String toString() {
return "Token{'" + text + "'," + start + "," + end + "," + tokenType + "}";
}
}
// escape and unescape methods
private static final Escape[] escapes = {
// plain escaped
new Escape(ESCAPE_CHAR, ESCAPE_CHAR), new Escape(ESCAPE_B, B),
new Escape(ESCAPE_N, N), new Escape(ESCAPE_R, R),
new Escape(ESCAPE_T, T),
new Escape(FULL_ESCAPE_CHAR, FULL_ESCAPE_CHAR)};
/**
* Escape a single command line argument for the Shell. Same as calling
* escape(arg, <code>false</code>)
*
* @param arg the unescaped argument
* @return the escaped argument
*/
public String escape(String arg) {
return doEscape(arg, false); // don't force quotation
}
/**
* Escape a single command line argument for the Shell.
*
* @param arg the unescaped argument
* @param forceQuote 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 interpreter
* concern, and this class needs to be interpreter specific.
*/
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);
// insert escape sequences
for (int i = 0; i < arg.length(); i++) {
char c = arg.charAt(i);
for (int j = 0; j < escapes.length; j++) {
if (escapes[j].plain == c) {
sb.append(ESCAPE_CHAR);
c = escapes[j].escaped;
break;
}
}
forceQuote |= (c == SPACE_CHAR || c == QUOTE_CHAR);
sb.append(c);
}
if (forceQuote) {
sb.insert(0, FULL_ESCAPE_CHAR);
sb.append(FULL_ESCAPE_CHAR);
}
return sb.toString();
}
public String escape(String arg, boolean forceQuote) {
return CommandLine.doEscape(arg, forceQuote);
}
private static class Escape {
final char plain;
final char escaped;
Escape(char plain, char escaped) {
this.plain = plain;
this.escaped = escaped;
}
}
/**
* Get the IO stream context for executing the command. The result is
* guaranteed to be non-null and to have at least 4 entries.
*
* @return the stream context as described above.
*/
public CommandIO[] getStreams() {
return ios.clone();
}
/**
* Set the IO stream context for executing the command.
*
* @param ios the command's new stream context.
*/
public void setStreams(CommandIO[] ios) {
if (ios.length < 4) {
throw new IllegalArgumentException("need >= 4 CommandIO objects");
}
this.ios = ios.clone();
}
/**
* Perform command line argument parsing in preparation to invoking a command.
* This locates the command's class and a suitable command line syntax, then
* parses against the Syntax, binding the command arguments to Argument objects
* in an ArgumentBundle object obtained from the Command object.
* <p>
* Note that the async invokers don't use this method. Instead, they obtain the
* CommandInfo in the parent thread, then create the command instance and do
* the argument parsing in the child thread / proclet / isolate.
*
* @param shell the context for resolving command aliases and locating syntaxes
* @return a CompandInfo which includes the command instance to which the arguments have been bound
* @throws ShellException if the chosen syntax doesn't match the command line arguments or there
* was a problem instantiating the command
*/
public CommandInfo parseCommandLine(Shell shell) throws ShellException {
String cmd = (commandToken == null) ? "" : commandToken.text.trim();
if (cmd.equals("")) {
throw new ShellFailureException("no command name");
}
CommandInfo cmdInfo = shell.getCommandInfo(cmd);
cmdInfo.parseCommandLine(this);
return cmdInfo;
}
public void complete(CompletionInfo completions, CommandShell shell) throws CompletionException {
String cmd = (commandToken == null) ? "" : commandToken.text.trim();
if (!cmd.equals("") && (argumentTokens.length > 0 || argumentAnticipated)) {
CommandInfo ci;
try {
ci = getCommandInfo(shell);
} catch (ShellException ex) {
throw new CompletionException(ex.getMessage(), ex);
}
Command command;
try {
command = ci.createCommandInstance();
} catch (Throwable ex) {
throw new CompletionException("Problem creating a command instance", ex);
}
// Get the command's argument bundle and syntax
ArgumentBundle bundle = (command == null)
? ci.getArgumentBundle()
: command.getArgumentBundle();
SyntaxBundle syntaxes = ci.getSyntaxBundle();
if (syntaxes == null) {
syntaxes = shell.getSyntaxManager().getSyntaxBundle(cmd);
}
if (bundle == null) {
// We're missing the argument bundle. We assume this is a 'classic' Java application
// that does its own argument parsing and completion like a UNIX shell; i.e.
// completing each argument as a pathname.
Syntax syntax = new RepeatSyntax(new ArgumentSyntax("argument"));
syntaxes = new SyntaxBundle(cmd, syntax);
bundle = new ArgumentBundle(
new FileArgument("argument", Argument.MULTIPLE));
} else if (syntaxes == null) {
// We're missing the syntax, but we do have an argument bundle. Generate
// a default syntax from the bundle.
syntaxes = new SyntaxBundle(cmd, bundle.createDefaultSyntax());
}
try {
bundle.complete(this, syntaxes, completions);
} catch (CommandSyntaxException ex) {
throw new CompletionException("Command syntax problem", ex);
}
} else {
// We haven't got a command name yet, so complete the partial command name string
// as an AliasArgument.
ArgumentCompleter ac = new ArgumentCompleter(
new AliasArgument("cmdName", Argument.SINGLE), commandToken);
ac.complete(completions, shell);
}
}
public CommandInfo getCommandInfo(CommandShell shell) throws ShellException {
if (commandInfo == null) {
String cmd = (commandToken == null) ? "" : commandToken.text.trim();
if (!cmd.equals("")) {
commandInfo = shell.getCommandInfo(cmd);
}
}
return commandInfo;
}
public void setCommandInfo(CommandInfo commandInfo) {
this.commandInfo = commandInfo;
}
public CommandInfo getCommandInfo() {
if (commandInfo == null) {
throw new IllegalStateException("commandInfo not set");
}
return commandInfo;
}
public boolean isInternal() {
return getCommandInfo().isInternal();
}
}