All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jline.console.impl.SystemRegistryImpl Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2002-2023, the original author(s).
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.jline.console.impl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jline.builtins.Completers.FilesCompleter;
import org.jline.builtins.Completers.OptDesc;
import org.jline.builtins.Completers.OptionCompleter;
import org.jline.builtins.ConfigurationPath;
import org.jline.builtins.Options;
import org.jline.builtins.Options.HelpException;
import org.jline.builtins.Styles;
import org.jline.console.*;
import org.jline.console.ConsoleEngine.ExecutionResult;
import org.jline.reader.*;
import org.jline.reader.Parser.ParseContext;
import org.jline.reader.impl.completer.AggregateCompleter;
import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.reader.impl.completer.NullCompleter;
import org.jline.reader.impl.completer.StringsCompleter;
import org.jline.reader.impl.completer.SystemCompleter;
import org.jline.terminal.Attributes;
import org.jline.terminal.Attributes.InputFlag;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.*;

import static org.jline.keymap.KeyMap.ctrl;

/**
 * Aggregate command registries.
 *
 * @author Matti Rinta-Nikkola
 */
public class SystemRegistryImpl implements SystemRegistry {

    public enum Pipe {
        FLIP,
        NAMED,
        AND,
        OR
    }

    private static final Class[] BUILTIN_REGISTRIES = {Builtins.class, ConsoleEngineImpl.class};
    private CommandRegistry[] commandRegistries;
    private Integer consoleId;
    protected final Parser parser;
    protected final ConfigurationPath configPath;
    protected final Supplier workDir;
    private final Map subcommands = new HashMap<>();
    private final Map pipeName = new HashMap<>();
    private final Map commandExecute = new HashMap<>();
    private final Map> commandInfos = new HashMap<>();
    private Exception exception;
    private final CommandOutputStream outputStream;
    private ScriptStore scriptStore = new ScriptStore();
    private NamesAndValues names = new NamesAndValues();
    private final SystemCompleter customSystemCompleter = new SystemCompleter();
    private final AggregateCompleter customAggregateCompleter = new AggregateCompleter(new ArrayList<>());
    private boolean commandGroups = true;
    private Function scriptDescription;

    @SuppressWarnings("this-escape")
    public SystemRegistryImpl(Parser parser, Terminal terminal, Supplier workDir, ConfigurationPath configPath) {
        this.parser = parser;
        this.workDir = workDir;
        this.configPath = configPath;
        outputStream = new CommandOutputStream(terminal);
        pipeName.put(Pipe.FLIP, "|;");
        pipeName.put(Pipe.NAMED, "|");
        pipeName.put(Pipe.AND, "&&");
        pipeName.put(Pipe.OR, "||");
        commandExecute.put("exit", new CommandMethods(this::exit, this::exitCompleter));
        commandExecute.put("help", new CommandMethods(this::help, this::helpCompleter));
    }

    public void rename(Pipe pipe, String name) {
        if (name.matches("/w+") || pipeName.containsValue(name)) {
            throw new IllegalArgumentException();
        }
        pipeName.put(pipe, name);
    }

    @Override
    public Collection getPipeNames() {
        return pipeName.values();
    }

    @Override
    public void setCommandRegistries(CommandRegistry... commandRegistries) {
        this.commandRegistries = commandRegistries;
        for (int i = 0; i < commandRegistries.length; i++) {
            if (commandRegistries[i] instanceof ConsoleEngine) {
                if (consoleId != null) {
                    throw new IllegalArgumentException();
                } else {
                    this.consoleId = i;
                    ((ConsoleEngine) commandRegistries[i]).setSystemRegistry(this);
                    this.scriptStore = new ScriptStore((ConsoleEngine) commandRegistries[i]);
                    this.names = new NamesAndValues(configPath);
                }
            } else if (commandRegistries[i] instanceof SystemRegistry) {
                throw new IllegalArgumentException();
            }
        }
        SystemRegistry.add(this);
    }

    @Override
    public void initialize(File script) {
        if (consoleId != null) {
            try {
                consoleEngine().execute(script);
            } catch (Exception e) {
                trace(e);
            }
        }
    }

    @Override
    public Set commandNames() {
        Set out = new HashSet<>();
        for (CommandRegistry r : commandRegistries) {
            out.addAll(r.commandNames());
        }
        out.addAll(localCommandNames());
        return out;
    }

    private Set localCommandNames() {
        return commandExecute.keySet();
    }

    @Override
    public Map commandAliases() {
        Map out = new HashMap<>();
        for (CommandRegistry r : commandRegistries) {
            out.putAll(r.commandAliases());
        }
        return out;
    }

    @Override
    public Object consoleOption(String name) {
        return consoleOption(name, null);
    }

    @Override
    public  T consoleOption(String name, T defVal) {
        T out = defVal;
        if (consoleId != null) {
            out = consoleEngine().consoleOption(name, defVal);
        }
        return out;
    }

    @Override
    public void setConsoleOption(String name, Object value) {
        if (consoleId != null) {
            consoleEngine().setConsoleOption(name, value);
        }
    }

    /**
     * Register subcommand registry
     * @param command main command
     * @param subcommandRegistry subcommand registry
     */
    @Override
    public void register(String command, CommandRegistry subcommandRegistry) {
        subcommands.put(command, subcommandRegistry);
        commandExecute.put(command, new CommandMethods(this::subcommand, this::emptyCompleter));
    }

    private List localCommandInfo(String command) {
        try {
            CommandRegistry subCommand = subcommands.get(command);
            if (subCommand != null) {
                registryHelp(subCommand);
            } else {
                localExecute(command, new String[] {"--help"});
            }
        } catch (HelpException e) {
            exception = null;
            return JlineCommandRegistry.compileCommandInfo(e.getMessage());
        } catch (Exception e) {
            trace(e);
        }
        return new ArrayList<>();
    }

    @Override
    public List commandInfo(String command) {
        int id = registryId(command);
        List out = new ArrayList<>();
        if (id > -1) {
            if (!commandInfos.containsKey(command)) {
                commandInfos.put(command, commandRegistries[id].commandInfo(command));
            }
            out = commandInfos.get(command);
        } else if (scriptStore.hasScript(command) && consoleEngine() != null) {
            out = consoleEngine().commandInfo(command);
        } else if (isLocalCommand(command)) {
            out = localCommandInfo(command);
        }
        return out;
    }

    @Override
    public boolean hasCommand(String command) {
        return registryId(command) > -1 || isLocalCommand(command);
    }

    public void setGroupCommandsInHelp(boolean commandGroups) {
        this.commandGroups = commandGroups;
    }

    public SystemRegistryImpl groupCommandsInHelp(boolean commandGroups) {
        this.commandGroups = commandGroups;
        return this;
    }

    private boolean isLocalCommand(String command) {
        return commandExecute.containsKey(command);
    }

    @Override
    public boolean isCommandOrScript(ParsedLine line) {
        return isCommandOrScript(parser.getCommand(line.words().get(0)));
    }

    @Override
    public boolean isCommandOrScript(String command) {
        if (hasCommand(command)) {
            return true;
        }
        return scriptStore.hasScript(command);
    }

