org.nanocom.console.Application.java Source code

Java tutorial

Introduction

Here is the source code for org.nanocom.console.Application.java

Source

/*
 * This file is part of the Console package.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

package org.nanocom.console;

import java.util.*;
import java.util.Map.Entry;
import org.apache.commons.lang3.ArrayUtils;
import static org.apache.commons.lang3.StringUtils.*;
import static org.apache.commons.lang3.SystemUtils.*;
import org.nanocom.console.command.Command;
import org.nanocom.console.command.HelpCommand;
import org.nanocom.console.command.ListCommand;
import org.nanocom.console.helper.DialogHelper;
import org.nanocom.console.helper.FormatterHelper;
import org.nanocom.console.helper.HelperSet;
import org.nanocom.console.input.*;
import org.nanocom.console.output.ConsoleOutput;
import org.nanocom.console.output.ConsoleOutputInterface;
import org.nanocom.console.output.OutputInterface;
import org.nanocom.console.output.OutputInterface.VerbosityLevel;

/**
 * An Application is the container for a collection of commands.
 *
 * It is the main entry point of a Console application.
 *
 * Usage:
 *
 *     Application app = new Application("myapp", "1.0 (stable)");
 *     app.add(new SimpleCommand());
 *     app.run();
 *
 * @author Arnaud Kleinpeter <arnaud.kleinpeter at gmail dot com>
 */
public class Application {

    private Map<String, Command> commands;
    private boolean wantHelps = false;
    private Command runningCommand;
    private String name;
    private String version;
    private boolean catchExceptions;
    private boolean autoExit;
    private InputDefinition definition;
    private HelperSet helperSet;

    /**
     * Constructor.
     *
     * @param name    The name of the application
     * @param version The version of the application
     */
    public Application(String name, String version) {
        init(name, version);
    }

    /**
     * Constructor.
     *
     * @param name The name of the application
     */
    public Application(String name) {
        init(name, "UNKNOWN");
    }

    /**
     * Constructor.
     */
    public Application() {
        init("UNKNOWN", "UNKNOWN");
    }

    private void init(String name, String version) {
        this.name = name;
        this.version = version;
        catchExceptions = true;
        autoExit = true;
        commands = new LinkedHashMap<String, Command>();
        helperSet = getDefaultHelperSet();
        definition = getDefaultInputDefinition();

        for (Command command : getDefaultCommands()) {
            add(command);
        }
    }

    /**
     * Runs the current application.
     *
     * @return 0 if everything went fine, or an error code
     *
     * @throws RuntimeException When doRun returns a RuntimeException
     */
    public int run() throws RuntimeException {
        return run(null);
    }

    /**
     * Runs the current application.
     *
     * @param input An Input instance
     *
     * @return 0 if everything went fine, or an error code
     *
     * @throws RuntimeException When doRun returns a RuntimeException
     */
    public int run(InputInterface input) throws RuntimeException {
        return run(input, null);
    }

    /**
     * Runs the current application.
     *
     * @param input  An Input instance
     * @param output An Output instance
     *
     * @return 0 if everything went fine, or an error code
     *
     * @throws RuntimeException When doRun returns a RuntimeException
     */
    public int run(InputInterface input, OutputInterface output) throws RuntimeException {
        if (null == input) {
            input = new ArgsInput(new String[0]);
        }

        if (null == output) {
            output = new ConsoleOutput();
        }

        int statusCode;

        try {
            statusCode = doRun(input, output);
        } catch (RuntimeException e) {
            if (!catchExceptions) {
                throw e;
            }

            if (output instanceof ConsoleOutputInterface) {
                renderException(e, ((ConsoleOutputInterface) output).getErrorOutput());
            } else {
                renderException(e, output);
            }

            statusCode = e.hashCode();
        }

        if (autoExit) {
            if (statusCode > 255) {
                statusCode = 255;
            }

            System.exit(statusCode);
        }

        return statusCode;
    }

