/* * $Id$ * * Copyright (C) 2003-2015 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.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.NoSuchElementException; import org.apache.log4j.Logger; import org.jnode.shell.CommandLine.Token; import org.jnode.shell.help.Help; import org.jnode.shell.help.HelpException; import org.jnode.shell.help.HelpFactory; /** * This interpreter simply parses the command line into a command name and * arguments, with simple quoting and escaping. This class also provides * infrastructure that is reused in the RedirectingInterpreter class. * * @author crawley@jnode.org */ public class DefaultInterpreter implements CommandInterpreter { public static final Factory FACTORY = new Factory() { public CommandInterpreter create() { return new DefaultInterpreter(); } public String getName() { return "default"; } }; static final String[] NO_ARGS = new String[0]; private static final Logger LOG = Logger.getLogger(DefaultInterpreter.class); public static final int REDIRECTS_FLAG = 0x01; // Token types. public static final int LITERAL = 0; public static final int STRING = 1; public static final int CLOSED = 2; public static final int SPECIAL = 4; // Recognized meta-characters 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 GET_INPUT_FROM_CHAR = '<'; public static final char PIPE_CHAR = '|'; public static final char COMMENT_CHAR = '#'; // Recognized '\' escapes 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'; @Override public String getName() { return "default"; } /** * {@inheritDoc} * * The default interpreter treats a command script as a sequence of commands. * Commands are expected to consist of exactly one line. Any line whose first non-whitespace * character is '#' will be ignored. Command line arguments from the script are not supported, * and will result in a {@link ShellException} being thrown. */ @Override public int interpret(CommandShell shell, Reader reader, boolean script, String alias, String[] args) throws ShellException { if (args != null && args.length > 0) { throw new ShellInvocationException( "The " + getName() + " interpreter does not support script file arguments"); } try { BufferedReader br = new BufferedReader(reader); String line; int rc = 0; while ((line = br.readLine()) != null) { line = line.trim(); if (line.length() > 0 && !line.startsWith("#")) { rc = interpret(shell, line); } if (!script) { break; } } return rc; } catch (IOException ex) { throw new ShellInvocationException("Problem reading command: " + ex.getMessage(), ex); } finally { try { reader.close(); } catch (IOException ex) { // ignore } } } @Override public Completable parsePartial(CommandShell shell, String line) throws ShellException { CommandLine res = doParseCommandLine(line); return res == null ? new CommandLine("", null) : res; } @Override public boolean help(CommandShell shell, String line, PrintWriter pw) throws ShellException { CommandLine cmd = doParseCommandLine(line); CommandInfo cmdInfo = cmd.getCommandInfo(shell); if (cmdInfo != null) { try { Help help = HelpFactory.getHelpFactory().getHelp(cmd.getCommandName(), cmdInfo); help.usage(pw); return true; } catch (HelpException ex) { LOG.info("Unexpected error while getting help for alias / class '" + cmd.getCommandName() + "': " + ex.getMessage(), ex); } } return false; } protected int interpret(CommandShell shell, String line) throws ShellException { CommandLine cmd = doParseCommandLine(line); if (cmd == null) { return 0; } return shell.invoke(cmd, null, null); } private CommandLine doParseCommandLine(String line) throws ShellException { Tokenizer tokenizer = new Tokenizer(line); if (!tokenizer.hasNext()) { return null; } CommandLine.Token commandToken = tokenizer.next(); LinkedList<CommandLine.Token> tokenList = new LinkedList<CommandLine.Token>(); while (tokenizer.hasNext()) { tokenList.add(tokenizer.next()); } CommandLine.Token[] argTokens = tokenList.toArray(new CommandLine.Token[tokenList.size()]); CommandLine res = new CommandLine(commandToken, argTokens, null); res.setArgumentAnticipated(tokenizer.whitespaceAfterLast()); return res; } @Override public String escapeWord(String word) { return escapeWord(word, false); } protected String escapeWord(String word, boolean escapeRedirects) { final int len = word.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char ch = word.charAt(i); switch (ch) { case ESCAPE_B: sb.append(ESCAPE_CHAR).append(B); break; case ESCAPE_N: sb.append(ESCAPE_CHAR).append(N); break; case ESCAPE_R: sb.append(ESCAPE_CHAR).append(R); break; case ESCAPE_T: sb.append(ESCAPE_CHAR).append(T); break; case ESCAPE_CHAR: sb.append(ESCAPE_CHAR).append(ESCAPE_CHAR); break; case FULL_ESCAPE_CHAR: sb.append(ESCAPE_CHAR).append(FULL_ESCAPE_CHAR); break; case QUOTE_CHAR: sb.append(ESCAPE_CHAR).append(QUOTE_CHAR); break; case COMMENT_CHAR: sb.append(ESCAPE_CHAR).append(COMMENT_CHAR); break; case SPACE_CHAR: sb.append(ESCAPE_CHAR).append(SPACE_CHAR); break; case PIPE_CHAR: if (escapeRedirects) { sb.append(ESCAPE_CHAR).append(PIPE_CHAR); } else { sb.append(PIPE_CHAR); } break; case SEND_OUTPUT_TO_CHAR: if (escapeRedirects) { sb.append(ESCAPE_CHAR).append(SEND_OUTPUT_TO_CHAR); } else { sb.append(SEND_OUTPUT_TO_CHAR); } break; case GET_INPUT_FROM_CHAR: if (escapeRedirects) { sb.append(ESCAPE_CHAR).append(GET_INPUT_FROM_CHAR); } else { sb.append(GET_INPUT_FROM_CHAR); } break; default: sb.append(ch); } } return sb.toString(); } /** * Get and expand the default command prompt. */ public String getPrompt(CommandShell shell, boolean continuation) { String prompt = shell.getProperty(CommandShell.PROMPT_PROPERTY_NAME); final StringBuffer result = new StringBuffer(); boolean commandMode = false; StringReader reader = new StringReader(prompt); int i; try { while ((i = reader.read()) != -1) { char c = (char) i; if (commandMode) { switch (c) { case 'P': result.append(new File(System.getProperty(CommandShell.DIRECTORY_PROPERTY_NAME, ""))); break; case 'G': result.append("> "); break; case 'D': final Date now = new Date(); DateFormat.getDateTimeInstance().format(now, result, null); break; default: result.append(c); } commandMode = false; } else { switch (c) { case '$': commandMode = true; break; default: result.append(c); } } } } catch (IOException ex) { // A StringReader shouldn't give an IOException unless we close it ... which we don't! LOG.error("Impossible", ex); } return result.toString(); } @Override public boolean supportsMultiline() { return false; } /** * A simple command line tokenizer for the 'built-in' interpreters. It * understands quoting, some '\' escapes, and (depending on constructor * flags) certain "special" symbols. */ protected static class Tokenizer implements SymbolSource<CommandLine.Token> { private int pos = 0; private final ArrayList<CommandLine.Token> tokens = new ArrayList<Token>(8); private boolean whiteSpaceAfterLast; /** * Instantiate a command line tokenizer for a given input String. * * @param line the input String. * @param flags flags controlling the tokenization. * @throws ShellException */ public Tokenizer(String line, int flags) throws ShellSyntaxException { tokenize(line, flags); } public Tokenizer(String line) throws ShellSyntaxException { this(line, 0); } /** * Returns if there are no more tokens to return. * * @return <code>true</code> if there is another token; * <code>false</code> otherwise */ public boolean hasNext() { return pos < tokens.size(); } /** * Extract the next token string and return it. * * @return the next token */ public CommandLine.Token next() throws NoSuchElementException { if (!hasNext()) { throw new NoSuchElementException(); } return tokens.get(pos++); } private void tokenize(String s, int flags) throws ShellSyntaxException { int pos = 0; while (true) { // Skip spaces before start of token whiteSpaceAfterLast = false; while (pos < s.length() && s.charAt(pos) == SPACE_CHAR) { pos++; whiteSpaceAfterLast = true; } if (pos >= s.length()) { break; } // Parse a token boolean inFullEscape = false; boolean inQuote = false; int type = LITERAL; int start = pos; 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 ShellSyntaxException( "escape char ('\\') not followed by a character"); } char ch; switch (ch = s.charAt(pos++)) { case N: token.append(ESCAPE_N); break; case B: token.append(ESCAPE_B); break; case R: token.append(ESCAPE_R); break; case T: token.append(ESCAPE_T); break; default: token.append(ch); } 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 GET_INPUT_FROM_CHAR: case SEND_OUTPUT_TO_CHAR: case PIPE_CHAR: if (inFullEscape || inQuote || (flags & REDIRECTS_FLAG) == 0) { token.append(currentChar); } else { finished = true; if (token.length() == 0) { token.append(currentChar); type = SPECIAL; } else { pos--; // the special character terminates the // literal. } } break; default: token.append(currentChar); } } tokens.add(new CommandLine.Token(token.toString(), type, start, pos)); } } /** * This operation it not supported. */ public void remove() { throw new UnsupportedOperationException("remove"); } /** * Return the Token returned by the last successful call to next(). * * @return the last token. */ public CommandLine.Token last() { return tokens.get(pos - 1); } public Token peek() throws NoSuchElementException { if (!hasNext()) { throw new NoSuchElementException(); } return tokens.get(pos + 1); } public void seek(int pos) throws NoSuchElementException { if (pos < 0 || pos > tokens.size()) { throw new NoSuchElementException(); } this.pos = pos; } public int tell() { return pos; } public boolean whitespaceAfterLast() { return whiteSpaceAfterLast; } } }