    public void addCompleter(Completer completer) {
        if (completer instanceof SystemCompleter) {
            SystemCompleter sc = (SystemCompleter) completer;
            if (sc.isCompiled()) {
                customAggregateCompleter.getCompleters().add(sc);
            } else {
                customSystemCompleter.add(sc);
            }
        } else {
            customAggregateCompleter.getCompleters().add(completer);
        }
    }

    @Override
    public SystemCompleter compileCompleters() {
        throw new IllegalStateException("Use method completer() to retrieve Completer!");
    }

    private SystemCompleter _compileCompleters() {
        SystemCompleter out = CommandRegistry.aggregateCompleters(commandRegistries);
        SystemCompleter local = new SystemCompleter();
        for (String command : commandExecute.keySet()) {
            CommandRegistry subCommand = subcommands.get(command);
            if (subCommand != null) {
                for (Map.Entry> entry :
                        subCommand.compileCompleters().getCompleters().entrySet()) {
                    for (Completer cc : entry.getValue()) {
                        if (!(cc instanceof ArgumentCompleter)) {
                            throw new IllegalArgumentException();
                        }
                        List cmps = ((ArgumentCompleter) cc).getCompleters();
                        cmps.add(0, NullCompleter.INSTANCE);
                        cmps.set(1, new StringsCompleter(entry.getKey()));
                        Completer last = cmps.get(cmps.size() - 1);
                        if (last instanceof OptionCompleter) {
                            ((OptionCompleter) last).setStartPos(cmps.size() - 1);
                            cmps.set(cmps.size() - 1, last);
                        }
                        local.add(command, new ArgumentCompleter(cmps));
                    }
                }
            } else {
                local.add(
                        command, commandExecute.get(command).compileCompleter().apply(command));
            }
        }
        local.add(customSystemCompleter);
        out.add(local);
        out.compile();
        return out;
    }

    @Override
    public Completer completer() {
        List completers = new ArrayList<>();
        completers.add(_compileCompleters());
        completers.add(customAggregateCompleter);
        if (consoleId != null) {
            completers.addAll(consoleEngine().scriptCompleters());
            completers.add(new PipelineCompleter(workDir, pipeName, names).doCompleter());
        }
        return new AggregateCompleter(completers);
    }

    private CmdDesc localCommandDescription(String command) {
        if (!isLocalCommand(command)) {
            throw new IllegalArgumentException();
        }
        try {
            localExecute(command, new String[] {"--help"});
        } catch (HelpException e) {
            exception = null;
            return JlineCommandRegistry.compileCommandDescription(e.getMessage());
        } catch (Exception e) {
            trace(e);
        }
        return null;
    }

    @Override
    public CmdDesc commandDescription(List args) {
        CmdDesc out = new CmdDesc(false);
        String command = args.get(0);
        int id = registryId(command);
        if (id > -1) {
            out = commandRegistries[id].commandDescription(args);
        } else if (scriptStore.hasScript(command) && consoleEngine() != null) {
            out = consoleEngine().commandDescription(args);
        } else if (isLocalCommand(command)) {
            out = localCommandDescription(command);
        }
        return out;
    }

    private CmdDesc commandDescription(CommandRegistry subreg) {
        List main = new ArrayList<>();
        Map> options = new HashMap<>();
        StyleResolver helpStyle = Styles.helpStyle();
        for (String sc : new TreeSet<>(subreg.commandNames())) {
            for (String info : subreg.commandInfo(sc)) {
                main.add(HelpException.highlightSyntax(sc + " -  " + info, helpStyle, true));
                break;
            }
        }
        return new CmdDesc(main, ArgDesc.doArgNames(Collections.singletonList("")), options);
    }

    public void setScriptDescription(Function scriptDescription) {
        this.scriptDescription = scriptDescription;
    }

    @Override
    public CmdDesc commandDescription(CmdLine line) {
        CmdDesc out = null;
        String cmd = parser.getCommand(line.getArgs().get(0));
        switch (line.getDescriptionType()) {
            case COMMAND:
                if (isCommandOrScript(cmd) && !names.hasPipes(line.getArgs())) {
                    List args = line.getArgs();
                    CommandRegistry subCommand = subcommands.get(cmd);
                    if (subCommand != null) {
                        String c = args.size() > 1 ? args.get(1) : null;
                        if (c == null || subCommand.hasCommand(c)) {
                            if (c != null && c.equals("help")) {
                                out = null;
                            } else if (c != null) {
                                out = subCommand.commandDescription(Collections.singletonList(c));
                            } else {
                                out = commandDescription(subCommand);
                            }
                        } else {
                            out = commandDescription(subCommand);
                        }
                        if (out != null) {
                            out.setSubcommand(true);
                        }
                    } else {
                        args.set(0, cmd);
                        out = commandDescription(args);
                    }
                }
                break;
            case METHOD:
            case SYNTAX:
                if (!isCommandOrScript(cmd) && scriptDescription != null) {
                    out = scriptDescription.apply(line);
                }
                break;
        }
        return out;
    }

    @Override
    public Object invoke(String command, Object... args) throws Exception {
        Object out = null;
        command = ConsoleEngine.plainCommand(command);
        args = args == null ? new Object[] {null} : args;
        int id = registryId(command);
        if (id > -1) {
            out = commandRegistries[id].invoke(commandSession(), command, args);
        } else if (isLocalCommand(command)) {
            out = localExecute(command, args);
        } else if (consoleId != null) {
            out = consoleEngine().invoke(commandSession(), command, args);
        }
        return out;
    }

    private Object localExecute(String command, Object[] args) throws Exception {
        if (!isLocalCommand(command)) {
            throw new IllegalArgumentException();
        }
        Object out = commandExecute.get(command).execute().apply(new CommandInput(command, args, commandSession()));
        if (exception != null) {
            throw exception;
        }
        return out;
    }

    public Terminal terminal() {
        return commandSession().terminal();
    }

    private CommandSession commandSession() {
        return outputStream.getCommandSession();
    }

    private static class CommandOutputStream {
        private final PrintStream origOut;
        private final PrintStream origErr;
        private final Terminal origTerminal;
        private OutputStream outputStream;
        private Terminal terminal;
        private String output;
        private CommandRegistry.CommandSession commandSession;
        private boolean redirecting = false;

        public CommandOutputStream(Terminal terminal) {
            this.origOut = System.out;
            this.origErr = System.err;
            this.origTerminal = terminal;
            this.terminal = terminal;
            PrintStream ps = new PrintStream(terminal.output());
            this.commandSession = new CommandRegistry.CommandSession(terminal, terminal.input(), ps, ps);
        }

        public void redirect() {
            outputStream = new ByteArrayOutputStream();
        }

        public void redirect(File file, boolean append) throws IOException {
            if (!file.exists()) {
                try {
                    file.createNewFile();
                } catch (IOException e) {
                    (new File(file.getParent())).mkdirs();
                    file.createNewFile();
                }
            }
            outputStream = new FileOutputStream(file, append);
        }

