package org.dcache.util.cli;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import com.google.common.io.LineProcessor;
import jline.console.ConsoleReader;
import org.fusesource.jansi.Ansi;
import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.Objects;
import java.util.concurrent.Callable;
import dmg.util.CommandException;
import dmg.util.CommandExitException;
import dmg.util.CommandPanicException;
import dmg.util.CommandSyntaxException;
import dmg.util.CommandThrowableException;
import dmg.util.command.Command;
import dmg.util.command.HelpFormat;
import org.dcache.util.Args;
import static com.google.common.base.Strings.isNullOrEmpty;
import static org.fusesource.jansi.Ansi.Color.RED;
/**
* A simple framework for providing a CLI Shell. A basic application has as
* main method like:
* {@code
* public static void main(String[] arguments) throws Throwable
* {
* try (BasicShell shell = new BasicShell()) {
* shell.start(new Args(arguments));
* }
* }
* }
*/
public abstract class ShellApplication implements Closeable
{
protected final ConsoleReader console = new ConsoleReader() {
@Override
public void print(CharSequence s) throws IOException
{
/* See https://github.com/jline/jline2/issues/205 */
getOutput().append(s);
}
};
private final CommandInterpreter commandInterpreter;
private final boolean isAnsiSupported;
private final boolean hasConsole;
public ShellApplication() throws Exception
{
commandInterpreter = new CommandInterpreter();
commandInterpreter.addCommandScanner(new AnnotatedCommandScanner());
commandInterpreter.addCommandListener(commandInterpreter.new HelpCommands());
commandInterpreter.addCommandListener(this);
hasConsole = System.console() != null;
isAnsiSupported = console.getTerminal().isAnsiSupported() && hasConsole;
}
/**
* Start processing the command(s), based on the supplied arguments.
*/
protected void start(Args args) throws Throwable
{
if (args.hasOption("h")) {
System.out.println("Usage: " + getCommandName() + " [-e] [-f=<file>]|[-]|[COMMAND]");
System.out.println();
System.out.println("Use '" + getCommandName() + " help' for an overview of available commands.");
System.exit(0);
}
Ansi.setEnabled(isAnsiSupported);
if (args.hasOption("f")) {
try (InputStream in = new FileInputStream(args.getOption("f"))) {
execute(new BufferedInputStream(in), System.out, args.hasOption("e"));
}
} else if (args.argc() == 1 && args.argv(0).equals("-")) {
execute(System.in, System.out, args.hasOption("e"));
} else if (args.argc() > 0) {
execute(args);
} else if (!hasConsole) {
execute(System.in, System.out, args.hasOption("e"));
} else {
console();
}
}
/** Provide the command name, as typed in by the user. */
protected abstract String getCommandName();
/**
* Execute multiple commands where each command is read as a line from
* the supplied InputStream and the commands' output is sent to the supplied
* PrintStream, optionally prefixed by the command.
*/
public void execute(InputStream in, final PrintStream out, final boolean echo) throws IOException
{
CharStreams.readLines(
new InputStreamReader(in, Charsets.US_ASCII),
new LineProcessor<Object>()
{
@Override
public boolean processLine(String line) throws IOException
{
try {
if (echo) {
out.println(line);
}
Args args = new Args(line);
if (args.argc() == 0) {
return true;
}
String s = Objects.toString(commandInterpreter.command(args), null);
if (!isNullOrEmpty(s)) {
out.println(s);
}
return true;
} catch (CommandException e) {
throw new IOException(e);
}
}
@Override
public Object getResult()
{
return null;
}
});
}
/**
* Executes a single command with the output being printed to the console.
*/
public void execute(Args args) throws Throwable
{
if (args.argc() == 0) {
return;
}
String out;
try {
if (isAnsiSupported && args.argc() > 0) {
if (args.argv(0).equals("help")) {
args.shift();
args = new Args("help -format=" + HelpFormat.ANSI + " " + args.toString());
}
}
try {
out = Objects.toString(commandInterpreter.command(args), null);
} catch (CommandThrowableException e) {
throw e.getCause();
}
} catch (CommandSyntaxException e) {
Ansi sb = Ansi.ansi();
sb.fg(RED).a("Syntax error: " + e.getMessage() + "\n").reset();
String help = e.getHelpText();
if (help != null) {
sb.a(help);
}
out = sb.toString();
} catch (CommandExitException e) {
throw e;
} catch (CommandPanicException e) {
Ansi sb = Ansi.ansi();
sb.fg(RED).a("Bug detected! ").reset().a("Please email the following details to <support@dcache.org>:\n");
Throwable t = e.getCause() == null ? e : e.getCause();
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw));
out = sb.a(sw.toString()).toString();
} catch (Exception e) {
out = Ansi.ansi().fg(RED).a(e.getMessage()).reset().toString();
}
if (!isNullOrEmpty(out)) {
console.print(out);
if (out.charAt(out.length() - 1) != '\n') {
console.println();
}
}
console.flush();
}
/**
* Start an interactive session. The user is supplied a prompt and
* their input is executed as a command. This repeats until they indicate
* that they wish to exit the session.
*/
public void console() throws Throwable
{
onInteractiveStart();
try {
while (true) {
String prompt = Ansi.ansi().bold().a(getPrompt()).boldOff().toString();
String str = console.readLine(prompt);
if (str == null) {
console.println();
break;
}
execute(new Args(str));
}
} catch (CommandExitException ignored) {
}
}
/**
* Method called exactly once when starting an interactive session.
*/
protected void onInteractiveStart() throws IOException
{
console.println("Type 'help' for help on commands.");
console.println("Type 'exit' or Ctrl+D to exit.");
}
/**
* The prompt that will be supplied to the user. It is recommended that
* the prompt end with a space. The returned text should not be wrapped in
* ANSI escape sequences.
*/
protected String getPrompt()
{
return "# ";
}
/**
* This method allows for a clean shutdown on exit.
*/
@Override
public void close() throws IOException
{
// not needed for the abstract case.
}
@Command(name = "exit", hint = "exit the shell")
public class ExitComamnd implements Callable<Serializable>
{
@Override
public Serializable call() throws CommandExitException
{
throw new CommandExitException();
}
}
}