    /**
     * Runs the current application.
     *
     * @param input  An Input instance
     * @param output An Output instance
     *
     * @return 0 if everything went fine, or an error code
     */
    public int doRun(InputInterface input, OutputInterface output) throws RuntimeException {
        String commandName = getCommandName(input);

        if (true == input.hasParameterOption(Arrays.asList("--ansi"))) {
            output.setDecorated(true);
        } else if (true == input.hasParameterOption(Arrays.asList("--no-ansi"))) {
            output.setDecorated(false);
        }

        if (true == input.hasParameterOption(Arrays.asList("--help", "-h"))) {
            if (null == commandName) {
                commandName = "help";
                Map<String, String> arrayInputParams = new HashMap<String, String>();
                arrayInputParams.put("command", "help");
                input = new ArrayInput(arrayInputParams);
            } else {
                wantHelps = true;
            }
        }

        if (true == input.hasParameterOption(Arrays.asList("--no-interaction", "-n"))) {
            input.setInteractive(false);
        }

        if (null != System.console() && getHelperSet().has("dialog")) {
            // TODO
            // InputStream inputStream = ((DialogHelper) getHelperSet().get("dialog")).getInputStream();
            /*if (!posix_isatty(inputStream)) {
            input.setInteractive(false);
            }*/
        }

        if (true == input.hasParameterOption(Arrays.asList("--quiet", "-q"))) {
            output.setVerbosity(VerbosityLevel.QUIET);
        } else if (true == input.hasParameterOption(Arrays.asList("--verbose", "-v"))) {
            output.setVerbosity(VerbosityLevel.VERBOSE);
        }

        if (true == input.hasParameterOption(Arrays.asList("--version", "-V"))) {
            output.writeln(getLongVersion());
            return 0;
        }

        if (null == commandName) {
            commandName = "list";
            Map<String, String> arrayInputParams = new HashMap<String, String>();
            arrayInputParams.put("command", "list");
            input = new ArrayInput(arrayInputParams);
        }

        // The command name MUST be the first element of the input
        Command command = find(commandName);

        runningCommand = command;
        int statusCode = command.run(input, output);
        runningCommand = null;

        return statusCode;
    }

    /**
     * Set a helper set to be used with the command.
     *
     * @param helperSet The helper set
     */
    public void setHelperSet(HelperSet helperSet) {
        this.helperSet = helperSet;
    }

    /**
     * Get the helper set associated with the command.
     *
     * @return The HelperSet instance associated with this command
     */
    public HelperSet getHelperSet() {
        return helperSet;
    }

    /**
     * Gets the InputDefinition related to this Application.
     *
     * @return The InputDefinition instance
     */
    public InputDefinition getDefinition() {
        return definition;
    }

    /**
     * Gets the help message.
     *
     * @return A help message.
     */
    public String getHelp() {
        List<String> messages = new ArrayList<String>();
        messages.add(getLongVersion());
        messages.add(EMPTY);
        messages.add("<comment>Usage:</comment>");
        messages.add("  [options] command [arguments]" + "\n");
        messages.add("<comment>Options:</comment>");

        for (InputOption option : getDefinition().getOptions().values()) {
            messages.add(String.format("  %-29s %s %s", String.format("<info>--%s</info>", option.getName()),
                    null != option.getShortcut() ? String.format("<info>-%s</info>", option.getShortcut()) : "  ",
                    option.getDescription()));
        }

        return join(messages, LINE_SEPARATOR);
    }

    /**
     * Sets whether to catch exceptions or not during commands execution.
     *
     * @param catchExceptions Whether to catch exceptions or not during commands execution
     */
    public void setCatchExceptions(boolean catchExceptions) {
        this.catchExceptions = catchExceptions;
    }

    /**
     * Sets whether to automatically exit after a command execution or not.
     *
     * @param autoExit Whether to automatically exit after a command execution or not
     */
    public void setAutoExit(boolean autoExit) {
        this.autoExit = autoExit;
    }

    /**
     * Gets the name of the application.
     *
     * @return The application name
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the application name.
     *
     * @param name The application name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Gets the application version.
     *
     * @return The application version
     */
    public String getVersion() {
        return version;
    }

    /**
     * Sets the application version.
     *
     * @param version The application version
     */
    public void setVersion(String version) {
        this.version = version;
    }

    /**
     * Returns the long version of the application.
     *
     * @return The long application version
     */
    public String getLongVersion() {
        if (false == "UNKNOWN".equals(getName()) && false == "UNKNOWN".equals(getVersion())) {
            return String.format("<info>%s</info> version <comment>%s</comment>", getName(), getVersion());
        }

        return "<info>Console Tool</info>";
    }