        public void open(boolean redirectColor) throws IOException {
            if (redirecting || outputStream == null) {
                return;
            }
            output = null;
            PrintStream out = new PrintStream(outputStream);
            System.setOut(out);
            System.setErr(out);
            String input = ctrl('X') + "q";
            InputStream in = new ByteArrayInputStream(input.getBytes());
            Attributes attrs = new Attributes();
            if (OSUtils.IS_WINDOWS) {
                attrs.setInputFlag(InputFlag.IGNCR, true);
            }
            try {
                terminal = TerminalBuilder.builder()
                        .streams(in, outputStream)
                        .attributes(attrs)
                        .type((redirectColor ? Terminal.TYPE_DUMB_COLOR : Terminal.TYPE_DUMB))
                        .build();
                this.commandSession = new CommandRegistry.CommandSession(terminal, terminal.input(), out, out);
                redirecting = true;
            } catch (IOException e) {
                reset();
                throw e;
            }
        }

        public void close() {
            if (!redirecting) {
                return;
            }
            try {
                terminal.flush();
                if (outputStream instanceof ByteArrayOutputStream) {
                    output = outputStream.toString();
                }
                terminal.close();
            } catch (Exception e) {
                // ignore
            }
            reset();
        }

        public void resetOutput() {
            output = null;
        }

        private void reset() {
            outputStream = null;
            System.setOut(origOut);
            System.setErr(origErr);
            terminal = null;
            terminal = origTerminal;
            PrintStream ps = new PrintStream(terminal.output());
            this.commandSession = new CommandRegistry.CommandSession(terminal, terminal.input(), ps, ps);
            redirecting = false;
        }

        public CommandRegistry.CommandSession getCommandSession() {
            return commandSession;
        }

        public String getOutput() {
            return output;
        }

        public boolean isRedirecting() {
            return redirecting;
        }

        public boolean isByteOutputStream() {
            return outputStream instanceof ByteArrayOutputStream;
        }
    }

    @Override
    public boolean isCommandAlias(String command) {
        if (consoleEngine() == null) {
            return false;
        }
        ConsoleEngine consoleEngine = consoleEngine();
        if (!parser.validCommandName(command) || !consoleEngine.hasAlias(command)) {
            return false;
        }
        String value = consoleEngine.getAlias(command).split("\\s+")[0];
        return !names.isPipe(value);
    }

    private String replaceCommandAlias(String variable, String command, String rawLine) {
        ConsoleEngine consoleEngine = consoleEngine();
        assert consoleEngine != null;
        return variable == null
                ? rawLine.replaceFirst(command + "(\\b|$)", consoleEngine.getAlias(command))
                : rawLine.replaceFirst("=" + command + "(\\b|$)", "=" + consoleEngine.getAlias(command));
    }

