/*
* $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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.apache.log4j.Logger;
import org.jnode.shell.help.Help;
import org.jnode.shell.help.HelpException;
import org.jnode.shell.help.HelpFactory;
import org.jnode.shell.io.CommandIO;
import org.jnode.shell.io.CommandInput;
import org.jnode.shell.io.CommandOutput;
import org.jnode.shell.syntax.Argument;
import org.jnode.shell.syntax.FileArgument;
/**
* This command interpreter supports simple input and output redirection and
* pipelines.
*
* @author crawley@jnode.org
*/
public class RedirectingInterpreter extends DefaultInterpreter implements
ThreadExitListener {
public static final Factory FACTORY = new Factory() {
public CommandInterpreter create() {
return new RedirectingInterpreter();
}
public String getName() {
return "redirecting";
}
};
private static final Logger LOG = Logger.getLogger(RedirectingInterpreter.class);
@Override
public String getName() {
return "redirecting";
}
@Override
protected int interpret(CommandShell shell, String line) throws ShellException {
Tokenizer tokenizer = new Tokenizer(line, REDIRECTS_FLAG);
List<CommandDescriptor> commands = new LinkedList<CommandDescriptor>();
parse(tokenizer, commands, false);
int len = commands.size();
if (len == 0) {
return 0; // empty command line.
} else if (len == 1) {
return runCommand(shell, commands.get(0));
} else {
return runPipeline(shell, commands);
}
}
@Override
public Completable parsePartial(CommandShell shell, String line)
throws ShellException {
Tokenizer tokenizer = new Tokenizer(line, REDIRECTS_FLAG);
List<CommandDescriptor> commands = new LinkedList<CommandDescriptor>();
return parse(tokenizer, commands, true);
}
@Override
public boolean help(CommandShell shell, String line, PrintWriter pw) throws ShellException {
Tokenizer tokenizer = new Tokenizer(line, REDIRECTS_FLAG);
List<CommandDescriptor> commands = new LinkedList<CommandDescriptor>();
parse(tokenizer, commands, true);
int len = commands.size();
if (len == 0) {
return false;
}
// We'll show the help for the last command in the pipeline.
CommandLine cmd = commands.get(len - 1).commandLine;
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;
}
@Override
public String escapeWord(String word) {
return escapeWord(word, true);
}
/**
* This method parses the shell input into command lines. If we are completing,
* then we return a Completable that will do completion for / after the last token
* according to the parser's syntactic context. (Normally the Completable is a
* CommandLine, but if we at / expecting a redirection filename, it will be a
* Completer for the filename.)
*
* @param tokenizer the source of shell input tokens
* @param commands a list for accumulating the parsed commands / redirections
* @param completing if <code>true</code> we are completing.
* @return a Completer or <code>null</code>
* @throws ShellSyntaxException
*/
private Completable parse(Tokenizer tokenizer,
List<CommandDescriptor> commands, boolean completing)
throws ShellSyntaxException {
boolean wspAfter = tokenizer.whitespaceAfterLast();
boolean pipeTo = false;
List<CommandLine.Token> args = new ArrayList<CommandLine.Token>();
while (tokenizer.hasNext()) {
CommandLine.Token commandToken = tokenizer.next();
if (commandToken.tokenType == SPECIAL) {
throw new ShellSyntaxException("Misplaced '" +
commandToken.text + "': expected a command name");
}
CommandLine.Token from = null;
CommandLine.Token to = null;
pipeTo = false;
args.clear();
while (tokenizer.hasNext()) {
CommandLine.Token token = tokenizer.next();
if (token.tokenType == SPECIAL) {
if (token.text.equals("<")) {
from = parseFileName(tokenizer, "<");
if (from == null && !completing) {
throw new ShellSyntaxException("no filename after '<'");
} else if (completing &&
(from == null || (!tokenizer.hasNext() && !wspAfter))) {
return new ArgumentCompleter(
new FileArgument("?", Argument.MANDATORY, null), from);
}
continue;
} else if (token.text.equals(">")) {
to = parseFileName(tokenizer, ">");
if (to == null && !completing) {
throw new ShellSyntaxException("no filename after '>'");
} else if (completing &&
(to == null || (!tokenizer.hasNext() && !wspAfter))) {
return new ArgumentCompleter(
new FileArgument("?", Argument.MANDATORY, null), to);
}
continue;
} else if (token.text.equals("|")) {
pipeTo = true;
break;
} else {
throw new ShellSyntaxException(
"unrecognized symbol: '" + token + "'");
}
} else {
args.add(token);
}
}
CommandLine.Token[] argVec =
args.toArray(new CommandLine.Token[args.size()]);
CommandLine cl = new CommandLine(commandToken, argVec, null);
commands.add(new CommandDescriptor(cl, from, to, pipeTo));
}
if (pipeTo && !completing) {
throw new ShellSyntaxException("no command after '|'");
}
if (completing) {
if (pipeTo || commands.isEmpty()) {
return new CommandLine("", null);
} else {
CommandLine res = commands.get(commands.size() - 1).commandLine;
res.setArgumentAnticipated(wspAfter);
return res;
}
} else {
return null;
}
}
private CommandLine.Token parseFileName(Tokenizer tokenizer, String special)
throws ShellSyntaxException {
if (!tokenizer.hasNext()) {
return null;
}
CommandLine.Token token = tokenizer.next();
if (token.tokenType == SPECIAL) {
throw new ShellSyntaxException("misplaced '" + token + "'");
}
if (token.text.isEmpty()) {
throw new ShellSyntaxException("empty '" + special + "' file name");
}
return token;
}
private int runCommand(CommandShell shell, CommandDescriptor desc)
throws ShellException {
CommandIO in = CommandLine.DEFAULT_STDIN;
CommandIO out = CommandLine.DEFAULT_STDOUT;
CommandIO err = CommandLine.DEFAULT_STDERR;
try {
try {
if (desc.fromFileName != null) {
in = new CommandInput(new FileInputStream(desc.fromFileName.text));
}
} catch (IOException ex) {
throw new ShellInvocationException("cannot open '" +
desc.fromFileName.text + "': " + ex.getMessage());
}
try {
if (desc.toFileName != null) {
out = new CommandOutput(new FileOutputStream(desc.toFileName.text));
}
} catch (IOException ex) {
throw new ShellInvocationException("cannot open '" +
desc.toFileName.text + "': " + ex.getMessage());
}
desc.commandLine.setStreams(new CommandIO[] {in, out, err, CommandLine.DEFAULT_STDERR});
return shell.invoke(desc.commandLine, null, null);
} finally {
try {
if (desc.fromFileName != null) {
in.close();
}
} catch (IOException ex) {
// squash
}
try {
if (desc.toFileName != null && out != null) {
out.close();
}
} catch (IOException ex) {
// squash
}
}
}
private synchronized int runPipeline(CommandShell shell,
List<CommandDescriptor> descs) throws ShellException {
int nosStages = descs.size();
try {
// Create all the threads for the pipeline, wiring up their input
// and output streams.
int stageNo = 0;
PipedOutputStream pipeOut = null;
for (CommandDescriptor desc : descs) {
CommandIO in = CommandLine.DEFAULT_STDIN;
CommandIO out = CommandLine.DEFAULT_STDOUT;
CommandIO err = CommandLine.DEFAULT_STDERR;
desc.openedStreams = new ArrayList<CommandIO>(2);
try {
// redirect from
if (desc.fromFileName != null) {
in = new CommandInput(new FileInputStream(desc.fromFileName.text));
desc.openedStreams.add(in);
}
} catch (IOException ex) {
throw new ShellInvocationException("cannot open '" +
desc.fromFileName.text + "': " + ex.getMessage());
}
try {
// redirect to
if (desc.toFileName != null) {
out = new CommandOutput(new FileOutputStream(desc.toFileName.text));
desc.openedStreams.add(out);
}
} catch (IOException ex) {
throw new ShellInvocationException("cannot open '" +
desc.toFileName + "': " + ex.getMessage());
}
if (stageNo > 0) {
// pipe from
if (pipeOut != null) {
// the previous stage is sending stdout to the pipe
if (in == CommandLine.DEFAULT_STDIN) {
// this stage is going to read from the pipe
PipedInputStream pipeIn = new PipedInputStream();
try {
pipeIn.connect(pipeOut);
} catch (IOException ex) {
throw new ShellInvocationException(
"Problem connecting pipe", ex);
}
in = new CommandInput(pipeIn);
desc.openedStreams.add(in);
} else {
// this stage has redirected stdin from a file ...
// so go back and replace the previous stage's
// pipeOut with devnull
CommandDescriptor prev = descs.get(stageNo - 1);
CommandIO[] prevIOs = prev.commandLine.getStreams();
try {
pipeOut.close();
} catch (IOException ex) {
// squash
}
prevIOs[Command.STD_OUT] = CommandLine.DEVNULL;
}
} else {
// the previous stage has explicitly redirected stdout
if (in == CommandLine.DEFAULT_STDIN) {
// this stage hasn't redirected stdin, so we need to
// give it a NullInputStream to suck on.
in = CommandLine.DEVNULL;
}
}
}
if (stageNo < nosStages - 1) {
// this stage is not the last one, and it hasn't redirected
// its stdout, so it will write to a pipe
if (out == CommandLine.DEFAULT_STDOUT) {
pipeOut = new PipedOutputStream();
out = new CommandOutput(new PrintStream(pipeOut));
desc.openedStreams.add(out);
}
}
desc.commandLine.setStreams(new CommandIO[] {in, out, err, CommandLine.DEFAULT_STDERR});
try {
desc.thread = shell.invokeAsynchronous(desc.commandLine);
} catch (UnsupportedOperationException ex) {
throw new ShellInvocationException(
"The current invoker does not support pipelines", ex);
}
stageNo++;
}
currentDescriptors = descs;
threadsLeft = descs.size();
// Start all threads.
for (CommandDescriptor desc : descs) {
desc.thread.start(this);
}
// Wait until they have finished
while (threadsLeft > 0) {
try {
wait();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return -1;
}
}
return descs.get(nosStages - 1).thread.getReturnCode();
} finally {
// Close any remaining streams.
for (CommandDescriptor desc : descs) {
if (desc.openedStreams != null) {
for (CommandIO stream : desc.openedStreams) {
try {
stream.close();
} catch (IOException ex) {
// squash
}
}
}
}
// TODO deal with any left over threads
}
}
private List<CommandDescriptor> currentDescriptors;
private int threadsLeft;
/**
* Callback to deal with a thread that has exited.
*
* @param thread
*/
public synchronized void notifyThreadExited(CommandThread thread) {
// If the thread owned any input or output streams, they need to
// be closed. In particular, this will cause the next downstream
// command in a pipeline to see an "end of file".
for (CommandDescriptor desc : currentDescriptors) {
if (thread == desc.thread) {
if (desc.openedStreams == null) {
throw new ShellFailureException("bad thread exit callback");
}
for (CommandIO stream : desc.openedStreams) {
try {
stream.close();
} catch (IOException ex) {
// squash
}
}
desc.openedStreams = null;
threadsLeft--;
notify();
break;
}
}
}
private static class CommandDescriptor {
public final CommandLine commandLine;
public final CommandLine.Token fromFileName;
public final CommandLine.Token toFileName;
public final boolean pipeTo;
public List<CommandIO> openedStreams;
public CommandThread thread;
public CommandDescriptor(CommandLine commandLine,
CommandLine.Token fromFileName, CommandLine.Token toFileName,
boolean pipeTo) {
super();
this.commandLine = commandLine;
this.fromFileName = fromFileName;
this.toFileName = toFileName;
this.pipeTo = pipeTo;
}
}
}