    /**
     * Registers a new command.
     *
     * @param name The command name
     *
     * @return The newly created command
     */
    public Command register(String name) {
        return add(new Command(name));
    }

    /**
     * Adds an array of command objects.
     *
     * @param commands An array of commands
     */
    public void addCommands(Collection<Command> commands) {
        for (Command command : commands) {
            add(command);
        }
    }

    /**
     * Adds a command object.
     *
     * If a command with the same name already exists, it will be overridden.
     *
     * @param command A Command object
     *
     * @return The registered command
     */
    public Command add(Command command) {
        command.setApplication(this);

        if (!command.isEnabled()) {
            command.setApplication(null);

            return null;
        }

        commands.put(command.getName(), command);

        for (String alias : command.getAliases()) {
            commands.put(alias, command);
        }

        return command;
    }

    /**
     * Returns a registered command by name or alias.
     *
     * @param name The command name or alias
     *
     * @return A Command object
     *
     * @throws IllegalArgumentException When command name given does not exist
     */
    public Command get(String name) throws IllegalArgumentException {
        if (!commands.containsKey(name)) {
            throw new IllegalArgumentException(String.format("The command \"%s\" does not exist.", name));
        }

        Command command = commands.get(name);

        if (wantHelps) {
            wantHelps = false;

            HelpCommand helpCommand = (HelpCommand) get("help");
            helpCommand.setCommand(command);

            return helpCommand;
        }

        return command;
    }

    /**
     * Returns true if the command exists, false otherwise.
     *
     * @param name The command name or alias
     *
     * @return True if the command exists, false otherwise
     */
    public boolean has(String name) {
        return commands.containsKey(name);
    }

    /**
     * Returns an array of all unique namespaces used by currently registered commands.
     *
     * It does not returns the global namespace which always exists.
     *
     * @return A set of namespaces
     */
    public Set<String> getNamespaces() {
        Set<String> namespaces = new LinkedHashSet<String>();
        for (Command command : commands.values()) {
            namespaces.add(extractNamespace(command.getName()));

            for (String alias : command.getAliases()) {
                namespaces.add(extractNamespace(alias));
            }
        }

        namespaces.remove(EMPTY); // deletes global namespace
        return namespaces;
    }

    /**
     * Finds a registered namespace by a name or an abbreviation.
     *
     * @param namespace A namespace or abbreviation to search for
     *
     * @return A registered namespace
     *
     * @throws IllegalArgumentException When namespace is incorrect or ambiguous
     */
    public String findNamespace(String namespace) {
        Map<String, List<String>> allNamespaces = new LinkedHashMap<String, List<String>>();
        for (String n : getNamespaces()) {
            allNamespaces.put(n, new ArrayList<String>(Arrays.asList(n.split(":"))));
        }

        List<String> found = new ArrayList<String>();
        int i = 0;
        for (String part : namespace.split(":")) {
            Set<String> filteredNamespaces = new LinkedHashSet<String>();

            for (List<String> subNamespaces : allNamespaces.values()) {
                if (i < subNamespaces.size()) {
                    filteredNamespaces.add(subNamespaces.get(i));
                }
            }

            Map<String, List<String>> abbrevs = getAbbreviations(new ArrayList<String>(filteredNamespaces));

            if (!abbrevs.containsKey(part)) {
                StringBuilder message = new StringBuilder();
                message.append(String.format("There are no commands defined in the \"%s\" namespace.", namespace));

                if (1 <= i) {
                    part = String.format("%s:%s", join(found, ':'), part);
                }

                Set<String> alternatives = findAlternativeNamespace(part, abbrevs);

                if (!alternatives.isEmpty()) {
                    if (1 == alternatives.size()) {
                        message.append("\n\nDid you mean this?\n    ");
                    } else {
                        message.append("\n\nDid you mean one of these?\n    ");
                    }

                    message.append(join(alternatives, "\n    "));
                }

                throw new IllegalArgumentException(message.toString());
            }

            if (abbrevs.get(part).size() > 1) {
                String abbreviationSuggestions = getAbbreviationSuggestions(abbrevs.get(part));

                throw new IllegalArgumentException(String.format("The namespace \"%s\" is ambiguous (%s).",
                        namespace, abbreviationSuggestions));
            }

            found.add(abbrevs.get(part).get(0));
            i++;
        }

        return join(found, ':');
    }

    /**
     * Finds a command by name or alias.
     *
     * Contrary to get, this command tries to find the best
     * match if you give it an abbreviation of a name or alias.
     *
     * @param name A command name or a command alias
     *
     * @return A Command instance
     *
     * @throws IllegalArgumentException When command name is incorrect or ambiguous
     */
    public Command find(String name) throws IllegalArgumentException {
        // Namespace
        String namespace = EMPTY;
        String searchName = name;
        int pos = name.indexOf(':');
        if (pos >= 0) {
            namespace = findNamespace(name.substring(0, pos));
            searchName = namespace + name.substring(pos);
        }

        // Name
        Set<String> searchCommands = new LinkedHashSet<String>();
        for (Command command : commands.values()) {
            if (extractNamespace(command.getName()).equals(namespace)) {
                searchCommands.add(command.getName());
            }
        }

        Map<String, List<String>> abbrevs = getAbbreviations(searchCommands);
        if (abbrevs.containsKey(searchName) && 1 == abbrevs.get(searchName).size()) {
            return get(abbrevs.get(searchName).get(0));
        }

        if (abbrevs.containsKey(searchName) && 1 < abbrevs.get(searchName).size()) {
            String suggestions = getAbbreviationSuggestions(abbrevs.get(searchName));

            throw new IllegalArgumentException(
                    String.format("Command \"%s\" is ambiguous (%s).", name, suggestions));
        }

        // Aliases
        Set<String> aliases = new LinkedHashSet<String>();
        for (Command command : commands.values()) {
            for (String alias : command.getAliases()) {
                if (extractNamespace(alias).equals(namespace)) {
                    aliases.add(alias);
                }
            }
        }

        Map<String, List<String>> aliasesMap = getAbbreviations(aliases);
        if (!aliasesMap.containsKey(searchName)) {
            StringBuilder message = new StringBuilder();
            message.append(String.format("Command \"%s\" is not defined.", name));

            Set<String> alternatives = findAlternativeCommands(searchName, abbrevs);
            if (!alternatives.isEmpty()) {
                if (1 == alternatives.size()) {
                    message.append("\n\nDid you mean this?\n    ");
                } else {
                    message.append("\n\nDid you mean one of these?\n    ");
                }

                message.append(join(alternatives, "\n    "));
            }

            throw new IllegalArgumentException(message.toString());
        }

        if (1 < aliasesMap.get(searchName).size()) {
            throw new IllegalArgumentException(String.format("Command \"%s\" is ambiguous (%s).", name,
                    getAbbreviationSuggestions(aliasesMap.get(searchName))));
        }

        return get(aliasesMap.get(searchName).get(0));
    }

    /**
     * Gets the commands.
     *
     * The map keys are the full names and the values the command instances.
     *
     * @return A map of Command instances
     */
    public Map<String, Command> all() {
        return all(null);
    }

    /**
     * Gets the commands registered in the given namespace.
     *
     * The map keys are the full names and the values the command instances.
     *
     * @param namespace A namespace name
     *
     * @return A map of Command instances
     */
    public Map<String, Command> all(String namespace) {
        if (null == namespace) {
            return new HashMap<String, Command>(commands);
        }

        Map<String, Command> namespacedCommands = new HashMap<String, Command>();
        for (Command command : commands.values()) {
            if (namespace
                    .equals(extractNamespace(command.getName(), Integer.valueOf(namespace.split(":").length)))) {
                namespacedCommands.put(command.getName(), command);
            }
        }

        return namespacedCommands;
    }

    /**
     * Returns a map of possible abbreviations given a set of names.
     *
     * @param names A collection of names
     *
     * @return A map of abbreviations
     */
    public static Map<String, List<String>> getAbbreviations(Collection<String> names) {
        Map<String, List<String>> abbrevs = new LinkedHashMap<String, List<String>>();
        for (String name : names) {
            for (int len = name.length() - 1; len > 0; --len) {
                String abbrev = name.substring(0, len);
                if (!abbrevs.containsKey(abbrev)) {
                    abbrevs.put(abbrev, new ArrayList<String>(Arrays.asList(name)));
                } else {
                    abbrevs.get(abbrev).add(name);
                }
            }
        }

        // Non-abbreviations always get entered, even if they aren't unique
        for (String name : names) {
            abbrevs.put(name, new ArrayList<String>(Arrays.asList(name)));
        }

        return abbrevs;
    }

    /**
     * Returns a text representation of the Application.
     *
     * @param namespace An optional namespace name
     * @param raw       Whether to return raw command list
     *
     * @return A string representing the Application
     */
    public String asText(String namespace, boolean raw) {
        Map<String, Command> cmds = null != namespace ? all(findNamespace(namespace)) : commands;

        int width = 0;
        for (Command command : cmds.values()) {
            width = command.getName().length() > width ? command.getName().length() : width;
        }
        width += 2;

        if (raw) {
            List<String> messages = new ArrayList<String>();
            for (Map<String, Command> commandsMap : sortCommands(cmds).values()) {
                for (Command command : commandsMap.values()) {
                    String description = null != command.getDescription() ? command.getDescription() : EMPTY;
                    messages.add(
                            String.format("%-" + String.valueOf(width) + "s %s", command.getName(), description));
                }
            }

            return join(messages, LINE_SEPARATOR);
        }

        List<String> messages = new ArrayList<String>();
        messages.add(getHelp());
        messages.add(EMPTY);

        if (null != namespace) {
            messages.add(
                    String.format("<comment>Available commands for the \"%s\" namespace:</comment>", namespace));
        } else {
            messages.add("<comment>Available commands:</comment>");
        }

        // Add commands by namespace
        for (Entry<String, Map<String, Command>> commandsMap : sortCommands(cmds).entrySet()) {
            if (null == namespace && !"_global".equals(commandsMap.getKey())) {
                messages.add("<comment>" + commandsMap.getKey() + "</comment>");
            }

            for (Entry<String, Command> command : commandsMap.getValue().entrySet()) {
                String description = null != command.getValue().getDescription()
                        ? command.getValue().getDescription()
                        : EMPTY;
                messages.add(String.format("  <info>%-" + String.valueOf(width) + "s</info> %s", command.getKey(),
                        description));
            }
        }

        return join(messages, LINE_SEPARATOR);
    }

    /**
     * Returns a text representation of the Application.
     *
     * @param namespace An optional namespace name
     *
     * @return A string representing the Application
     */
    public String asText(String namespace) {
        return asText(namespace, false);
    }

    /**
     * Returns a text representation of the Application.
     *
     * @return A string representing the Application
     */
    public String asText() {
        return asText(null, false);
    }

    /**
     * Renders a caught exception.
     *
     * @param e      An exception instance
     * @param output An OutputInterface instance
     */
    public void renderException(Exception e, OutputInterface output) {
        Throwable t = e;
        do {
            String title = String.format("  [%s]  ", t.getClass().getSimpleName());
            int len = title.length();
            Integer width = getTerminalWidth();

            if (null == width) {
                width = Integer.MAX_VALUE;
            } else {
                width--;
            }

            List<String> lines = new ArrayList<String>();
            String[] splittedMessage = t.getMessage().split("\r?\n");

            for (String line : splittedMessage) {
                String[] lines2 = split(line, width - 4);
                for (String line2 : lines2) {
                    lines.add(String.format("  %s  ", line2));
                    len = Math.max(line2.length() + 4, len);
                }
            }

            List<String> messages = new ArrayList<String>();
            messages.add(repeat(" ", len));
            messages.add(title + repeat(" ", Math.max(0, len - title.length())));

            for (String line : lines) {
                messages.add(line + repeat(" ", len - line.length()));
            }

            messages.add(repeat(" ", len));

            output.writeln(EMPTY);
            output.writeln(EMPTY);

            for (String message : messages) {
                output.writeln(String.format("<error>%s</error>", message));
            }

            output.writeln(EMPTY);
            output.writeln(EMPTY);

            if (OutputInterface.VerbosityLevel.VERBOSE.equals(output.getVerbosity())) {
                output.writeln("<comment>Exception trace:</comment>");

                // exception related properties
                StackTraceElement[] trace = t.getStackTrace();
                StackTraceElement[] fullTrace = ArrayUtils
                        .addAll(new StackTraceElement[] { new StackTraceElement(EMPTY, trace[0].getMethodName(),
                                trace[0].getFileName(), trace[0].getLineNumber()) }, trace);

                int count = fullTrace.length;
                int i;
                for (i = 0; i < count; i++) {
                    String clazz = null != fullTrace[i].getClassName() ? fullTrace[i].getClassName() : EMPTY;
                    String function = fullTrace[i].getMethodName();
                    String file = null != fullTrace[i].getFileName() ? fullTrace[i].getFileName() : "n/a";
                    String line = String.valueOf(fullTrace[i].getLineNumber());

                    output.writeln(String.format(" %s%s() at <info>%s:%s</info>", clazz, function, file, line));
                }

                output.writeln(EMPTY);
                output.writeln(EMPTY);
            }
        } while (null != (t = t.getCause()));

        if (null != runningCommand) {
            output.writeln(
                    String.format("<info>%s</info>", String.format(runningCommand.getSynopsis(), getName())));
            output.writeln(EMPTY);
            output.writeln(EMPTY);
        }
    }

    /**
     * Tries to figure out the terminal width in which this application runs.
     *
     * @return The terminal width if found, null otherwise
     */
    protected Integer getTerminalWidth() {
        String ansicon = System.getenv("ANSICON");
        if (IS_OS_WINDOWS && null != ansicon && -1 < ansicon.indexOf('x')) {
            return Integer.valueOf(ansicon.substring(0, ansicon.indexOf('x')));
        }

        // TODO
        /*if (preg_match(\"{rows.(\d+);.columns.(\d+);}i\", getSttyColumns(), match)) {
        return match[1];
        }*/

        return null;
    }

    /**
     * Tries to figure out the terminal height in which this application runs.
     *
     * @return
     */
    protected Integer getTerminalHeight() {
        String ansicon = System.getenv("ANSICON");
        if (IS_OS_WINDOWS && null != ansicon) {
            return Integer.valueOf(ansicon.trim().replaceAll("^d+xd+ (d+x(d+))", "1"));
        }

        // TODO
        /*if (preg_match("{rows.(\d+);.columns.(\d+);}i", getSttyColumns(), match)) {
        return match[2];
        }*/

        return null;
    }

    /**
     * Gets the name of the command based on input.
     *
     * @param input The input interface
     *
     * @return The command name
     */
    protected String getCommandName(InputInterface input) {
        return input.getFirstArgument();
    }

    /**
     * Gets the default input definition.
     *
     * @return An InputDefinition instance
     */
    protected InputDefinition getDefaultInputDefinition() {
        return new InputDefinition(new InputParameterInterface[] {
                new InputArgument("command", InputArgument.REQUIRED, "The command to execute"),
                new InputOption("--help", "-h", InputOption.VALUE_NONE, "Display this help message."),
                new InputOption("--quiet", "-q", InputOption.VALUE_NONE, "Do not output any message."),
                new InputOption("--verbose", "-v", InputOption.VALUE_NONE, "Increase verbosity of messages."),
                new InputOption("--version", "-V", InputOption.VALUE_NONE, "Display this application version."),
                new InputOption("--ansi", EMPTY, InputOption.VALUE_NONE, "Force ANSI output."),
                new InputOption("--no-ansi", EMPTY, InputOption.VALUE_NONE, "Disable ANSI output."),
                new InputOption("--no-interaction", "-n", InputOption.VALUE_NONE,
                        "Do not ask any interactive question.") });
    }

    /**
     * Gets the default commands that should always be available.
     *
     * @return An array of default Command instances
     */
    protected List<Command> getDefaultCommands() {
        return Arrays.<Command>asList(new HelpCommand(), new ListCommand());
    }

    /**
     * Gets the default helper set with the helpers that should always be available.
     *
     * @return A HelperSet instance
     */
    protected HelperSet getDefaultHelperSet() {
        return new HelperSet(Arrays.asList(new FormatterHelper(), new DialogHelper()));
    }

    /**
     * Runs and parses stty -a if it's available, suppressing any error output.
     *
     * @return
     *
     * TODO
     */
    /*private String getSttyColumns() {
    descriptorspec = array(1 => array("pipe", "w"), 2 => array("pipe", "w"));
    process = proc_open("stty -a | grep columns", descriptorspec, pipes, null, null, array("suppress_errors" => true));
    if (is_resource(process)) {
        info = stream_get_contents(pipes[1]);
        fclose(pipes[1]);
        fclose(pipes[2]);
        proc_close(process);
        
        return info;
    }
    return ""; // Not implemented
    }*/

    /**
     * Sorts commands in alphabetical order.
     *
     * @param commands An associative array of commands to sort
     *
     * @return A sorted array of commands
     */
    private Map<String, Map<String, Command>> sortCommands(Map<String, Command> commands) {
        Map<String, Map<String, Command>> namespacedCommands = new TreeMap<String, Map<String, Command>>();
        for (Entry<String, Command> command : commands.entrySet()) {
            String key = extractNamespace(command.getKey(), 1);
            if (key.isEmpty()) {
                key = "_global";
            }

            if (null == namespacedCommands.get(key)) {
                namespacedCommands.put(key, new TreeMap<String, Command>());
            }

            namespacedCommands.get(key).put(command.getKey(), command.getValue());
        }

        return namespacedCommands;
    }

    /**
     * Returns abbreviated suggestions in string format.
     *
     * @param abbrevs Abbreviated suggestions to convert
     *
     * @return A formatted string of abbreviated suggestions
     */
    private String getAbbreviationSuggestions(List<String> abbrevs) {
        return String.format("%s, %s%s", abbrevs.get(0), abbrevs.get(1),
                abbrevs.size() > 2 ? String.format(" and %d more", abbrevs.size() - 2) : EMPTY);
    }

    /**
     * Returns the namespace part of the command name.
     *
     * @param name  The full name of the command
     * @param limit The maximum number of parts of the namespace
     *
     * @return The namespace of the command
     */
    private String extractNamespace(String name, Integer limit) {
        String[] parts = name.split(":");
        parts = ArrayUtils.<String>subarray(parts, 0, parts.length - 1);

        return 0 == parts.length ? EMPTY
                : join(null == limit ? parts : ArrayUtils.<String>subarray(parts, 0, limit), ':');
    }

    private String extractNamespace(String name) {
        return extractNamespace(name, null);
    }

    /**
     * Finds alternative commands of name.
     *
     * @param name    The full name of the command
     * @param abbrevs The abbreviations
     *
     * @return A sorted array of similar commands
     */
    private Set<String> findAlternativeCommands(String name, Map<String, List<String>> abbrevs) {
        return findAlternatives(name, commands.keySet(), abbrevs);
    }

    /**
     * Finds alternative namespace of name.
     *
     * @param name    The full name of the namespace
     * @param abbrevs The abbreviations
     *
     * @return A sorted set of similar namespace
     */
    private Set<String> findAlternativeNamespace(String name, Map<String, List<String>> abbrevs) {
        return findAlternatives(name, getNamespaces(), abbrevs);
    }

    /**
     * Finds alternative of name among collection,
     * if nothing is found in collection, try in abbrevs.
     *
     * @param name       The string
     * @param collection The collection
     * @param abbrevs    The abbreviations
     *
     * @return A sorted set of similar string
     */
    private Set<String> findAlternatives(String name, Collection<String> collection,
            Map<String, List<String>> abbrevs) {
        Map<Integer, List<String>> alternatives = new TreeMap<Integer, List<String>>();
        for (String item : collection) {
            int lev = getLevenshteinDistance(name, item);
            if (lev <= name.length() / 3 || item.contains(name)) {
                if (!alternatives.containsKey(lev)) {
                    alternatives.put(lev, new ArrayList<String>());
                }
                alternatives.get(lev).add(item);
            }
        }

        if (alternatives.isEmpty()) {
            for (Entry<String, List<String>> values : abbrevs.entrySet()) {
                int lev = getLevenshteinDistance(name, values.getKey());
                if (lev <= name.length() / 3 || values.getKey().indexOf(name) > -1) {
                    for (String value : values.getValue()) {
                        if (!alternatives.containsKey(lev)) {
                            alternatives.put(lev, new ArrayList<String>());
                        }
                        alternatives.get(lev).add(value);
                    }
                }
            }
        }

        Set<String> result = new LinkedHashSet<String>(alternatives.size());
        for (List<String> alt : alternatives.values()) {
            result.addAll(alt);
        }

        return result;
    }

    private String[] split(String string, int width) {
        int length = length(string);
        List<String> strings = new ArrayList<String>();
        int offset = 0;
        while (offset < length - 1) {
            strings.add(string.substring(offset, Math.min(offset + width, length)));
            offset += width;
        }

        String[] result = new String[strings.size()];
        int i = 0;
        for (String value : strings) {
            result[i++] = value;
        }

        return result;
    }
}