    private String replacePipeAlias(
            ArgsParser ap, String pipeAlias, List args, Map> customPipes) {
        ConsoleEngine consoleEngine = consoleEngine();
        assert consoleEngine != null;
        String alias = pipeAlias;
        for (int j = 0; j < args.size(); j++) {
            alias = alias.replaceAll("\\s\\$" + j + "\\b", " " + args.get(j));
            alias = alias.replaceAll("\\$\\{" + j + "(|:-.*)}", args.get(j));
        }
        alias = alias.replaceAll("\\$\\{@}", consoleEngine.expandToList(args));
        alias = alias.replaceAll("\\$@", consoleEngine.expandToList(args));
        alias = alias.replaceAll("\\s+\\$\\d\\b", "");
        alias = alias.replaceAll("\\s+\\$\\{\\d+}", "");
        alias = alias.replaceAll("\\$\\{\\d+}", "");
        Matcher matcher = Pattern.compile("\\$\\{\\d+:-(.*?)}").matcher(alias);
        if (matcher.find()) {
            alias = matcher.replaceAll("$1");
        }
        ap.parse(alias);
        List ws = ap.args();
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < ws.size(); i++) {
            if (ws.get(i).equals(pipeName.get(Pipe.NAMED))) {
                if (i + 1 < ws.size() && consoleEngine.hasAlias(ws.get(i + 1))) {
                    args.clear();
                    String innerPipe = consoleEngine.getAlias(ws.get(++i));
                    while (i < ws.size() - 1 && !names.isPipe(ws.get(i + 1), customPipes.keySet())) {
                        args.add(ws.get(++i));
                    }
                    sb.append(replacePipeAlias(ap, innerPipe, args, customPipes));
                } else {
                    sb.append(ws.get(i)).append(' ');
                }
            } else {
                sb.append(ws.get(i)).append(' ');
            }
        }
        return sb.toString();
    }

    private void replacePipeAliases(ConsoleEngine consoleEngine, Map> customPipes, ArgsParser ap) {
        List words = ap.args();
        if (consoleEngine != null && words.contains(pipeName.get(Pipe.NAMED))) {
            StringBuilder sb = new StringBuilder();
            boolean trace = false;
            for (int i = 0; i < words.size(); i++) {
                if (words.get(i).equals(pipeName.get(Pipe.NAMED))) {
                    if (i + 1 < words.size() && consoleEngine.hasAlias(words.get(i + 1))) {
                        trace = true;
                        List args = new ArrayList<>();
                        String pipeAlias = consoleEngine.getAlias(words.get(++i));
                        while (i < words.size() - 1 && !names.isPipe(words.get(i + 1), customPipes.keySet())) {
                            args.add(words.get(++i));
                        }
                        sb.append(replacePipeAlias(ap, pipeAlias, args, customPipes));
                    } else {
                        sb.append(words.get(i)).append(' ');
                    }
                } else {
                    sb.append(words.get(i)).append(' ');
                }
            }
            ap.parse(sb.toString());
            if (trace) {
                consoleEngine.trace(ap.line());
            }
        }
    }

    private List compileCommandLine(String commandLine) {
        List out = new ArrayList<>();
        ArgsParser ap = new ArgsParser(parser);
        ap.parse(commandLine);
        ConsoleEngine consoleEngine = consoleEngine();
        Map> customPipes = consoleEngine != null ? consoleEngine.getPipes() : new HashMap<>();
        replacePipeAliases(consoleEngine, customPipes, ap);
        List words = ap.args();
        String nextRawLine = ap.line();
        int first = 0;
        int last;
        List pipes = new ArrayList<>();
        String pipeSource = null;
        String rawLine = null;
        String pipeResult = null;
        if (isCommandAlias(ap.command())) {
            ap.parse(replaceCommandAlias(ap.variable(), ap.command(), nextRawLine));
            replacePipeAliases(consoleEngine, customPipes, ap);
            nextRawLine = ap.line();
            words = ap.args();
        }
        if (!names.hasPipes(words)) {
            out.add(new CommandData(ap, false, nextRawLine, ap.variable(), null, false, ""));
        } else {
            //
            // compile pipe line
            //
            do {
                String rawCommand = parser.getCommand(words.get(first));
                String command = ConsoleEngine.plainCommand(rawCommand);
                String variable = parser.getVariable(words.get(first));
                if (isCommandAlias(command)) {
                    ap.parse(replaceCommandAlias(variable, command, nextRawLine));
                    replacePipeAliases(consoleEngine, customPipes, ap);
                    rawCommand = ap.rawCommand();
                    command = ap.command();
                    words = ap.args();
                    first = 0;
                }
                if (scriptStore.isConsoleScript(command) && !rawCommand.startsWith(":")) {
                    throw new IllegalArgumentException("Commands must be used in pipes with colon prefix!");
                }
                last = words.size();
                File file = null;
                boolean append = false;
                boolean pipeStart = false;
                boolean skipPipe = false;
                List _words = new ArrayList<>();
                //
                // find next pipe
                //
                for (int i = first; i < last; i++) {
                    if (words.get(i).equals(">") || words.get(i).equals(">>")) {
                        pipes.add(words.get(i));
                        append = words.get(i).equals(">>");
                        if (i + 1 >= last) {
                            throw new IllegalArgumentException();
                        }
                        file = redirectFile(words.get(i + 1));
                        last = i + 1;
                        break;
                    } else if (consoleId == null) {
                        _words.add(words.get(i));
                    } else if (words.get(i).equals(pipeName.get(Pipe.FLIP))) {
                        if (variable != null || file != null || pipeResult != null || consoleId == null) {
                            throw new IllegalArgumentException();
                        }
                        pipes.add(words.get(i));
                        last = i;
                        variable = "_pipe" + (pipes.size() - 1);
                        break;
                    } else if (words.get(i).equals(pipeName.get(Pipe.NAMED))
                            || (words.get(i).matches("^.*[^a-zA-Z0-9 ].*$") && customPipes.containsKey(words.get(i)))) {
                        String pipe = words.get(i);
                        if (pipe.equals(pipeName.get(Pipe.NAMED))) {
                            if (i + 1 >= last) {
                                throw new IllegalArgumentException("Pipe is NULL!");
                            }
                            pipe = words.get(i + 1);
                            if (!pipe.matches("\\w+") || !customPipes.containsKey(pipe)) {
                                throw new IllegalArgumentException("Unknown or illegal pipe name: " + pipe);
                            }
                        }
                        pipes.add(pipe);
                        last = i;
                        if (pipeSource == null) {
                            pipeSource = "_pipe" + (pipes.size() - 1);
                            pipeResult = variable;
                            variable = pipeSource;
                            pipeStart = true;
                        }
                        break;
                    } else if (words.get(i).equals(pipeName.get(Pipe.OR))
                            || words.get(i).equals(pipeName.get(Pipe.AND))) {
                        if (variable != null || pipeSource != null) {
                            pipes.add(words.get(i));
                        } else if (pipes.size() > 0
                                && (pipes.get(pipes.size() - 1).equals(">")
                                        || pipes.get(pipes.size() - 1).equals(">>"))) {
                            pipes.remove(pipes.size() - 1);
                            out.get(out.size() - 1).setPipe(words.get(i));
                            skipPipe = true;
                        } else {
                            pipes.add(words.get(i));
                            pipeSource = "_pipe" + (pipes.size() - 1);
                            pipeResult = variable;
                            variable = pipeSource;
                            pipeStart = true;
                        }
                        last = i;
                        break;
                    } else {
                        _words.add(words.get(i));
                    }
                }
                if (last == words.size()) {
                    pipes.add("END_PIPE");
                } else if (skipPipe) {
                    first = last + 1;
                    continue;
                }
                //
                // compose pipe command
                //
                String subLine = last < words.size() || first > 0 ? String.join(" ", _words) : ap.line();
                if (last + 1 < words.size()) {
                    nextRawLine = String.join(" ", words.subList(last + 1, words.size()));
                }
                boolean done = true;
                boolean statement = false;
                List arglist = new ArrayList<>();
                if (_words.size() > 0) {
                    arglist.addAll(_words.subList(1, _words.size()));
                }
                if (rawLine != null || (pipes.size() > 1 && customPipes.containsKey(pipes.get(pipes.size() - 2)))) {
                    done = false;
                    if (rawLine == null) {
                        rawLine = pipeSource;
                    }
                    if (customPipes.containsKey(pipes.get(pipes.size() - 2))) {
                        List fixes = customPipes.get(pipes.get(pipes.size() - 2));
                        if (pipes.get(pipes.size() - 2).matches("\\w+")) {
                            int idx = subLine.indexOf(" ");
                            subLine = idx > 0 ? subLine.substring(idx + 1) : "";
                        }
                        rawLine += fixes.get(0)
                                + (consoleId != null ? consoleEngine().expandCommandLine(subLine) : subLine)
                                + fixes.get(1);
                        statement = true;
                    }
                    if (pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.FLIP))
                            || pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.AND))
                            || pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.OR))) {
                        done = true;
                        pipeSource = null;
                        if (variable != null) {
                            rawLine = variable + " = " + rawLine;
                        }
                    }
                    if (last + 1 >= words.size() || file != null) {
                        done = true;
                        pipeSource = null;
                        if (pipeResult != null) {
                            rawLine = pipeResult + " = " + rawLine;
                        }
                    }
                } else if (pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.FLIP)) || pipeStart) {
                    if (pipeStart && pipeResult != null) {
                        subLine = subLine.substring(subLine.indexOf("=") + 1);
                    }
                    rawLine = flipArgument(command, subLine, pipes, arglist);
                    rawLine = variable + "=" + rawLine;
                } else {
                    rawLine = flipArgument(command, subLine, pipes, arglist);
                }
                if (done) {
                    //
                    // add composed command to return list
                    //
                    out.add(new CommandData(
                            ap, statement, rawLine, variable, file, append, pipes.get(pipes.size() - 1)));
                    if (pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.AND))
                            || pipes.get(pipes.size() - 1).equals(pipeName.get(Pipe.OR))) {
                        pipeSource = null;
                        pipeResult = null;
                    }
                    rawLine = null;
                }
                first = last + 1;
            } while (first < words.size());
        }
        return out;
    }

    private File redirectFile(String name) {
        File out;
        if (name.equals("null")) {
            out = OSUtils.IS_WINDOWS ? new File("NUL") : new File("/dev/null");
        } else {
            out = new File(name);
        }
        return out;
    }

    private static class ArgsParser {
        private int round = 0;
        private int curly = 0;
        private int square = 0;
        private boolean quoted;
        private boolean doubleQuoted;
        private String line;
        private String command = "";
        private String variable = "";
        private List args;
        private final Parser parser;

        public ArgsParser(Parser parser) {
            this.parser = parser;
        }

        private void reset() {
            round = 0;
            curly = 0;
            square = 0;
            quoted = false;
            doubleQuoted = false;
        }

        private void next(String arg) {
            char prevChar = ' ';
            for (int i = 0; i < arg.length(); i++) {
                char c = arg.charAt(i);
                if (!parser.isEscapeChar(prevChar)) {
                    if (!quoted && !doubleQuoted) {
                        if (c == '(') {
                            round++;
                        } else if (c == ')') {
                            round--;
                        } else if (c == '{') {
                            curly++;
                        } else if (c == '}') {
                            curly--;
                        } else if (c == '[') {
                            square++;
                        } else if (c == ']') {
                            square--;
                        } else if (c == '"') {
                            doubleQuoted = true;
                        } else if (c == '\'') {
                            quoted = true;
                        }
                    } else if (quoted && c == '\'') {
                        quoted = false;
                    } else if (doubleQuoted && c == '"') {
                        doubleQuoted = false;
                    }
                }
                prevChar = c;
            }
        }

        private boolean isEnclosed() {
            return round == 0 && curly == 0 && square == 0 && !quoted && !doubleQuoted;
        }

        public boolean isEnclosed(String arg) {
            reset();
            next(arg);
            return isEnclosed();
        }

        private void enclosedArgs(List words) {
            args = new ArrayList<>();
            reset();
            boolean first = true;
            StringBuilder sb = new StringBuilder();
            for (String a : words) {
                next(a);
                if (!first) {
                    sb.append(" ");
                }
                if (isEnclosed()) {
                    sb.append(a);
                    args.add(sb.toString());
                    sb = new StringBuilder();
                    first = true;
                } else {
                    sb.append(a);
                    first = false;
                }
            }
            if (!first) {
                args.add(sb.toString());
            }
        }

        public void parse(String line) {
            this.line = line;
            ParsedLine pl = parser.parse(line, 0, ParseContext.SPLIT_LINE);
            enclosedArgs(pl.words());
            if (!args.isEmpty()) {
                this.command = parser.getCommand(args.get(0));
                if (!parser.validCommandName(command)) {
                    this.command = "";
                }
                this.variable = parser.getVariable(args.get(0));
            } else {
                this.line = "";
            }
        }

        public String line() {
            return line;
        }

        public String command() {
            return ConsoleEngine.plainCommand(command);
        }

        public String rawCommand() {
            return command;
        }

        public String variable() {
            return variable;
        }

        public List args() {
            return args;
        }

        private int closingQuote(String arg) {
            int out = -1;
            char prevChar = ' ';
            for (int i = 1; i < arg.length(); i++) {
                char c = arg.charAt(i);
                if (!parser.isEscapeChar(prevChar)) {
                    if (c == arg.charAt(0)) {
                        out = i;
                        break;
                    }
                }
                prevChar = c;
            }
            return out;
        }

        private String unquote(String arg) {
            if (arg.length() > 1 && (arg.startsWith("\"") && arg.endsWith("\""))
                    || (arg.startsWith("'") && arg.endsWith("'"))) {
                if (closingQuote(arg) == arg.length() - 1) {
                    return arg.substring(1, arg.length() - 1);
                }
            }
            return arg;
        }

        /**
         * Unescapes a string that contains standard Java escape sequences.
         * 
    *
  • \b \f \n \r \t \" \' : * BS, FF, NL, CR, TAB, double and single quote.
  • *
  • \X \XX \XXX : Octal character * specification (0 - 377, 0x00 - 0xFF).
  • *
  • \uXXXX : Hexadecimal based Unicode character.
  • *
* * @param arg * A string optionally containing standard java escape sequences. * @return The translated string. * * @author Udo Klimaschewski, https://gist.github.com/uklimaschewski/6741769 */ private String unescape(String arg) { if (arg == null || !parser.isEscapeChar('\\')) { return arg; } StringBuilder sb = new StringBuilder(arg.length()); for (int i = 0; i < arg.length(); i++) { char ch = arg.charAt(i); if (ch == '\\') { char nextChar = (i == arg.length() - 1) ? '\\' : arg.charAt(i + 1); // Octal escape? if (nextChar >= '0' && nextChar <= '7') { String code = "" + nextChar; i++; if ((i < arg.length() - 1) && arg.charAt(i + 1) >= '0' && arg.charAt(i + 1) <= '7') { code += arg.charAt(i + 1); i++; if ((i < arg.length() - 1) && arg.charAt(i + 1) >= '0' && arg.charAt(i + 1) <= '7') { code += arg.charAt(i + 1); i++; } } sb.append((char) Integer.parseInt(code, 8)); continue; } switch (nextChar) { case '\\': ch = '\\'; break; case 'b': ch = '\b'; break; case 'f': ch = '\f'; break; case 'n': ch = '\n'; break; case 'r': ch = '\r'; break; case 't': ch = '\t'; break; case '\"': ch = '\"'; break; case '\'': ch = '\''; break; case ' ': ch = ' '; break; // Hex Unicode: u???? case 'u': if (i >= arg.length() - 5) { ch = 'u'; break; } int code = Integer.parseInt( "" + arg.charAt(i + 2) + arg.charAt(i + 3) + arg.charAt(i + 4) + arg.charAt(i + 5), 16); sb.append(Character.toChars(code)); i += 5; continue; } i++; } sb.append(ch); } return sb.toString(); } } private String flipArgument( final String command, final String subLine, final List pipes, List arglist) { String out; if (pipes.size() > 1 && pipes.get(pipes.size() - 2).equals(pipeName.get(Pipe.FLIP))) { String s = isCommandOrScript(command) ? "$" : ""; out = subLine + " " + s + "_pipe" + (pipes.size() - 2); if (!command.isEmpty()) { arglist.add(s + "_pipe" + (pipes.size() - 2)); } } else { out = subLine; } return out; } protected static class CommandData { private final String rawLine; private String command; private String[] args; private final File file; private final boolean append; private final String variable; private String pipe; public CommandData( ArgsParser parser, boolean statement, String rawLine, String variable, File file, boolean append, String pipe) { this.rawLine = rawLine; this.variable = variable; this.file = file; this.append = append; this.pipe = pipe; this.args = new String[] {}; this.command = ""; if (!statement) { parser.parse(rawLine); this.command = parser.command(); if (parser.args().size() > 1) { this.args = new String[parser.args().size() - 1]; for (int i = 1; i < parser.args().size(); i++) { args[i - 1] = parser.unescape(parser.unquote(parser.args().get(i))); } } } } public void setPipe(String pipe) { this.pipe = pipe; } public File file() { return file; } public boolean append() { return append; } public String variable() { return variable; } public String command() { return command; } public String[] args() { return args; } public String rawLine() { return rawLine; } public String pipe() { return pipe; } @Override public String toString() { return "[" + "rawLine:" + rawLine + ", " + "command:" + command + ", " + "args:" + Arrays.asList(args) + ", " + "variable:" + variable + ", " + "file:" + file + ", " + "append:" + append + ", " + "pipe:" + pipe + "]"; } } private static class ScriptStore { ConsoleEngine engine; Map scripts = new HashMap<>(); public ScriptStore() {} public ScriptStore(ConsoleEngine engine) { this.engine = engine; } public void refresh() { if (engine != null) { scripts = engine.scripts(); } } public boolean hasScript(String name) { return scripts.containsKey(name); } public boolean isConsoleScript(String name) { return scripts.getOrDefault(name, false); } public Set getScripts() { return scripts.keySet(); } } @SuppressWarnings("serial") public static class UnknownCommandException extends Exception { public UnknownCommandException(String message) { super(message); } } private Object execute(String command, String rawLine, String[] args) throws Exception { if (!parser.validCommandName(command)) { throw new UnknownCommandException("Invalid command: " + rawLine); } Object out; if (isLocalCommand(command)) { out = localExecute(command, consoleId != null ? consoleEngine().expandParameters(args) : args); } else { int id = registryId(command); if (id > -1) { Object[] _args = consoleId != null ? consoleEngine().expandParameters(args) : args; out = commandRegistries[id].invoke(outputStream.getCommandSession(), command, _args); } else if (scriptStore.hasScript(command) && consoleEngine() != null) { out = consoleEngine().execute(command, rawLine, args); } else { throw new UnknownCommandException("Unknown command: " + command); } } return out; } @Override public Object execute(String line) throws Exception { if (line.trim().isEmpty() || line.trim().startsWith("#")) { return null; } long start = new Date().getTime(); Object out = null; boolean statement = false; boolean postProcessed = false; int errorCount = 0; scriptStore.refresh(); List cmds = compileCommandLine(line); ConsoleEngine consoleEngine = consoleEngine(); for (CommandData cmd : cmds) { if (cmd.file() != null && scriptStore.isConsoleScript(cmd.command())) { throw new IllegalArgumentException("Console script output cannot be redirected!"); } try { outputStream.close(); if (consoleEngine != null && !consoleEngine.isExecuting()) { trace(cmd); } exception = null; statement = false; postProcessed = false; if (cmd.variable() != null || cmd.file() != null) { if (cmd.file() != null) { outputStream.redirect(cmd.file(), cmd.append()); } else if (consoleId != null) { outputStream.redirect(); } outputStream.open(consoleOption("redirectColor", false)); } boolean consoleScript = false; try { out = execute(cmd.command(), cmd.rawLine(), cmd.args()); } catch (UnknownCommandException e) { if (consoleEngine == null) { throw e; } consoleScript = true; } if (consoleEngine != null) { if (consoleScript) { statement = cmd.command().isEmpty() || !scriptStore.hasScript(cmd.command()); if (statement && outputStream.isByteOutputStream()) { outputStream.close(); } out = consoleEngine.execute(cmd.command(), cmd.rawLine(), cmd.args()); } if (cmd.pipe().equals(pipeName.get(Pipe.OR)) || cmd.pipe().equals(pipeName.get(Pipe.AND))) { ExecutionResult er = postProcess(cmd, statement, consoleEngine, out); postProcessed = true; consoleEngine.println(er.result()); out = null; boolean success = er.status() == 0; if ((cmd.pipe().equals(pipeName.get(Pipe.OR)) && success) || (cmd.pipe().equals(pipeName.get(Pipe.AND)) && !success)) { break; } } } } catch (HelpException e) { trace(e); } catch (Exception e) { errorCount++; if (cmd.pipe().equals(pipeName.get(Pipe.OR))) { trace(e); postProcessed = true; } else { throw e; } } finally { if (!postProcessed && consoleEngine != null) { out = postProcess(cmd, statement, consoleEngine, out).result(); } } } if (errorCount == 0) { names.extractNames(line); } Log.debug("execute: ", new Date().getTime() - start, " msec"); return out; } private ExecutionResult postProcess( CommandData cmd, boolean statement, ConsoleEngine consoleEngine, Object result) { ExecutionResult out; if (cmd.file() != null) { int status = 1; if (cmd.file().exists()) { long delta = new Date().getTime() - cmd.file().lastModified(); status = delta < 100 ? 0 : 1; } out = new ExecutionResult(status, result); } else if (!statement) { outputStream.close(); out = consoleEngine.postProcess(cmd.rawLine(), result, outputStream.getOutput()); } else if (cmd.variable() != null) { if (consoleEngine.hasVariable(cmd.variable())) { out = consoleEngine.postProcess(consoleEngine.getVariable(cmd.variable())); } else { out = consoleEngine.postProcess(result); } out = new ExecutionResult(out.status(), null); } else { out = consoleEngine.postProcess(result); } return out; } public void cleanUp() { outputStream.close(); outputStream.resetOutput(); if (consoleEngine() != null) { consoleEngine().purge(); } } private void trace(CommandData commandData) { if (consoleEngine() != null) { consoleEngine().trace(commandData); } else { AttributedStringBuilder asb = new AttributedStringBuilder(); asb.append(commandData.rawLine(), AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)) .println(terminal()); } } @Override public void trace(Throwable exception) { outputStream.close(); ConsoleEngine consoleEngine = consoleEngine(); if (consoleEngine != null) { if (!(exception instanceof Options.HelpException)) { consoleEngine.putVariable("exception", exception); } consoleEngine.trace(exception); } else { trace(false, exception); } } @Override public void trace(boolean stack, Throwable exception) { if (exception instanceof Options.HelpException) { Options.HelpException.highlight((exception).getMessage(), Styles.helpStyle()) .print(terminal()); } else if (exception instanceof UnknownCommandException) { AttributedStringBuilder asb = new AttributedStringBuilder(); asb.append(exception.getMessage(), Styles.prntStyle().resolve(".em")); asb.toAttributedString().println(terminal()); } else if (stack) { exception.printStackTrace(); } else { String message = exception.getMessage(); AttributedStringBuilder asb = new AttributedStringBuilder(); asb.style(Styles.prntStyle().resolve(".em")); if (message != null) { asb.append(exception.getClass().getSimpleName()).append(": ").append(message); } else { asb.append("Caught exception: "); asb.append(exception.getClass().getCanonicalName()); } asb.toAttributedString().println(terminal()); Log.debug("Stack: ", exception); } } @Override public void close() { names.save(); } public ConsoleEngine consoleEngine() { return consoleId != null ? (ConsoleEngine) commandRegistries[consoleId] : null; } private boolean isBuiltinRegistry(CommandRegistry registry) { for (Class c : BUILTIN_REGISTRIES) { if (c == registry.getClass()) { return true; } } return false; } private void printHeader(String header) { AttributedStringBuilder asb = new AttributedStringBuilder().tabs(2); asb.append("\t"); asb.append(header, HelpException.defaultStyle().resolve(".ti")); asb.append(":"); asb.toAttributedString().println(terminal()); } private void printCommandInfo(String command, String info, int max) { AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(4, max + 4)); asb.append("\t"); asb.append(command, HelpException.defaultStyle().resolve(".co")); asb.append("\t"); asb.append(info); asb.setLength(terminal().getWidth()); asb.toAttributedString().println(terminal()); } private void printCommands(Collection commands, int max) { AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(4, max + 4)); int col = 0; asb.append("\t"); col += 4; boolean done = false; for (String c : commands) { asb.append(c, HelpException.defaultStyle().resolve(".co")); asb.append("\t"); col += max; if (col + max > terminal().getWidth()) { asb.toAttributedString().println(terminal()); asb = new AttributedStringBuilder().tabs(Arrays.asList(4, max + 4)); col = 0; asb.append("\t"); col += 4; done = true; } else { done = false; } } if (!done) { asb.toAttributedString().println(terminal()); } terminal().flush(); } private String doCommandInfo(List info) { return info != null && info.size() > 0 ? info.get(0) : " "; } private boolean isInTopics(List args, String name) { return args.isEmpty() || args.contains(name); } private Options parseOptions(String[] usage, Object[] args) throws HelpException { Options opt = Options.compile(usage).parse(args); if (opt.isSet("help")) { throw new HelpException(opt.usage()); } return opt; } private Object help(CommandInput input) { String groupsOption = commandGroups ? "nogroups" : "groups"; String groupsHelp = commandGroups ? " --nogroups Commands are not grouped by registries" : " --groups Commands are grouped by registries"; final String[] usage = { "help - command help", "Usage: help [TOPIC...]", " -? --help Displays command help", groupsHelp, " -i --info List commands with a short command info" }; try { Options opt = parseOptions(usage, input.args()); boolean doTopic = false; boolean cg = commandGroups; boolean info = false; if (!opt.args().isEmpty() && opt.args().size() == 1) { try { String[] args = {"--help"}; String command = opt.args().get(0); execute(command, command + " " + args[0], args); } catch (UnknownCommandException e) { doTopic = true; } catch (Exception e) { exception = e; } } else { doTopic = true; if (opt.isSet(groupsOption)) { cg = !cg; } if (opt.isSet("info")) { info = true; } } if (doTopic) { helpTopic(opt.args(), cg, info); } } catch (Exception e) { exception = e; } return null; } private void helpTopic(List topics, boolean commandGroups, boolean info) { Set commands = commandNames(); commands.addAll(scriptStore.getScripts()); boolean withInfo = commands.size() < terminal().getHeight() || !topics.isEmpty() || info; int max = Collections.max(commands, Comparator.comparing(String::length)).length() + 1; TreeMap builtinCommands = new TreeMap<>(); TreeMap systemCommands = new TreeMap<>(); if (!commandGroups && topics.isEmpty()) { TreeSet ordered = new TreeSet<>(commands); if (withInfo) { for (String c : ordered) { List infos = commandInfo(c); String cmdInfo = infos.isEmpty() ? "" : infos.get(0); printCommandInfo(c, cmdInfo, max); } } else { printCommands(ordered, max); } } else { for (CommandRegistry r : commandRegistries) { if (isBuiltinRegistry(r)) { for (String c : r.commandNames()) { builtinCommands.put(c, doCommandInfo(commandInfo(c))); } } } for (String c : localCommandNames()) { systemCommands.put(c, doCommandInfo(commandInfo(c))); exception = null; } if (isInTopics(topics, "System")) { printHeader("System"); if (withInfo) { for (Map.Entry entry : systemCommands.entrySet()) { printCommandInfo(entry.getKey(), entry.getValue(), max); } } else { printCommands(systemCommands.keySet(), max); } } if (isInTopics(topics, "Builtins") && !builtinCommands.isEmpty()) { printHeader("Builtins"); if (withInfo) { for (Map.Entry entry : builtinCommands.entrySet()) { printCommandInfo(entry.getKey(), entry.getValue(), max); } } else { printCommands(builtinCommands.keySet(), max); } } for (CommandRegistry r : commandRegistries) { if (isBuiltinRegistry(r) || !isInTopics(topics, r.name()) || r.commandNames().isEmpty()) { continue; } TreeSet cmds = new TreeSet<>(r.commandNames()); printHeader(r.name()); if (withInfo) { for (String c : cmds) { printCommandInfo(c, doCommandInfo(commandInfo(c)), max); } } else { printCommands(cmds, max); } } if (consoleId != null && isInTopics(topics, "Scripts") && !scriptStore.getScripts().isEmpty()) { printHeader("Scripts"); if (withInfo) { for (String c : scriptStore.getScripts()) { printCommandInfo(c, doCommandInfo(commandInfo(c)), max); } } else { printCommands(scriptStore.getScripts(), max); } } } terminal().flush(); } private Object exit(CommandInput input) { final String[] usage = { "exit - exit from app/script", "Usage: exit [OBJECT]", " -? --help Displays command help" }; try { Options opt = parseOptions(usage, input.xargs()); ConsoleEngine consoleEngine = consoleEngine(); if (!opt.argObjects().isEmpty() && consoleEngine != null) { try { consoleEngine.putVariable( "_return", opt.argObjects().size() == 1 ? opt.argObjects().get(0) : opt.argObjects()); } catch (Exception e) { trace(e); } } exception = new EndOfFileException(); } catch (Exception e) { exception = e; } return null; } private void registryHelp(CommandRegistry registry) throws Exception { List tabs = new ArrayList<>(); tabs.add(0); tabs.add(9); int max = registry.commandNames().stream() .map(String::length) .max(Integer::compareTo) .get(); tabs.add(10 + max); AttributedStringBuilder sb = new AttributedStringBuilder().tabs(tabs); sb.append(" - "); sb.append(registry.name()); sb.append(" registry"); sb.append("\n"); boolean first = true; for (String c : new TreeSet<>(registry.commandNames())) { if (first) { sb.append("Summary:"); first = false; } sb.append("\t"); sb.append(c); sb.append("\t"); sb.append(registry.commandInfo(c).get(0)); sb.append("\n"); } throw new HelpException(sb.toString()); } private Object subcommand(CommandInput input) { Object out = null; try { if (input.args().length > 0 && subcommands.get(input.command()).hasCommand(input.args()[0])) { out = subcommands .get(input.command()) .invoke( input.session(), input.args()[0], input.xargs().length > 1 ? Arrays.copyOfRange(input.xargs(), 1, input.xargs().length) : new Object[] {}); } else { registryHelp(subcommands.get(input.command())); } } catch (Exception e) { exception = e; } return out; } private List commandOptions(String command) { try { localExecute(command, new String[] {"--help"}); } catch (HelpException e) { exception = null; return JlineCommandRegistry.compileCommandOptions(e.getMessage()); } catch (Exception e) { trace(e); } return null; } private List registryNames() { List out = new ArrayList<>(); out.add("System"); out.add("Builtins"); if (consoleId != null) { out.add("Scripts"); } for (CommandRegistry r : commandRegistries) { if (!isBuiltinRegistry(r)) { out.add(r.name()); } } out.addAll(commandNames()); out.addAll(scriptStore.getScripts()); return out; } private List emptyCompleter(String command) { return new ArrayList<>(); } private List helpCompleter(String command) { List completers = new ArrayList<>(); List params = new ArrayList<>(); params.add(new StringsCompleter(this::registryNames)); params.add(NullCompleter.INSTANCE); completers.add( new ArgumentCompleter(NullCompleter.INSTANCE, new OptionCompleter(params, this::commandOptions, 1))); return completers; } private List exitCompleter(String command) { List completers = new ArrayList<>(); completers.add(new ArgumentCompleter( NullCompleter.INSTANCE, new OptionCompleter(NullCompleter.INSTANCE, this::commandOptions, 1))); return completers; } private int registryId(String command) { for (int i = 0; i < commandRegistries.length; i++) { if (commandRegistries[i].hasCommand(command)) { return i; } } return -1; } private static class PipelineCompleter implements Completer { private final NamesAndValues names; private final Supplier workDir; private final Map pipeName; public PipelineCompleter(Supplier workDir, Map pipeName, NamesAndValues names) { this.workDir = workDir; this.pipeName = pipeName; this.names = names; } public Completer doCompleter() { ArgumentCompleter out = new ArgumentCompleter(this); out.setStrict(false); return out; } @Override public void complete(LineReader reader, ParsedLine commandLine, List candidates) { assert commandLine != null; assert candidates != null; ArgsParser ap = new ArgsParser(reader.getParser()); ap.parse(commandLine.line().substring(0, commandLine.cursor())); List args = ap.args(); if (args.size() < 2 || !names.hasPipes(args)) { return; } boolean enclosed = ap.isEnclosed(args.get(args.size() - 1)); String pWord = commandLine.words().get(commandLine.wordIndex() - 1); if (enclosed && pWord.equals(pipeName.get(Pipe.NAMED))) { for (String name : names.namedPipes()) { candidates.add(new Candidate(name, name, null, null, null, null, true)); } } else if (enclosed && pWord.equals(">") || pWord.equals(">>")) { Completer c = new FilesCompleter(workDir); c.complete(reader, commandLine, candidates); } else { String buffer = commandLine.word().substring(0, commandLine.wordCursor()); String param = buffer; String curBuf = ""; int lastDelim = names.indexOfLastDelim(buffer); if (lastDelim > -1) { param = buffer.substring(lastDelim + 1); curBuf = buffer.substring(0, lastDelim + 1); } if (curBuf.startsWith("--") && !curBuf.contains("=")) { doCandidates(candidates, names.options(), curBuf, "", param); } else if (param.length() == 0) { doCandidates(candidates, names.fieldsAndValues(), curBuf, "", ""); } else if (param.contains(".")) { int point = buffer.lastIndexOf("."); param = buffer.substring(point + 1); curBuf = buffer.substring(0, point + 1); doCandidates(candidates, names.fields(), curBuf, "", param); } else if (names.encloseBy(param).length() == 1) { lastDelim++; String postFix = names.encloseBy(param); param = buffer.substring(lastDelim + 1); curBuf = buffer.substring(0, lastDelim + 1); doCandidates(candidates, names.quoted(), curBuf, postFix, param); } else { doCandidates(candidates, names.fieldsAndValues(), curBuf, "", param); } } } private void doCandidates( List candidates, Collection fields, String curBuf, String postFix, String hint) { if (fields == null) { return; } for (String s : fields) { if (s != null && s.startsWith(hint)) { candidates.add(new Candidate( AttributedString.stripAnsi(curBuf + s + postFix), s, null, null, null, null, false)); } } } } private class NamesAndValues { private final String[] delims = { "&", "\\|", "\\{", "\\}", "\\[", "\\]", "\\(", "\\)", "\\+", "-", "\\*", "=", ">", "<", "~", "!", ":", ",", ";" }; private Path fileNames; private final Map> names = new HashMap<>(); private List namedPipes; public NamesAndValues() { this(null); } @SuppressWarnings("unchecked") public NamesAndValues(ConfigurationPath configPath) { names.put("fields", new ArrayList<>()); names.put("values", new ArrayList<>()); names.put("quoted", new ArrayList<>()); names.put("options", new ArrayList<>()); ConsoleEngine consoleEngine = consoleEngine(); if (configPath != null && consoleEngine != null) { try { fileNames = configPath.getUserConfig("pipeline-names.json", true); Map> temp = (Map>) consoleEngine.slurp(fileNames); for (Entry> entry : temp.entrySet()) { names.get(entry.getKey()).addAll(entry.getValue()); } } catch (Exception e) { // ignore } } } public boolean isPipe(String arg) { Map> customPipes = consoleEngine() != null ? consoleEngine().getPipes() : new HashMap<>(); return isPipe(arg, customPipes.keySet()); } public boolean hasPipes(Collection args) { Map> customPipes = consoleEngine() != null ? consoleEngine().getPipes() : new HashMap<>(); for (String a : args) { if (isPipe(a, customPipes.keySet()) || a.contains(">") || a.contains(">>")) { return true; } } return false; } private boolean isPipe(String arg, Set pipes) { return pipeName.containsValue(arg) || pipes.contains(arg); } public void extractNames(String line) { if (parser.getCommand(line).equals("pipe")) { return; } ArgsParser ap = new ArgsParser(parser); ap.parse(line); List args = ap.args(); int pipeId = 0; for (String a : args) { if (isPipe(a)) { break; } pipeId++; } if (pipeId < args.size()) { StringBuilder sb = new StringBuilder(); int redirectPipe = -1; for (int i = pipeId + 1; i < args.size(); i++) { String arg = args.get(i); if (!isPipe(arg) && !namedPipes().contains(arg) && !arg.matches("\\d+") && redirectPipe != i - 1) { if (arg.equals(">") || arg.equals(">>")) { redirectPipe = i; } else if (arg.matches("\\w+(\\(\\))?")) { addValues(arg); } else if (arg.matches("--\\w+(=.*|)$") && arg.length() > 4) { int idx = arg.indexOf('='); if (idx > 0) { if (idx > 4) { addOptions(arg.substring(2, idx)); } sb.append(arg.substring(idx + 1)); sb.append(" "); } else if (idx == -1) { addOptions(arg.substring(2)); } } else { sb.append(arg); sb.append(" "); } } else { redirectPipe = -1; } } if (sb.length() > 0) { String rest = sb.toString(); for (String d : delims) { rest = rest.replaceAll(d, " "); } String[] words = rest.split("\\s+"); for (String w : words) { if (w.length() < 3 || w.matches("\\d+")) { continue; } if (isQuoted(w)) { addQuoted(w.substring(1, w.length() - 1)); } else if (w.contains(".")) { for (String f : w.split("\\.")) { if (!f.matches("\\d+") && f.matches("\\w+")) { addFields(f); } } } else if (w.matches("\\w+")) { addValues(w); } } } } namedPipes = null; } public String encloseBy(String param) { boolean quoted = param.length() > 0 && (param.startsWith("\"") || param.startsWith("'") || param.startsWith("/")); if (quoted && param.length() > 1) { quoted = !param.endsWith(Character.toString(param.charAt(0))); } return quoted ? Character.toString(param.charAt(0)) : ""; } private boolean isQuoted(String word) { return word.length() > 1 && ((word.startsWith("\"") && word.endsWith("\"")) || (word.startsWith("'") && word.endsWith("'")) || (word.startsWith("/") && word.endsWith("/"))); } public int indexOfLastDelim(String word) { int out = -1; for (String d : delims) { int x = word.lastIndexOf(d.replace("\\", "")); if (x > out) { out = x; } } return out; } private void addFields(String field) { add("fields", field); } private void addValues(String arg) { add("values", arg); } private void addQuoted(String arg) { add("quoted", arg); } private void addOptions(String arg) { add("options", arg); } private void add(String where, String value) { if (value.length() < 3) { return; } names.get(where).remove(value); names.get(where).add(0, value); } public List namedPipes() { if (namedPipes == null) { namedPipes = consoleId != null ? consoleEngine().getNamedPipes() : new ArrayList<>(); } return namedPipes; } public List values() { return names.get("values"); } public List fields() { return names.get("fields"); } public List quoted() { return names.get("quoted"); } public List options() { return names.get("options"); } private Set fieldsAndValues() { Set out = new HashSet<>(); out.addAll(fields()); out.addAll(values()); return out; } private void truncate(String where, int maxSize) { if (names.get(where).size() > maxSize) { names.put(where, names.get(where).subList(0, maxSize)); } } public void save() { ConsoleEngine consoleEngine = consoleEngine(); if (consoleEngine != null && fileNames != null) { int maxSize = consoleEngine.consoleOption("maxValueNames", 100); truncate("fields", maxSize); truncate("values", maxSize); truncate("quoted", maxSize); truncate("options", maxSize); consoleEngine.persist(fileNames, names); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy