diff --git a/.github/workflows/master-build.yml b/.github/workflows/master-build.yml index 090d9d4c6..76d70238e 100644 --- a/.github/workflows/master-build.yml +++ b/.github/workflows/master-build.yml @@ -56,7 +56,7 @@ jobs: strategy: matrix: os: [ ubuntu-latest, windows-latest ] - java: [ '1.8', '11' ] + java: [ '1.8', '11', '14' ] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 5d254fa22..c02ef55c3 100644 --- a/README.md +++ b/README.md @@ -37,18 +37,19 @@ The big bundle is named: jline-${jline.version}.jar -The dependencies are minimal: you may use JLine without any dependency on *nix systems, but in order to support windows or more advanced usage, you will need to add either [`jansi`](https://repo1.maven.org/maven2/org/fusesource/jansi/jansi/1.17/jansi-1.17.jar) or [`jna`](https://repo1.maven.org/maven2/net/java/dev/jna/jna/4.5.1/jna-4.5.1.jar) library. +The dependencies are minimal: you may use JLine without any dependency on *nix systems, but in order to support windows or more advanced usage, you will need to add either [`Jansi`](https://repo1.maven.org/maven2/org/fusesource/jansi/jansi/1.18/jansi-1.18.jar) or [`JNA`](https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.3.1/jna-5.3.1.jar) library. -You can also use finer grained jars: +You can also use fine grained jars: * `jline-terminal`: the `Terminal` api and implementations -* `jline-terminal-jansi`: terminal implementations leveraging the `jansi` library -* `jline-terminal-jna`: terminal implementations leveraging the `jna` library +* `jline-terminal-jansi`: terminal implementations leveraging the `Jansi` library +* `jline-terminal-jna`: terminal implementations leveraging the `JNA` library * `jline-reader`: the line reader (including completion, history, etc...) -* `jline-groovy`: jline [ScriptEngine](https://github.com/jline/jline3/blob/master/reader/src/main/java/org/jline/reader/ScriptEngine.java) implementation using Groovy * `jline-style`: styling api * `jline-remote-ssh`: helpers for using jline with [Mina SSHD](http://mina.apache.org/sshd-project/) * `jline-remote-telnet`: helpers for using jline over telnet (including a telnet server implementation) * `jline-builtins`: several high level tools: `less` pager, `nano` editor, `screen` multiplexer, etc... +* `jline-console`: command registry, object printer and widget implementations +* `jline-groovy`: `ScriptEngine` implementation using Groovy ## Supported platforms @@ -100,8 +101,9 @@ All the jars and releases are available from Maven Central, so you'll find every ## Requirements -* Maven 3.3+ (prefer included maven-wrapper) +* Maven 3.3+ * Java 8+ +* Graal 19.3+ (native-image) Check out and build: @@ -111,6 +113,12 @@ cd jline3 ./build rebuild ``` +Build Graal native-image demo: + +```sh +./build rebuild -Pnative-image +``` + ## Results The following artifacts are build: @@ -125,11 +133,12 @@ The fine grained bundles are located at: terminal-jansi/target/jline-jansi-${jline.version}.jar terminal-jna/target/jline-jna-${jline.version}.jar reader/target/jline-reader-${jline.version}.jar - groovy/target/jline-groovy-${jline.version}.jar style/target/jline-style-${jline.version}.jar - builtins/target/jline-builtins-${jline.version}.jar remote-telnet/target/jline-remote-telnet-${jline.version}.jar remote-ssh/target/jline-remote-ssh-${jline.version}.jar + builtins/target/jline-builtins-${jline.version}.jar + console/target/jline-console-${jline.version}.jar + groovy/target/jline-groovy-${jline.version}.jar Maven has a concept of `SNAPSHOT`. During development, the jline version will always ends with `-SNAPSHOT`, which means that the version is in development and not a release. @@ -143,8 +152,11 @@ To run the demo, simply use one of the following commands after having build `JL # Gogo terminal ./build demo -# groovy REPL +# Groovy REPL ./build repl + +# Graal native-image +./build graal ``` ## Continuous Integration diff --git a/appveyor.yml b/appveyor.yml index e938828b0..5ef2e7dea 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,12 +5,12 @@ install: Add-Type -AssemblyName System.IO.Compression.FileSystem if (!(Test-Path -Path "C:\maven" )) { (new-object System.Net.WebClient).DownloadFile( - 'http://www.us.apache.org/dist/maven/maven-3/3.2.5/binaries/apache-maven-3.2.5-bin.zip', + 'http://www.us.apache.org/dist/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip', 'C:\maven-bin.zip' ) [System.IO.Compression.ZipFile]::ExtractToDirectory("C:\maven-bin.zip", "C:\maven") } - - cmd: SET M2_HOME=C:\maven\apache-maven-3.2.5 + - cmd: SET M2_HOME=C:\maven\apache-maven-3.3.9 - cmd: SET PATH=%M2_HOME%\bin;%JAVA_HOME%\bin;%PATH% - cmd: SET MAVEN_OPTS=-XX:MaxPermSize=2g -Xmx4g - cmd: SET JAVA_OPTS=-XX:MaxPermSize=2g -Xmx4g diff --git a/build.config b/build.config index 92653bb97..d0eee320d 100644 --- a/build.config +++ b/build.config @@ -13,3 +13,7 @@ function command_demo() { function command_repl() { exec demo/jline-repl.sh $* } + +function command_graal() { + exec graal/target/graal $* +} diff --git a/builtins/pom.xml b/builtins/pom.xml index 5d27c7eea..c736cdccd 100644 --- a/builtins/pom.xml +++ b/builtins/pom.xml @@ -16,12 +16,16 @@ org.jline jline-parent - 3.14.1-SNAPSHOT + 3.18.1-SNAPSHOT jline-builtins JLine Builtins + + org.jline.builtins + + org.jline @@ -31,10 +35,6 @@ org.jline jline-style - - org.jline - jline-groovy - com.googlecode.juniversalchardet @@ -62,33 +62,31 @@ default-compile - - **/TTop.java - **/Builtins.java - + + **/ConsoleEngineImpl.java + **/TTop.java + -Xlint:all,-options -Werror - -profile - compact3 - + - noncompact + compact compile - - **/TTop.java - **/Builtins.java - + + **/ConsoleEngineImpl.java + **/TTop.java + -Xlint:all,-options -Werror -profile - compact3 + compact1 diff --git a/builtins/src/main/java/org/jline/builtins/Builtins.java b/builtins/src/main/java/org/jline/builtins/Builtins.java deleted file mode 100644 index 5a3fa4172..000000000 --- a/builtins/src/main/java/org/jline/builtins/Builtins.java +++ /dev/null @@ -1,575 +0,0 @@ -/* - * Copyright (c) 2002-2020, the original author or authors. - * - * 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.builtins; - -import java.io.InputStream; -import java.io.PrintStream; -import java.nio.file.Path; -import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.jline.builtins.Completers.FilesCompleter; -import org.jline.builtins.Completers.OptDesc; -import org.jline.builtins.Completers.OptionCompleter; -import org.jline.builtins.Completers.SystemCompleter; -import org.jline.builtins.Options.HelpException; -import org.jline.builtins.Widgets.ArgDesc; -import org.jline.builtins.Widgets.CmdDesc; -import org.jline.reader.*; -import org.jline.reader.LineReader.Option; -import org.jline.reader.impl.completer.ArgumentCompleter; -import org.jline.reader.impl.completer.NullCompleter; -import org.jline.reader.impl.completer.StringsCompleter; -import org.jline.terminal.Terminal; -import org.jline.utils.AttributedString; - -/** - * Builtins: create tab completers, execute and create descriptions for builtins commands. - * - * @author Matti Rinta-Nikkola - */ -public class Builtins implements CommandRegistry { - public enum Command {NANO - , LESS - , HISTORY - , WIDGET - , KEYMAP - , SETOPT - , SETVAR - , UNSETOPT - , TTOP}; - private ConfigurationPath configPath; - private final Function widgetCreator; - private final Supplier workDir; - private Map commandName = new HashMap<>(); - private Map nameCommand = new HashMap<>(); - private Map aliasCommand = new HashMap<>(); - private final Map commandExecute = new HashMap<>(); - private LineReader reader; - private Exception exception; - - public Builtins(Path workDir, ConfigurationPath configPath, Function widgetCreator) { - this(null, () -> workDir, configPath, widgetCreator); - } - - public Builtins(Set commands, Path workDir, ConfigurationPath configpath, Function widgetCreator) { - this(commands, () -> workDir, configpath, widgetCreator); - } - - public Builtins(Supplier workDir, ConfigurationPath configPath, Function widgetCreator) { - this(null, workDir, configPath, widgetCreator); - } - - public Builtins(Set commands, Supplier workDir, ConfigurationPath configpath, Function widgetCreator) { - this.configPath = configpath; - this.widgetCreator = widgetCreator; - this.workDir = workDir; - Set cmds = new HashSet<>(); - if (commands == null) { - cmds = new HashSet<>(EnumSet.allOf(Command.class)); - } else { - cmds = new HashSet<>(commands); - } - for (Command c: cmds) { - commandName.put(c, c.name().toLowerCase()); - } - doNameCommand(); - commandExecute.put(Command.NANO, new CommandMethods(this::nano, this::nanoCompleter)); - commandExecute.put(Command.LESS, new CommandMethods(this::less, this::lessCompleter)); - commandExecute.put(Command.HISTORY, new CommandMethods(this::history, this::historyCompleter)); - commandExecute.put(Command.WIDGET, new CommandMethods(this::widget, this::widgetCompleter)); - commandExecute.put(Command.KEYMAP, new CommandMethods(this::keymap, this::keymapCompleter)); - commandExecute.put(Command.SETOPT, new CommandMethods(this::setopt, this::setoptCompleter)); - commandExecute.put(Command.SETVAR, new CommandMethods(this::setvar, this::setvarCompleter)); - commandExecute.put(Command.UNSETOPT, new CommandMethods(this::unsetopt, this::unsetoptCompleter)); - commandExecute.put(Command.TTOP, new CommandMethods(this::ttop, this::ttopCompleter)); - } - - public Set commandNames() { - return nameCommand.keySet(); - } - - public Map commandAliases() { - return aliasCommand; - } - - private void doNameCommand() { - nameCommand = commandName.entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); - } - - public void setLineReader(LineReader reader) { - this.reader = reader; - } - - public void rename(Command command, String newName) { - if (nameCommand.containsKey(newName)) { - throw new IllegalArgumentException("Duplicate command name!"); - } else if (!commandName.containsKey(command)) { - throw new IllegalArgumentException("Command does not exists!"); - } - commandName.put(command, newName); - doNameCommand(); - } - - public void alias(String alias, String command) { - if (!nameCommand.keySet().contains(command)) { - throw new IllegalArgumentException("Command does not exists!"); - } - aliasCommand.put(alias, command); - } - - @Override - public boolean hasCommand(String name) { - if (nameCommand.containsKey(name) || aliasCommand.containsKey(name)) { - return true; - } - return false; - } - - @Override - public SystemCompleter compileCompleters() { - SystemCompleter out = new SystemCompleter(); - for (Map.Entry entry: commandName.entrySet()) { - out.add(entry.getValue(), commandExecute.get(entry.getKey()).compileCompleter().apply(entry.getValue())); - } - out.addAliases(aliasCommand); - return out; - } - - private Command command(String name) { - Command out = null; - if (!hasCommand(name)) { - throw new IllegalArgumentException("Command does not exists!"); - } - if (aliasCommand.containsKey(name)) { - name = aliasCommand.get(name); - } - if (nameCommand.containsKey(name)) { - out = nameCommand.get(name); - } else { - throw new IllegalArgumentException("Command does not exists!"); - } - return out; - } - - @Override - public Object execute(CommandRegistry.CommandSession session, String command, String[] args) throws Exception { - exception = null; - commandExecute.get(command(command)).execute().accept(new CommandInput(args, session)); - if (exception != null) { - throw exception; - } - return null; - } - - private List commandOptions(String command) { - try { - execute(new CommandRegistry.CommandSession(), command, new String[] {"--help"}); - } catch (HelpException e) { - return compileCommandOptions(e.getMessage()); - } catch (Exception e) { - - } - return null; - } - - private void less(CommandInput input) { - try { - Commands.less(input.terminal(), input.in(), input.out(), input.err(), workDir.get(), input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private void nano(CommandInput input) { - try { - Commands.nano(input.terminal(), input.out(), input.err(), workDir.get(), input.args(), configPath); - } catch (Exception e) { - this.exception = e; - } - } - - private void history(CommandInput input) { - try { - Commands.history(reader, input.out(), input.err(), workDir.get(), input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private void widget(CommandInput input) { - try { - Commands.widget(reader, input.out(), input.err(), widgetCreator, input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private void keymap(CommandInput input) { - try { - Commands.keymap(reader, input.out(), input.err(), input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private void setopt(CommandInput input) { - try { - Commands.setopt(reader, input.out(), input.err(), input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private void setvar(CommandInput input) { - try { - Commands.setvar(reader, input.out(), input.err(), input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private void unsetopt(CommandInput input) { - try { - Commands.unsetopt(reader, input.out(), input.err(), input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private void ttop(CommandInput input) { - try { - TTop.ttop(input.terminal(), input.out(), input.err(), input.args()); - } catch (Exception e) { - this.exception = e; - } - } - - private List unsetOptions(boolean set) { - List out = new ArrayList<>(); - for (Option option : Option.values()) { - if (set == (reader.isSet(option) == option.isDef())) { - out.add((option.isDef() ? "no-" : "") + option.toString().toLowerCase().replace('_', '-')); - } - } - return out; - } - - private Set allWidgets() { - Set out = new HashSet<>(); - for (String s: reader.getWidgets().keySet()) { - out.add(s); - out.add(reader.getWidgets().get(s).toString()); - } - return out; - } - - private List nanoCompleter(String name) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(new FilesCompleter(workDir) - , this::commandOptions - , 1) - )); - return completers; - } - - private List lessCompleter(String name) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(new FilesCompleter(workDir) - , this::commandOptions - , 1) - )); - return completers; - } - - private List historyCompleter(String name) { - List completers = new ArrayList<>(); - List optDescs = commandOptions(commandName.get(Command.HISTORY)); - for (OptDesc o : optDescs) { - if (o.shortOption() != null && (o.shortOption().equals("-A") || o.shortOption().equals("-W") - || o.shortOption().equals("-R"))) { - o.setValueCompleter(new FilesCompleter(workDir)); - } - } - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(NullCompleter.INSTANCE - , optDescs - , 1) - )); - return completers; - } - - private List widgetCompleter(String name) { - List completers = new ArrayList<>(); - List optDescs = commandOptions(commandName.get(Command.WIDGET)); - Candidate aliasOption = new Candidate("-A", "-A", null, null, null, null, true); - Iterator i = optDescs.iterator(); - while (i.hasNext()) { - OptDesc o = i.next(); - if (o.shortOption() != null) { - if (o.shortOption().equals("-D")) { - o.setValueCompleter(new StringsCompleter(() -> reader.getWidgets().keySet())); - } else if (o.shortOption().equals("-A")) { - aliasOption = new Candidate(o.shortOption(), o.shortOption(), null, o.description(), null, null, true); - i.remove(); - } - } - } - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(NullCompleter.INSTANCE - , optDescs - , 1) - )); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter(aliasOption), new StringsCompleter(() -> allWidgets()) - , new StringsCompleter(() -> reader.getWidgets().keySet()), NullCompleter.INSTANCE)); - return completers; - } - - private List keymapCompleter(String name) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(NullCompleter.INSTANCE - , this::commandOptions - , 1) - )); - return completers; - } - - private List setvarCompleter(String name) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter(() -> reader.getVariables().keySet()), NullCompleter.INSTANCE)); - return completers; - } - - private List setoptCompleter(String name) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter(() -> unsetOptions(true)))); - return completers; - } - - private List unsetoptCompleter(String name) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter(() -> unsetOptions(false)))); - return completers; - } - - private List ttopCompleter(String name) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(NullCompleter.INSTANCE - , this::commandOptions - , 1) - )); - return completers; - } - - private static String[] splitToLines(String message) { - return message.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n"); - } - - private static AttributedString highlightComment(String comment) { - return HelpException.highlightComment(comment, HelpException.defaultStyle()); - } - - public static CmdDesc compileCommandDescription(String helpMessage) { - List main = new ArrayList<>(); - Map> options = new HashMap<>(); - String[] msg = splitToLines(helpMessage); - String prevOpt = null; - boolean mainDone = false; - boolean start = false; - for (String s: msg) { - if (!start) { - if (s.trim().startsWith("Usage: ")) { - s = s.split("Usage:")[1]; - start = true; - } else { - continue; - } - } - if (s.matches("^\\s+-.*$")) { - mainDone = true; - int ind = s.lastIndexOf(" "); - if (ind > 0) { - String o = s.substring(0, ind); - String d = s.substring(ind); - if (o.trim().length() > 0) { - prevOpt = o.trim(); - options.put(prevOpt, new ArrayList<>(Arrays.asList(highlightComment(d.trim())))); - } - } - } else if (s.matches("^[\\s]{20}.*$") && prevOpt != null && options.containsKey(prevOpt)) { - int ind = s.lastIndexOf(" "); - if (ind > 0) { - options.get(prevOpt).add(highlightComment(s.substring(ind).trim())); - } - } else { - prevOpt = null; - } - if (!mainDone) { - main.add(HelpException.highlightSyntax(s.trim(), HelpException.defaultStyle())); - } - } - return new CmdDesc(main, ArgDesc.doArgNames(Arrays.asList("")), options); - } - - public static List compileCommandOptions(String helpMessage) { - List out = new ArrayList<>(); - String[] msg = splitToLines(helpMessage); - boolean start = false; - for (String s: msg) { - if (!start) { - if (s.trim().startsWith("Usage: ")) { - s = s.split("Usage:")[1]; - start = true; - } - continue; - } - if (s.matches("^\\s+-.*$")) { - int ind = s.lastIndexOf(" "); - if (ind > 0) { - String[] op = s.substring(0, ind).trim().split("\\s+"); - String d = s.substring(ind).trim(); - String so = null; - String lo = null; - if (op.length == 1) { - if (op[0].startsWith("--")) { - lo = op[0]; - } else { - so = op[0]; - } - } else { - so = op[0]; - lo = op[1]; - } - lo = lo == null ? lo : lo.split("=")[0]; - out.add(new OptDesc(so, lo, d)); - } - } - } - return out; - } - - public static List compileCommandInfo(String helpMessage) { - List out = new ArrayList<>(); - String[] msg = splitToLines(helpMessage); - boolean first = true; - for (String s : msg) { - if (s.trim().startsWith("Usage: ")) { - break; - } else { - if (first && s.contains(" - ")) { - out.add(s.substring(s.indexOf(" - ") + 3).trim()); - } else { - out.add(s.trim()); - } - first = false; - } - } - return out; - } - - public static class CommandInput { - String[] args; - Object[] xargs; - Terminal terminal; - InputStream in; - PrintStream out; - PrintStream err; - - public CommandInput(String[] args, CommandRegistry.CommandSession session) { - this(args, null, session); - } - - public CommandInput(String[] args, Object[] xargs, CommandRegistry.CommandSession session) { - this(args, session.terminal(), session.in(), session.out(), session.err()); - if (xargs != null) { - this.xargs = xargs; - this.args = new String[xargs.length]; - for (int i = 0; i < xargs.length; i++) { - this.args[i] = xargs[i] != null ? xargs[i].toString() : ""; - } - } - } - - public CommandInput(String[] args, Terminal terminal, InputStream in, PrintStream out, PrintStream err) { - this.args = args; - this.terminal = terminal; - this.in = in; - this.out = out; - this.err = err; - } - - public String[] args() { - return args; - } - - public Object[] xargs() { - return xargs; - } - - public Terminal terminal() { - return terminal; - } - - public InputStream in() { - return in; - } - - public PrintStream out() { - return out; - } - - public PrintStream err() { - return err; - } - - } - - public static class CommandMethods { - Consumer execute; - Function executeFunction; - Function> compileCompleter; - - public CommandMethods(Function execute, Function> compileCompleter) { - this.executeFunction = execute; - this.compileCompleter = compileCompleter; - } - - public CommandMethods(Consumer execute, Function> compileCompleter) { - this.execute = execute; - this.compileCompleter = compileCompleter; - } - - public Consumer execute() { - return execute; - } - - public Function executeFunction() { - return executeFunction; - } - - public Function> compileCompleter() { - return compileCompleter; - } - } - -} - diff --git a/builtins/src/main/java/org/jline/builtins/Commands.java b/builtins/src/main/java/org/jline/builtins/Commands.java index 69ca2617a..a2bfa189e 100644 --- a/builtins/src/main/java/org/jline/builtins/Commands.java +++ b/builtins/src/main/java/org/jline/builtins/Commands.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author or authors. + * Copyright (c) 2002-2020, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -8,6 +8,7 @@ */ package org.jline.builtins; +import java.io.BufferedReader; import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -40,13 +41,11 @@ import java.util.function.Supplier; import org.jline.builtins.Completers.CompletionData; -import org.jline.builtins.Options; import org.jline.builtins.Options.HelpException; import org.jline.builtins.Source.StdInSource; import org.jline.builtins.Source.URLSource; import org.jline.keymap.KeyMap; import org.jline.reader.Binding; -import org.jline.reader.ConfigurationPath; import org.jline.reader.Highlighter; import org.jline.reader.History; import org.jline.reader.LineReader; @@ -57,6 +56,7 @@ import org.jline.terminal.Terminal; import org.jline.utils.AttributedStringBuilder; import org.jline.utils.AttributedStyle; +import org.jline.utils.StyleResolver; public class Commands { @@ -1034,4 +1034,181 @@ public static void setvar(LineReader lineReader, PrintStream out, PrintStream er } } + public static void colors(Terminal terminal, PrintStream out, String[] argv) throws HelpException, IOException { + String[] usage = { + "colors - view 256-color table", + "Usage: colors [OPTIONS]", + " -? --help Displays command help", + " -c --columns=COLUMNS Number of columns in name table", + " -n --name Color name table (default number table)", + " -s --small View 16-color table (default 256-color)" + }; + Options opt = Options.compile(usage).parse(argv); + if (opt.isSet("help")) { + throw new Options.HelpException(opt.usage()); + } + int columns = 256; + if (opt.isSet("columns")) { + columns = opt.getNumber("columns"); + } + new Colors(terminal, out).printColors(opt.isSet("name"), opt.isSet("small"), columns); + } + + private static class Colors { + boolean name; + private final Terminal terminal; + private final PrintStream out; + private final List colors = Arrays.asList("black","red","green","yellow","blue","magenta","cyan","white" + , "!black","!red","!green","!yellow","!blue","!magenta","!cyan","!white"); + + public Colors(Terminal terminal, PrintStream out) { + this.terminal = terminal; + this.out = out; + } + + private String getStyle(String color) { + String out; + char fg = ' '; + if (name) { + out = "bg:~" + color.substring(1); + fg = color.charAt(0); + } else if (color.substring(1).matches("\\d+")) { + out = "48;5;" + color.substring(1); + fg = color.charAt(0); + } else { + out = "bg:" + color; + } + if (color.startsWith("!") || color.equals("white") || fg == 'b') { + out += ",fg:black"; + } else { + out += ",fg:!white"; + } + return out; + } + + private String foreground(int idx) { + String fg = "w"; + if ((idx > 6 && idx < 16) + || (idx > 33 && idx < 52) + || (idx > 69 && idx < 88) + || (idx > 105 && idx < 124) + || (idx > 141 && idx < 160) + || (idx > 177 && idx < 196) + || (idx > 213 && idx < 232) + || idx > 243) { + fg = "b"; + } + return fg; + } + + private String addPadding(int width, String field) { + int s = width - field.length(); + int left = s/2; + StringBuilder lp = new StringBuilder(); + StringBuilder rp = new StringBuilder(); + for (int i = 0; i < left; i++) { + lp.append(" "); + } + for (int i = 0; i < s - left; i++) { + rp.append(" "); + } + return lp.toString() + field + rp.toString(); + } + + public void printColors(boolean name, boolean small, int columns) throws IOException { + this.name = name; + AttributedStringBuilder asb = new AttributedStringBuilder(); + int width = terminal.getWidth(); + String tableName = small ? " 16-color " : "256-color "; + if (!name) { + out.print(tableName); + out.print("table, fg: "); + if (!small) { + out.print("/ 38;5;"); + } + out.println(); + out.print(" bg: "); + if (!small) { + out.print("/ 48;5;"); + } + out.println("\n"); + boolean narrow = width < 180; + for (String c : colors) { + AttributedStyle ss = new StyleResolver(this::getStyle).resolve('.' + c, null); + asb.style(ss); + asb.append(addPadding(11,c)); + asb.style(AttributedStyle.DEFAULT); + if (c.equals("white")) { + if (narrow || small) { + asb.append('\n'); + } else { + asb.append(" "); + } + } else if (c.equals("!white")) { + asb.append('\n'); + } + } + asb.append('\n'); + if (!small) { + for (int i = 16; i < 256; i++) { + String fg = foreground(i); + String code = Integer.toString(i); + AttributedStyle ss = new StyleResolver(this::getStyle).resolve("." + fg + code, null); + asb.style(ss); + String str = " "; + if (i < 100) { + str = " "; + } else if (i > 231) { + str = i % 2 == 0 ? " " : " "; + } + asb.append(str).append(code).append(' '); + if (i == 51 || i == 87 || i == 123 || i == 159 || i == 195 || i == 231 + || narrow + && (i == 33 || i == 69 || i == 105 || i == 141 || i == 177 || i == 213 || i == 243) + ) { + asb.style(AttributedStyle.DEFAULT); + asb.append('\n'); + if (i == 231) { + asb.append('\n'); + } + } + } + } + } else { + out.print(tableName); + out.println("table, fg:~ OR 38;5;"); + out.println(" bg:~ OR 48;5;"); + out.println(); + InputStream inputStream = new Source.ResourceSource("/org/jline/utils/colors.txt", null).read(); + BufferedReader reader = new BufferedReader(new java.io.InputStreamReader(inputStream)); + String line = reader.readLine(); + int col = 0; + Integer idx = 0; + int colWidth = 22; + while (line != null) { + line = line.trim(); + if (!line.isEmpty() && !line.startsWith("#")) { + String fg = foreground(idx); + AttributedStyle ss = new StyleResolver(this::getStyle).resolve("." + fg + line, null); + asb.style(ss); + asb.append(String.valueOf(idx)).append(addPadding(colWidth - idx.toString().length(), line)); + col++; + idx++; + if ((col + 1)*colWidth > width || col + 1 > columns) { + col = 0; + asb.style(AttributedStyle.DEFAULT); + asb.append('\n'); + } + } + if (small && idx == 16) { + break; + } + line = reader.readLine(); + } + reader.close(); + } + asb.toAttributedString().println(terminal); + } + } + } diff --git a/builtins/src/main/java/org/jline/builtins/Completers.java b/builtins/src/main/java/org/jline/builtins/Completers.java index 90063f027..18dcd976b 100644 --- a/builtins/src/main/java/org/jline/builtins/Completers.java +++ b/builtins/src/main/java/org/jline/builtins/Completers.java @@ -20,10 +20,8 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Function; @@ -32,15 +30,14 @@ import org.jline.reader.Candidate; import org.jline.reader.LineReader; import org.jline.reader.LineReader.Option; -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.ParsedLine; import org.jline.terminal.Terminal; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; +import org.jline.utils.OSUtils; +import org.jline.utils.StyleResolver; public class Completers { @@ -330,6 +327,7 @@ protected String getSeparator(boolean useForwardSlash) { */ public static class FileNameCompleter implements org.jline.reader.Completer { + protected static StyleResolver resolver = Styles.lsStyle(); public void complete(LineReader reader, ParsedLine commandLine, final List candidates) { assert commandLine != null; @@ -363,10 +361,10 @@ public void complete(LineReader reader, ParsedLine commandLine, final List> completers = new HashMap<>(); - private Map aliasCommand = new HashMap<>(); - private StringsCompleter commands; - private boolean compiled = false; - - public SystemCompleter() {} - - @Override - public void complete(LineReader reader, ParsedLine commandLine, List candidates) { - if (!compiled) { - throw new IllegalStateException(); - } - assert commandLine != null; - assert candidates != null; - if (commandLine.words().size() > 0) { - if (commandLine.words().size() == 1) { - String buffer = commandLine.words().get(0); - int eq = buffer.indexOf('='); - if (eq < 0) { - commands.complete(reader, commandLine, candidates); - } else if (reader.getParser().validVariableName(buffer.substring(0, eq))) { - String curBuf = buffer.substring(0, eq + 1); - for (String c: completers.keySet()) { - candidates.add(new Candidate(AttributedString.stripAnsi(curBuf+c) - , c, null, null, null, null, true)); - } - } - } else { - String cmd = reader.getParser().getCommand(commandLine.words().get(0)); - if (command(cmd) != null) { - completers.get(command(cmd)).get(0).complete(reader, commandLine, candidates); - } - } - } - } - - public boolean isCompiled() { - return compiled; - } - - private String command(String cmd) { - String out = null; - if (cmd != null) { - if (completers.containsKey(cmd)) { - out = cmd; - } else if (aliasCommand.containsKey(cmd)) { - out = aliasCommand.get(cmd); - } - } - return out; - } - - public void add(String command, List completers) { - for (org.jline.reader.Completer c : completers) { - add(command, c); - } - } - - public void add(List commands, org.jline.reader.Completer completer) { - for (String c: commands) { - add(c, completer); - } - } - - public void add(String command, org.jline.reader.Completer completer) { - Objects.requireNonNull(command); - if (compiled) { - throw new IllegalStateException(); - } - if (!completers.containsKey(command)) { - completers.put(command, new ArrayList()); - } - if (completer instanceof ArgumentCompleter) { - ((ArgumentCompleter) completer).setStrictCommand(false); - } - completers.get(command).add(completer); - } - - public void add(SystemCompleter other) { - if (other.isCompiled()) { - throw new IllegalStateException(); - } - for (Map.Entry> entry: other.getCompleters().entrySet()) { - for (org.jline.reader.Completer c: entry.getValue()) { - add(entry.getKey(), c); - } - } - addAliases(other.getAliases()); - } - - public void addAliases(Map aliasCommand) { - if (compiled) { - throw new IllegalStateException(); - } - this.aliasCommand.putAll(aliasCommand); - } - - public Map getAliases() { - return aliasCommand; - } - - public void compile() { - if (compiled) { - return; - } - Map> compiledCompleters = new HashMap<>(); - for (Map.Entry> entry: completers.entrySet()) { - if (entry.getValue().size() == 1) { - compiledCompleters.put(entry.getKey(), entry.getValue()); - } else { - compiledCompleters.put(entry.getKey(), new ArrayList()); - compiledCompleters.get(entry.getKey()).add(new AggregateCompleter(entry.getValue())); - } - } - completers = compiledCompleters; - Set cmds = new HashSet<>(completers.keySet()); - cmds.addAll(aliasCommand.keySet()); - commands = new StringsCompleter(cmds); - compiled = true; - } - - public Map> getCompleters() { - return completers; - } - } - public static class OptDesc { private String shortOption; private String longOption; @@ -705,14 +580,14 @@ protected static List compile(Map> optionValues, Co for (Map.Entry> entry: optionValues.entrySet()) { if (entry.getKey().startsWith("--")) { out.add(new OptDesc(null, entry.getKey(), new StringsCompleter(entry.getValue()))); - } else if (entry.getKey().matches("-[a-zA-Z]{1}")) { + } else if (entry.getKey().matches("-[a-zA-Z]")) { out.add(new OptDesc(entry.getKey(), null, new StringsCompleter(entry.getValue()))); } } for (String o: options) { if (o.startsWith("--")) { out.add(new OptDesc(null, o)); - } else if (o.matches("-[a-zA-Z]{1}")) { + } else if (o.matches("-[a-zA-Z]")) { out.add(new OptDesc(o, null)); } } @@ -815,9 +690,10 @@ protected boolean completeValue(LineReader reader, final ParsedLine commandLine, if (v.startsWith(partialValue)) { out = true; String val = c.value(); - if (valueCompleter instanceof Completers.FilesCompleter - || valueCompleter instanceof Completers.DirectoriesCompleter) { - val = FileNameCompleter.getDisplay(reader.getTerminal(), Paths.get(c.value())); + if (valueCompleter instanceof FileNameCompleter) { + FileNameCompleter cc = (FileNameCompleter)valueCompleter; + String sep = cc.getSeparator(reader.isSet(LineReader.Option.USE_FORWARD_SLASH)); + val = cc.getDisplay(reader.getTerminal(), Paths.get(c.value()), FileNameCompleter.resolver, sep); } candidates.add(new Candidate(curBuf + v, val, null, null, null, null, c.complete())); } @@ -931,6 +807,10 @@ public OptionCompleter(Collection options, int startPos) { this.startPos = startPos; } + public void setStartPos(int startPos) { + this.startPos = startPos; + } + @Override public void complete(LineReader reader, final ParsedLine commandLine, List candidates) { assert commandLine != null; @@ -941,12 +821,12 @@ public void complete(LineReader reader, final ParsedLine commandLine, List usedOptions = new ArrayList<>(); for (int i = startPos; i < words.size(); i++) { @@ -985,11 +865,14 @@ public void complete(LineReader reader, final ParsedLine commandLine, List 1 && shortOptionValueCompleter(command, words.get(words.size() - 2)) != null) { shortOptionValueCompleter(command, words.get(words.size() - 2)).complete(reader, commandLine, candidates); + } else if (words.size() > 1 && longOptionValueCompleter(command, words.get(words.size() - 2)) != null) { + longOptionValueCompleter(command, words.get(words.size() - 2)).complete(reader, commandLine, candidates); } else if (!argsCompleters.isEmpty()) { int args = -1; for (int i = startPos; i < words.size(); i++) { if (!words.get(i).startsWith("-")) { - if (i > 0 && shortOptionValueCompleter(command, words.get(i - 1)) == null) { + if (i > 0 && shortOptionValueCompleter(command, words.get(i - 1)) == null + && longOptionValueCompleter(command, words.get(i - 1)) == null) { args++; } } @@ -1004,6 +887,15 @@ public void complete(LineReader reader, final ParsedLine commandLine, List optDescs = commandOptions == null ? options : commandOptions.apply(command); + OptDesc option = findOptDesc(optDescs, opt); + return option.hasValue() ? option.valueCompleter() : null; + } + private org.jline.reader.Completer shortOptionValueCompleter(String command, String opt) { if (!opt.matches("-[a-zA-Z]+")) { return null; @@ -1038,4 +930,18 @@ private OptDesc findOptDesc(Collection optDescs, String opt) { return new OptDesc(); } } + + public static class AnyCompleter implements org.jline.reader.Completer { + public static final AnyCompleter INSTANCE = new AnyCompleter(); + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + String buffer = commandLine.word().substring(0, commandLine.wordCursor()); + candidates.add(new Candidate(AttributedString.stripAnsi(buffer) + , buffer, null, null, null, null, true)); + } + } + } diff --git a/reader/src/main/java/org/jline/reader/ConfigurationPath.java b/builtins/src/main/java/org/jline/builtins/ConfigurationPath.java similarity index 98% rename from reader/src/main/java/org/jline/reader/ConfigurationPath.java rename to builtins/src/main/java/org/jline/builtins/ConfigurationPath.java index d44c2bb3b..c15d49368 100644 --- a/reader/src/main/java/org/jline/reader/ConfigurationPath.java +++ b/builtins/src/main/java/org/jline/builtins/ConfigurationPath.java @@ -6,7 +6,7 @@ * * https://opensource.org/licenses/BSD-3-Clause */ -package org.jline.reader; +package org.jline.builtins; import java.io.IOException; import java.nio.file.Path; diff --git a/builtins/src/main/java/org/jline/builtins/ConsoleOptionGetter.java b/builtins/src/main/java/org/jline/builtins/ConsoleOptionGetter.java new file mode 100644 index 000000000..955736cc2 --- /dev/null +++ b/builtins/src/main/java/org/jline/builtins/ConsoleOptionGetter.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.builtins; + +public interface ConsoleOptionGetter { + + /** + * Return console option value + * @param name the option name + * @return option value + */ + Object consoleOption(String name); +} diff --git a/builtins/src/main/java/org/jline/builtins/Less.java b/builtins/src/main/java/org/jline/builtins/Less.java index ffee353d8..1d0a8f797 100644 --- a/builtins/src/main/java/org/jline/builtins/Less.java +++ b/builtins/src/main/java/org/jline/builtins/Less.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author or authors. + * Copyright (c) 2002-2020, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -22,12 +22,7 @@ import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -38,7 +33,6 @@ import org.jline.builtins.Source.URLSource; import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; -import org.jline.reader.ConfigurationPath; import org.jline.terminal.Attributes; import org.jline.terminal.Size; import org.jline.terminal.Terminal; @@ -73,7 +67,7 @@ public class Less { public boolean ignoreCaseAlways; public boolean noKeypad; public boolean noInit; - protected List tabs = Arrays.asList(4); + protected List tabs = Collections.singletonList(4); protected String syntaxName; private String historyLog = null; @@ -112,11 +106,12 @@ public class Less { protected final Size size = new Size(); SyntaxHighlighter syntaxHighlighter; - private List syntaxFiles = new ArrayList<>(); + private final List syntaxFiles = new ArrayList<>(); private boolean highlight = true; + private boolean nanorcIgnoreErrors; public static String[] usage() { - final String[] usage = { + return new String[]{ "less - file pager", "Usage: less [OPTIONS] [FILES]", " -? --help Show help", @@ -136,7 +131,6 @@ public static String[] usage() { " --ignorercfiles Don't look at the system's lessrc nor at the user's lessrc.", " -H --historylog=name Log search strings to file, so they can be retrieved in later sessions" }; - return usage; } public Less(Terminal terminal, Path currentDir) { @@ -164,7 +158,8 @@ public Less(Terminal terminal, Path currentDir, Options opts, ConfigurationPath PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:/usr/share/nano/*.nanorc"); try { Files.find(Paths.get("/usr/share/nano"), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path)) - .forEach(p -> syntaxFiles.add(p)); + .forEach(syntaxFiles::add); + nanorcIgnoreErrors = true; } catch (IOException e) { errorMessage = "Encountered error while reading nanorc files"; } @@ -202,6 +197,7 @@ public Less(Terminal terminal, Path currentDir, Options opts, ConfigurationPath } if (opts.isSet("syntax")) { syntaxName = opts.get("syntax"); + nanorcIgnoreErrors = false; } if (opts.isSet("no-init")) { noInit = true; @@ -233,7 +229,7 @@ private void parseConfig(Path file) throws IOException { if (parts.get(1).contains("*") || parts.get(1).contains("?")) { PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parts.get(1)); Files.find(Paths.get(new File(parts.get(1)).getParent()), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path)) - .forEach(p -> syntaxFiles.add(p)); + .forEach(syntaxFiles::add); } else { syntaxFiles.add(Paths.get(parts.get(1))); } @@ -687,7 +683,7 @@ private void moveToMatch(boolean forward, boolean spanFiles) throws IOException } private class LineEditor { - private int begPos; + private final int begPos; public LineEditor(int begPos) { this.begPos = begPos; @@ -841,10 +837,10 @@ private void addFile() throws IOException, InterruptedException { LineEditor lineEditor = new LineEditor(begPos); while (true) { checkInterrupted(); - Operation op = null; - switch (op=bindingReader.readBinding(fileKeyMap)) { + Operation op; + switch (op = bindingReader.readBinding(fileKeyMap)) { case ACCEPT: - String name = buffer.toString().substring(begPos); + String name = buffer.substring(begPos); addSource(name); try { openSource(); @@ -894,8 +890,8 @@ private boolean search() throws IOException, InterruptedException { LineEditor lineEditor = new LineEditor(begPos); while (true) { checkInterrupted(); - Operation op = null; - switch (op=bindingReader.readBinding(searchKeyMap)) { + Operation op; + switch (op = bindingReader.readBinding(searchKeyMap)) { case UP: buffer.setLength(0); buffer.append(type); @@ -910,7 +906,7 @@ private boolean search() throws IOException, InterruptedException { break; case ACCEPT: try { - String _pattern = buffer.toString().substring(1); + String _pattern = buffer.substring(1); if (type == '&') { displayPattern = _pattern.length() > 0 ? _pattern : null; getPattern(true); @@ -969,7 +965,7 @@ private void help() throws IOException { try { openSource(); display(false); - Operation op = null; + Operation op; do { checkInterrupted(); op = bindingReader.readBinding(keys, null, false); @@ -998,7 +994,7 @@ protected void openSource() throws IOException { reader.close(); wasOpen = true; } - boolean open = false; + boolean open; boolean displayMessage = false; do { Source source = sources.get(sourceIdx); @@ -1021,7 +1017,7 @@ protected void openSource() throws IOException { if (sourceIdx == 0) { syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, null, "none"); } else { - syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, source.getName(), syntaxName); + syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, source.getName(), syntaxName, nanorcIgnoreErrors); } open = true; if (displayMessage) { @@ -1277,7 +1273,7 @@ int getStrictPositiveNumberInBuffer(int def) { } private Pair nextLine2display(int line, Pattern dpCompiled) throws IOException { - AttributedString curLine = null; + AttributedString curLine; do { curLine = getLine(line++); } while (!toBeDisplayed(curLine, dpCompiled)); @@ -1285,7 +1281,7 @@ private Pair nextLine2display(int line, Pattern dpCom } private Pair prevLine2display(int line, Pattern dpCompiled) throws IOException { - AttributedString curLine = null; + AttributedString curLine; do { curLine = getLine(line--); } while (line > 0 && !toBeDisplayed(curLine, dpCompiled)); diff --git a/builtins/src/main/java/org/jline/builtins/Nano.java b/builtins/src/main/java/org/jline/builtins/Nano.java index 696e99dcc..b5299d161 100644 --- a/builtins/src/main/java/org/jline/builtins/Nano.java +++ b/builtins/src/main/java/org/jline/builtins/Nano.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author or authors. + * Copyright (c) 2002-2020, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -20,6 +20,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; +import java.net.URL; import java.nio.charset.Charset; import java.nio.file.FileSystems; import java.nio.file.Files; @@ -41,10 +42,10 @@ import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; -import org.jline.reader.ConfigurationPath; import org.jline.reader.Editor; import org.jline.terminal.Attributes; import org.jline.terminal.Attributes.ControlChar; @@ -78,6 +79,7 @@ public class Nano implements Editor { protected final Size size; protected final Path root; protected final int vsusp; + private final List syntaxFiles = new ArrayList<>(); // Keys protected KeyMap keys; @@ -131,10 +133,9 @@ public class Nano implements Editor { protected List cutbuffer = new ArrayList<>(); protected boolean mark = false; protected boolean highlight = true; - private List syntaxFiles = new ArrayList<>(); private boolean searchToReplace = false; - protected boolean readNewBuffer = true; + private boolean nanorcIgnoreErrors; protected enum WriteMode { WRITE, @@ -155,7 +156,7 @@ protected enum CursorMovement { } public static String[] usage() { - final String[] usage = { + return new String[]{ "nano - edit files", "Usage: nano [OPTIONS] [FILES]", " -? --help Show help", @@ -184,7 +185,6 @@ public static String[] usage() { " -E --tabstospaces Convert typed tabs to spaces.", " -i --autoindent Indent new lines to the previous line's indentation." }; - return usage; } protected class Buffer { @@ -210,7 +210,7 @@ protected class Buffer { protected Buffer(String file) { this.file = file; - this.syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, file, syntaxName); + this.syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, file, syntaxName, nanorcIgnoreErrors); } void open() throws IOException { @@ -433,7 +433,7 @@ LinkedList computeOffsets(String line) { } boolean isBreakable(char ch) { - return atBlanks ? ch == ' ' : true; + return !atBlanks || ch == ' '; } void moveToChar(int pos) { @@ -480,7 +480,6 @@ boolean backspace(int count) { lines.remove(line + 1); offsets.remove(line + 1); count--; - dirty = true; } else { int nb = Math.min(pos, count); int curPos = length(text.substring(0, pos - nb)); @@ -489,8 +488,8 @@ boolean backspace(int count) { offsets.set(line, computeOffsets(text)); moveToChar(curPos); count -= nb; - dirty = true; } + dirty = true; } ensureCursorVisible(); return true; @@ -528,7 +527,7 @@ boolean moveRight(int chars, boolean fromBeginning) { firstColumnToDisplay = 0; offsetInLine = 0; column = 0; - chars = chars <= length(getLine(line)) ? chars : length(getLine(line)); + chars = Math.min(chars, length(getLine(line))); } boolean ret = true; while (--chars >= 0) { @@ -758,14 +757,14 @@ List computeHeader() { int nb = max - p1.length() - "File: ...".length(); int cut; cut = Math.max(0, Math.min(p0.length(), p0.length() - nb)); - middle = "File: ..." + p0.substring(cut, p0.length()) + p1; + middle = "File: ..." + p0.substring(cut) + p1; } if (middle == null || middle.length() > max) { left = null; max = mend - 2; int nb = max - "File: ...".length(); int cut = Math.max(0, Math.min(src.length(), src.length() - nb)); - middle = "File: ..." + src.substring(cut, src.length()); + middle = "File: ..." + src.substring(cut); if (middle.length() > max) { middle = middle.substring(0, max); } @@ -945,7 +944,7 @@ public void moveTo(int x, int y) { public void gotoLine(int x, int y) { line = y < lines.size() ? y : lines.size() - 1; - x = x <= length(lines.get(line)) ? x : length(lines.get(line)); + x = Math.min(x, length(lines.get(line))); firstLineToDisplay = line > 0 ? line - 1 : line; offsetInLine = 0; offsetInLineToDisplay = 0; @@ -1419,30 +1418,48 @@ void replaceFromCursor(int chars, String string) { } } + /** + * Java implementation of nanorc highlighter + * + * @author Matti Rinta-Nikkola + */ public static class SyntaxHighlighter { - private List rules = new ArrayList<>(); + private final List rules = new ArrayList<>(); + private boolean startEndHighlight; private int ruleStartId = 0; private SyntaxHighlighter() {} protected static SyntaxHighlighter build(List syntaxFiles, String file, String syntaxName) { + return build(syntaxFiles, file, syntaxName, false); + } + + protected static SyntaxHighlighter build(List syntaxFiles, String file, String syntaxName + , boolean ignoreErrors) { SyntaxHighlighter out = new SyntaxHighlighter(); List defaultRules = new ArrayList<>(); - if (syntaxName == null || (syntaxName != null && !syntaxName.equals("none"))) { - for (Path p: syntaxFiles) { - NanorcParser parser = new NanorcParser(p, syntaxName, file); - try { - parser.parse(); - if (parser.matches()) { - out.addRules(parser.getHighlightRules()); - return out; - } else if (parser.isDefault()) { - defaultRules.addAll(parser.getHighlightRules()); + try { + if (syntaxName == null || (syntaxName != null && !syntaxName.equals("none"))) { + for (Path p : syntaxFiles) { + try { + NanorcParser parser = new NanorcParser(p, syntaxName, file); + parser.parse(); + if (parser.matches()) { + out.addRules(parser.getHighlightRules()); + return out; + } else if (parser.isDefault()) { + defaultRules.addAll(parser.getHighlightRules()); + } + } catch (IOException e) { + // ignore } - } catch (IOException e) { } + out.addRules(defaultRules); + } + } catch (PatternSyntaxException e) { + if (!ignoreErrors) { + throw e; } - out.addRules(defaultRules); } return out; } @@ -1472,7 +1489,7 @@ public static SyntaxHighlighter build(Path nanorc, String syntaxName) { Paths.get(new File(parts.get(1)).getParent()), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path)) - .forEach(p -> syntaxFiles.add(p)); + .forEach(syntaxFiles::add); } else { syntaxFiles.add(Paths.get(parts.get(1))); } @@ -1483,6 +1500,31 @@ public static SyntaxHighlighter build(Path nanorc, String syntaxName) { reader.close(); out = build(syntaxFiles, null, syntaxName); } catch (Exception e) { + // ignore + } + return out; + } + + /** + * Build SyntaxHighlighter + * + * @param nanorcUrl Url of nanorc file + * @return SyntaxHighlighter + */ + public static SyntaxHighlighter build(String nanorcUrl) { + SyntaxHighlighter out = new SyntaxHighlighter(); + InputStream inputStream; + try { + if (nanorcUrl.startsWith("classpath:")) { + inputStream = new Source.ResourceSource(nanorcUrl.substring(10), null).read(); + } else { + inputStream = new Source.URLSource(new URL(nanorcUrl), null).read(); + } + NanorcParser parser = new NanorcParser(inputStream, null, null); + parser.parse(); + out.addRules(parser.getHighlightRules()); + } catch (IOException e) { + // ignore } return out; } @@ -1493,6 +1535,7 @@ private void addRules(List rules) { public void reset() { ruleStartId = 0; + startEndHighlight = false; } public AttributedString highlight(String string) { @@ -1509,7 +1552,9 @@ public AttributedString highlight(AttributedString line) { } AttributedStringBuilder asb = new AttributedStringBuilder(); asb.append(line); - for (int i = ruleStartId; i < rules.size(); i++) { + int startId = ruleStartId; + boolean endHighlight = startEndHighlight; + for (int i = startId; i < (endHighlight ? startId + 1 : rules.size()); i++) { HighlightRule rule = rules.get(i); switch (rule.getType()) { case PATTERN: @@ -1521,12 +1566,12 @@ public AttributedString highlight(AttributedString line) { Matcher end = rule.getEnd().matcher(asb.toAttributedString()); while (!done) { AttributedStringBuilder a = new AttributedStringBuilder(); - if (ruleStartId == i) { // first rule should never be type - // START_END or we will fail here! + if (startEndHighlight && ruleStartId == i) { if (end.find()) { - a.append(asb.columnSubSequence(0, end.end()),rule.getStyle()); + a.append(asb.columnSubSequence(0, end.end()), rule.getStyle()); a.append(asb.columnSubSequence(end.end(), asb.length())); ruleStartId = 0; + startEndHighlight = false; } else { a.append(asb, rule.getStyle()); done = true; @@ -1540,6 +1585,7 @@ public AttributedString highlight(AttributedString line) { a.append(asb.columnSubSequence(end.end(), asb.length())); } else { ruleStartId = i; + startEndHighlight = true; a.append(asb.columnSubSequence(start.start(),asb.length()), rule.getStyle()); done = true; } @@ -1554,13 +1600,14 @@ public AttributedString highlight(AttributedString line) { } return asb.toAttributedString(); } + } private static class HighlightRule { - public enum RuleType {PATTERN, START_END}; - private RuleType type; + public enum RuleType {PATTERN, START_END} + private final RuleType type; private Pattern pattern; - private AttributedStyle style; + private final AttributedStyle style; private Pattern start; private Pattern end; @@ -1621,30 +1668,42 @@ public static RuleType evalRuleType(List colorCfg) { private static class NanorcParser { private static final String DEFAULT_SYNTAX = "default"; - private File file; - private String name; - private String target; + private final String name; + private final String target; + private final List highlightRules = new ArrayList<>(); + private final BufferedReader reader; private boolean matches = false; - private List highlightRules = new ArrayList<>(); private String syntaxName; - public NanorcParser(Path file, String name) { - this(file, name, null); + public NanorcParser(Path file, String name, String target) throws IOException { + this(new Source.PathSource(file, null).read(), name, target); } - public NanorcParser(Path file, String name, String target) { - this.file = file.toFile(); + public NanorcParser(InputStream in, String name, String target) { + this.reader = new BufferedReader(new InputStreamReader(in)); this.name = name; this.target = target; } public void parse() throws IOException { - BufferedReader reader = new BufferedReader(new FileReader(file)); String line = reader.readLine(); - while (line!= null) { + while (line != null) { line = line.trim(); if (line.length() > 0 && !line.startsWith("#")) { - line = line.replaceAll("\\\\<", "\\\\b").replaceAll("\\\\>", "\\\\b").replaceAll("\\[\\[:space:\\]\\]", "\\\\s"); + line = line.replaceAll("\\\\<", "\\\\b") + .replaceAll("\\\\>", "\\\\b") + .replaceAll("\\[:alnum:]", "\\\\p{Alnum}") + .replaceAll("\\[:alpha:]", "\\\\p{Alpha}") + .replaceAll("\\[:blank:]", "\\\\p{Blank}") + .replaceAll("\\[:cntrl:]", "\\\\p{Cntrl}") + .replaceAll("\\[:digit:]", "\\\\p{Digit}") + .replaceAll("\\[:graph:]", "\\\\p{Graph}") + .replaceAll("\\[:lower:]", "\\\\p{Lower}") + .replaceAll("\\[:print:]", "\\\\p{Print}") + .replaceAll("\\[:punct:]", "\\\\p{Punct}") + .replaceAll("\\[:space:]", "\\\\s") + .replaceAll("\\[:upper:]", "\\\\p{Upper}") + .replaceAll("\\[:xdigit:]", "\\\\p{XDigit}"); List parts = Parser.split(line); if (parts.get(0).equals("syntax")) { syntaxName = parts.get(1); @@ -1718,42 +1777,63 @@ private Integer toColor(String styleString) { out += AttributedStyle.MAGENTA; } else if (styleString.equals("cyan")) { out += AttributedStyle.CYAN; + } else if (styleString.matches("\\d+")) { + out = Integer.parseInt(styleString); } } return out; } + private AttributedStyle setStyle(String name, AttributedStyle style) { + AttributedStyle out = style; + switch (name) { + case "blink": + out = style.blink(); + break; + case "bold": + out = style.bold(); + break; + case "conceal": + out = style.conceal(); + break; + case "faint": + out = style.faint(); + break; + case "hidden": + out = style.hidden(); + break; + case "inverse": + out = style.inverse(); + break; + case "italic": + out = style.italic(); + break; + case "underline": + out = style.underline(); + break; + default: + } + return out; + } + private void addHighlightRule(List parts, boolean caseInsensitive) { AttributedStyle style = AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT); String[] styleStrings = parts.get(1).split(","); - Integer fcolor = toColor(styleStrings[0]); - Integer bcolor = styleStrings.length > 1 ? toColor(styleStrings[1]) : null; + List styles = Arrays.asList("blink", "bold", "conceal", "faint", "hidden", "inverse", "italic", "underline"); + int colorStart = 0; + // GNU nano version >= v5 support styles: bold and italic, rest jline extension + while (styles.contains(styleStrings[colorStart])) { + style = setStyle(styleStrings[colorStart], style); + colorStart++; + } + Integer fcolor = toColor(styleStrings[colorStart]); + Integer bcolor = styleStrings.length > 1 + colorStart ? toColor(styleStrings[colorStart + 1]) : null; if (fcolor != null) { style = style.foreground(fcolor); } if (bcolor != null) { style = style.background(bcolor); } - // extended nanorc.. - if (styleStrings.length > 2) { - if (styleStrings[2].equals("blink")) { - style = style.blink(); - } else if (styleStrings[2].equals("bold")) { - style = style.bold(); - } else if (styleStrings[2].equals("conceal")) { - style = style.conceal(); - } else if (styleStrings[2].equals("faint")) { - style = style.faint(); - } else if (styleStrings[2].equals("hidden")) { - style = style.hidden(); - } else if (styleStrings[2].equals("inverse")) { - style = style.inverse(); - } else if (styleStrings[2].equals("italic")) { - style = style.italic(); - } else if (styleStrings[2].equals("underline")) { - style = style.underline(); - } - } if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PATTERN) { for (int i = 2; i < parts.size(); i++) { @@ -1777,7 +1857,7 @@ private Pattern doPattern(String regex, boolean caseInsensitive) { protected static class Parser { protected static List split(String s) { - List out = new ArrayList(); + List out = new ArrayList<>(); if (s.length() == 0) { return out; } @@ -1786,13 +1866,20 @@ protected static List split(String s) { for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '"') { - depth = depth == 0 ? 1 : 0; - } else if (c ==' ' && depth == 0 && sb.length() > 0) { + if (depth == 0) { + depth = 1; + } else { + char nextChar = i < s.length() - 1 ? s.charAt(i + 1) : ' '; + if (nextChar == ' ') { + depth = 0; + } + } + } else if (c == ' ' && depth == 0 && sb.length() > 0) { out.add(stripQuotes(sb.toString())); sb = new StringBuilder(); continue; } - if (sb.length() > 0 || (c!=' ' && c!='\t')) { + if (sb.length() > 0 || (c != ' ' && c != '\t')) { sb.append(c); } } @@ -1812,8 +1899,8 @@ private static String stripQuotes(String s) { } protected static class PatternHistory { - private Path historyFile; - private int size = 100; + private final Path historyFile; + private final int size = 100; private List patterns = new ArrayList<>(); private int patternId = -1; private boolean lastMoveUp = false; @@ -1881,9 +1968,7 @@ public void add(String pattern) { if (pattern.trim().length() == 0) { return; } - if (patterns.contains(pattern)) { - patterns.remove(pattern); - } + patterns.remove(pattern); if (patterns.size() > size) { patterns.remove(patterns.size() - 1); } @@ -1906,6 +1991,7 @@ public void persist() { } } } catch (Exception e) { + // ignore } } @@ -1922,6 +2008,7 @@ private void load() { } } } catch (Exception e) { + // ignore } } @@ -1952,7 +2039,7 @@ public Nano(Terminal terminal, Path root, Options opts, ConfigurationPath config terminal.setAttributes(attrs); } Path nanorc = configPath != null ? configPath.getConfig("jnanorc") : null; - boolean ignorercfiles = opts!=null && opts.isSet("ignorercfiles"); + boolean ignorercfiles = opts != null && opts.isSet("ignorercfiles"); if (nanorc != null && !ignorercfiles) { try { parseConfig(nanorc); @@ -1963,14 +2050,19 @@ public Nano(Terminal terminal, Path root, Options opts, ConfigurationPath config PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:/usr/share/nano/*.nanorc"); try { Files.find(Paths.get("/usr/share/nano"), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path)) - .forEach(p -> syntaxFiles.add(p)); + .forEach(syntaxFiles::add); + nanorcIgnoreErrors = true; } catch (IOException e) { errorMessage = "Encountered error while reading nanorc files"; } } if (opts != null) { this.restricted = opts.isSet("restricted"); - this.syntaxName = opts.isSet("syntax") ? opts.get("syntax") : null; + this.syntaxName = null; + if (opts.isSet("syntax")) { + this.syntaxName = opts.get("syntax"); + nanorcIgnoreErrors = false; + } if (opts.isSet("backup")) { writeBackup = true; } @@ -2047,7 +2139,7 @@ private void parseConfig(Path file) throws IOException { if (parts.get(1).contains("*") || parts.get(1).contains("?")) { PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parts.get(1)); Files.find(Paths.get(new File(parts.get(1)).getParent()), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path)) - .forEach(p -> syntaxFiles.add(p)); + .forEach(syntaxFiles::add); } else { syntaxFiles.add(Paths.get(parts.get(1))); } @@ -2976,7 +3068,6 @@ void searchAndReplace() { int col = searchBackwards ? buffer.length(buffer.getLine(re[0])) - re[1] : re[1]; int match = re[0]*10000 + col; if (matches.contains(match)) { - found = false; break; } else { matches.add(match); @@ -3008,7 +3099,7 @@ void searchAndReplace() { } message = "Replaced " + replaced + " occurrences"; } catch (Exception e) { - return; + // ignore } finally { searchToReplace = false; matchedLength = -1; diff --git a/builtins/src/main/java/org/jline/builtins/Options.java b/builtins/src/main/java/org/jline/builtins/Options.java index 499b79b0e..a51266739 100644 --- a/builtins/src/main/java/org/jline/builtins/Options.java +++ b/builtins/src/main/java/org/jline/builtins/Options.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2019, the original author or authors. + * Copyright (c) 2002-2020, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -38,7 +38,6 @@ import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; @@ -525,33 +524,25 @@ public HelpException(String message) { super(message); } - public static final String DEFAULT_COLORS = "ti=1;34:co=1:ar=3:op=33"; - public static StyleResolver defaultStyle() { - return style(DEFAULT_COLORS); - } - - public static StyleResolver style(String str) { - Map colors = Arrays.stream(str.split(":")) - .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')), - s -> s.substring(s.indexOf('=') + 1))); - return new StyleResolver(colors::get); + return Styles.helpStyle(); } - public static AttributedString highlight(String msg, StyleResolver resolver) { - Matcher tm = Pattern.compile("(^|\\n)(Usage:)").matcher(msg); + public static AttributedString highlight(String msg, StyleResolver resolver) { + Matcher tm = Pattern.compile("(^|\\n)(Usage|Summary)(:)").matcher(msg); if (tm.find()) { + boolean subcommand = tm.group(2).equals("Summary"); AttributedStringBuilder asb = new AttributedStringBuilder(msg.length()); // Command AttributedStringBuilder acommand = new AttributedStringBuilder() .append(msg.substring(0, tm.start(2))) - .styleMatches(Pattern.compile("(?:^\\s*)([a-z]+[a-z-]*){1}\\b"), + .styleMatches(Pattern.compile("(?:^\\s*)([a-z]+[a-zA-Z0-9-]*)\\b"), Collections.singletonList(resolver.resolve(".co"))); asb.append(acommand); // Title - asb.styled(resolver.resolve(".ti"), "Usage").append(":"); + asb.styled(resolver.resolve(".ti"), tm.group(2)).append(":"); // Syntax - for (String line : msg.substring(tm.end(2)).split("\n")) { + for (String line : msg.substring(tm.end(3)).split("\n")) { int ind = line.lastIndexOf(" "); String syntax, comment; if (ind > 20) { @@ -562,7 +553,7 @@ public static AttributedString highlight(String msg, StyleResolver resolver) { comment = ""; } - asb.append(_highlightSyntax(syntax, resolver)); + asb.append(_highlightSyntax(syntax, resolver, subcommand)); asb.append(_highlightComment(comment, resolver)); asb.append("\n"); } @@ -572,15 +563,19 @@ public static AttributedString highlight(String msg, StyleResolver resolver) { } } + public static AttributedString highlightSyntax(String syntax, StyleResolver resolver, boolean subcommands) { + return _highlightSyntax(syntax, resolver, subcommands).toAttributedString(); + } + public static AttributedString highlightSyntax(String syntax, StyleResolver resolver) { - return _highlightSyntax(syntax, resolver).toAttributedString(); + return _highlightSyntax(syntax, resolver, false).toAttributedString(); } public static AttributedString highlightComment(String comment, StyleResolver resolver) { return _highlightComment(comment, resolver).toAttributedString(); } - private static AttributedStringBuilder _highlightSyntax(String syntax, StyleResolver resolver) { + private static AttributedStringBuilder _highlightSyntax(String syntax, StyleResolver resolver, boolean subcommand) { StringBuilder indent = new StringBuilder(); for (char c : syntax.toCharArray()) { if (c != ' ') { @@ -590,24 +585,26 @@ private static AttributedStringBuilder _highlightSyntax(String syntax, StyleReso } AttributedStringBuilder asyntax = new AttributedStringBuilder().append(syntax.substring(indent.length())); // command - asyntax.styleMatches(Pattern.compile("(?:^)([a-z]+[a-z-]*){1}\\b"), + asyntax.styleMatches(Pattern.compile("(?:^)([a-z]+[a-zA-Z0-9-]*)\\b"), Collections.singletonList(resolver.resolve(".co"))); - // argument - asyntax.styleMatches(Pattern.compile("(?:<|\\[|\\s|=)([A-Za-z]+[A-Za-z_-]*){1}\\b"), - Collections.singletonList(resolver.resolve(".ar"))); - // option - asyntax.styleMatches(Pattern.compile("(?:^|\\s|\\[)(-\\$|-\\?|[-]{1,2}[A-Za-z-]+\\b){1}"), - Collections.singletonList(resolver.resolve(".op"))); + if (!subcommand) { + // argument + asyntax.styleMatches(Pattern.compile("(?:<|\\[|\\s|=)([A-Za-z]+[A-Za-z_-]*)\\b"), + Collections.singletonList(resolver.resolve(".ar"))); + // option + asyntax.styleMatches(Pattern.compile("(?:^|\\s|\\[)(-\\$|-\\?|[-]{1,2}[A-Za-z-]+\\b)"), + Collections.singletonList(resolver.resolve(".op"))); + } return new AttributedStringBuilder().append(indent).append(asyntax); } private static AttributedStringBuilder _highlightComment(String comment, StyleResolver resolver) { AttributedStringBuilder acomment = new AttributedStringBuilder().append(comment); // option - acomment.styleMatches(Pattern.compile("(?:\\s|\\[)(-\\$|-\\?|[-]{1,2}[A-Za-z-]+\\b){1}"), + acomment.styleMatches(Pattern.compile("(?:\\s|\\[)(-\\$|-\\?|[-]{1,2}[A-Za-z-]+\\b)"), Collections.singletonList(resolver.resolve(".op"))); // argument in comment - acomment.styleMatches(Pattern.compile("(?:\\s)([a-z]+[-]+[a-z]+|[A-Z_]{2,}){1}(?:\\s)"), + acomment.styleMatches(Pattern.compile("(?:\\s)([a-z]+[-]+[a-z]+|[A-Z_]{2,})(?:\\s)"), Collections.singletonList(resolver.resolve(".ar"))); return acomment; diff --git a/builtins/src/main/java/org/jline/builtins/Styles.java b/builtins/src/main/java/org/jline/builtins/Styles.java new file mode 100644 index 000000000..249c8b719 --- /dev/null +++ b/builtins/src/main/java/org/jline/builtins/Styles.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.builtins; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import org.jline.utils.StyleResolver; + +public class Styles { + private static final String DEFAULT_LS_COLORS = "di=1;91:ex=1;92:ln=1;96:fi="; + private static final String DEFAULT_HELP_COLORS = "ti=1;34:co=1:ar=3:op=33"; + private static final String DEFAULT_PRNT_COLORS = "th=1;34:rn=1;34:mk=1;34:em=31:vs=32"; + private static final String LS_COLORS = "LS_COLORS"; + private static final String HELP_COLORS = "HELP_COLORS"; + private static final String PRNT_COLORS = "PRNT_COLORS"; + + private static final String KEY = "([a-z]{2}|\\*\\.[a-zA-Z0-9]+)"; + private static final String VALUE = "[0-9]*(;[0-9]+){0,2}"; + private static final String ANSI_STYLE_PATTERN = KEY + "=" + VALUE + "(:" + KEY + "=" + VALUE + ")*(:|)"; + + public static StyleResolver lsStyle() { + return style(LS_COLORS, DEFAULT_LS_COLORS); + } + + public static StyleResolver helpStyle() { + return style(HELP_COLORS, DEFAULT_HELP_COLORS); + } + + public static StyleResolver prntStyle() { + return style(PRNT_COLORS, DEFAULT_PRNT_COLORS); + } + + public static boolean isAnsiStylePattern(String style) { + return style.matches(ANSI_STYLE_PATTERN); + } + + private static StyleResolver style(String name, String defStyle) { + String style = consoleOption(name); + if (style == null) { + style = defStyle; + } + return style(style); + } + + private static String consoleOption(String name) { + String out = null; + try { + ConsoleOptionGetter cog = (ConsoleOptionGetter)Class.forName("org.jline.console.SystemRegistry") + .getDeclaredMethod("get").invoke(null); + if (cog != null) { + out = (String)cog.consoleOption(name); + if (out != null && !out.matches(ANSI_STYLE_PATTERN)) { + out = null; + } + } + } catch (Exception e) { + } + if (out == null) { + out = System.getenv(name); + if (out != null && !out.matches(ANSI_STYLE_PATTERN)) { + out = null; + } + } + return out; + } + + private static StyleResolver style(String style) { + Map colors = Arrays.stream(style.split(":")) + .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')), + s -> s.substring(s.indexOf('=') + 1))); + return new StyleResolver(colors::get); + } +} diff --git a/builtins/src/main/java/org/jline/builtins/SystemRegistryImpl.java b/builtins/src/main/java/org/jline/builtins/SystemRegistryImpl.java deleted file mode 100644 index 84ad8ca90..000000000 --- a/builtins/src/main/java/org/jline/builtins/SystemRegistryImpl.java +++ /dev/null @@ -1,1371 +0,0 @@ -/* - * Copyright (c) 2002-2020, the original author or authors. - * - * 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.builtins; - -import static org.jline.keymap.KeyMap.ctrl; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.PrintStream; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.jline.builtins.Completers.OptDesc; -import org.jline.builtins.Completers.OptionCompleter; -import org.jline.builtins.CommandRegistry; -import org.jline.builtins.ConsoleEngine.ExecutionResult; -import org.jline.builtins.Widgets; -import org.jline.builtins.Builtins.CommandMethods; -import org.jline.builtins.Options.HelpException; -import org.jline.reader.Completer; -import org.jline.reader.ConfigurationPath; -import org.jline.reader.EndOfFileException; -import org.jline.reader.ParsedLine; -import org.jline.reader.Parser; -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.terminal.Attributes; -import org.jline.terminal.Attributes.InputFlag; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; -import org.jline.utils.OSUtils; - -/** - * Aggregate command registeries. - * - * @author Matti Rinta-Nikkola - */ -public class SystemRegistryImpl implements SystemRegistry { - public enum Command { - EXIT, HELP - }; - - public enum Pipe { - FLIP, NAMED, AND, OR - } - - private static final Class[] BUILTIN_REGISTERIES = { Builtins.class, ConsoleEngineImpl.class }; - private CommandRegistry[] commandRegistries; - private Integer consoleId; - private Parser parser; - private ConfigurationPath configPath; - private Map commandName = new HashMap<>(); - private Map nameCommand = new HashMap<>(); - private Map aliasCommand = new HashMap<>(); - private Map pipeName = new HashMap<>(); - private final Map commandExecute = new HashMap<>(); - private Map> commandInfos = new HashMap<>(); - private Exception exception; - private CommandOutputStream outputStream; - private ScriptStore scriptStore = new ScriptStore(); - - public SystemRegistryImpl(Parser parser, Terminal terminal, ConfigurationPath configPath) { - this.parser = parser; - this.configPath = configPath; - outputStream = new CommandOutputStream(terminal); - Set cmds = new HashSet<>(EnumSet.allOf(Command.class)); - for (Command c : cmds) { - commandName.put(c, c.name().toLowerCase()); - } - doNameCommand(); - pipeName.put(Pipe.FLIP, "|;"); - pipeName.put(Pipe.NAMED, "|"); - pipeName.put(Pipe.AND, "&&"); - pipeName.put(Pipe.OR, "||"); - commandExecute.put(Command.EXIT, new CommandMethods(this::exit, this::exitCompleter)); - commandExecute.put(Command.HELP, new CommandMethods(this::help, this::helpCompleter)); - } - - private void doNameCommand() { - nameCommand = commandName.entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); - } - - 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]); - } - } 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 nameCommand.keySet(); - } - - @Override - public Map commandAliases() { - Map out = new HashMap<>(); - for (CommandRegistry r : commandRegistries) { - out.putAll(r.commandAliases()); - } - out.putAll(aliasCommand); - return out; - } - - private Command command(String name) { - Command out = null; - if (!isLocalCommand(name)) { - throw new IllegalArgumentException("Command does not exists!"); - } - if (aliasCommand.containsKey(name)) { - name = aliasCommand.get(name); - } - if (nameCommand.containsKey(name)) { - out = nameCommand.get(name); - } else { - throw new IllegalArgumentException("Command does not exists!"); - } - return out; - } - - private List localCommandInfo(String command) { - try { - localExecute(command, new String[] { "--help" }); - } catch (HelpException e) { - exception = null; - return Builtins.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)) { - 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); - } - - private boolean isLocalCommand(String command) { - return nameCommand.containsKey(command) || aliasCommand.containsKey(command); - } - - private boolean isCommandOrScript(String command) { - if (hasCommand(command)) { - return true; - } - return scriptStore.hasScript(command); - } - - @Override - public Completers.SystemCompleter compileCompleters() { - Completers.SystemCompleter out = CommandRegistry.aggregateCompleters(commandRegistries); - Completers.SystemCompleter local = new Completers.SystemCompleter(); - for (Map.Entry entry : commandName.entrySet()) { - local.add(entry.getValue(), commandExecute.get(entry.getKey()).compileCompleter().apply(entry.getValue())); - } - local.addAliases(aliasCommand); - out.add(local); - out.compile(); - return out; - } - - @Override - public Completer completer() { - List completers = new ArrayList<>(); - completers.add(compileCompleters()); - if (consoleId != null) { - completers.addAll(consoleEngine().scriptCompleters()); - } - return new AggregateCompleter(completers); - } - - private Widgets.CmdDesc localCommandDescription(String command) { - if (!isLocalCommand(command)) { - throw new IllegalArgumentException(); - } - try { - localExecute(command, new String[] { "--help" }); - } catch (HelpException e) { - exception = null; - return Builtins.compileCommandDescription(e.getMessage()); - } catch (Exception e) { - trace(e); - } - return null; - } - - @Override - public Widgets.CmdDesc commandDescription(String command) { - Widgets.CmdDesc out = new Widgets.CmdDesc(false); - int id = registryId(command); - if (id > -1) { - out = commandRegistries[id].commandDescription(command); - } else if (scriptStore.hasScript(command)) { - out = consoleEngine().commandDescription(command); - } else if (isLocalCommand(command)) { - out = localCommandDescription(command); - } - return out; - } - - @Override - public Widgets.CmdDesc commandDescription(Widgets.CmdLine line) { - Widgets.CmdDesc out = null; - switch (line.getDescriptionType()) { - case COMMAND: - String cmd = parser.getCommand(line.getArgs().get(0)); - if (isCommandOrScript(cmd) && !hasPipes(line.getArgs())) { - out = commandDescription(cmd); - } - break; - case METHOD: - case SYNTAX: - // TODO - break; - } - return out; - } - - @Override - public Object invoke(String command, Object... args) throws Exception { - Object out = null; - command = ConsoleEngine.plainCommand(command); - int id = registryId(command); - if (id > -1) { - out = commandRegistries[id].invoke(commandSession(), command, args); - } else if (isLocalCommand(command)) { - String[] _args = new String[args.length]; - for (int i = 0; i < args.length; i++) { - if (!(args[i] instanceof String)) { - throw new IllegalArgumentException(); - } - _args[i] = args[i].toString(); - } - out = localExecute(command, _args); - } else if (consoleId != null) { - out = consoleEngine().invoke(commandSession(), command, args); - } - return out; - } - - @Override - public Object execute(String command, String[] args) throws Exception { - Object out = null; - int id = registryId(command); - if (id > -1) { - out = commandRegistries[id].execute(commandSession(), command, args); - } else if (isLocalCommand(command)) { - out = localExecute(command, args); - } - return out; - } - - public Object localExecute(String command, String[] args) throws Exception { - if (!isLocalCommand(command)) { - throw new IllegalArgumentException(); - } - Object out = commandExecute.get(command(command)).executeFunction() - .apply(new Builtins.CommandInput(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 PrintStream origOut; - private PrintStream origErr; - private Terminal origTerminal; - private ByteArrayOutputStream byteOutputStream; - private FileOutputStream fileOutputStream; - private PrintStream out; - private InputStream in; - 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() throws IOException { - byteOutputStream = 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(); - } - } - fileOutputStream = new FileOutputStream(file, append); - } - - public void open() throws IOException { - if (redirecting || (byteOutputStream == null && fileOutputStream == null)) { - return; - } - OutputStream outputStream = byteOutputStream != null ? byteOutputStream : fileOutputStream; - out = new PrintStream(outputStream); - System.setOut(out); - System.setErr(out); - String input = ctrl('X') + "q"; - in = new ByteArrayInputStream( input.getBytes() ); - Attributes attrs = new Attributes(); - if (OSUtils.IS_WINDOWS) { - attrs.setInputFlag(InputFlag.IGNCR, true); - } - terminal = TerminalBuilder.builder() - .streams(in, outputStream) - .attributes(attrs) - .type(Terminal.TYPE_DUMB).build(); - this.commandSession = new CommandRegistry.CommandSession(terminal, terminal.input(), out, out); - redirecting = true; - } - - public void flush() { - if (out == null) { - return; - } - try { - out.flush(); - if (byteOutputStream != null) { - byteOutputStream.flush(); - output = byteOutputStream.toString(); - } else if (fileOutputStream != null) { - fileOutputStream.flush(); - } - } catch (Exception e) { - - } - } - - public void close() { - if (out == null) { - return; - } - try { - in.close(); - flush(); - if (byteOutputStream != null) { - byteOutputStream.close(); - byteOutputStream = null; - } else if (fileOutputStream != null) { - fileOutputStream.close(); - fileOutputStream = null; - } - out.close(); - out = null; - } catch (Exception e) { - - } - } - - public CommandRegistry.CommandSession getCommandSession() { - return commandSession; - } - - public String getOutput() { - return output; - } - - public boolean isRedirecting() { - return redirecting; - } - - public boolean isByteStream() { - return redirecting && byteOutputStream != null; - } - - public void reset() { - if (redirecting) { - out = null; - byteOutputStream = null; - fileOutputStream = null; - output = 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 void closeAndReset() { - close(); - reset(); - } - } - - private boolean isPipe(String arg) { - Map> customPipes = consoleId != null ? consoleEngine().getPipes() : new HashMap<>(); - return isPipe(arg, customPipes.keySet()); - } - - private boolean hasPipes(Collection args) { - Map> customPipes = consoleId != 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); - } - - private boolean isCommandAlias(String command) { - if (consoleId == null || !parser.validCommandName(command) || !consoleEngine().hasAlias(command)) { - return false; - } - String value = consoleEngine().getAlias(command).split("\\s+")[0]; - return !isPipe(value); - } - - private String replaceCommandAlias(String variable, String command, String rawLine) { - return variable == null ? rawLine.replaceFirst(command + "(\\b|$)", consoleEngine().getAlias(command)) - : rawLine.replaceFirst("=" + command + "(\\b|$)", "=" + consoleEngine().getAlias(command)); - } - - private List compileCommandLine(String commandLine) { - List out = new ArrayList<>(); - ArgsParser ap = new ArgsParser(); - ap.parse(parser, commandLine); - // - // manage pipe aliases - // - List ws = ap.args(); - Map> customPipes = consoleId != null ? consoleEngine().getPipes() : new HashMap<>(); - if (consoleId != null && ws.contains(pipeName.get(Pipe.NAMED))) { - StringBuilder sb = new StringBuilder(); - boolean trace = false; - 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))) { - trace = true; - List args = new ArrayList<>(); - String pipeAlias = consoleEngine().getAlias(ws.get(++i)); - while (i < ws.size() - 1 && !isPipe(ws.get(i + 1), customPipes.keySet())) { - args.add(ws.get(++i)); - } - for (int j = 0; j < args.size(); j++) { - pipeAlias = pipeAlias.replaceAll("\\s\\$" + j + "\\b", " " + args.get(j)); - pipeAlias = pipeAlias.replaceAll("\\$\\{" + j + "(|:-.*)\\}", args.get(j)); - } - pipeAlias = pipeAlias.replaceAll("\\s+\\$\\d\\b", ""); - pipeAlias = pipeAlias.replaceAll("\\s+\\$\\{\\d+\\}", ""); - pipeAlias = pipeAlias.replaceAll("\\$\\{\\d+\\}", ""); - Matcher matcher = Pattern.compile("\\$\\{\\d+:-(.*?)\\}").matcher(pipeAlias); - if (matcher.find()) { - pipeAlias = matcher.replaceAll("$1"); - } - sb.append(pipeAlias); - } else { - sb.append(ws.get(i)); - } - } else { - sb.append(ws.get(i)); - } - sb.append(" "); - } - ap.parse(parser, sb.toString()); - if (trace) { - consoleEngine().trace(ap.line()); - } - } - List words = ap.args(); - String nextRawLine = ap.line(); - int first = 0; - int last = words.size(); - List pipes = new ArrayList<>(); - String pipeSource = null; - String rawLine = null; - String pipeResult = null; - if (!hasPipes(words)) { - if (isCommandAlias(ap.command())) { - nextRawLine = replaceCommandAlias(ap.variable(), ap.command(), nextRawLine); - } - out.add(new CommandData(parser, 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(parser, replaceCommandAlias(variable, command, nextRawLine)); - 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 (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 ? _words.stream().collect(Collectors.joining(" ")) - : ap.line(); - if (last + 1 < words.size()) { - nextRawLine = words.subList(last + 1, words.size()).stream().collect(Collectors.joining(" ")); - } - 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(parser, 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 = null; - 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; - - public ArgsParser() {} - - 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 (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; - } - - 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(Parser parser, String line) { - this.line = line; - ParsedLine pl = parser.parse(line, 0, ParseContext.SPLIT_LINE); - enclosedArgs(pl.words()); - this.command = parser.getCommand(args.get(0)); - if (!parser.validCommandName(command)) { - this.command = ""; - } - this.variable = parser.getVariable(args.get(0)); - } - - 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 String flipArgument(final String command, final String subLine, final List pipes, List arglist) { - String out = null; - 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 String rawLine; - private String command; - private String[] args; - private File file; - private boolean append; - private String variable; - private String pipe; - - public CommandData (Parser 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[] {}; - if (!statement) { - ParsedLine plf = parser.parse(rawLine, 0, ParseContext.ACCEPT_LINE); - if (plf.words().size() > 1) { - this.args = plf.words().subList(1, plf.words().size()).toArray(new String[0]); - } - this.command = ConsoleEngine.plainCommand(parser.getCommand(plf.words().get(0))); - } else { - this.args = new String[] {}; - this.command = ""; - } - } - - 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() { - StringBuilder sb = new StringBuilder(); - sb.append("["); - sb.append("rawLine:").append(rawLine); - sb.append(", "); - sb.append("command:").append(command); - sb.append(", "); - sb.append("args:").append(Arrays.asList(args)); - sb.append(", "); - sb.append("variable:").append(variable); - sb.append(", "); - sb.append("file:").append(file); - sb.append(", "); - sb.append("append:").append(append); - sb.append(", "); - sb.append("pipe:").append(pipe); - sb.append("]"); - return sb.toString(); - } - - } - - 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(); - } - - } - - @Override - public Object execute(String line) throws Exception { - if (line.isEmpty() || line.trim().startsWith("#")) { - return null; - } - Object out = null; - boolean statement = false; - boolean postProcessed = false; - scriptStore.refresh(); - List cmds = compileCommandLine(line); - for (int i = 0; i < cmds.size(); i++) { - CommandData cmd = cmds.get(i); - if (cmd.file() != null && scriptStore.isConsoleScript(cmd.command())) { - throw new IllegalArgumentException("Console script output cannot be redirected!"); - } - try { - outputStream.closeAndReset(); - if (consoleId != 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(); - } - boolean consoleScript = false; - if (parser.validCommandName(cmd.command())) { - if (isLocalCommand(cmd.command())) { - out = localExecute(cmd.command(), cmd.args()); - } else { - int id = registryId(cmd.command()); - if (id > -1) { - if (consoleId != null) { - out = commandRegistries[id].invoke(outputStream.getCommandSession(), cmd.command(), - consoleEngine().expandParameters(cmd.args())); - } else { - out = commandRegistries[id].execute(outputStream.getCommandSession(), cmd.command(), - cmd.args()); - } - } else { - consoleScript = true; - } - } - } else { - consoleScript = true; - } - if (consoleId != null) { - if (consoleScript) { - if (cmd.command().isEmpty() || !scriptStore.hasScript(cmd.command())) { - statement = true; - } - if (statement && outputStream.isByteStream()) { - outputStream.closeAndReset(); - } - 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, out); - postProcessed = true; - consoleEngine().println(er.result()); - out = null; - boolean success = er.status() == 0 ? true : false; - 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) { - if (cmd.pipe().equals(pipeName.get(Pipe.OR))) { - trace(e); - postProcessed = true; - } else { - throw e; - } - } finally { - if (!postProcessed && consoleId != null) { - out = postProcess(cmd, statement, out).result(); - } - } - } - return out; - } - - private ExecutionResult postProcess(CommandData cmd, boolean statement, Object result) { - ExecutionResult out = new ExecutionResult(result != null ? 0 : 1, result); - 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.flush(); - outputStream.close(); - out = consoleEngine().postProcess(cmd.rawLine(), result, outputStream.getOutput()); - outputStream.reset(); - } else if (cmd.variable() != null) { - if (consoleEngine().hasVariable(cmd.variable())) { - out = consoleEngine().postProcess(consoleEngine().getVariable(cmd.variable())); - } else { - out = consoleEngine().postProcess(result); - } - if (!cmd.variable().startsWith("_")) { - out = new ExecutionResult(out.status(), null); - } - } else { - out = consoleEngine().postProcess(result); - } - return out; - } - - public void cleanUp() { - if (outputStream.isRedirecting()) { - outputStream.closeAndReset(); - } - if (consoleId != null) { - consoleEngine().purge(); - } - } - - private void trace(CommandData commandData) { - if (consoleId != null) { - consoleEngine().trace(commandData); - } else { - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append(commandData.rawLine(), AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)).println(terminal()); - } - } - - @Override - public void trace(Exception exception) { - if (outputStream.isRedirecting()) { - outputStream.closeAndReset(); - } - if (consoleId != null) { - consoleEngine().putVariable("exception", exception); - consoleEngine().trace(exception); - } else { - trace(false, exception); - } - } - - @Override - public void trace(boolean stack, Exception exception) { - if (exception instanceof Options.HelpException) { - Options.HelpException.highlight((exception).getMessage(), Options.HelpException.defaultStyle()).print(terminal()); - } else if (stack) { - exception.printStackTrace(); - } else { - String message = exception.getMessage(); - AttributedStringBuilder asb = new AttributedStringBuilder(); - if (message != null) { - asb.append(message, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); - } else { - asb.append("Caught exception: ", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); - asb.append(exception.getClass().getCanonicalName(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); - } - asb.toAttributedString().println(terminal()); - } - } - - private ConsoleEngine consoleEngine() { - return consoleId != null ? (ConsoleEngine) commandRegistries[consoleId] : null; - } - - private boolean isBuiltinRegistry(CommandRegistry registry) { - for (Class c : BUILTIN_REGISTERIES) { - 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.size() > 0 ? info.get(0) : " "; - } - - private boolean isInArgs(List args, String name) { - return args.isEmpty() || args.contains(name); - } - - private Object help(Builtins.CommandInput input) { - final String[] usage = { "help - command help" - , "Usage: help [TOPIC...]", - " -? --help Displays command help", }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; - } - - Set commands = commandNames(); - commands.addAll(scriptStore.getScripts()); - boolean withInfo = commands.size() < terminal().getHeight() || !opt.args().isEmpty() ? true : false; - int max = Collections.max(commands, Comparator.comparing(String::length)).length() + 1; - TreeMap builtinCommands = new TreeMap<>(); - for (CommandRegistry r : commandRegistries) { - if (isBuiltinRegistry(r)) { - for (String c : r.commandNames()) { - builtinCommands.put(c, doCommandInfo(commandInfo(c))); - } - } - } - for (String c : localCommandNames()) { - builtinCommands.put(c, doCommandInfo(commandInfo(c))); - exception = null; - } - if (isInArgs(opt.args(), "Builtins")) { - 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) || !isInArgs(opt.args(), r.name())) { - 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 && isInArgs(opt.args(), "Scripts")) { - printHeader("Scripts"); - if (withInfo) { - for (String c : scriptStore.getScripts()) { - printCommandInfo(c, doCommandInfo(commandInfo(c)), max); - } - } else { - printCommands(scriptStore.getScripts(), max); - } - } - terminal().flush(); - return null; - } - - private Object exit(Builtins.CommandInput input) { - final String[] usage = { "exit - exit from app/script" - , "Usage: exit [OBJECT]" - , " -? --help Displays command help" - }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - } else { - if (!opt.args().isEmpty() && consoleId != null) { - try { - Object[] ret = consoleEngine().expandParameters(opt.args().toArray(new String[0])); - consoleEngine().putVariable("_return", ret.length == 1 ? ret[0] : ret); - } catch (Exception e) { - trace(e); - } - } - exception = new EndOfFileException(); - } - return null; - } - - private List commandOptions(String command) { - try { - localExecute(command, new String[] { "--help" }); - } catch (HelpException e) { - exception = null; - return Builtins.compileCommandOptions(e.getMessage()); - } catch (Exception e) { - trace(e); - } - return null; - } - - private List registryNames() { - List out = new ArrayList<>(); - out.add("Builtins"); - if (consoleId != null) { - out.add("Scripts"); - } - for (CommandRegistry r : commandRegistries) { - if (isBuiltinRegistry(r)) { - continue; - } - out.add(r.name()); - } - return out; - } - - private List helpCompleter(String command) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE, - new OptionCompleter(new StringsCompleter(this::registryNames), 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; - } -} diff --git a/builtins/src/main/java/org/jline/builtins/TTop.java b/builtins/src/main/java/org/jline/builtins/TTop.java index 3d30ac0db..616eae1df 100644 --- a/builtins/src/main/java/org/jline/builtins/TTop.java +++ b/builtins/src/main/java/org/jline/builtins/TTop.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2016, the original author or authors. + * Copyright (c) 2002-2020, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -147,7 +147,7 @@ public void run() throws IOException, InterruptedException { comparator = buildComparator(sort); delay = delay > 0 ? Math.max(delay, 100) : 1000; if (stats == null || stats.isEmpty()) { - stats = Arrays.asList(STAT_TID, STAT_NAME, STAT_STATE, STAT_CPU_TIME, STAT_LOCK_OWNER_ID); + stats = new ArrayList<>(Arrays.asList(STAT_TID, STAT_NAME, STAT_STATE, STAT_CPU_TIME, STAT_LOCK_OWNER_ID)); } Boolean isThreadContentionMonitoringEnabled = null; @@ -229,6 +229,9 @@ public void run() throws IOException, InterruptedException { } while (op != Operation.EXIT); } catch (InterruptedException ie) { // Do nothing + } catch (Error err) { + Log.info("Error: ", err); + return; } finally { terminal.setAttributes(attr); if (prevHandler != null) { diff --git a/builtins/src/main/java/org/jline/builtins/Widgets.java b/builtins/src/main/java/org/jline/builtins/Widgets.java deleted file mode 100644 index 486b90bf2..000000000 --- a/builtins/src/main/java/org/jline/builtins/Widgets.java +++ /dev/null @@ -1,1460 +0,0 @@ -/* - * Copyright (c) 2002-2020, the original author or authors. - * - * 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.builtins; - -import static org.jline.keymap.KeyMap.ctrl; -import static org.jline.keymap.KeyMap.alt; - -import java.security.InvalidParameterException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.function.Function; -import java.util.regex.Pattern; - -import org.jline.builtins.Options.HelpException; -import org.jline.keymap.KeyMap; -import org.jline.reader.Binding; -import org.jline.reader.Buffer; -import org.jline.reader.LineReader; -import org.jline.reader.Parser; -import org.jline.reader.LineReader.SuggestionType; -import org.jline.reader.Parser.ParseContext; -import org.jline.reader.Reference; -import org.jline.reader.Widget; -import org.jline.reader.impl.BufferImpl; -import org.jline.utils.AttributedString; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; -import org.jline.utils.Status; - -/** - * Brackets/quotes autopairing and command autosuggestion widgets for jline applications. - * - * @author Matti Rinta-Nikkola - */ -public abstract class Widgets { - protected static final String AP_TOGGLE = "autopair-toggle"; - protected static final String AP_INSERT = "_autopair-insert"; - protected static final String AP_BACKWARD_DELETE_CHAR = "_autopair-backward-delete-char"; - protected static final String TT_TOGGLE = "tailtip-toggle"; - protected static final String TT_ACCEPT_LINE = "_tailtip-accept-line"; - - private final LineReader reader; - - public Widgets(LineReader reader) { - this.reader = reader; - } - - public void addWidget(String name, Widget widget) { - reader.getWidgets().put(name, namedWidget(name, widget)); - } - - private Widget namedWidget(final String name, final Widget widget) { - return new Widget() { - @Override - public String toString() { - return name; - } - @Override - public boolean apply() { - return widget.apply(); - } - }; - } - - public void callWidget(String name) { - if (!name.startsWith("_") && !name.endsWith("-toggle")) { - name = "." + name; - } - reader.callWidget(name); - } - - public void executeWidget(String name) { - // WORK-AROUND - getKeyMap().bind(new Reference(name), alt(ctrl('X'))); - reader.runMacro(alt(ctrl('X'))); - // The line below should be executed inside readLine()!!! - // Create LineReader method executeWidget() maybe??? - // - // widget(name).apply(); - } - - public void aliasWidget(String orig, String alias) { - reader.getWidgets().put(alias, widget(orig)); - } - - public String getWidget(String name) { - return widget(name).toString(); - } - - public boolean existsWidget(String name) { - try { - widget(name); - return true; - } catch(Exception e) { - } - return false; - } - - private Widget widget(String name) { - Widget out = null; - if (name.startsWith(".")) { - out = reader.getBuiltinWidgets().get(name.substring(1)); - } else { - out = reader.getWidgets().get(name); - } - if (out == null) { - throw new InvalidParameterException("widget: no such widget " + name); - } - return out; - } - - public Parser parser() { - return reader.getParser(); - } - - public KeyMap getKeyMap() { - return reader.getKeyMaps().get(LineReader.MAIN); - } - - public Buffer buffer() { - return reader.getBuffer(); - } - - public void replaceBuffer(Buffer buffer) { - reader.getBuffer().copyFrom(buffer); - } - - public List args() { - return reader.getParser().parse(buffer().toString(), 0, ParseContext.COMPLETE).words(); - } - - public String prevChar() { - return String.valueOf((char)reader.getBuffer().prevChar()); - } - - public String currChar() { - return String.valueOf((char)reader.getBuffer().currChar()); - } - - public String lastBinding() { - return reader.getLastBinding(); - } - - public void putString(String string) { - reader.getBuffer().write(string); - } - - public String tailTip() { - return reader.getTailTip(); - } - - public void setTailTip(String tailTip) { - reader.setTailTip(tailTip); - } - - public void setErrorPattern(Pattern errorPattern) { - reader.getHighlighter().setErrorPattern(errorPattern); - } - - public void setErrorIndex(int errorIndex) { - reader.getHighlighter().setErrorIndex(errorIndex); - } - - public void clearTailTip() { - reader.setTailTip(""); - } - - public void setSuggestionType(SuggestionType type) { - reader.setAutosuggestion(type); - } - - public void addDescription(List desc) { - Status.getStatus(reader.getTerminal()).update(desc); - } - - public void clearDescription() { - initDescription(0); - } - - public void initDescription(int size) { - Status status = Status.getStatus(reader.getTerminal(), false); - if (size > 0) { - if (status == null) { - status = Status.getStatus(reader.getTerminal()); - } - status.setBorder(true); - List as = new ArrayList<>(); - for (int i = 0; i < size; i++) { - as.add(new AttributedString("")); - } - addDescription(as); - executeWidget(LineReader.REDRAW_LINE); - } else if (status != null) { - if (size < 0) { - status.update(null); - executeWidget(LineReader.REDRAW_LINE); - } else { - status.clear(); - } - } - } - - public void destroyDescription() { - initDescription(-1); - } - - /** - * Creates and manages widgets that auto-closes, deletes and skips over matching delimiters intelligently. - */ - public static class AutopairWidgets extends Widgets { - /* - * Inspired by zsh-autopair - * https://github.com/hlissner/zsh-autopair - */ - private static final Map LBOUNDS; - private static final Map RBOUNDS; - private final Map pairs; - private final Map defaultBindings = new HashMap<>(); - private boolean enabled; - - { - pairs = new HashMap<>(); - pairs.put("`", "`"); - pairs.put("'", "'"); - pairs.put("\"", "\""); - pairs.put("[", "]"); - pairs.put("(", ")"); - pairs.put(" ", " "); - } - static { - LBOUNDS = new HashMap<>(); - LBOUNDS.put("all", "[.:/\\!]"); - LBOUNDS.put("quotes", "[\\]})a-zA-Z0-9]"); - LBOUNDS.put("spaces", "[^{(\\[]"); - LBOUNDS.put("braces", ""); - LBOUNDS.put("`", "`"); - LBOUNDS.put("\"", "\""); - LBOUNDS.put("'", "'"); - RBOUNDS = new HashMap<>(); - RBOUNDS.put("all", "[\\[{(<,.:?/%$!a-zA-Z0-9]"); - RBOUNDS.put("quotes", "[a-zA-Z0-9]"); - RBOUNDS.put("spaces", "[^\\]})]"); - RBOUNDS.put("braces", ""); - RBOUNDS.put("`", ""); - RBOUNDS.put("\"", ""); - RBOUNDS.put("'", ""); - } - - public AutopairWidgets(LineReader reader) { - this(reader, false); - } - - public AutopairWidgets(LineReader reader, boolean addCurlyBrackets) { - super(reader); - if (existsWidget(AP_INSERT)) { - throw new IllegalStateException("AutopairWidgets already created!"); - } - if (addCurlyBrackets) { - pairs.put("{", "}"); - } - addWidget(AP_INSERT, this::autopairInsert); - addWidget("_autopair-close", this::autopairClose); - addWidget(AP_BACKWARD_DELETE_CHAR, this::autopairDelete); - addWidget(AP_TOGGLE, this::toggleKeyBindings); - - KeyMap map = getKeyMap(); - for (Map.Entry p: pairs.entrySet()) { - defaultBindings.put(p.getKey(), map.getBound(p.getKey())); - if (!p.getKey().equals(p.getValue())) { - defaultBindings.put(p.getValue(), map.getBound(p.getValue())); - } - } - } - - public void enable() { - if (!enabled) { - executeWidget(AP_TOGGLE); - } - } - - public void disable() { - if (enabled) { - executeWidget(AP_TOGGLE); - } - } - - public boolean toggle() { - boolean before = enabled; - executeWidget(AP_TOGGLE); - return !before; - } - - /* - * Widgets - */ - public boolean autopairInsert() { - if (pairs.containsKey(lastBinding())) { - if (canSkip(lastBinding())) { - callWidget(LineReader.FORWARD_CHAR); - } else if (canPair(lastBinding())) { - callWidget(LineReader.SELF_INSERT); - putString(pairs.get(lastBinding())); - callWidget(LineReader.BACKWARD_CHAR); - } else { - callWidget(LineReader.SELF_INSERT); - } - } else { - callWidget(LineReader.SELF_INSERT); - } - return true; - } - - public boolean autopairClose() { - if (pairs.containsValue(lastBinding()) - && currChar().equals(lastBinding())) { - callWidget(LineReader.FORWARD_CHAR); - } else { - callWidget(LineReader.SELF_INSERT); - } - return true; - } - - public boolean autopairDelete() { - if (pairs.containsKey(prevChar()) && pairs.get(prevChar()).equals(currChar()) - && canDelete(prevChar())) { - callWidget(LineReader.DELETE_CHAR); - } - callWidget(LineReader.BACKWARD_DELETE_CHAR); - return true; - } - - public boolean toggleKeyBindings() { - if (enabled) { - defaultBindings(); - } else { - customBindings(); - } - return enabled; - } - /* - * key bindings... - * - */ - private void customBindings() { - boolean ttActive = tailtipEnabled(); - if (ttActive) { - callWidget(TT_TOGGLE); - } - KeyMap map = getKeyMap(); - for (Map.Entry p: pairs.entrySet()) { - map.bind(new Reference(AP_INSERT), p.getKey()); - if (!p.getKey().equals(p.getValue())) { - map.bind(new Reference("_autopair-close"), p.getValue()); - } - } - aliasWidget(AP_BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR); - if (ttActive) { - callWidget(TT_TOGGLE); - } - enabled = true; - } - - private void defaultBindings() { - KeyMap map = getKeyMap(); - for (Map.Entry p: pairs.entrySet()) { - map.bind(defaultBindings.get(p.getKey()), p.getKey()); - if (!p.getKey().equals(p.getValue())) { - map.bind(defaultBindings.get(p.getValue()), p.getValue()); - } - } - aliasWidget("." + LineReader.BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR); - if (tailtipEnabled()) { - callWidget(TT_TOGGLE); - callWidget(TT_TOGGLE); - } - enabled = false; - } - /* - * helpers - */ - private boolean tailtipEnabled() { - return getWidget(LineReader.ACCEPT_LINE).equals(TT_ACCEPT_LINE); - } - - private boolean canPair(String d) { - if (balanced(d) && !nexToBoundary(d)) { - if (d.equals(" ") && (prevChar().equals(" ") || currChar().equals(" "))) { - return false; - } else { - return true; - } - } - return false; - } - - private boolean canSkip(String d) { - if (pairs.get(d).equals(d) && d.charAt(0) != ' ' && currChar().equals(d) - && balanced(d)) { - return true; - } - return false; - } - - private boolean canDelete(String d) { - if (balanced(d)) { - return true; - } - return false; - } - - private boolean balanced(String d) { - boolean out = false; - Buffer buf = buffer(); - String lbuf = buf.upToCursor(); - String rbuf = buf.substring(lbuf.length()); - String regx1 = pairs.get(d).equals(d)? d : "\\"+d; - String regx2 = pairs.get(d).equals(d)? pairs.get(d) : "\\"+pairs.get(d); - int llen = lbuf.length() - lbuf.replaceAll(regx1, "").length(); - int rlen = rbuf.length() - rbuf.replaceAll(regx2, "").length(); - if (llen == 0 && rlen == 0) { - out = true; - } else if (d.charAt(0) == ' ') { - out = true; - } else if (pairs.get(d).equals(d)) { - if ( llen == rlen || (llen + rlen) % 2 == 0 ) { - out = true; - } - } else { - int l2len = lbuf.length() - lbuf.replaceAll(regx2, "").length(); - int r2len = rbuf.length() - rbuf.replaceAll(regx1, "").length(); - int ltotal = llen - l2len; - int rtotal = rlen - r2len; - if (ltotal < 0) { - ltotal = 0; - } - if (ltotal >= rtotal) { - out = true; - } - } - return out; - } - - private boolean boundary(String lb, String rb) { - if ((lb.length() > 0 && prevChar().matches(lb)) - || - (rb.length() > 0 && currChar().matches(rb))) { - return true; - } - return false; - } - - private boolean nexToBoundary(String d) { - List bk = new ArrayList<>(); - bk.add("all"); - if (d.matches("['\"`]")) { - bk.add("quotes"); - } else if (d.matches("[{\\[(<]")) { - bk.add("braces"); - } else if (d.charAt(0) == ' ') { - bk.add("spaces"); - } - if (LBOUNDS.containsKey(d) && RBOUNDS.containsKey(d)) { - bk.add(d); - } - for (String k: bk) { - if (boundary(LBOUNDS.get(k), RBOUNDS.get(k))) { - return true; - } - } - return false; - } - } - - /** - * Creates and manages widgets for as you type command line suggestions. - * Suggestions are created using a using command history. - */ - public static class AutosuggestionWidgets extends Widgets { - private boolean enabled = false; - - public AutosuggestionWidgets(LineReader reader) { - super(reader); - if (existsWidget("_autosuggest-forward-char")) { - throw new IllegalStateException("AutosuggestionWidgets already created!"); - } - addWidget("_autosuggest-forward-char", this::autosuggestForwardChar); - addWidget("_autosuggest-end-of-line", this::autosuggestEndOfLine); - addWidget("_autosuggest-forward-word", this::partialAccept); - addWidget("autosuggest-toggle", this::toggleKeyBindings); - } - - public void disable() { - defaultBindings(); - } - - public void enable() { - customBindings(); - } - /* - * Widgets - */ - public boolean partialAccept() { - Buffer buffer = buffer(); - if (buffer.cursor() == buffer.length()) { - int curPos = buffer.cursor(); - buffer.write(tailTip()); - buffer.cursor(curPos); - replaceBuffer(buffer); - callWidget(LineReader.FORWARD_WORD); - Buffer newBuf = new BufferImpl(); - newBuf.write(buffer().substring(0, buffer().cursor())); - replaceBuffer(newBuf); - } else { - callWidget(LineReader.FORWARD_WORD); - } - return true; - } - - public boolean autosuggestForwardChar() { - return accept(LineReader.FORWARD_CHAR); - } - - public boolean autosuggestEndOfLine() { - return accept(LineReader.END_OF_LINE); - } - - public boolean toggleKeyBindings() { - if (enabled) { - defaultBindings(); - } else { - customBindings(); - } - return enabled; - } - - - private boolean accept(String widget) { - Buffer buffer = buffer(); - if (buffer.cursor() == buffer.length()) { - putString(tailTip()); - } else { - callWidget(widget); - } - return true; - } - /* - * key bindings... - * - */ - private void customBindings() { - if (enabled) { - return; - } - aliasWidget("_autosuggest-forward-char", LineReader.FORWARD_CHAR); - aliasWidget("_autosuggest-end-of-line", LineReader.END_OF_LINE); - aliasWidget("_autosuggest-forward-word", LineReader.FORWARD_WORD); - enabled = true; - setSuggestionType(SuggestionType.HISTORY); - } - - private void defaultBindings() { - if (!enabled) { - return; - } - aliasWidget("." + LineReader.FORWARD_CHAR, LineReader.FORWARD_CHAR); - aliasWidget("." + LineReader.END_OF_LINE, LineReader.END_OF_LINE); - aliasWidget("." + LineReader.FORWARD_WORD, LineReader.FORWARD_WORD); - enabled = false; - setSuggestionType(SuggestionType.NONE); - } - } - - /** - * Creates and manages widgets for as you type command line suggestions. - * Suggestions are created using a command completer data and/or positional argument descriptions. - */ - public static class TailTipWidgets extends Widgets { - public enum TipType { - /** - * Prepare command line suggestions using a command positional argument descriptions. - */ - TAIL_TIP, - /** - * Prepare command line suggestions using a command completer data. - */ - COMPLETER, - /** - * Prepare command line suggestions using either a command positional argument descriptions if exists - * or command completers data. - */ - COMBINED - } - private boolean enabled = false; - private CommandDescriptions cmdDescs; - private TipType tipType; - private int descriptionSize = 0; - private boolean descriptionEnabled = true; - private boolean descriptionCache = true; - - /** - * Creates tailtip widgets used in command line suggestions. Suggestions are created using a command - * positional argument names. If argument descriptions do not exists command completer data will be used. - * Status bar for argument descriptions will not be created. - * - * @param reader LineReader. - * @param tailTips Commands options and positional argument descriptions. - * @throws IllegalStateException If widgets are already created. - */ - public TailTipWidgets(LineReader reader, Map tailTips) { - this(reader, tailTips, 0, TipType.COMBINED); - } - - /** - * Creates tailtip widgets used in command line suggestions. - * Status bar for argument descriptions will not be created. - * - * @param reader LineReader. - * @param tailTips Commands options and positional argument descriptions. - * @param tipType Defines which data will be used for suggestions. - * @throws IllegalStateException If widgets are already created. - */ - public TailTipWidgets(LineReader reader, Map tailTips, TipType tipType) { - this(reader, tailTips, 0, tipType); - } - - /** - * Creates tailtip widgets used in command line suggestions. Suggestions are created using a command - * positional argument names. If argument descriptions do not exists command completer data will be used. - * - * @param reader LineReader. - * @param tailTips Commands options and positional argument descriptions. - * @param descriptionSize Size of the status bar. - * @throws IllegalStateException If widgets are already created. - */ - public TailTipWidgets(LineReader reader, Map tailTips, int descriptionSize) { - this(reader, tailTips, descriptionSize, TipType.COMBINED); - } - - /** - * Creates tailtip widgets used in command line suggestions. - * - * @param reader LineReader. - * @param tailTips Commands options and positional argument descriptions. - * @param descriptionSize Size of the status bar. - * @param tipType Defines which data will be used for suggestions. - * @throws IllegalStateException If widgets are already created. - */ - public TailTipWidgets(LineReader reader, Map tailTips, int descriptionSize, TipType tipType) { - this(reader, tailTips, descriptionSize, tipType, null); - } - - /** - * Creates tailtip widgets used in command line suggestions. - * - * @param reader LineReader. - * @param descFun Function that returns command description. - * @param descriptionSize Size of the status bar. - * @param tipType Defines which data will be used for suggestions. - * @throws IllegalStateException If widgets are already created. - */ - public TailTipWidgets(LineReader reader, Function descFun, int descriptionSize, TipType tipType) { - this(reader, null, descriptionSize, tipType, descFun); - } - - private TailTipWidgets(LineReader reader - , Map tailTips - , int descriptionSize, TipType tipType, Function descFun) { - super(reader); - if (existsWidget(TT_ACCEPT_LINE)) { - throw new IllegalStateException("TailTipWidgets already created!"); - } - this.cmdDescs = tailTips != null ? new CommandDescriptions(tailTips) : new CommandDescriptions(descFun); - this.descriptionSize = descriptionSize; - this.tipType = tipType; - addWidget(TT_ACCEPT_LINE, this::tailtipAcceptLine); - addWidget("_tailtip-self-insert", this::tailtipInsert); - addWidget("_tailtip-backward-delete-char", this::tailtipBackwardDelete); - addWidget("_tailtip-delete-char", this::tailtipDelete); - addWidget("_tailtip-expand-or-complete", this::tailtipComplete); - addWidget("_tailtip-redisplay", this::tailtipUpdateStatus); - addWidget("tailtip-window", this::toggleWindow); - addWidget(TT_TOGGLE, this::toggleKeyBindings); - } - - public void setTailTips(Map tailTips) { - cmdDescs.setDescriptions(tailTips); - } - - public void setDescriptionSize(int descriptionSize) { - this.descriptionSize = descriptionSize; - initDescription(descriptionSize); - } - - public int getDescriptionSize() { - return descriptionSize; - } - - public void setTipType(TipType type) { - this.tipType = type; - if (tipType == TipType.TAIL_TIP) { - setSuggestionType(SuggestionType.TAIL_TIP); - } else { - setSuggestionType(SuggestionType.COMPLETER); - } - } - - public TipType getTipType() { - return tipType; - } - - public boolean isEnabled() { - return enabled; - } - - public void disable() { - if (enabled) { - executeWidget(Widgets.TT_TOGGLE); - } - } - - public void enable() { - if (!enabled) { - toggleKeyBindings(); - } - } - - public void setDescriptionCache(boolean cache) { - this.descriptionCache = cache; - } - - /* - * widgets - */ - public boolean tailtipComplete() { - return doTailTip(LineReader.EXPAND_OR_COMPLETE); - } - - public boolean tailtipAcceptLine() { - if (tipType != TipType.TAIL_TIP) { - setSuggestionType(SuggestionType.COMPLETER); - } - clearDescription(); - setErrorPattern(null); - setErrorIndex(-1); - cmdDescs.clearTemporaryDescs(); - return clearTailTip(LineReader.ACCEPT_LINE); - } - - public boolean tailtipBackwardDelete() { - return doTailTip(autopairEnabled() ? AP_BACKWARD_DELETE_CHAR : LineReader.BACKWARD_DELETE_CHAR); - } - - private boolean clearTailTip(String widget) { - clearTailTip(); - callWidget(widget); - return true; - } - - public boolean tailtipDelete() { - clearTailTip(); - return doTailTip(LineReader.DELETE_CHAR); - } - - public boolean tailtipInsert() { - return doTailTip(autopairEnabled() ? AP_INSERT : LineReader.SELF_INSERT); - } - - public boolean tailtipUpdateStatus() { - return doTailTip(LineReader.REDISPLAY); - } - - private boolean doTailTip(String widget) { - Buffer buffer = buffer(); - callWidget(widget); - Pair cmdkey = null; - List args = args(); - if (buffer.length() == buffer.cursor()) { - cmdkey = cmdDescs.evaluateCommandLine(buffer.toString(), args); - } else { - cmdkey = cmdDescs.evaluateCommandLine(buffer.toString(), buffer.cursor()); - } - CmdDesc cmdDesc = cmdDescs.getDescription(cmdkey.getU()); - if (cmdDesc == null) { - setErrorPattern(null); - setErrorIndex(-1); - clearDescription(); - resetTailTip(); - } else if (cmdDesc.isValid()) { - if (cmdkey.getV()) { - if (cmdDesc.isCommand() && buffer.length() == buffer.cursor()) { - doCommandTailTip(widget, cmdDesc, args); - } - } else { - doDescription(cmdDesc.getMainDescription(descriptionSize)); - setErrorPattern(cmdDesc.getErrorPattern()); - setErrorIndex(cmdDesc.getErrorIndex()); - } - } - return true; - } - - private void doCommandTailTip(String widget, CmdDesc cmdDesc, List args) { - int argnum = 0; - String prevArg = ""; - for (String a : args) { - if (!a.startsWith("-")) { - if (!prevArg.matches("-[a-zA-Z]{1}") || !cmdDesc.optionWithValue(prevArg)) { - argnum++; - } - } - prevArg = a; - } - String lastArg = ""; - prevArg = args.get(args.size() - 1); - if (!prevChar().equals(" ") && args.size() > 1) { - lastArg = args.get(args.size() - 1); - prevArg = args.get(args.size() - 2); - } - int bpsize = argnum; - boolean doTailTip = true; - boolean noCompleters = false; - if (widget.endsWith(LineReader.BACKWARD_DELETE_CHAR)) { - setSuggestionType(SuggestionType.TAIL_TIP); - noCompleters = true; - if (!lastArg.startsWith("-")) { - if (!prevArg.matches("-[a-zA-Z]{1}") || !cmdDesc.optionWithValue(prevArg)) { - bpsize--; - } - } - if (prevChar().equals(" ")) { - bpsize++; - } - } else if (!prevChar().equals(" ")) { - doTailTip = false; - } - if (cmdDesc != null) { - if (lastArg.startsWith("-")) { - if (lastArg.matches("-[a-zA-Z]{1}[a-zA-Z0-9]+")) { - if (cmdDesc.optionWithValue(lastArg.substring(0,2))) { - doDescription(cmdDesc.getOptionDescription(lastArg.substring(0,2), descriptionSize)); - setTipType(tipType); - } else { - doDescription(cmdDesc.getOptionDescription("-" + lastArg.substring(lastArg.length() - 1), descriptionSize)); - setSuggestionType(SuggestionType.TAIL_TIP); - noCompleters = true; - } - } else { - doDescription(cmdDesc.getOptionDescription(lastArg, descriptionSize)); - if (!lastArg.contains("=")) { - setSuggestionType(SuggestionType.TAIL_TIP); - noCompleters = true; - } else { - setTipType(tipType); - } - } - } else if (!widget.endsWith(LineReader.BACKWARD_DELETE_CHAR)){ - setTipType(tipType); - } - if (bpsize > 0 && doTailTip) { - List params = cmdDesc.getArgsDesc(); - if (!noCompleters) { - setSuggestionType(tipType == TipType.COMPLETER ? SuggestionType.COMPLETER : SuggestionType.TAIL_TIP); - } - if (bpsize - 1 < params.size()) { - if (!lastArg.startsWith("-")) { - List d = null; - if (!prevArg.matches("-[a-zA-Z]{1}") || !cmdDesc.optionWithValue(prevArg)) { - d = params.get(bpsize - 1).getDescription(); - } else { - d = cmdDesc.getOptionDescription(prevArg, descriptionSize); - } - if (d == null || d.isEmpty()) { - d = cmdDesc.getMainDescription(descriptionSize); - } - doDescription(d); - } - StringBuilder tip = new StringBuilder(); - for (int i = bpsize - 1; i < params.size(); i++) { - tip.append(params.get(i).getName()); - tip.append(" "); - } - setTailTip(tip.toString()); - } else if (!params.isEmpty() && params.get(params.size() - 1).getName().startsWith("[")) { - setTailTip(params.get(params.size() - 1).getName()); - doDescription(params.get(params.size() - 1).getDescription()); - } - } else if (doTailTip) { - resetTailTip(); - } - } else { - clearDescription(); - resetTailTip(); - } - } - - private void resetTailTip() { - setTailTip(""); - if (tipType != TipType.TAIL_TIP) { - setSuggestionType(SuggestionType.COMPLETER); - } - } - - private void doDescription(List desc) { - if (descriptionSize == 0 || !descriptionEnabled) { - return; - } - if (desc.isEmpty()) { - clearDescription(); - } else if (desc.size() == descriptionSize) { - addDescription(desc); - } else if (desc.size() > descriptionSize) { - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append(desc.get(descriptionSize - 1)).append("...", new AttributedStyle(AttributedStyle.INVERSE)); - List mod = new ArrayList<>(desc.subList(0, descriptionSize-1)); - mod.add(asb.toAttributedString()); - addDescription(mod); - } else if (desc.size() < descriptionSize) { - while (desc.size() != descriptionSize) { - desc.add(new AttributedString("")); - } - addDescription(desc); - } - } - - private boolean autopairEnabled() { - Binding binding = getKeyMap().getBound("("); - if (binding instanceof Reference && ((Reference)binding).name().equals(AP_INSERT)) { - return true; - } - return false; - } - - public boolean toggleWindow() { - descriptionEnabled = !descriptionEnabled; - if (descriptionEnabled) { - initDescription(descriptionSize); - } else { - destroyDescription(); - } - return true; - } - - public boolean toggleKeyBindings() { - if (enabled) { - defaultBindings(); - destroyDescription(); - } else { - customBindings(); - if (descriptionEnabled) { - initDescription(descriptionSize); - } - } - return enabled; - } - - /* - * key bindings... - * - */ - private boolean defaultBindings() { - if (!enabled) { - return false; - } - aliasWidget("." + LineReader.ACCEPT_LINE, LineReader.ACCEPT_LINE); - aliasWidget("." + LineReader.BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR); - aliasWidget("." + LineReader.DELETE_CHAR, LineReader.DELETE_CHAR); - aliasWidget("." + LineReader.EXPAND_OR_COMPLETE, LineReader.EXPAND_OR_COMPLETE); - aliasWidget("." + LineReader.SELF_INSERT, LineReader.SELF_INSERT); - aliasWidget("." + LineReader.REDISPLAY, LineReader.REDISPLAY); - KeyMap map = getKeyMap(); - map.bind(new Reference(LineReader.INSERT_CLOSE_PAREN), ")"); - - setSuggestionType(SuggestionType.NONE); - if (autopairEnabled()) { - callWidget(AP_TOGGLE); - callWidget(AP_TOGGLE); - } - enabled = false; - return true; - } - - private void customBindings() { - if (enabled) { - return; - } - aliasWidget(TT_ACCEPT_LINE, LineReader.ACCEPT_LINE); - aliasWidget("_tailtip-backward-delete-char", LineReader.BACKWARD_DELETE_CHAR); - aliasWidget("_tailtip-delete-char", LineReader.DELETE_CHAR); - aliasWidget("_tailtip-expand-or-complete", LineReader.EXPAND_OR_COMPLETE); - aliasWidget("_tailtip-self-insert", LineReader.SELF_INSERT); - aliasWidget("_tailtip-redisplay", LineReader.REDISPLAY); - KeyMap map = getKeyMap(); - map.bind(new Reference("_tailtip-self-insert"), ")"); - - if (tipType != TipType.TAIL_TIP) { - setSuggestionType(SuggestionType.COMPLETER); - } else { - setSuggestionType(SuggestionType.TAIL_TIP); - } - enabled = true; - } - - private class CommandDescriptions { - Map descriptions = new HashMap<>(); - Map temporaryDescs = new HashMap<>(); - Map volatileDescs = new HashMap<>(); - Function descFun; - - public CommandDescriptions(Map descriptions) { - this.descriptions = new HashMap<>(descriptions); - } - - public CommandDescriptions(Function descFun) { - this.descFun = descFun; - } - - public void setDescriptions(Map descriptions) { - this.descriptions = new HashMap<>(descriptions); - } - - public Pair evaluateCommandLine(String line, int curPos) { - return evaluateCommandLine(line, args(), curPos); - } - - public Pair evaluateCommandLine(String line, List args) { - return evaluateCommandLine(line, args, line.length()); - } - - private Pair evaluateCommandLine(String line, List args, int curPos) { - String cmd = null; - CmdLine.DescriptionType descType = CmdLine.DescriptionType.METHOD; - String head = line.substring(0, curPos); - String tail = line.substring(curPos); - if (prevChar().equals(")")) { - descType = CmdLine.DescriptionType.SYNTAX; - cmd = head; - } else { - if (line.length() == curPos) { - cmd = args != null && (args.size() > 1 || (args.size() == 1 - && line.endsWith(" "))) ? parser().getCommand(args.get(0)) : null; - descType = CmdLine.DescriptionType.COMMAND; - } - int brackets = 0; - for (int i = head.length() - 1; i >= 0; i--) { - if (head.charAt(i) == ')') { - brackets++; - } else if (head.charAt(i) == '(') { - brackets--; - } - if (brackets < 0) { - descType = CmdLine.DescriptionType.METHOD; - head = head.substring(0, i); - cmd = head; - break; - } - } - if (descType == CmdLine.DescriptionType.METHOD) { - brackets = 0; - for (int i = 0; i < tail.length(); i++) { - if (tail.charAt(i) == ')') { - brackets++; - } else if (tail.charAt(i) == '(') { - brackets--; - } - if (brackets > 0) { - tail = tail.substring(i + 1); - break; - } - } - } - } - if (cmd != null && descFun != null) { - if (!descriptionCache && descType == CmdLine.DescriptionType.COMMAND) { - CmdDesc c = descFun.apply(new CmdLine(line, head, tail, args, descType)); - volatileDescs.put(cmd, c); - } else if (!descriptions.containsKey(cmd) && !temporaryDescs.containsKey(cmd)) { - if (descType == CmdLine.DescriptionType.COMMAND) { - CmdDesc c = descFun.apply(new CmdLine(line, head, tail, args, descType)); - if (c != null) { - descriptions.put(cmd, c); - } else { - temporaryDescs.put(cmd, c); - } - } else if (descType == CmdLine.DescriptionType.METHOD) { - temporaryDescs.put(cmd, descFun.apply(new CmdLine(line, head, tail, args, descType))); - } else { - temporaryDescs.put(cmd, descFun.apply(new CmdLine(line, head, tail, args, descType))); - } - } - } - return new Pair(cmd, descType == CmdLine.DescriptionType.COMMAND ? true : false); - } - - public CmdDesc getDescription(String command) { - CmdDesc out = null; - if (descriptions.containsKey(command)) { - out = descriptions.get(command); - } else if (temporaryDescs.containsKey(command)) { - out = temporaryDescs.get(command); - } else if (volatileDescs.containsKey(command)) { - out = volatileDescs.get(command); - volatileDescs.remove(command); - } - return out; - } - - public void clearTemporaryDescs() { - temporaryDescs = new HashMap<>(); - } - - } - - } - - public static class CmdLine { - public enum DescriptionType { - /** - * Cursor is at the end of line. The args[0] is completed, the line does not have unclosed opening parenthesis - * and does not end to the closing parenthesis. - */ - COMMAND, - /** - * The part of the line from beginning til cursor has unclosed opening parenthesis. - */ - METHOD, - /** - * The part of the line from beginning til cursor ends to the closing parenthesis. - */ - SYNTAX}; - private String line; - private String head; - private String tail; - private List args; - private DescriptionType descType; - - /** - * CmdLine class constructor. - * @param line Command line - * @param head Command line til cursor, method parameters and opening parenthesis before the cursor are removed. - * @param tail Command line after cursor, method parameters and closing parenthesis after the cursor are removed. - * @param args Parsed command line arguments. - * @param descType Request COMMAND, METHOD or SYNTAX description - */ - public CmdLine(String line, String head, String tail, List args, DescriptionType descType) { - this.line = line; - this.head = head; - this.tail = tail; - this.args = new ArrayList<>(args); - this.descType = descType; - } - - public String getLine() { - return line; - } - - public String getHead() { - return head; - } - - public String getTail() { - return tail; - } - - public List getArgs() { - return args; - } - - public DescriptionType getDescriptionType() { - return descType; - } - } - - public static class ArgDesc { - private String name; - private List description = new ArrayList(); - - public ArgDesc(String name) { - this(name, new ArrayList()); - } - - public ArgDesc(String name, List description) { - this.name = name; - this.description = new ArrayList<>(description); - } - - public String getName() { - return name; - } - - public List getDescription() { - return description; - } - - public static List doArgNames(List names) { - List out = new ArrayList<>(); - for (String n: names) { - out.add(new ArgDesc(n)); - } - return out; - } - } - - public static class CmdDesc { - private List mainDesc; - private List argsDesc; - private TreeMap> optsDesc; - private Pattern errorPattern; - private int errorIndex = -1; - private boolean valid = true; - private boolean command = false; - - public CmdDesc() { - command = false; - } - - public CmdDesc(boolean valid) { - this.valid = valid; - } - - public CmdDesc(List argsDesc) { - this(new ArrayList<>(), argsDesc, new HashMap<>()); - } - - public CmdDesc(List argsDesc, Map> optsDesc) { - this(new ArrayList<>(), argsDesc, optsDesc); - } - - public CmdDesc(List mainDesc, List argsDesc, Map> optsDesc) { - this.argsDesc = new ArrayList<>(argsDesc); - this.optsDesc = new TreeMap<>(optsDesc); - if (mainDesc.isEmpty() && optsDesc.containsKey("main")) { - this.mainDesc = new ArrayList<>(optsDesc.get("main")); - this.optsDesc.remove("main"); - } else { - this.mainDesc = new ArrayList<>(mainDesc); - } - this.command = true; - } - - protected boolean isValid() { - return valid; - } - - protected boolean isCommand() { - return command; - } - - public CmdDesc mainDesc(List mainDesc) { - this.mainDesc = new ArrayList<>(mainDesc); - return this; - } - - public void setMainDesc(List mainDesc) { - this.mainDesc = new ArrayList<>(mainDesc); - } - - public void setErrorPattern(Pattern errorPattern) { - this.errorPattern = errorPattern; - } - - public Pattern getErrorPattern() { - return errorPattern; - } - - public void setErrorIndex(int errorIndex) { - this.errorIndex = errorIndex; - } - - public int getErrorIndex() { - return errorIndex; - } - - protected List getArgsDesc() { - return argsDesc; - } - - protected List getMainDescription(int descriptionSize) { - List out = new ArrayList<>(); - if (mainDesc == null) { - // do nothing - } else if (mainDesc.size() <= descriptionSize) { - out = mainDesc; - } else { - int tabs = 0; - int row = 0; - for (AttributedString as: mainDesc) { - if (as.columnLength() >= tabs) { - tabs = as.columnLength() + 2; - } - row++; - } - row = 0; - List descList = new ArrayList<>(); - for (int i = 0; i < descriptionSize; i++) { - descList.add(new AttributedString("")); - } - for (AttributedString as: mainDesc) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); - asb.append(descList.get(row)); - asb.append(as); - asb.append("\t"); - descList.remove(row); - descList.add(row, asb.toAttributedString()); - row++; - if (row >= descriptionSize) { - row = 0; - } - } - out = new ArrayList<>(descList); - } - return out; - } - - protected List getOptionDescription(String opt, int descriptionSize) { - List out = new ArrayList<>(); - if (!opt.startsWith("-")) { - return out; - } else { - int ind = opt.indexOf("="); - if (ind > 0) { - opt = opt.substring(0, ind); - } - } - List matched = new ArrayList<>(); - int tabs = 0; - for (String key: optsDesc.keySet()) { - for (String k: key.split("\\s+")) { - if (k.trim().startsWith(opt)) { - matched.add(key); - if (key.length() >= tabs) { - tabs = key.length() + 2; - } - break; - } - } - } - if (matched.size() == 1) { - out.add(highlightOption(matched.get(0))); - for (AttributedString as: optsDesc.get(matched.get(0))) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(8); - asb.append("\t"); - asb.append(as); - out.add(asb.toAttributedString()); - } - } else if (matched.size() <= descriptionSize) { - for (String key: matched) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); - asb.append(highlightOption(key)); - asb.append("\t"); - asb.append(optionDescription(key)); - out.add(asb.toAttributedString()); - } - } else if (matched.size() <= 2*descriptionSize) { - List keyList = new ArrayList<>(); - int row = 0; - int columnWidth = 2*tabs; - while (columnWidth < 50) { - columnWidth += tabs; - } - for (String key: matched) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); - if (row < descriptionSize) { - asb.append(highlightOption(key)); - asb.append("\t"); - asb.append(optionDescription(key)); - if (asb.columnLength() > columnWidth - 2) { - AttributedString trunc = asb.columnSubSequence(0, columnWidth - 5); - asb = new AttributedStringBuilder().tabs(tabs); - asb.append(trunc); - asb.append("...", new AttributedStyle(AttributedStyle.INVERSE)); - asb.append(" "); - } else { - for (int i = asb.columnLength(); i < columnWidth; i++) { - asb.append(" "); - } - } - keyList.add(asb.toAttributedString().columnSubSequence(0, columnWidth)); - } else { - asb.append(keyList.get(row - descriptionSize)); - asb.append(highlightOption(key)); - asb.append("\t"); - asb.append(optionDescription(key)); - keyList.remove(row - descriptionSize); - keyList.add(row - descriptionSize, asb.toAttributedString()); - - } - row++; - } - out = new ArrayList<>(keyList); - } else { - List keyList = new ArrayList<>(); - for (int i = 0; i < descriptionSize; i++) { - keyList.add(new AttributedString("")); - } - int row = 0; - for (String key: matched) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); - asb.append(keyList.get(row)); - asb.append(highlightOption(key)); - asb.append("\t"); - keyList.remove(row); - keyList.add(row, asb.toAttributedString()); - row++; - if (row >= descriptionSize) { - row = 0; - } - } - out = new ArrayList<>(keyList); - } - return out; - } - - protected boolean optionWithValue(String option) { - for (String key: optsDesc.keySet()) { - if (key.matches("(^|.*\\s)" + option + "($|=.*|\\s.*)")) { - if (key.contains("=")) { - return true; - } else { - return false; - } - } - } - return false; - } - - private AttributedString optionDescription(String key) { - return optsDesc.get(key).size() > 0 ? optsDesc.get(key).get(0) : new AttributedString(""); - } - - private AttributedString highlightOption(String option) { - return new AttributedStringBuilder() - .append(option, HelpException.defaultStyle().resolve(".op")) - .toAttributedString(); - } - } - - static class Pair { - final U u; final V v; - public Pair(U u, V v) { - this.u = u; - this.v = v; - } - public U getU() { - return u; - } - public V getV() { - return v; - } - } - -} diff --git a/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt b/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt index 4ad2fe419..57e905fbf 100644 --- a/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt +++ b/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt @@ -21,7 +21,7 @@ Main nano help text ^T (F12) Invoke the spell checker, if available ^_ (F13) (M-G) Go to line and column number -^\ (F14) (M-R) Replace a string or a regular expression + (F14) (M-R) Replace a string or a regular expression ^^ (F15) (M-A) Mark text at the cursor position (F16) (M-W) Repeat last search diff --git a/builtins/src/test/java/org/jline/builtins/NanoTest.java b/builtins/src/test/java/org/jline/builtins/NanoTest.java index 334315e84..1bc3b8bda 100644 --- a/builtins/src/test/java/org/jline/builtins/NanoTest.java +++ b/builtins/src/test/java/org/jline/builtins/NanoTest.java @@ -16,6 +16,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; public class NanoTest { @@ -29,7 +30,8 @@ public void nanoBufferLineOverflow() throws Exception { } terminal.processInputByte(KeyMap.ctrl('X').getBytes()[0]); terminal.processInputByte('n'); - Nano nano = new Nano(terminal, new File("target/test.txt")); + String[] argv = {"--ignorercfiles"}; + Nano nano = new Nano(terminal, Paths.get("target/test.txt"), Options.compile(Nano.usage()).parse(argv)); nano.run(); } } diff --git a/builtins/src/test/java/org/jline/builtins/OptionCompleterTest.java b/builtins/src/test/java/org/jline/builtins/OptionCompleterTest.java index e4a85103b..b2ad75a1e 100644 --- a/builtins/src/test/java/org/jline/builtins/OptionCompleterTest.java +++ b/builtins/src/test/java/org/jline/builtins/OptionCompleterTest.java @@ -8,14 +8,11 @@ */ package org.jline.builtins; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.ArrayList; import org.jline.builtins.Completers.OptionCompleter; import org.jline.builtins.Completers.OptDesc; -import org.jline.reader.LineReader; import org.jline.reader.impl.completer.NullCompleter; import org.jline.reader.impl.completer.StringsCompleter; import org.jline.reader.impl.completer.ArgumentCompleter; @@ -42,6 +39,11 @@ public void testOptions() throws Exception { assertBuffer("command ", new TestBuffer("c").tab()); assertBuffer("command -s", new TestBuffer("command -").tab()); assertBuffer("command -s val ", new TestBuffer("command -s v").tab()); + assertBuffer("command -sval ", new TestBuffer("command -sv").tab()); + assertBuffer("command --sopt val ", new TestBuffer("command --sopt v").tab()); + assertBuffer("command --sopt=", new TestBuffer("command --sop").tab()); + assertBuffer("command --sopt=val ", new TestBuffer("command --sopt=v").tab()); + assertBuffer("command -sval ", new TestBuffer("command -sv").tab()); assertBuffer("command -s val bar ", new TestBuffer("command -s val b").tab()); assertBuffer("command -s val bar --option ", new TestBuffer("command -s val bar --o").tab()); assertBuffer("command -s val bar --option foo ", new TestBuffer("command -s val bar --option f").tab()); diff --git a/builtins/src/test/java/org/jline/example/Example.java b/builtins/src/test/java/org/jline/example/Example.java index 05875c549..7ed810bf4 100644 --- a/builtins/src/test/java/org/jline/example/Example.java +++ b/builtins/src/test/java/org/jline/example/Example.java @@ -8,11 +8,6 @@ */ package org.jline.example; -import static java.time.temporal.ChronoField.HOUR_OF_DAY; -import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; -import static org.jline.builtins.Completers.TreeCompleter.node; - -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Paths; import java.time.LocalDate; @@ -23,42 +18,29 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; -import java.util.regex.Pattern; -import org.jline.builtins.Builtins; -import org.jline.builtins.CommandRegistry; -import org.jline.builtins.Completers; -import org.jline.builtins.Completers.SystemCompleter; +import org.jline.builtins.*; import org.jline.builtins.Completers.TreeCompleter; import org.jline.builtins.Options.HelpException; -import org.jline.builtins.Widgets.ArgDesc; -import org.jline.builtins.Widgets.AutopairWidgets; -import org.jline.builtins.Widgets.AutosuggestionWidgets; -import org.jline.builtins.Widgets.CmdDesc; -import org.jline.builtins.Widgets.CmdLine; -import org.jline.builtins.Widgets.TailTipWidgets; -import org.jline.builtins.Widgets.TailTipWidgets.TipType; +import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; import org.jline.reader.*; import org.jline.reader.LineReader.Option; -import org.jline.reader.LineReader.SuggestionType; import org.jline.reader.impl.DefaultParser; import org.jline.reader.impl.DefaultParser.Bracket; import org.jline.reader.impl.LineReaderImpl; -import org.jline.reader.impl.completer.AggregateCompleter; import org.jline.reader.impl.completer.ArgumentCompleter; import org.jline.reader.impl.completer.StringsCompleter; -import org.jline.reader.impl.completer.NullCompleter; -import org.jline.terminal.Cursor; -import org.jline.terminal.MouseEvent; -import org.jline.terminal.Terminal; -import org.jline.terminal.TerminalBuilder; -import org.jline.utils.AttributedString; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; -import org.jline.utils.InfoCmp; +import org.jline.terminal.*; +import org.jline.utils.*; import org.jline.utils.InfoCmp.Capability; -import org.jline.utils.Status; + +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static org.jline.builtins.Completers.TreeCompleter.node; +import static org.jline.keymap.KeyMap.ctrl; +import static org.jline.keymap.KeyMap.key; + public class Example { @@ -69,7 +51,7 @@ public static void usage() { , " -system terminalBuilder.system(false)" , " +system terminalBuilder.system(true)" , " Completors:" - , " argument an argument completor & autosuggestion" + , " argumet an argument completor" , " files a completor that completes file names" , " none no completors" , " param a paramenter completer using Java functional interface" @@ -94,360 +76,108 @@ public static void usage() { } } - private static Map compileTailTips() { - Map tailTips = new HashMap<>(); - Map> optDesc = new HashMap<>(); - optDesc.put("--optionA", Arrays.asList(new AttributedString("optionA description..."))); - optDesc.put("--noitpoB", Arrays.asList(new AttributedString("noitpoB description..."))); - optDesc.put("--optionC", Arrays.asList(new AttributedString("optionC description...") - , new AttributedString("line2"))); - Map> widgetOpts = new HashMap<>(); - List mainDesc = Arrays.asList(new AttributedString("widget -N new-widget [function-name]") - , new AttributedString("widget -D widget ...") - , new AttributedString("widget -A old-widget new-widget") - , new AttributedString("widget -U string ...") - , new AttributedString("widget -l [options]") - ); - widgetOpts.put("-N", Arrays.asList(new AttributedString("Create new widget"))); - widgetOpts.put("-D", Arrays.asList(new AttributedString("Delete widgets"))); - widgetOpts.put("-A", Arrays.asList(new AttributedString("Create alias to widget"))); - widgetOpts.put("-U", Arrays.asList(new AttributedString("Push characters to the stack"))); - widgetOpts.put("-l", Arrays.asList(new AttributedString("List user-defined widgets"))); - - tailTips.put("widget", new CmdDesc(mainDesc, ArgDesc.doArgNames(Arrays.asList("[pN...]")), widgetOpts)); - tailTips.put("foo12", new CmdDesc(ArgDesc.doArgNames(Arrays.asList("param1", "param2", "[paramN...]")))); - tailTips.put("foo11", new CmdDesc(Arrays.asList( - new ArgDesc("param1",Arrays.asList(new AttributedString("Param1 description...") - , new AttributedString("line 2: This is a very long line that does exceed the terminal width." - +" The line will be truncated automatically (by Status class) before printing out.") - , new AttributedString("line 3") - , new AttributedString("line 4") - , new AttributedString("line 5") - , new AttributedString("line 6") - )) - , new ArgDesc("param2",Arrays.asList(new AttributedString("Param2 description...") - , new AttributedString("line 2") - )) - , new ArgDesc("param3", new ArrayList<>()) - ), optDesc)); - return tailTips; - } - - private static class MasterRegistry { - private final CommandRegistry[] commandRegistries; - private Parser parser; - - public MasterRegistry(CommandRegistry... commandRegistries) { - this.commandRegistries = commandRegistries; - } - - public void setParser(Parser parser) { - this.parser = parser; - } - - public void help() { - System.out.println("List of available commands:"); - for (CommandRegistry r : commandRegistries) { - TreeSet commands = new TreeSet<>(r.commandNames()); - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(2); - asb.append("\t"); - asb.append(r.getClass().getSimpleName()); - asb.append(":"); - System.out.println(asb.toString()); - for (String c: commands) { - asb = new AttributedStringBuilder().tabs(Arrays.asList(4,20)); - asb.append("\t"); - asb.append(c); - asb.append("\t"); - asb.append(r.commandInfo(c).get(0)); - System.out.println(asb.toString()); - } - } - System.out.println(" Additional help:"); - System.out.println(" --help"); - } - - public CmdDesc commandDescription(CmdLine line) { - CmdDesc out = null; - switch (line.getDescriptionType()) { - case COMMAND: - String cmd = parser.getCommand(line.getArgs().get(0)); - for (CommandRegistry r : commandRegistries) { - if (r.hasCommand(cmd)) { - out = r.commandDescription(cmd); - break; - } - } - break; - case METHOD: - out = methodDescription(line); - break; - case SYNTAX: - out = new CmdDesc(false); - break; - } - return out; - } - - private CmdDesc methodDescription(CmdLine line) { - List keywords = Arrays.asList("if", "while", "for"); - for (String s: keywords) { - if (Pattern.compile("\\b" + s + "\\s*$").matcher(line.getHead()).find()){ - return null; - } - } - List mainDesc = new ArrayList<>(); - try { - // For example if using groovy you can create involved object - // dynamically from line string and then inspect method's - // parameters using reflection - if (line.getLine().length() > 20) { - throw new IllegalArgumentException("Failed to create object from source: " + line.getLine()); - } - mainDesc = Arrays.asList(new AttributedString("method1(int arg1, List arg2)") - , new AttributedString("method1(int arg1, Map arg2)") - ); - } catch (Exception e) { - for (String s: e.getMessage().split("\n")) { - mainDesc.add(new AttributedString(s, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED))); - } - } - return new CmdDesc(mainDesc, new ArrayList<>(), new HashMap<>()); + public static void help() { + String[] help = { + "List of available commands:" + , " Builtin:" + , " history list history of commands" + , " less file pager" + , " nano nano editor" + , " setopt set options" + , " ttop display and update sorted information about threads" + , " unsetopt unset options" + , " Example:" + , " cls clear screen" + , " help list available commands" + , " exit exit from example app" + , " select select option" + , " set set lineReader variable" + , " sleep sleep 3 seconds" + , " testkey display key events" + , " tput set terminal capability" + , " Additional help:" + , " --help"}; + for (String u: help) { + System.out.println(u); } } - private static class ExampleCommands implements CommandRegistry { - private LineReader reader; - private AutosuggestionWidgets autosuggestionWidgets; - private TailTipWidgets tailtipWidgets; - private AutopairWidgets autopairWidgets; - private final Map commandExecute = new HashMap<>(); - private final Map> commandInfo = new HashMap<>(); - private Map aliasCommand = new HashMap<>(); - private Exception exception; - - public ExampleCommands() { - commandExecute.put("tput", new Builtins.CommandMethods(this::tput, this::tputCompleter)); - commandExecute.put("testkey", new Builtins.CommandMethods(this::testkey, this::defaultCompleter)); - commandExecute.put("clear", new Builtins.CommandMethods(this::clear, this::defaultCompleter)); - commandExecute.put("sleep", new Builtins.CommandMethods(this::sleep, this::defaultCompleter)); - commandExecute.put("autopair", new Builtins.CommandMethods(this::autopair, this::defaultCompleter)); - commandExecute.put("autosuggestion", new Builtins.CommandMethods(this::autosuggestion, this::autosuggestionCompleter)); - - commandInfo.put("tput", Arrays.asList("set terminal capability")); - commandInfo.put("testkey", Arrays.asList("display key events")); - commandInfo.put("clear", Arrays.asList("clear screen")); - commandInfo.put("sleep", Arrays.asList("sleep 3 seconds")); - commandInfo.put("autopair", Arrays.asList("toggle brackets/quotes autopair key bindings")); - commandInfo.put("autosuggestion", Arrays.asList("set autosuggestion modality: history, completer, tailtip or none")); - } - - public void setLineReader(LineReader reader) { - this.reader = reader; - } - - public void setAutosuggestionWidgets(AutosuggestionWidgets autosuggestionWidgets) { - this.autosuggestionWidgets = autosuggestionWidgets; - } - - public void setTailTipWidgets(TailTipWidgets tailtipWidgets) { - this.tailtipWidgets = tailtipWidgets; - } - - public void setAutopairWidgets(AutopairWidgets autopairWidgets) { - this.autopairWidgets = autopairWidgets; - } - - private Terminal terminal() { - return reader.getTerminal(); - } - - public Set commandNames() { - return commandExecute.keySet(); - } - - public Map commandAliases() { - return aliasCommand; - } - - public List commandInfo(String command) { - return commandInfo.get(command(command)); - } - - public boolean hasCommand(String command) { - if (commandExecute.containsKey(command) || aliasCommand.containsKey(command)) { - return true; - } - return false; - } - - private String command(String name) { - if (commandExecute.containsKey(name)) { - return name; - } else if (aliasCommand.containsKey(name)) { - return aliasCommand.get(name); - } - return null; - } - - public Completers.SystemCompleter compileCompleters() { - SystemCompleter out = new SystemCompleter(); - for (String c : commandExecute.keySet()) { - out.add(c, commandExecute.get(c).compileCompleter().apply(c)); + private static class OptionSelector { + private enum Operation {FORWARD_ONE_LINE, BACKWARD_ONE_LINE, EXIT} + private final Terminal terminal; + private final List lines = new ArrayList<>(); + private final Size size = new Size(); + private final BindingReader bindingReader; + + public OptionSelector(Terminal terminal, String title, Collection options) { + this.terminal = terminal; + this.bindingReader = new BindingReader(terminal.reader()); + lines.add(title); + lines.addAll(options); + } + + private List displayLines(int cursorRow) { + List out = new ArrayList<>(); + int i = 0; + for (String s : lines) { + if (i == cursorRow) { + out.add(new AttributedStringBuilder().append(s, AttributedStyle.INVERSE).toAttributedString()); + } else { + out.add(new AttributedString(s)); + } + i++; } - out.addAliases(aliasCommand); return out; } - public Object execute(CommandRegistry.CommandSession session, String command, String[] args) throws Exception { - exception = null; - commandExecute.get(command(command)).execute().accept(new Builtins.CommandInput(args, session)); - if (exception != null) { - throw exception; - } - return null; - } - - public CmdDesc commandDescription(String command) { - // TODO - return new CmdDesc(false); + private void bindKeys(KeyMap map) { + map.bind(Operation.FORWARD_ONE_LINE, "e", ctrl('E'), key(terminal, Capability.key_down)); + map.bind(Operation.BACKWARD_ONE_LINE, "y", ctrl('Y'), key(terminal, Capability.key_up)); + map.bind(Operation.EXIT,"\r"); } - private void tput(Builtins.CommandInput input) { - String[] argv = input.args(); + public String select() { + Display display = new Display(terminal, true); + Attributes attr = terminal.enterRawMode(); try { - if (argv.length == 1 && !argv[0].equals("--help") && !argv[0].equals("-?")) { - Capability vcap = Capability.byName(argv[0]); - if (vcap != null) { - terminal().puts(vcap); - } else { - terminal().writer().println("Unknown capability"); - } - } else { - terminal().writer().println("Usage: tput "); - } - } catch (Exception e) { - exception = e; - } - } - - private void testkey(Builtins.CommandInput input) { - try { - terminal().writer().write("Input the key event(Enter to complete): "); - terminal().writer().flush(); - StringBuilder sb = new StringBuilder(); + terminal.puts(Capability.enter_ca_mode); + terminal.puts(Capability.keypad_xmit); + terminal.writer().flush(); + size.copy(terminal.getSize()); + display.clear(); + display.reset(); + int selectRow = 1; + KeyMap keyMap = new KeyMap<>(); + bindKeys(keyMap); while (true) { - int c = ((LineReaderImpl) reader).readCharacter(); - if (c == 10 || c == 13) break; - sb.append(new String(Character.toChars(c))); - } - terminal().writer().println(KeyMap.display(sb.toString())); - terminal().writer().flush(); - } catch (Exception e) { - exception = e; - } - } - - private void clear(Builtins.CommandInput input) { - try { - terminal().puts(Capability.clear_screen); - terminal().flush(); - } catch (Exception e) { - exception = e; - } - } - - private void sleep(Builtins.CommandInput input) { - try { - Thread.sleep(3000); - } catch (Exception e) { - exception = e; - } - } - - private void autopair(Builtins.CommandInput input) { - try { - terminal().writer().print("Autopair widgets are "); - if (autopairWidgets.toggle()) { - terminal().writer().println("enabled."); - } else { - terminal().writer().println("disabled."); - } - } catch (Exception e) { - exception = e; - } - } - - private void autosuggestion(Builtins.CommandInput input) { - String[] argv = input.args(); - try { - if (argv.length > 0) { - String type = argv[0].toLowerCase(); - if (type.startsWith("his")) { - tailtipWidgets.disable(); - autosuggestionWidgets.enable(); - } else if (type.startsWith("tai")) { - autosuggestionWidgets.disable(); - tailtipWidgets.enable(); - if (argv.length > 1) { - String mode = argv[1].toLowerCase(); - if (mode.startsWith("tai")) { - tailtipWidgets.setTipType(TipType.TAIL_TIP); - } else if (mode.startsWith("comp")) { - tailtipWidgets.setTipType(TipType.COMPLETER); - } else if (mode.startsWith("comb")) { - tailtipWidgets.setTipType(TipType.COMBINED); + display.resize(size.getRows(), size.getColumns()); + display.update(displayLines(selectRow), size.cursorPos(0, lines.get(0).length())); + Operation op = bindingReader.readBinding(keyMap); + switch(op) { + case FORWARD_ONE_LINE: + selectRow++; + if (selectRow > lines.size() - 1) { + selectRow = 1; } - } - } else if (type.startsWith("com")) { - autosuggestionWidgets.disable(); - tailtipWidgets.disable(); - reader.setAutosuggestion(SuggestionType.COMPLETER); - } else if (type.startsWith("non")) { - autosuggestionWidgets.disable(); - tailtipWidgets.disable(); - reader.setAutosuggestion(SuggestionType.NONE); - } else { - terminal().writer().println("Usage: autosuggestion history|completer|tailtip|none"); - } - } else { - if (tailtipWidgets.isEnabled()) { - terminal().writer().println("Autosuggestion: tailtip/" + tailtipWidgets.getTipType()); - } else { - terminal().writer().println("Autosuggestion: " + reader.getAutosuggestion()); + break; + case BACKWARD_ONE_LINE: + selectRow--; + if (selectRow < 1) { + selectRow = lines.size() - 1; + } + break; + case EXIT: + return lines.get(selectRow); } } - } catch (Exception e) { - exception = e; + } finally { + terminal.setAttributes(attr); + terminal.puts(Capability.exit_ca_mode); + terminal.puts(Capability.keypad_local); + terminal.writer().flush(); } } - - private List defaultCompleter(String command) { - return Arrays.asList(NullCompleter.INSTANCE); - } - - private Set capabilities() { - return InfoCmp.getCapabilitiesByName().keySet(); - } - - private List tputCompleter(String command) { - return Arrays.asList(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter(this::capabilities) - )); - } - - private List autosuggestionCompleter(String command) { - List out = new ArrayList<>(); - out.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter("history", "completer", "none") - , NullCompleter.INSTANCE)); - out.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter("tailtip") - , new StringsCompleter("tailtip", "completer", "combined") - , NullCompleter.INSTANCE)); - return out; - } } public static void main(String[] args) throws IOException { @@ -458,7 +188,6 @@ public static void main(String[] args) throws IOException { String trigger = null; boolean color = false; boolean timer = false; - boolean argument = false; TerminalBuilder builder = TerminalBuilder.builder(); @@ -504,7 +233,10 @@ public static void main(String[] args) throws IOException { completer = new Completers.FileNameCompleter(); break; case "simple": - completer = new StringsCompleter("foo", "bar", "baz"); + DefaultParser p3 = new DefaultParser(); + p3.setEscapeChars(new char[]{}); + parser = p3; + completer = new StringsCompleter("foo", "bar", "baz", "pip pop"); break; case "quotes": DefaultParser p = new DefaultParser(); @@ -520,34 +252,29 @@ public static void main(String[] args) throws IOException { case "status": completer = new StringsCompleter("foo", "bar", "baz"); callbacks.add(reader -> { - new Thread(() -> { + Thread thread = new Thread(() -> { int counter = 0; while (true) { try { Status status = Status.getStatus(reader.getTerminal()); counter++; - status.update(Arrays.asList(new AttributedStringBuilder().append("counter: " + counter).toAttributedString())); + status.update(Arrays.asList(new AttributedStringBuilder() + .append("counter: " + counter) + .toAttributedString())); ((LineReaderImpl) reader).redisplay(); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } - }).start(); + }); + thread.setDaemon(true); + thread.start(); }); break; case "argument": - argument = true; completer = new ArgumentCompleter( - new Completer() { - @Override - public void complete(LineReader reader, ParsedLine line, List candidates) { - candidates.add(new Candidate("foo11", "foo11", null, "complete cmdDesc", null, null, true)); - candidates.add(new Candidate("foo12", "foo12", null, "cmdDesc -names only", null, null, true)); - candidates.add(new Candidate("foo13", "foo13", null, "-", null, null, true)); - candidates.add(new Candidate("widget", "widget", null, "cmdDesc with short options", null, null, true)); - } - }, + new StringsCompleter("foo11", "foo12", "foo13"), new StringsCompleter("foo21", "foo22", "foo23"), new Completer() { @Override @@ -639,55 +366,18 @@ public void complete(LineReader reader, ParsedLine line, List candida } } } - // - // Command registeries - // - Builtins builtins = new Builtins(Paths.get(""), null, null); - builtins.rename(Builtins.Command.TTOP, "top"); - builtins.alias("zle", "widget"); - builtins.alias("bindkey", "keymap"); - ExampleCommands exampleCommands = new ExampleCommands(); - MasterRegistry masterRegistry = new MasterRegistry(builtins, exampleCommands); - masterRegistry.setParser(parser); - // - // Command completers - // - AggregateCompleter finalCompleter = new AggregateCompleter(CommandRegistry.compileCompleters(builtins, exampleCommands) - , completer != null ? completer : NullCompleter.INSTANCE); - // - // Terminal & LineReader - // + Terminal terminal = builder.build(); System.out.println(terminal.getName()+": "+terminal.getType()); System.out.println("\nhelp: list available commands"); LineReader reader = LineReaderBuilder.builder() .terminal(terminal) - .completer(finalCompleter) + .completer(completer) .parser(parser) .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M%P > ") .variable(LineReader.INDENTATION, 2) .option(Option.INSERT_BRACKET, true) - .option(Option.EMPTY_WORD_OPTIONS, false) .build(); - // - // widgets - // - AutopairWidgets autopairWidgets = new AutopairWidgets(reader); - AutosuggestionWidgets autosuggestionWidgets = new AutosuggestionWidgets(reader); - TailTipWidgets tailtipWidgets = null; - if (argument) { - tailtipWidgets = new TailTipWidgets(reader, compileTailTips(), 5, TipType.COMPLETER); - } else { - tailtipWidgets = new TailTipWidgets(reader, masterRegistry::commandDescription, 5, TipType.COMPLETER); - } - // - // complete command registeries - // - builtins.setLineReader(reader); - exampleCommands.setLineReader(reader); - exampleCommands.setAutosuggestionWidgets(autosuggestionWidgets); - exampleCommands.setTailTipWidgets(tailtipWidgets); - exampleCommands.setAutopairWidgets(autopairWidgets); if (timer) { Executors.newScheduledThreadPool(1) @@ -725,13 +415,10 @@ public void complete(LineReader reader, ParsedLine line, List candida if (!callbacks.isEmpty()) { Thread.sleep(2000); } - // - // REPL-loop - // - CommandRegistry.CommandSession session = new CommandRegistry.CommandSession(terminal); while (true) { + String line = null; try { - String line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null); + line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null); line = line.trim(); if (color) { @@ -754,21 +441,80 @@ public void complete(LineReader reader, ParsedLine line, List candida } ParsedLine pl = reader.getParser().parse(line, 0); String[] argv = pl.words().subList(1, pl.words().size()).toArray(new String[0]); - String cmd = parser.getCommand(pl.word()); - if ("help".equals(cmd) || "?".equals(cmd)) { - masterRegistry.help(); + if ("set".equals(pl.word())) { + if (pl.words().size() == 3) { + reader.setVariable(pl.words().get(1), pl.words().get(2)); + } else { + terminal.writer().println("Usage: set "); + } } - else if (builtins.hasCommand(cmd)) { - builtins.execute(session, cmd, argv); + else if ("tput".equals(pl.word())) { + if (pl.words().size() == 2) { + Capability vcap = Capability.byName(pl.words().get(1)); + if (vcap != null) { + terminal.puts(vcap); + } else { + terminal.writer().println("Unknown capability"); + } + } else { + terminal.writer().println("Usage: tput "); + } + } + else if ("testkey".equals(pl.word())) { + terminal.writer().write("Input the key event(Enter to complete): "); + terminal.writer().flush(); + StringBuilder sb = new StringBuilder(); + while (true) { + int c = ((LineReaderImpl) reader).readCharacter(); + if (c == 10 || c == 13) break; + sb.append(new String(Character.toChars(c))); + } + terminal.writer().println(KeyMap.display(sb.toString())); + terminal.writer().flush(); + } + else if ("cls".equals(pl.word())) { + terminal.puts(Capability.clear_screen); + terminal.flush(); + } + else if ("sleep".equals(pl.word())) { + Thread.sleep(3000); } - else if (exampleCommands.hasCommand(cmd)) { - exampleCommands.execute(session, cmd, argv); + else if ("nano".equals(pl.word())) { + Commands.nano(terminal, System.out, System.err, + Paths.get(""), + argv); + } + else if ("less".equals(pl.word())) { + Commands.less(terminal, System.in, System.out, System.err, + Paths.get(""), + argv); + } + else if ("history".equals(pl.word())) { + Commands.history(reader, System.out, System.err, Paths.get(""),argv); + } + else if ("setopt".equals(pl.word())) { + Commands.setopt(reader, System.out, System.err, argv); + } + else if ("unsetopt".equals(pl.word())) { + Commands.unsetopt(reader, System.out, System.err, argv); + } + else if ("ttop".equals(pl.word())) { + TTop.ttop(terminal, System.out, System.err, argv); + } + else if ("help".equals(pl.word()) || "?".equals(pl.word())) { + help(); + } + else if ("select".equals(pl.word())) { + OptionSelector selector = new OptionSelector(terminal, "Select number>" + , Arrays.asList("one", "two", "three", "four")); + String selected = selector.select(); + System.out.println("You selected number " + selected); } } catch (HelpException e) { HelpException.highlight(e.getMessage(), HelpException.defaultStyle()).print(terminal); } - catch (IllegalArgumentException|FileNotFoundException e) { + catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } catch (UserInterruptException e) { @@ -777,9 +523,6 @@ else if (exampleCommands.hasCommand(cmd)) { catch (EndOfFileException e) { return; } - catch (Exception e) { - e.printStackTrace(); - } } } catch (Throwable t) { @@ -787,4 +530,4 @@ else if (exampleCommands.hasCommand(cmd)) { } } -} \ No newline at end of file +} diff --git a/changelog.md b/changelog.md index c3e387de6..669d89e6e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,318 @@ # [JLine3](https://github.com/jline/jline3) +## [JLine 3.18.0][3_18_0] +[3_18_0]: https://repo1.maven.org/maven2/org/jline/jline/3.18.0/ + +* [`e567eb70`](https://github.com/jline/jline3/commit/e567eb70e5247473cb3f3725a066384046e7ba21) Remove transitive dependency on the jline bundle +* [`6168d2b7`](https://github.com/jline/jline3/commit/6168d2b7167d28665a10fe836ae03facd7f4612e) Avoid javadoc warnings and do not generate javadoc for demos +* [`be78b98b`](https://github.com/jline/jline3/commit/be78b98b995281dada3743e9e280dd703195ec47) Upgrade to Jansi 2.1.0 +* [`a6707274`](https://github.com/jline/jline3/commit/a67072745e3197b83317d9173406fcf9c7ea4dc7) Upgrade plugins to recente releases +* [`f4dd7a88`](https://github.com/jline/jline3/commit/f4dd7a886155c409adbb44d3714fc04cc4c7f342) Move javadoc generation into a separate profile (active by default) +* [`717fad86`](https://github.com/jline/jline3/commit/717fad869b01b2bb91ac9a3b91b00fd35a8bfb0f) Allow disabling the jline bundle module +* [`8ff1e1d3`](https://github.com/jline/jline3/commit/8ff1e1d35146a14b7c188c1c33d1020cb6ab0714) Support terminal palette, fixes #620 +* [`e2b6f97e`](https://github.com/jline/jline3/commit/e2b6f97ed0ebceb3c909415baeba71c72f819c37) Support for 24-bit colours, fixes #619 +* [`c20b1338`](https://github.com/jline/jline3/commit/c20b13380e3c1d90e43cdc0bbcb68493da910bca) Remove duplicate semicolon +* [`29d72f81`](https://github.com/jline/jline3/commit/29d72f817a6742c7a83e84812aadca300f57d9fb) Use style resolver to resolve completion styles, fixes #617 +* [`b07a7cf1`](https://github.com/jline/jline3/commit/b07a7cf1240c37d597d59c3f91faeeeddb932cfc) Fix wrong indentation +* [`99e130d6`](https://github.com/jline/jline3/commit/99e130d6ff2d004d18cb8d89c2ca5151493a0e02) Fix some tput limitations +* [`b77a0a8a`](https://github.com/jline/jline3/commit/b77a0a8afea5a50a3cfa514e510ca214a0857900) Inline completion re-sorts while tabbing when using groups, fixes #618 +* [`9d4a53b2`](https://github.com/jline/jline3/commit/9d4a53b2e0132334c3e2d77de5a964442bbe40ea) Bump Groovy to 3.0.7 +* [`d185f726`](https://github.com/jline/jline3/commit/d185f7264f5233554d57681edce214e625583837) Add variable menu-list-max and sort candidates in menu-list +* [`538b7fa8`](https://github.com/jline/jline3/commit/538b7fa81bf73710ff4d596807386a7d8524ede4) Option group-persist: after double tab keep candidates grouped, fixes #613 +* [`329768ca`](https://github.com/jline/jline3/commit/329768ca9e9ea72a42a35e5232055d124f52b06d) Graal demo: fix resolved demo target path +* [`ec1115df`](https://github.com/jline/jline3/commit/ec1115df688ca051fda2ca1a8e3e90d1d790978a) Failed to build JLine Graal demo: NoClassDefFoundError, fixes #615 +* [`54218bc3`](https://github.com/jline/jline3/commit/54218bc330b7ba9a4e77cb6cd9968d16d4f713b4) Nanorc parser: align with GNU nano version 5 +* [`82ca0f05`](https://github.com/jline/jline3/commit/82ca0f05501b29b99d66f27ebc8ef67e4f6bd9ec) Nanorc parser: replace Posix char class regexes with Java regexes +* [`172644f4`](https://github.com/jline/jline3/commit/172644f4281ff07951198129fdb62c85768b1e53) Nano highlighter xml highlighting differs considerable from GNU nano highlight, fixes #614 +* [`30860bc5`](https://github.com/jline/jline3/commit/30860bc5b947c53563b48ea5418de150f167acb8) nano/less ignore quietly PatternSyntaxException when using system installed nanorc files, fixes #609 +* [`764a6a6a`](https://github.com/jline/jline3/commit/764a6a6ad46099c26630f1e60490f955fbc3c661) Less fails with PatternSyntaxException #609 +* [`b1a17cbc`](https://github.com/jline/jline3/commit/b1a17cbc7d5f978c38d43d583e1e7bef05b8b670) SystemHighlighter highlight command aliases as commands +* [`0e5d510e`](https://github.com/jline/jline3/commit/0e5d510e3c1ebc38b8bd6b9027b9388c0c21792b) SystemHighlighter fixed NPE +* [`924d8ff3`](https://github.com/jline/jline3/commit/924d8ff346e1ff2107fe0a25bc598b31c14e097e) Builtins Commands.less(): add configPath parameter +* [`a2281234`](https://github.com/jline/jline3/commit/a2281234359bb9ce2ef5173b1953b102391ec6da) Secondary prompt: fix padding when primary prompt has line breaks +* [`a47ccc80`](https://github.com/jline/jline3/commit/a47ccc801e7f48e8d85b8653b8fead6b68b67daa) Jline completion has logic issues with terminal and prompt widths (StringIndexOutOfBoundsException), fixes #604 +* [`1a767236`](https://github.com/jline/jline3/commit/1a76723605f85f48fd06f35d51df60b11e187e7f) Document how I/O error in LineReader.readLine is reported, fixes #608 +* [`ca381eec`](https://github.com/jline/jline3/commit/ca381eec4885fab0523a19718db1a3e0ec8cbea9) Groovy REPL: highlight command and groovy language syntax +* [`bb5e85af`](https://github.com/jline/jline3/commit/bb5e85afd9009e9d3d917a19dcb0deab4b9b0922) Display command is incorrect when use here document, fixes #607 +* [`0ba7e813`](https://github.com/jline/jline3/commit/0ba7e8137e0ff132f05c4c176daccd149e14ec66) edit-and-execute widget: set BRACKETED_PASTE_OFF before editing +* [`23034dbf`](https://github.com/jline/jline3/commit/23034dbfb50ea4da2322be652c4a07345f646f39) SystemRegistryImpl: fixed IndexOutOfBoundsException +* [`d66e7349`](https://github.com/jline/jline3/commit/d66e7349017098458e7961acfa314198c666ada2) DefaultParser: fixed default variable regex +* [`eb3e07c8`](https://github.com/jline/jline3/commit/eb3e07c88782179f3b1c185eb5b7a8efd2fc9f67) SystemRegistryImpl: method consoleEngine() is now public +* [`6580789c`](https://github.com/jline/jline3/commit/6580789cd5eca02317007afe522c8f41060bbc45) Keep argument sorting in large argument list when formatting candidates for terminal into multiple columns +* [`d92701d4`](https://github.com/jline/jline3/commit/d92701d4b41cee07cd2110d1e0b20ab2982021d6) Auto suggestion error when type "\\" character, fixes #602 +* [`495b534a`](https://github.com/jline/jline3/commit/495b534afb1e0c7dbfb7780869a424c62d44fd7b) ConsoleEngineImpl: exclude pipe name aliases from command completion +* [`df991872`](https://github.com/jline/jline3/commit/df99187246f4021f4fe12a64f52cca9d55aeb809) Show auto-suggestions when the reader starts reading, fixes #598 +* [`ea6dd89c`](https://github.com/jline/jline3/commit/ea6dd89cdc98abdfc77c262444fca91da3c1080d) Autosuggestion choices are not refreshed after tab, fixes #545 +* [`2e6638cb`](https://github.com/jline/jline3/commit/2e6638cb6d4f27bc61bbb6ca6059e5df0384e2d0) Widgets: added widget name public constants +* [`a015a5df`](https://github.com/jline/jline3/commit/a015a5dfd66262ab463f24e2b25aec03c5fa2982) JLine option AUTO_MENU_LIST: bug fix for candidate list start position calculation +* [`a5686ab1`](https://github.com/jline/jline3/commit/a5686ab138225ee8214c68bda8c33fbaf2af5d26) JLine option AUTO_MENU_LIST: candidate list is wrongly positioned, fixes #600 +* [`ea8d0d3b`](https://github.com/jline/jline3/commit/ea8d0d3b87a6dd74876fc67f909c3b769433294f) SystemRegistryImpl: refactoring command output redirection +* [`40c4d324`](https://github.com/jline/jline3/commit/40c4d3241fc242525cf7050ec0b977494620065d) Add completion candidate suffix test, #425 +* [`404565b2`](https://github.com/jline/jline3/commit/404565b2ede2b9b83fab2c350b2806126a11e33f) Dumb terminal when piping input to console app disallows ANSI formatting, fixes #299 +* [`fe1f2717`](https://github.com/jline/jline3/commit/fe1f27179461fdecc88f2cf01de812784bddfad9) Windows gitbash: JLine will create dumb terminal if JNA lib is in classpath, fixes #599 +* [`1c9f16df`](https://github.com/jline/jline3/commit/1c9f16dfbe8e5e58df1740dd17ff5c5d0027ff34) JNA/Jansi isWindowsConsole() method return true only if both input and output streams are connected to a terminal +* [`542bfb64`](https://github.com/jline/jline3/commit/542bfb644c244d44c4bbc820c6b08ae897dad173) Ignore BRACKETED_PASTE if dumb terminal +* [`1d7fb07f`](https://github.com/jline/jline3/commit/1d7fb07f04d5380829329c9e7e0880643a2974bb) Windows CMD, redirecting input using JLine with Jansi fails, fixes #597 +* [`3f399ace`](https://github.com/jline/jline3/commit/3f399ace9e35da4ec1d6f3610dbf18372700777e) readLine() ignores any text in the buffer when OEF is reached, fixes #298 + +## [JLine 3.17.1][3_17_1] +[3_17_1]: https://repo1.maven.org/maven2/org/jline/jline/3.17.1/ + +* [`437e7f43`](https://github.com/jline/jline3/commit/437e7f430623e883520f3cc16a7be9982c8ac79f) Upgrade to jansi 2.0.1 +* [`15cf3895`](https://github.com/jline/jline3/commit/15cf3895e8782a32176bf62858907aab1a3e6ea2) Add native resource information +* [`9e5728c1`](https://github.com/jline/jline3/commit/9e5728c19600a67fc8d10ff48e49c0c00d9dcbfd) Fix unit test +* [`d609de12`](https://github.com/jline/jline3/commit/d609de124271b79e48cef3ce6a15b7606244bbcb) Fix console hangup on windows in combination with jansi after typing one char + +## [JLine 3.17.0][3_17_0] +[3_17_0]: https://repo1.maven.org/maven2/org/jline/jline/3.17.0/ + +* [`4dc7d445`](https://github.com/jline/jline3/commit/4dc7d445981af68cccb528fae8fe1880c7a93c78) Upgrade to jansi 2.0 +* [`04556a57`](https://github.com/jline/jline3/commit/04556a5757d06fe3ddee5a5989a5e6181d83a7bf) Remove unused import +* [`57aa5e1a`](https://github.com/jline/jline3/commit/57aa5e1a82daf42a7e094082de44eec506473595) Remove the getConsoleOutputCP on the AbstractWindowsTerminal +* [`299d0e91`](https://github.com/jline/jline3/commit/299d0e917e59342bdb3445c6ed6c63275256b011) Add select-option in Example, #592 +* [`ea98b90f`](https://github.com/jline/jline3/commit/ea98b90f5346008c5233da08a1fadda572d55a3d) ConsoleEngineImpl doc command: check that the page exists before launching browser +* [`5d4d46b2`](https://github.com/jline/jline3/commit/5d4d46b2d5760bf1b6f85e07248e9478e9ac1e26) Tab completion: if auto-menu-list=true and candidates do not fit to display show candidates in table view instead +* [`af9196e1`](https://github.com/jline/jline3/commit/af9196e153929f9abb9b4c6cae64269d772fb88a) SystemRegistryImpl: improve command and pipe alias compilation +* [`ad90e038`](https://github.com/jline/jline3/commit/ad90e038c4af3b3563e167d9f35d6d9c799dee32) Jline silently ignore streams(in, out) when using a system Terminal, fixes #576 +* [`7fdedc36`](https://github.com/jline/jline3/commit/7fdedc36976fc8b90465c05d69b8bf4af1b64776) SystemRegistryImpl: code clean up +* [`91a9af16`](https://github.com/jline/jline3/commit/91a9af1693d339315a4a5d794eaaf2bd5099c526) Using SystemRegistry without ConsoleEngine the unknown commands are quietly accepted, fixes #585 +* [`43178f5e`](https://github.com/jline/jline3/commit/43178f5e8f00d51d2c15c08b0f45b9aee83a134a) SystemRegistry: use terminal to flush and close streams in output redirection +* [`12dbfa48`](https://github.com/jline/jline3/commit/12dbfa481e39ae1241c20003ff5a92e4385221dd) improve toString for completer +* [`64af4390`](https://github.com/jline/jline3/commit/64af439095813430ae5fedd18e1c130ce919bf69) Autosuggestion history: escape also '+' char, fixes #584 +* [`9ec26880`](https://github.com/jline/jline3/commit/9ec268809c076294b9304da8bf0060e6bf51da38) Update README.md +* [`8053dc13`](https://github.com/jline/jline3/commit/8053dc13c0955d3e19c55f4c1a99bd222fe34506) Add option to do list view of autocomplete suggestions, #582 +* [`0d861ddb`](https://github.com/jline/jline3/commit/0d861ddb9081a924b20f699a07535319551f6962) Autosuggestion choices of packages names and directories are refreshed too early, fixes #545 +* [`740985bc`](https://github.com/jline/jline3/commit/740985bc1d60608b303b293f2227bf04bb6cc150) Groovy REPL: add widgets and key mappings to toggle Groovy completion options +* [`10aa3905`](https://github.com/jline/jline3/commit/10aa390532c524581d20efe22ccbc24a4ec835db) Groovy REPL: reviewed and fixed all methods completion +* [`5a4b6319`](https://github.com/jline/jline3/commit/5a4b631997fa861cc5023b4dafc1648e792be3a7) Groovy command grab: fix ArrayIndexOutOfBoundsException +* [`e6407044`](https://github.com/jline/jline3/commit/e6407044f00147c9fd26a8600810506661f4c108) Fixed maven-javadoc-plugin configurations +* [`25d3adc9`](https://github.com/jline/jline3/commit/25d3adc95e2b5f85a9fd70a25182e859d5cdeb7c) Bump Groovy to 3.06 +* [`280d75c9`](https://github.com/jline/jline3/commit/280d75c9b5d8a3b5d2ca8b76d10288abcaa863a4) Groovy REPL: chained metaMethods completion with inline Closure parameters +* [`546c8ff7`](https://github.com/jline/jline3/commit/546c8ff725675b876d2bb4314688e82613a9fe9b) Fix for typo in javadoc +* [`f8cec7b7`](https://github.com/jline/jline3/commit/f8cec7b738e4f14341d94852f209b73033cc90d3) Groovy REPL: improve closure vars type detection, add better test for immutable variables in ObjectCloner and fix ClassCastException in Utils.groovy +* [`6ece81d9`](https://github.com/jline/jline3/commit/6ece81d9abdc2119bf08d5dcc64aca8afb7d4d11) nano & less: code clean up +* [`efd9c70b`](https://github.com/jline/jline3/commit/efd9c70b0132b2237d2b30838310aafb28a6972c) REPL pipe alias can now be defined using also other aliases +* [`36d3bc75`](https://github.com/jline/jline3/commit/36d3bc7510b0402d4194e94de9b4a70307b7d3cb) Groovy identifiers completion a couple of improvements +* [`ce3c9308`](https://github.com/jline/jline3/commit/ce3c93081f359e9dc207f63f6d3baff9a9cc400d) Groovy REPL: add option to complete groovy metaMethods +* [`22faa8b2`](https://github.com/jline/jline3/commit/22faa8b27cfe3ead484c19bd687d3e6340fef802) Navigating grouped candidates using arrow keys is broken, fixes #580 +* [`d3860450`](https://github.com/jline/jline3/commit/d3860450112f723b79dcc4656159ead8e44e4c98) Groovy REPL: add widget to toggle candidate grouping and fix a bug in identifiers completion +* [`cda92e36`](https://github.com/jline/jline3/commit/cda92e3627c8c98986dcd54b0e4d5d34b7a5c249) Groovy REPL: add option to complete groovy identifiers +* [`ccf838c0`](https://github.com/jline/jline3/commit/ccf838c0e39194a093a989eb06bc1ac105bdd949) Groovy REPL: add option to complete private and protected classes, #577 +* [`d15889fe`](https://github.com/jline/jline3/commit/d15889fe5b4ab5b1542821837e26237d61bce811) Groovy REPL: add options to complete private and protected fields, methods and constructors +* [`1b6a3322`](https://github.com/jline/jline3/commit/1b6a33228109b9923136a1da69bb4cb45b6ee673) Command prompt and line buffer are overlaid with completion candidates, fixes #574 +* [`f364c00a`](https://github.com/jline/jline3/commit/f364c00a406254bb3fe683972e802ea8bb0b9869) Autosuggestion history: fixed search command regex pattern, #570 +* [`f6c12465`](https://github.com/jline/jline3/commit/f6c1246591354ada24c2b4e03eb4446a2cb105c3) Completers and Options: polish up regex patterns +* [`8f52fb3b`](https://github.com/jline/jline3/commit/8f52fb3ba3b0f818af9598e9508a5b9825702440) JLine script: redirecting command output to temporary variable will +* [`2d82d47a`](https://github.com/jline/jline3/commit/2d82d47ac4e91c18375dced41304475d88ee6c00) Auto suggestion error when type "\_" character, fixes #570 +* [`4d8aa743`](https://github.com/jline/jline3/commit/4d8aa74342d3436a4ef14afc9cc4dda92a1cc19a) Groovy REPL: improved constructor statement completion +* [`ff28596e`](https://github.com/jline/jline3/commit/ff28596e2bc74e047489869820ea813921b757d1) GroovyEngine Inspector: override only Closure vars with function def +* [`0d4fc5df`](https://github.com/jline/jline3/commit/0d4fc5dfa384b555f66012d82446dbcef450205d) Groovy REPL: no method completion nor descriptions if known command +* [`8b1b76ca`](https://github.com/jline/jline3/commit/8b1b76caac316a77a16ed836ad34f05bb26c5046) Groovy REPL completion: improved statement variables type detection +* [`6551251c`](https://github.com/jline/jline3/commit/6551251c1f987f4f74b303e4685dd5916f18346b) Groovy REPL: package tab completion failed: +* [`2894fa7b`](https://github.com/jline/jline3/commit/2894fa7ba8eb6e6f5332a5b83026f7cec105ac7a) Groovy REPL: improved var tab completion, restrictedCompletion=true +* [`db15475b`](https://github.com/jline/jline3/commit/db15475bb245860e95009dc252ced113b2fe4cca) Groovy REPL: constructor tab completion failed: NoClassDefFoundError, fixes #568 +* [`6158378e`](https://github.com/jline/jline3/commit/6158378e4b4682600a079f057470d5c0f0cd5fc1) Groovy REPL: improved completion of function & closure parameters +* [`a30080a8`](https://github.com/jline/jline3/commit/a30080a87fed8137e7bc00ba627c5e7ffd68ce28) DefaultParser: ArrayIndexOutOfBoundsException, fixes #567 +* [`bb8b6fe0`](https://github.com/jline/jline3/commit/bb8b6fe0792948f9d4f07071e40239a90dbdc78e) Groovy REPL: added Groovy options noSyntaxCheck and restrictedCompletion +* [`786dcd81`](https://github.com/jline/jline3/commit/786dcd811cfac24d01a68adadf248c0ffd6514bc) jline-console: code cleanup +* [`1484c4dd`](https://github.com/jline/jline3/commit/1484c4dda60e08efd24825861bf3d5ffbd559609) Groovy REPL method description: improved detection of method's Class + a few other minor fixes +* [`9c7acb9f`](https://github.com/jline/jline3/commit/9c7acb9f65c4d1984d543deb20be35c117305a87) Groovy REPL for statement: improved looping variable type detection +* [`80d4d3fa`](https://github.com/jline/jline3/commit/80d4d3fa7930c0eacbfbb4cf8003fb04eb92096b) widget package: minor fixes & code reformatting +* [`6ee50323`](https://github.com/jline/jline3/commit/6ee5032305fa4e61b9eed434f113e6496a506096) jrt classes: manage tab-completions & minor fixes and improvements +* [`19fedee9`](https://github.com/jline/jline3/commit/19fedee93392f064a1194c7ce6de1f2319e66f09) jline-groovy: minor fixes & code reformatting +* [`36f8ecdc`](https://github.com/jline/jline3/commit/36f8ecdcdbe37981e04d3658fad719af1f4fcefa) JLine app with and without console variables, fixes #565 +* [`2b5e1ae2`](https://github.com/jline/jline3/commit/2b5e1ae2a9181772eea911c3975e18845aef1571) JLine bundle javadoc is not been generated, fixes #564 +* [`949969e4`](https://github.com/jline/jline3/commit/949969e4cd167fafce2b0e7b7aae6a375ce4a0f2) Solaris automounter: createDirectories throws FileSystemException, fixes #559 +* [`e7eb5e06`](https://github.com/jline/jline3/commit/e7eb5e0626b8dd3fb8483b4ff6303a826f585d41) Merge pull request #562 from morris821028/master +* [`992587a5`](https://github.com/jline/jline3/commit/992587a565bed73c696b982f236bde6b18af7320) Example: fix hung on exit with 'status' option +* [`fdc2fb53`](https://github.com/jline/jline3/commit/fdc2fb53f9dc618bfccc3b20ae447cabce3a809f) Add support for embedded applications + +## [JLine 3.16.0][3_16_0] +[3_16_0]: https://repo1.maven.org/maven2/org/jline/jline/3.16.0/ + +* [`f867197e`](https://github.com/jline/jline3/commit/f867197effd901fa7e87c789b06eee1f185952fa) Add a unit test for #552 +* [`0af26c08`](https://github.com/jline/jline3/commit/0af26c08ea622c04a97b69b935726eef9e7f08e9) NonBlockingPumpReader.read() does not block, fixes #552 +* [`a0137601`](https://github.com/jline/jline3/commit/a01376015c3277b8e86e3a1b1ded18fb2d788ee6) GroovyEngine: renamed GROOVY_OPTION groovyColors to GROOVY_COLORS +* [`25824ac0`](https://github.com/jline/jline3/commit/25824ac0f450660ac847edd40a2bfa8f14465115) Autosuggestion choices not refreshed after tab #545, fixed regression caused by commit https://github.com/jline/jline3/commit/feb769018aab222614e4576aa63cc746b45224cc +* [`722f2ef7`](https://github.com/jline/jline3/commit/722f2ef79a1c6000518b262a5d18326d30cba677) GroovyEngine Inspector: manage for-each statement +* [`ef81ae72`](https://github.com/jline/jline3/commit/ef81ae721dc4a3ba947598ee331af7fee6dfa7f6) GroovyEngine: checkSyntax() ignore NullPointerException +* [`80423654`](https://github.com/jline/jline3/commit/804236549a36e46a5d262feafab26a4c9805bdbb) GroovyEngine: tab-completion manage lambda expression peculiarities +* [`ee3f01ba`](https://github.com/jline/jline3/commit/ee3f01baeb37248b42ea2fbc288a6dd8a23f5614) GroovyEngine: add try-catch & small refactoring +* [`0175f494`](https://github.com/jline/jline3/commit/0175f494827170ef8f9e8cd2a30f0376a3d222a5) Update README.md +* [`b6ffb521`](https://github.com/jline/jline3/commit/b6ffb521bdba6dc31361c709b46fa8a6c4ffe0c5) GroovyEngine: added methods markCache() and purgeCache() in Cloner API +* [`3b0d5d52`](https://github.com/jline/jline3/commit/3b0d5d52fd2c31fb92ad8aaa2f11a22aceab890a) rxvt-terminal: NullPointerException with `infocmp` warning, fixes #550 +* [`d2ac4ef4`](https://github.com/jline/jline3/commit/d2ac4ef422409bbd4b37a41d10e6b66fb54bb926) GroovyEngine: fixed chained method tab completion +* [`99e5efe6`](https://github.com/jline/jline3/commit/99e5efe60957094960e95df8a5522902502519f1) Improved PipelineCompleter +* [`c0f0e79b`](https://github.com/jline/jline3/commit/c0f0e79b06f0b692064542caac51cfee0e5b58b0) TailTipWidgets: fixed status bar message compilation +* [`8e979e10`](https://github.com/jline/jline3/commit/8e979e10fdcf509480c39ab7c677a515d9fdc7a3) Groovy: customizable colors +* [`458ec405`](https://github.com/jline/jline3/commit/458ec4053284cbd35928ee0f6f9a839e8d94e2f7) GroovyEngine: added syntax error highlighting +* [`e30759c8`](https://github.com/jline/jline3/commit/e30759c86d949d51d433e7022ac4fe929e2da264) GroovyEngine: tab completion requires min. one char to show candidates +* [`aafbf365`](https://github.com/jline/jline3/commit/aafbf365cb2a75f190cb985de2b973da368436aa) GroovyEngine: improve inner class tab completions +* [`f1817734`](https://github.com/jline/jline3/commit/f1817734d5b65f788995921a8d0107b7f499e563) GroovyEngine: display method descriptions with short type names +* [`dbf4118b`](https://github.com/jline/jline3/commit/dbf4118bf04cf6def23eebc32ba9944e06f22219) Auto suggestion error when type Asterisk character (*), fixes #548 +* [`3b797306`](https://github.com/jline/jline3/commit/3b79730668e573222abd83d54f97886089e1c8f0) GroovyEnine tab-completion: Inspector create closures from function def +* [`92b94dac`](https://github.com/jline/jline3/commit/92b94dacf34414f997d5c2e1e31b1ed7db78f034) GroovyEngine: tab-completion improvements and fixes +* [`915e942b`](https://github.com/jline/jline3/commit/915e942b99c5fcb2f1ac10b619a9c02db112c46a) TailTipWidgets: simplified if statement +* [`96defb80`](https://github.com/jline/jline3/commit/96defb80d9e5573d0a033ba1f7b541ba1e11fce5) TailTipWidgets: set LineReader variable errors=0 +* [`fb196ca9`](https://github.com/jline/jline3/commit/fb196ca9e87434172765bff06b9a8ede265504e4) GroovyEngine: display method descriptions on status bar +* [`272648ec`](https://github.com/jline/jline3/commit/272648eccea0c7f391b4431b9be22be44cb7cb24) SystemRegistry help customizations, fixes #547 +* [`991aae58`](https://github.com/jline/jline3/commit/991aae58c487db248b51287d9fe7acabc9bee1d0) GroovyEngine: tab-completion of object methods inside code block +* [`66fcd737`](https://github.com/jline/jline3/commit/66fcd7370e0332cdce046023b7717d6769a85b46) Builtins title is shown in command help without commands, fixes #546 +* [`6c81a5b1`](https://github.com/jline/jline3/commit/6c81a5b1e74b087e8745074dc4ff1958dadae2b5) Display: ArithmeticException: / by zero, fixes #526 +* [`ae8ff734`](https://github.com/jline/jline3/commit/ae8ff734cc033a0bc01a0d178a87f2a47cd1ae26) GroovyEngine: tab-completion for method parameters +* [`56b1f297`](https://github.com/jline/jline3/commit/56b1f297e34cfd9b18fc5f682f9f9451581a9e01) LineReaderImpl: added trace +* [`7d619144`](https://github.com/jline/jline3/commit/7d619144a1cffe644976d478fa37270f07fa540d) GroovyEngine: tab-completion for chained methods +* [`88cf67da`](https://github.com/jline/jline3/commit/88cf67da6511bdc9184d56e7fa9895feda546e6b) GroovyEngine: more tab-completions variables, methods and constructors +* [`53d00b85`](https://github.com/jline/jline3/commit/53d00b85db6c063ad22a444f0117605017d6649e) command grab: added --verbose option +* [`8618e471`](https://github.com/jline/jline3/commit/8618e471c99edc52a0421e743153c860a28dd8c1) JlineCommandRegistry: added traces +* [`5ae969ed`](https://github.com/jline/jline3/commit/5ae969edd75a7d86ee1673714d936b76bf5c546e) DefaultPrinter: added two methods to make prnt command options easily +* [`feb76901`](https://github.com/jline/jline3/commit/feb769018aab222614e4576aa63cc746b45224cc) Autosuggestion choices are not refreshed after tab, fixes #545 +* [`04aa1932`](https://github.com/jline/jline3/commit/04aa19320a058c4b3d69133d24f46b6d7cd44acb) GroovyEngine: added a few tab-completions (import, def, class, ...) +* [`fe632071`](https://github.com/jline/jline3/commit/fe632071442875017b623adc690cf0f11e9b8306) GroovyCommand: improved command tab-completions +* [`c6a09bd9`](https://github.com/jline/jline3/commit/c6a09bd9e1f3154c145384b2d90d4bcf87257197) Improved OptionCompleter long options value completion +* [`de558860`](https://github.com/jline/jline3/commit/de55886075b0941c923d4fec3ac7d06fe549086d) CommandRegistry interface: removed method commandDescription(command), +* [`add29245`](https://github.com/jline/jline3/commit/add292459554f7a6863fb2c3cea4d302674ff169) prnt command: fix options tab-completion, regression caused by #540 +* [`497f10c6`](https://github.com/jline/jline3/commit/497f10c6b8d62f6c720b5eea88a3e371f448345b) Merge pull request #543 from mattirn/commandRegistry-improvment +* [`f13b695a`](https://github.com/jline/jline3/commit/f13b695ac90e661388203973de7d2c3ccb5eaaa0) CommandRegistry interface: removed execute() method +* [`04eaadf4`](https://github.com/jline/jline3/commit/04eaadf4b532a9926d89476ebcf0f70964a86346) Update README.md +* [`64d127e4`](https://github.com/jline/jline3/commit/64d127e4232e0aff4bc7c3a91cb81ab2ac0264d7) Update README.md +* [`78b4c9f9`](https://github.com/jline/jline3/commit/78b4c9f98af825b5bdd63deb0970afa81294db08) Merge pull request #540 from mattirn/console-package +* [`aa84d181`](https://github.com/jline/jline3/commit/aa84d181f5b6ba00a42b01f45ba6106917542935) DefaultPrinter can now be used also without ScriptEngine +* [`f8b7615b`](https://github.com/jline/jline3/commit/f8b7615b15e6dad56480b039a9e5556567b768d0) prnt command: moved implementation to DefaultPrinter +* [`fcac9694`](https://github.com/jline/jline3/commit/fcac969406439b8c0d32bf01fbb30d1510a6f055) Console example: fixed widgets enabling/disabling, step II +* [`4c24ce18`](https://github.com/jline/jline3/commit/4c24ce18e2f718a82e0d9677db2bb38ae7b84706) Split Widgets class to org.jline.widget package +* [`b3428b42`](https://github.com/jline/jline3/commit/b3428b42e69925b0540ba6e2a6930848d6b517cb) ConsoleEngine: added setPrinter() method +* [`1aa0bda2`](https://github.com/jline/jline3/commit/1aa0bda27f7db310899482a8d4540b9a9abc5fec) Console example: fixed widgets enabling/disabling +* [`d31b6784`](https://github.com/jline/jline3/commit/d31b678470504f1af215b7b04a219fee2760526c) Added simplified example, fixes #537 +* [`16115a55`](https://github.com/jline/jline3/commit/16115a559e8b566ab51a5610204c52effbaa2e55) Added jline-console module +* [`10c3f209`](https://github.com/jline/jline3/commit/10c3f209446af58a847a9cf46199902820db48cb) Also test with jdk 14 + +## [JLine 3.15.0][3_15_0] +[3_15_0]: https://repo1.maven.org/maven2/org/jline/jline/3.15.0/ + +* [`1ccf81cf`](https://github.com/jline/jline3/commit/1ccf81cf962b6f61faebd7d140c52227bdcb7bcd) Merge pull request #536 from Marcono1234/AttributedStringBuilder-append-null +* [`a1551e7b`](https://github.com/jline/jline3/commit/a1551e7b13b8f332b4e0606f4bbc76422d1cba0d) Fix AttributedStringBuilder.append not handling null correctly +* [`6924e3e9`](https://github.com/jline/jline3/commit/6924e3e95d6dc9681121820462c35976678c084c) Removed key sequence ^\ from nano help, #441 +* [`67b2ba18`](https://github.com/jline/jline3/commit/67b2ba18710cc7422e7398b718f37a02ab7ec0ca) Fixed some javadoc typos +* [`56dbf56c`](https://github.com/jline/jline3/commit/56dbf56cf4847e716e1da328e2459f63d556e32e) PipelineCompleter: improved options completion +* [`5cd04e7c`](https://github.com/jline/jline3/commit/5cd04e7c7173349abab38aa5ca6c5079f7f334bc) Console printer: improved the management of boolean options +* [`482b9c2e`](https://github.com/jline/jline3/commit/482b9c2e397af25846d912864f1942c99a9d6ce7) Merge branch 'master' of https://github.com/jline/jline3.git +* [`0eb2410c`](https://github.com/jline/jline3/commit/0eb2410c3d7f614edc513ebe2225d7390a00c1a3) Nano SyntaxHighlighter: build highlighter from given nanorc url +* [`724e3772`](https://github.com/jline/jline3/commit/724e37725ae4bd4c2c5283f36ec58d6f6df13677) Update README.md +* [`f6717970`](https://github.com/jline/jline3/commit/f67179703ffcf7fb5c5e458041113782479f04c2) Bump groovy to 3.0.4 +* [`80b6f585`](https://github.com/jline/jline3/commit/80b6f585c9e91637e83693f8376bbd7badc82c92) jline demo: removed code duplication +* [`2ed99410`](https://github.com/jline/jline3/commit/2ed994106734c3151bebabb7f8698b32b74c568e) prnt command: added shortNames option +* [`ab29ae24`](https://github.com/jline/jline3/commit/ab29ae246e95ec009a5b0c12bff287a6243cf817) ConsoleEngineImpl: fixed method isHighlighted() +* [`21cfe927`](https://github.com/jline/jline3/commit/21cfe9279de52560849a438ac576fe974954aac4) ConsoleEngineImpl: fixed printing of empty Map +* [`013251f3`](https://github.com/jline/jline3/commit/013251f39c5383898ea9bfad6b426c3270f4d15b) repl demo: improved object highlighting +* [`3a4abf67`](https://github.com/jline/jline3/commit/3a4abf6796542868cf7842cb600cd9abc10a8771) repl demo: fixed init script +* [`7eb01785`](https://github.com/jline/jline3/commit/7eb01785ab715e4e15ed3d4f7f1d363e7323e4fe) Add support for rxvt terminals, fixes #533 +* [`f8aa6e45`](https://github.com/jline/jline3/commit/f8aa6e45d3865beb00411aa6658eee5aa144b245) Cut down verbosity of debug logging for unsupported signals, fixes 455 +* [`a6176bc6`](https://github.com/jline/jline3/commit/a6176bc6d3493b8dac285a03c08a2932067ab0ca) Add Automatic Module Name, fixes #530 +* [`80265b47`](https://github.com/jline/jline3/commit/80265b4795c0b77ca3d0471ce03bcb965b9fb8be) Fixed javadoc warnings +* [`3c83e59c`](https://github.com/jline/jline3/commit/3c83e59ce585a9f27fcd6c8eba5aad3aa1867f2e) Update README.md +* [`48663a77`](https://github.com/jline/jline3/commit/48663a775427fdf9c5ca72647c7e801f761b17c9) Merge pull request #531 from mattirn/graalvm +* [`875efcbd`](https://github.com/jline/jline3/commit/875efcbd0b2e404c89d8915539192a4d2111fb21) Appveyor: Bump maven to 3.3.9 +* [`1248c23c`](https://github.com/jline/jline3/commit/1248c23c81e5ca81fc0118a9d51dea7300dd6c0d) jline-groovy: improved object to string conversion +* [`00a46ad8`](https://github.com/jline/jline3/commit/00a46ad80f2e467d5376ae93d04bea1578544364) Repl demo: added jline & groovy docs key bindings to F1 and F2 +* [`67409e60`](https://github.com/jline/jline3/commit/67409e60e6201e44730290de91210ba45a6b6201) prnt command: fixed list printing indention +* [`b695c9b8`](https://github.com/jline/jline3/commit/b695c9b8bdb30ba4b82c5be049c8a36a12c76bb6) SystemRegistryImpl: fixed NPE +* [`4b28a0a4`](https://github.com/jline/jline3/commit/4b28a0a4e7599e26011eda558a0bb3c9ad21dfbb) GraalVM: added graal command in build.config +* [`1a9ee1ba`](https://github.com/jline/jline3/commit/1a9ee1baa8f90570855f0cf87f0b066acdae7dcf) SystemRegistryImpl: added method addCompleter() +* [`81e38b94`](https://github.com/jline/jline3/commit/81e38b948f8fce71b9f572b9b7a4414c8d6d63d9) slurp command can now deserialize also string variable context +* [`b0c272ea`](https://github.com/jline/jline3/commit/b0c272eaf71e1a2c9684b7f483383f22a1ffe0d3) Merge branch 'master' of https://github.com/jline/jline3.git +* [`7333eeb9`](https://github.com/jline/jline3/commit/7333eeb9d02362bfb581dba0c12b313837803027) FileNameCompleter: fixed Windows file name highlighting +* [`40900ef2`](https://github.com/jline/jline3/commit/40900ef27d9eecd461f04efdc2d3449f1776ed7b) GraalVM: added maven profile native-image +* [`6643730f`](https://github.com/jline/jline3/commit/6643730fec60bd0866228652d4c8a99f5371f2dd) GraalVM: added catch Error in ttop and removed it from graal demo +* [`0de635be`](https://github.com/jline/jline3/commit/0de635bedfccd025cc8634ebe890c4be1b4961e8) Support for cygwin recent versions, fix for #520 +* [`c5eca10d`](https://github.com/jline/jline3/commit/c5eca10d5f98788b795e33d82ec5817da1fd0f7a) GraalVM: fixed java.util.logging problem +* [`39069251`](https://github.com/jline/jline3/commit/3906925164d08416daad564150b15937db4e46e1) GraalVM Support #381 +* [`496492e9`](https://github.com/jline/jline3/commit/496492e98ee210e5b94ed2be2730788ee24b63d9) Fixed repl demo shell help, step II +* [`9613ec3b`](https://github.com/jline/jline3/commit/9613ec3bbc794696c1b5be3d16f1720207669d07) Widgets: executeWidget() restore old binding after runMacro() +* [`bf7f1ad0`](https://github.com/jline/jline3/commit/bf7f1ad040d45936b1cc77452403a1e3e598c925) Fixed repl demo shell help & improved console command completers +* [`8ff80c04`](https://github.com/jline/jline3/commit/8ff80c040b93aea9e5abc7bdbc408ac5aec839cf) TailTipWidget: highlight command main description if not highlighted +* [`4341151f`](https://github.com/jline/jline3/commit/4341151f334615b28eeb939e26521a51526da633) JlineCommandRegistry: improved command completer compilation +* [`bd35c22d`](https://github.com/jline/jline3/commit/bd35c22d07ee5ef000e01fabc633f3861ffa9836) Nano SyntaxHighlighter support also color codes 0-255 +* [`ba709739`](https://github.com/jline/jline3/commit/ba7097398f09778d8df8c305b0d8d1ce371cedb7) print map: check on maxrows & SystemRegistryImpl.invoke() fixed NPE +* [`b7ae0ead`](https://github.com/jline/jline3/commit/b7ae0ead1059f9203e19296d5582285234a4ec80) Customizable colors for ls, help and prnt commands, fixes #525 +* [`5cf8f030`](https://github.com/jline/jline3/commit/5cf8f030ead139114f9dd4f6ac23121d09b1ce6a) prnt command: use StyleResolver to highlight output +* [`d24883cd`](https://github.com/jline/jline3/commit/d24883cd999e0b2349beaee623b8436e13e2181f) prnt command: added valueStyle option +* [`f1529980`](https://github.com/jline/jline3/commit/f1529980f8b2071959a1f0a21289dad831240d84) Merge pull request #527 from mattirn/groovy-commands +* [`7f263440`](https://github.com/jline/jline3/commit/7f2634408f66fd7fe3802508fdb7a83f61bf9772) small improvements and bug fixes +* [`b024288b`](https://github.com/jline/jline3/commit/b024288bdf324ef6c2df065d311083b3b38f01d0) ConsoleEngineImpl: added doc command +* [`9cb2708a`](https://github.com/jline/jline3/commit/9cb2708a6e428cc6bc7f46f6ff5911604fdafa61) Repl demo: removed now obsolete SubCommands class example +* [`8be9242b`](https://github.com/jline/jline3/commit/8be9242b77210cf226c9dd4cea3e2e557716fa85) GroovyCommand: added grap command +* [`51b8bcf6`](https://github.com/jline/jline3/commit/51b8bcf67e5fa837861cb0f65d8bde7cdbdd38ed) Added javadocs in Printer and small fixes +* [`43748c7a`](https://github.com/jline/jline3/commit/43748c7ad60f45b03a4b29ea40deb9e396d54680) GroovyCommand: added command console & inspect command option --gui +* [`c1661890`](https://github.com/jline/jline3/commit/c166189033bfd1c9f597830e68078e139e4d6798) prnt command: improved Map collection value printing +* [`070d32c5`](https://github.com/jline/jline3/commit/070d32c58774d1953db9f51fca351d55b867d6ec) REPL console: added Printer interface and a groovy command inspect +* [`4179fbd9`](https://github.com/jline/jline3/commit/4179fbd9d1253b4a7c369dccf3dbe49b0705752c) Refactoring command registers, step III +* [`5f6a1e67`](https://github.com/jline/jline3/commit/5f6a1e671c64c24f60b2ff632bc230437cade149) Refactoring command registers, step II +* [`6dcb288d`](https://github.com/jline/jline3/commit/6dcb288d99b27500b219f36c092495ce84cce411) Refactoring command registers, step I +* [`a88409ff`](https://github.com/jline/jline3/commit/a88409ff589551036b782b274a8ac44b55b094f7) Refactor: moved CommandRegistry to org.jline.console package +* [`da71da6c`](https://github.com/jline/jline3/commit/da71da6c0283912ebdfbbd90ec2fd15de995fbcc) Refactor: added a new package org.jline.console +* [`d53681a6`](https://github.com/jline/jline3/commit/d53681a621d5dbfd5a8617cf135859c00f0399c4) prnt command: added options include and exclude +* [`0bd926dd`](https://github.com/jline/jline3/commit/0bd926dd3f86e476b85dbef71955a789e30b0315) prnt command: added options maxDepth and indention +* [`4694f748`](https://github.com/jline/jline3/commit/4694f748136dac425b9e2ed3ae930bcf988d51b0) Repl demo: added trace script +* [`e4c6f88c`](https://github.com/jline/jline3/commit/e4c6f88cf612d92ae2d61b701009c7bd2af8c8fb) SystemRegistryImpl: improved argument parsing +* [`bf35624c`](https://github.com/jline/jline3/commit/bf35624c4cbe5ae6d85e7c96f0ee9c281713b883) Repl demo: intercept Control-C +* [`d283a03d`](https://github.com/jline/jline3/commit/d283a03dee58b1fc729464f1d60b629b82f31851) TailTipWidgets: disabled command description cache as default + +* [`f67c0731`](https://github.com/jline/jline3/commit/f67c0731f3138c01c31950f358ff726d9491f776) CommandRegistry: added method commandDescription(List args) +* [`90a67407`](https://github.com/jline/jline3/commit/90a67407d3b1ef5ba2160cda91a98114131539ea) Updated changelog +* [`381e8cb7`](https://github.com/jline/jline3/commit/381e8cb73f7df7e07faedde2647771c42dc79fcb) Move the plugin to the management section + +## [JLine 3.14.1][3_14_1] +[3_14_1]: https://repo1.maven.org/maven2/org/jline/jline/3.14.1/ + +* [`81b6eade`](https://github.com/jline/jline3/commit/81b6eadeed147dcfe563ca7f18695064c1b5bab4) Fix signing +* [`df9f1f91`](https://github.com/jline/jline3/commit/df9f1f9133539c6715b407e5dc80a280d75fd395) Fix broken paste with remote connections, fixes #514 +* [`88c28ae2`](https://github.com/jline/jline3/commit/88c28ae2a660b521552ffa4b5b508ac63446212c) REPL parameter expansion, do not add quote chars on numeric parameters +* [`5438565f`](https://github.com/jline/jline3/commit/5438565f582b127d013f93baecc9d754c0156587) ScriptEngine added methods: getSerializationFormats() and +* [`664eef8f`](https://github.com/jline/jline3/commit/664eef8fa4adc023fea9c787c4df1271a1892ab1) Refactoring Builtins.CommandInput +* [`3cac1ad7`](https://github.com/jline/jline3/commit/3cac1ad7cc68de1a67d4fd0fff7af38a4a026d52) Refactoring and improved java docs +* [`e381d1b2`](https://github.com/jline/jline3/commit/e381d1b24db3db63ec724f5e4cfe38704d9174a8) ConsoleEngineImpl: improved command completer and help +* [`db9f36e5`](https://github.com/jline/jline3/commit/db9f36e578b219a94a73b41691de797470118186) NanoTest: ignore nanorc files +* [`f0d7f238`](https://github.com/jline/jline3/commit/f0d7f23802c93eb721dd04402b4e48683c80ff85) prnt command: reviewed map similarity comparison and value highlight +* [`f6e3c083`](https://github.com/jline/jline3/commit/f6e3c083eca4c5ddccc144c57150706959211abe) pipeline completer: added console option maxValueNames +* [`0e55bb51`](https://github.com/jline/jline3/commit/0e55bb51bd00702a52123ba77e0d8e39319c5eaf) REPL console: added pipeline tab completer +* [`817c59a9`](https://github.com/jline/jline3/commit/817c59a9285ea45cb447384578d85e3690f0ff50) Bump groovy to 3.0.2 +* [`2c46ae0a`](https://github.com/jline/jline3/commit/2c46ae0ada2013fe161f708596d6df47d10df4d8) Refactored repl demo and improved registered sub-command help +* [`8467b077`](https://github.com/jline/jline3/commit/8467b077310ac987c687772b1755067e85ffc951) prnt command: added more checks in table print decision +* [`38a909c0`](https://github.com/jline/jline3/commit/38a909c0ec1be56865944797a889caff21b98ba3) SystemRegistryImpl: improved help +* [`33f76291`](https://github.com/jline/jline3/commit/33f76291585396f47518002d9a3b03c03485fea0) prnt command: added options maxrows and maxColumnWidth +* [`c191801f`](https://github.com/jline/jline3/commit/c191801f2de1c9f7d5de219d4f0c9851c6c76ef7) Fix two regressions caused by pull request #518: +* [`d3336a04`](https://github.com/jline/jline3/commit/d3336a04718610b7eb7e2ba062bed029c27458e4) Merge pull request #518 from mattirn/subcommand +* [`f71d2c0d`](https://github.com/jline/jline3/commit/f71d2c0dd706b5b88ee75e841c9055622224d5e5) Update TerminalBuilder.java +* [`9696f11f`](https://github.com/jline/jline3/commit/9696f11fbb5b123445d6739451abbee4ced5b9ec) TailTip widget: improved sub-commands summary info description +* [`7318cf11`](https://github.com/jline/jline3/commit/7318cf1196c8ae6fac2ca43c6700952f0053e232) CommandRegistry: added registry command summary in default +* [`9ea33bed`](https://github.com/jline/jline3/commit/9ea33bedb114105235747bffbf5ca64124231499) Improved subcommands help +* [`b00a9a03`](https://github.com/jline/jline3/commit/b00a9a03a7fa36fe6c83780450570d4a374c4cfe) subcommands: added support for object parameters +* [`5a249551`](https://github.com/jline/jline3/commit/5a2495511f3a4601443e66c80473daa09ef44b7e) Command autosuggestion: support subcommands +* [`c2d2087d`](https://github.com/jline/jline3/commit/c2d2087dd0054cf50f14c7e04b56a118057067ef) SystemRegistry: register and manage subcommands execution and completion +* [`c4632055`](https://github.com/jline/jline3/commit/c4632055305c7b9cffd35f0f22383184b81e7fdf) Builtins.CommandInput: added field command +* [`9b7842ba`](https://github.com/jline/jline3/commit/9b7842ba964fa458a2261c863b63f9d9f2590035) Merge pull request #517 from mattirn/prnt-customize +* [`1feab624`](https://github.com/jline/jline3/commit/1feab6241026212d624df53b767e896588a35e78) prnt command: improved heterogeneous object list printing +* [`c41e2df5`](https://github.com/jline/jline3/commit/c41e2df51726e19253de99ea500871ad1d61692b) prnt command: improved Iterator and Iterable object printing +* [`af82e2cf`](https://github.com/jline/jline3/commit/af82e2cfc59bc0e5314792ecf4419f83d2fbebfc) Refactoring object printing +* [`9430ba01`](https://github.com/jline/jline3/commit/9430ba01ea07871d9a9131839e986966e4e4b50e) prnt command: improved map printing +* [`45234086`](https://github.com/jline/jline3/commit/4523408679914ccb4dc177279adc192f723e057c) prnt command: added option toString and custom highlight map values +* [`07b2df9f`](https://github.com/jline/jline3/commit/07b2df9fb322c2babdef6b8545aa5176cbb7ee8e) Expand parameter in file path, fixes #516 +* [`f30e34b2`](https://github.com/jline/jline3/commit/f30e34b2994d24a2e6cf7bc1b4b6e521914c4e90) prnt command customization +* [`02c7f67c`](https://github.com/jline/jline3/commit/02c7f67ce13acc567c4fc673c65203e23834eabf) ScriptEngine: added method to execute closure +* [`bc331f89`](https://github.com/jline/jline3/commit/bc331f8958524374e9eb4c6b115fe3ddabe953ec) prnt command: added configuration options columnsIn and columnsOut +* [`eedaff33`](https://github.com/jline/jline3/commit/eedaff33b61a327599bd970962cf9599971c2c2a) prnt command: added options oneRowTable, structsOnTable and columns +* [`6e61b976`](https://github.com/jline/jline3/commit/6e61b976b8d115fea25b20f82f8a48ea96e48cf9) Parser: fixed getCommand() method, step II +* [`71e35644`](https://github.com/jline/jline3/commit/71e35644a6799a79bd033539bdc08cea84ffa0a3) REPL console: parameter ${@} expansion and two new pipes in demo +* [`89123999`](https://github.com/jline/jline3/commit/891239993a26f4dc87f4bb4f723da9957975d6a3) REPL demo: added command to execute shell commands +* [`c9e16309`](https://github.com/jline/jline3/commit/c9e16309696f7b0f054516a98ca5e6cb1cda60e4) Parser: fixed getCommand() method +* [`61693df3`](https://github.com/jline/jline3/commit/61693df3cf26a97782cdbcebb50570a5eb1cb89a) Refactoring object printing +* [`b8d7936b`](https://github.com/jline/jline3/commit/b8d7936b9826fa8bf6ad65b247d7720206b578ca) REPL console: throw Exception if redirecting console script output +* [`6fbdb250`](https://github.com/jline/jline3/commit/6fbdb250b390214441381be7cf1c25c7a6f13e48) REPL console: allow the use of console scripts in pipe line +* [`01e0c542`](https://github.com/jline/jline3/commit/01e0c542326cf4a2239820cda2da6dd7d2952b72) REPL console: improved object printing +* [`089f9898`](https://github.com/jline/jline3/commit/089f9898538f16324532f960721bd3b0e94d6f59) REPL console: redirect output to null device (command > null) +* [`e839b9c5`](https://github.com/jline/jline3/commit/e839b9c56fa2ffaf1b9c6f6b5cc22ecd553836b4) Fix NPE when use SystemRegistry without ConsoleEngine, fixes #515 + ## [JLine 3.14.0][3_14_0] [3_14_0]: https://repo1.maven.org/maven2/org/jline/jline/3.14.0/ diff --git a/console/pom.xml b/console/pom.xml new file mode 100644 index 000000000..5f222ddda --- /dev/null +++ b/console/pom.xml @@ -0,0 +1,97 @@ + + + + + 4.0.0 + + + org.jline + jline-parent + 3.18.1-SNAPSHOT + + + jline-console + JLine Console + + + org.jline.console + + + + + org.jline + jline-builtins + + + org.jline + jline-style + + + + com.googlecode.juniversalchardet + juniversalchardet + true + + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + true + + + + default-compile + + + **/ConsoleEngineImpl.java + **/TTop.java + + + -Xlint:all,-options + -Werror + + + + + compact + + compile + + + + **/ConsoleEngineImpl.java + **/TTop.java + + + -Xlint:all,-options + -Werror + -profile + compact1 + + + + + + + + diff --git a/console/src/main/java/org/jline/console/ArgDesc.java b/console/src/main/java/org/jline/console/ArgDesc.java new file mode 100644 index 000000000..5c0d706be --- /dev/null +++ b/console/src/main/java/org/jline/console/ArgDesc.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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; + +import java.util.ArrayList; +import java.util.List; + +import org.jline.utils.AttributedString; + +public class ArgDesc { + private final String name; + private final List description; + + public ArgDesc(String name) { + this(name, new ArrayList<>()); + } + + public ArgDesc(String name, List description) { + this.name = name; + this.description = new ArrayList<>(description); + } + + public String getName() { + return name; + } + + public List getDescription() { + return description; + } + + public static List doArgNames(List names) { + List out = new ArrayList<>(); + for (String n: names) { + out.add(new ArgDesc(n)); + } + return out; + } + +} diff --git a/console/src/main/java/org/jline/console/CmdDesc.java b/console/src/main/java/org/jline/console/CmdDesc.java new file mode 100644 index 000000000..e631ada50 --- /dev/null +++ b/console/src/main/java/org/jline/console/CmdDesc.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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; + +import java.util.*; +import java.util.regex.Pattern; + +import org.jline.utils.AttributedString; + +public class CmdDesc { + private List mainDesc; + private List argsDesc; + private TreeMap> optsDesc; + private Pattern errorPattern; + private int errorIndex = -1; + private boolean valid = true; + private boolean command = false; + private boolean subcommand = false; + private boolean highlighted = true; + + public CmdDesc() { + command = false; + } + + public CmdDesc(boolean valid) { + this.valid = valid; + } + + public CmdDesc(List argsDesc) { + this(new ArrayList<>(), argsDesc, new HashMap<>()); + } + + public CmdDesc(List argsDesc, Map> optsDesc) { + this(new ArrayList<>(), argsDesc, optsDesc); + } + + public CmdDesc(List mainDesc, List argsDesc, Map> optsDesc) { + this.argsDesc = new ArrayList<>(argsDesc); + this.optsDesc = new TreeMap<>(optsDesc); + if (mainDesc.isEmpty() && optsDesc.containsKey("main")) { + this.mainDesc = new ArrayList<>(optsDesc.get("main")); + this.optsDesc.remove("main"); + } else { + this.mainDesc = new ArrayList<>(mainDesc); + } + this.command = true; + } + + public boolean isValid() { + return valid; + } + + public boolean isCommand() { + return command; + } + + public void setSubcommand(boolean subcommand) { + this.subcommand = subcommand; + } + + public boolean isSubcommand() { + return subcommand; + } + + public void setHighlighted(boolean highlighted) { + this.highlighted = highlighted; + } + + public boolean isHighlighted() { + return highlighted; + } + + public CmdDesc mainDesc(List mainDesc) { + this.mainDesc = new ArrayList<>(mainDesc); + return this; + } + + public void setMainDesc(List mainDesc) { + this.mainDesc = new ArrayList<>(mainDesc); + } + + public List getMainDesc() { + return mainDesc; + } + + public TreeMap> getOptsDesc() { + return optsDesc; + } + + public void setErrorPattern(Pattern errorPattern) { + this.errorPattern = errorPattern; + } + + public Pattern getErrorPattern() { + return errorPattern; + } + + public void setErrorIndex(int errorIndex) { + this.errorIndex = errorIndex; + } + + public int getErrorIndex() { + return errorIndex; + } + + public List getArgsDesc() { + return argsDesc; + } + + public boolean optionWithValue(String option) { + for (String key: optsDesc.keySet()) { + if (key.matches("(^|.*\\s)" + option + "($|=.*|\\s.*)")) { + return key.contains("="); + } + } + return false; + } + + public AttributedString optionDescription(String key) { + return optsDesc.get(key).size() > 0 ? optsDesc.get(key).get(0) : new AttributedString(""); + } + +} diff --git a/console/src/main/java/org/jline/console/CmdLine.java b/console/src/main/java/org/jline/console/CmdLine.java new file mode 100644 index 000000000..33b2c9c1e --- /dev/null +++ b/console/src/main/java/org/jline/console/CmdLine.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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; + +import java.util.ArrayList; +import java.util.List; + +public class CmdLine { + public enum DescriptionType { + /** + * Cursor is at the end of line. The args[0] is completed, the line does not have unclosed opening parenthesis + * and does not end to the closing parenthesis. + */ + COMMAND, + /** + * The part of the line from beginning till cursor has unclosed opening parenthesis. + */ + METHOD, + /** + * The part of the line from beginning till cursor ends to the closing parenthesis. + */ + SYNTAX} + + private final String line; + private final String head; + private final String tail; + private final List args; + private final DescriptionType descType; + + /** + * CmdLine class constructor. + * @param line Command line + * @param head Command line till cursor, method parameters and opening parenthesis before the cursor are removed. + * @param tail Command line after cursor, method parameters and closing parenthesis after the cursor are removed. + * @param args Parsed command line arguments. + * @param descType Request COMMAND, METHOD or SYNTAX description + */ + public CmdLine(String line, String head, String tail, List args, DescriptionType descType) { + this.line = line; + this.head = head; + this.tail = tail; + this.args = new ArrayList<>(args); + this.descType = descType; + } + + public String getLine() { + return line; + } + + public String getHead() { + return head; + } + + public String getTail() { + return tail; + } + + public List getArgs() { + return args; + } + + public DescriptionType getDescriptionType() { + return descType; + } +} diff --git a/console/src/main/java/org/jline/console/CommandInput.java b/console/src/main/java/org/jline/console/CommandInput.java new file mode 100644 index 000000000..851dc181d --- /dev/null +++ b/console/src/main/java/org/jline/console/CommandInput.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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; + +import java.io.InputStream; +import java.io.PrintStream; + +import org.jline.terminal.Terminal; + +public class CommandInput { + String command; + String[] args; + Object[] xargs; + Terminal terminal; + InputStream in; + PrintStream out; + PrintStream err; + + public CommandInput(String command, Object[] xargs, CommandRegistry.CommandSession session) { + if (xargs != null) { + this.xargs = xargs; + this.args = new String[xargs.length]; + for (int i = 0; i < xargs.length; i++) { + this.args[i] = xargs[i] != null ? xargs[i].toString() : null; + } + } + this.command = command; + this.terminal = session.terminal(); + this.in = session.in(); + this.out = session.out(); + this.err = session.err(); + } + + public CommandInput(String command, Object[] args, Terminal terminal, InputStream in, PrintStream out, PrintStream err) { + this(command, args, new CommandRegistry.CommandSession(terminal, in, out, err)); + } + + public String command() { + return command; + } + + public String[] args() { + return args; + } + + public Object[] xargs() { + return xargs; + } + + public Terminal terminal() { + return terminal; + } + + public InputStream in() { + return in; + } + + public PrintStream out() { + return out; + } + + public PrintStream err() { + return err; + } + + public CommandRegistry.CommandSession session() { + return new CommandRegistry.CommandSession(terminal, in, out, err); + } + +} diff --git a/console/src/main/java/org/jline/console/CommandMethods.java b/console/src/main/java/org/jline/console/CommandMethods.java new file mode 100644 index 000000000..4f6355b6f --- /dev/null +++ b/console/src/main/java/org/jline/console/CommandMethods.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.jline.reader.Completer; + +public class CommandMethods { + Function execute; + Function> compileCompleter; + + public CommandMethods(Function execute, Function> compileCompleter) { + this.execute = execute; + this.compileCompleter = compileCompleter; + } + + public CommandMethods(Consumer execute, Function> compileCompleter) { + this.execute = (CommandInput i) -> { + execute.accept(i); + return null; + }; + this.compileCompleter = compileCompleter; + } + + public Function execute() { + return execute; + } + + public Function> compileCompleter() { + return compileCompleter; + } + +} diff --git a/builtins/src/main/java/org/jline/builtins/CommandRegistry.java b/console/src/main/java/org/jline/console/CommandRegistry.java similarity index 56% rename from builtins/src/main/java/org/jline/builtins/CommandRegistry.java rename to console/src/main/java/org/jline/console/CommandRegistry.java index ad9a1be55..1af04b64f 100644 --- a/builtins/src/main/java/org/jline/builtins/CommandRegistry.java +++ b/console/src/main/java/org/jline/console/CommandRegistry.java @@ -6,18 +6,14 @@ * * https://opensource.org/licenses/BSD-3-Clause */ -package org.jline.builtins; +package org.jline.console; -import org.jline.builtins.Completers; -import org.jline.builtins.Widgets; -import org.jline.builtins.Options.HelpException; +import org.jline.reader.impl.completer.SystemCompleter; import org.jline.terminal.Terminal; import java.io.InputStream; import java.io.PrintStream; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Store command information, compile tab completers and execute registered commands. @@ -31,8 +27,8 @@ public interface CommandRegistry { * @param commandRegistries command registeries which completers is to be aggregated * @return uncompiled SystemCompleter */ - static Completers.SystemCompleter aggregateCompleters(CommandRegistry ... commandRegistries) { - Completers.SystemCompleter out = new Completers.SystemCompleter(); + static SystemCompleter aggregateCompleters(CommandRegistry ... commandRegistries) { + SystemCompleter out = new SystemCompleter(); for (CommandRegistry r: commandRegistries) { out.add(r.compileCompleters()); } @@ -44,8 +40,8 @@ static Completers.SystemCompleter aggregateCompleters(CommandRegistry ... comman * @param commandRegistries command registeries which completers is to be aggregated and compile * @return compiled SystemCompleter */ - static Completers.SystemCompleter compileCompleters(CommandRegistry ... commandRegistries) { - Completers.SystemCompleter out = aggregateCompleters(commandRegistries); + static SystemCompleter compileCompleters(CommandRegistry ... commandRegistries) { + SystemCompleter out = aggregateCompleters(commandRegistries); out.compile(); return out; } @@ -71,18 +67,10 @@ default String name() { /** * Returns a short info about command known by this registry. + * @param command the command name * @return a short info about command */ - default List commandInfo(String command) { - try { - invoke(new CommandSession(), command, new Object[] {"--help"}); - } catch (HelpException e) { - return Builtins.compileCommandInfo(e.getMessage()); - } catch (Exception e) { - - } - throw new IllegalArgumentException("default CommandRegistry.commandInfo() method must be overridden in class " + this.getClass().getCanonicalName()); - } + List commandInfo(String command); /** * Returns whether a command with the specified name is known to this registry. @@ -96,41 +84,19 @@ default List commandInfo(String command) { * information for all registered commands. * @return a SystemCompleter that can provide command completion for all registered commands */ - Completers.SystemCompleter compileCompleters(); + SystemCompleter compileCompleters(); /** * Returns a command description for use in the JLine Widgets framework. - * @param command name of the command whose description to return + * Default method must be overridden to return sub command descriptions. + * @param args command (args[0]) and its arguments * @return command description for JLine TailTipWidgets to be displayed * in the terminal status bar. */ - default Widgets.CmdDesc commandDescription(String command) { - try { - invoke(new CommandSession(), command, new Object[] {"--help"}); - } catch (HelpException e) { - return Builtins.compileCommandDescription(e.getMessage()); - } catch (Exception e) { - - } - throw new IllegalArgumentException("default CommandRegistry.commandDescription() method must be overridden in class " + this.getClass().getCanonicalName()); - } - - /** - * Execute a command that have only string parameters and options. Implementation of the method is required - * when aggregating command registries using SystemRegistry. - * @param session the data of the current command session - * @param command the name of the command - * @param args arguments of the command - * @return result of the command execution - * @throws Exception in case of error - */ - default Object execute(CommandSession session, String command, String[] args) throws Exception { - throw new IllegalArgumentException("CommandRegistry method execute(String command, String[] args) is not implemented!"); - } + CmdDesc commandDescription(List args); /** - * Execute a command. If command has other than string parameters a custom implementation is required. - * This method will be called only when we have ConsoleEngine in SystemRegistry. + * Execute a command. * @param session the data of the current command session * @param command the name of the command * @param args arguments of the command @@ -138,17 +104,10 @@ default Object execute(CommandSession session, String command, String[] args) th * @throws Exception in case of error */ default Object invoke(CommandSession session, String command, Object... args) throws Exception { - String[] _args = new String[args.length]; - for (int i = 0; i < args.length; i++) { - if (!(args[i] instanceof String)) { - throw new IllegalArgumentException(); - } - _args[i] = args[i].toString(); - } - return execute(session, command, _args); + throw new IllegalStateException("CommandRegistry method invoke(session, command, ... args) is not implemented!"); } - public static class CommandSession { + class CommandSession { private final Terminal terminal; private final InputStream in; private final PrintStream out; diff --git a/builtins/src/main/java/org/jline/builtins/ConsoleEngine.java b/console/src/main/java/org/jline/console/ConsoleEngine.java similarity index 83% rename from builtins/src/main/java/org/jline/builtins/ConsoleEngine.java rename to console/src/main/java/org/jline/console/ConsoleEngine.java index a163e22de..f70a179b7 100644 --- a/builtins/src/main/java/org/jline/builtins/ConsoleEngine.java +++ b/console/src/main/java/org/jline/console/ConsoleEngine.java @@ -6,13 +6,14 @@ * * https://opensource.org/licenses/BSD-3-Clause */ -package org.jline.builtins; +package org.jline.console; import java.io.File; +import java.io.IOException; +import java.nio.file.Path; import java.util.List; import java.util.Map; -import org.jline.builtins.CommandRegistry; import org.jline.reader.Completer; import org.jline.reader.LineReader; import org.jline.reader.Widget; @@ -60,6 +61,13 @@ static String plainCommand(String command) { */ String expandCommandLine(String line); + /** + * Expands parameter list to string + * @param params list of script parameters + * @return expanded parameters list + */ + String expandToList(List params); + /** * Returns all scripts found from PATH * @return map keys have script file names and value is true if it is console script @@ -92,12 +100,42 @@ static String plainCommand(String command) { */ Map> getPipes(); + /** + * Returns named pipe names + * @return list of named pipe names + */ + List getNamedPipes(); + /** * Returns script and variable completers * @return script and variable completers */ List scriptCompleters(); + /** + * Persist object to file + * @param file file where object should be written + * @param object object to persist + */ + void persist(Path file, Object object); + + /** + * Read object from file + * @param file file from where object should be read + * @return object + * @throws IOException in case of error + */ + Object slurp(Path file) throws IOException; + + /** + * Read console option value + * @param option type + * @param option option name + * @param defval default value + * @return option value + */ + T consoleOption(String option, T defval); + /** * Executes command line that does not contain known command by the system registry. * If the line is neither JLine or ScriptEngine script it will be evaluated @@ -158,13 +196,6 @@ default Object execute(File script) throws Exception { */ void println(Object object); - /** - * Print object. - * @param options println options - * @param object object to print - */ - void println(Map options, Object object); - /** * Create console variable * @param name name of the variable @@ -182,7 +213,7 @@ default Object execute(File script) throws Exception { /** * Test if variable with name exists * @param name name of the variable - * @return true if variable with name exists + * @return true if variable with name exists */ boolean hasVariable(String name); @@ -204,7 +235,7 @@ default Object execute(File script) throws Exception { */ boolean isExecuting(); - static class ExecutionResult { + class ExecutionResult { final int status; final Object result; @@ -222,10 +253,10 @@ public Object result() { } } - static class WidgetCreator implements Widget { - private ConsoleEngine consoleEngine; - private Object function; - private String name; + class WidgetCreator implements Widget { + private final ConsoleEngine consoleEngine; + private final Object function; + private final String name; public WidgetCreator(ConsoleEngine consoleEngine, String function) { this.consoleEngine = consoleEngine; diff --git a/console/src/main/java/org/jline/console/Printer.java b/console/src/main/java/org/jline/console/Printer.java new file mode 100644 index 000000000..34018dd72 --- /dev/null +++ b/console/src/main/java/org/jline/console/Printer.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Print object to the console. + * + * @author Matti Rinta-Nikkola + */ +public interface Printer { + // + // option names + // + // 1) command options + // + /** + * Value: Boolean
+ * Applies: TABLE
+ * Ignore columnsOut configuration. + */ + String ALL = "all"; + /** + * Value: {@code List}
+ * Applies: TABLE
+ * Display given columns on table. + */ + String COLUMNS = "columns"; + /** + * Value: {@code List}
+ * Applies: TABLE
+ * Exclude given columns on table. + */ + String EXCLUDE = "exclude"; + /** + * Value: {@code List}
+ * Applies: TABLE
+ * Include given columns on table. + */ + String INCLUDE = "include"; + /** + * Value: Integer
+ * Applies: MAP
+ * Indention size. + */ + String INDENTION = "indention"; + /** + * Value: Integer
+ * Applies: MAP and TABLE
+ * Maximum column width. + */ + String MAX_COLUMN_WIDTH = "maxColumnWidth"; + /** + * Value: Integer
+ * Applies: MAP
+ * Maximum depth objects are resolved. + */ + String MAX_DEPTH = "maxDepth"; + /** + * Value: Integer
+ * Applies: MAP and TABLE
+ * Maximum number of lines to display. + */ + String MAXROWS = "maxrows"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Display one row data on table. + */ + String ONE_ROW_TABLE = "oneRowTable"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Display table row numbers. + */ + String ROWNUM = "rownum"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Truncate table column names: property.field to field. + */ + String SHORT_NAMES = "shortNames"; + /** + * Value: Boolean
+ * Applies: MAP and TABLE
+ * Ignore all options defined in PRNT_OPTIONS. + */ + String SKIP_DEFAULT_OPTIONS = "skipDefaultOptions"; + /** + * Value: Boolean
+ * Applies: TABLE
+ * Display object structures and lists on table. + */ + String STRUCT_ON_TABLE = "structsOnTable"; + /** + * Value: String
+ * Use nanorc STYLE
+ */ + String STYLE = "style"; + /** + * Value: Boolean
+ * Applies: MAP and TABLE
+ * Use object's toString() method to get print value + * DEFAULT: object's fields are put to property map before printing + */ + String TO_STRING = "toString"; + /** + * Value: String
+ * Applies: MAP and TABLE
+ * Nanorc syntax style used to highlight values. + */ + String VALUE_STYLE = "valueStyle"; + /** + * Value: Integer
+ * Applies: MAP and TABLE
+ * Display width (default terminal width). + */ + String WIDTH = "width"; + // + // 2) additional PRNT_OPTIONS + // + /** + * Value: {@code List}
+ * Applies: TABLE
+ * These map values will be added to the table before all the other keys. + */ + String COLUMNS_IN = "columnsIn"; + /** + * Value: {@code List}
+ * Applies: TABLE
+ * These map values will not be inserted to the table. + */ + String COLUMNS_OUT = "columnsOut"; + /** + * Value: {@code Map}.
+ * Applies: TABLE
+ * If command result map key matches with regex the highlight function is applied + * to the corresponding map value. The regex = * is processed after all the other regexes and the highlight + * function will be applied to all map values that have not been already highlighted. + */ + String HIGHLIGHT_VALUE = "highlightValue"; + /** + * Value: Double
+ * Applies: MAP and TABLE
+ * default value 0.8 i.e. if at least of 4 of the 5 results map keys match with reference key set the + * result will be printed out as a table. + */ + String MAP_SIMILARITY = "mapSimilarity"; + /** + * Value: {@code Map}
+ * Applies: MAP and TABLE
+ * Overrides the ScriptEngine toMap() method. + */ + String OBJECT_TO_MAP = "objectToMap"; + /** + * Value: {@code Map}
+ * Applies: MAP and TABLE
+ * Overrides the ScriptEngine toString() method. + */ + String OBJECT_TO_STRING = "objectToString"; + + List BOOLEAN_KEYS = Arrays.asList(ALL, ONE_ROW_TABLE, ROWNUM, SHORT_NAMES, SKIP_DEFAULT_OPTIONS + , STRUCT_ON_TABLE, TO_STRING); + + default void println(Object object) { + println(new HashMap<>(), object); + } + + void println(Map options, Object object); + + default Exception prntCommand(CommandInput input) { + return null; + } + +} diff --git a/reader/src/main/java/org/jline/reader/ScriptEngine.java b/console/src/main/java/org/jline/console/ScriptEngine.java similarity index 57% rename from reader/src/main/java/org/jline/reader/ScriptEngine.java rename to console/src/main/java/org/jline/console/ScriptEngine.java index 675d8dd88..1e0ff9d8d 100644 --- a/reader/src/main/java/org/jline/reader/ScriptEngine.java +++ b/console/src/main/java/org/jline/console/ScriptEngine.java @@ -6,12 +6,14 @@ * * https://opensource.org/licenses/BSD-3-Clause */ -package org.jline.reader; +package org.jline.console; import java.io.File; import java.nio.file.Path; import java.util.*; +import org.jline.reader.Completer; + /** * Manage scriptEngine variables, statements and script execution. * @@ -31,23 +33,29 @@ public interface ScriptEngine { */ Collection getExtensions(); + /** + * + * @return script tab completer + */ + Completer getScriptCompleter(); + /** * Tests if console variable exists - * @param name + * @param name variable name * @return true if variable exists */ boolean hasVariable(String name); /** * Creates variable - * @param name of the variable - * @param value of the variable + * @param name variable name + * @param value value */ void put(String name, Object value); /** * Gets variable value - * @param name of the variable + * @param name variable name * @return value of the variable */ Object get(String name); @@ -62,87 +70,95 @@ default Map find() { /** * Gets all the variables that match the name. Name can contain * wild cards. - * @param name of the variable - * @return map of the variables + * @param name variable name + * @return map the variables */ Map find(String name); /** - * Deletes variables. Variable name cab contain * wild cards. - * @param vars + * Deletes variables. Variable name can contain * wild cards. + * @param vars variables to be deleted */ void del(String... vars); /** - * Converts object to JSON string. - * @param object object to convert to JSON + * Serialize object to JSON string. + * @param object object to serialize to JSON * @return formatted JSON string */ String toJson(Object object); /** * Converts object to string. - * @param object object to convert to string + * @param object the object * @return object string value */ String toString(Object object); /** * Converts object fields to map. - * @param object object to convert to map + * @param object the object * @return object fields map */ Map toMap(Object object); /** - * Substitute variable reference with its value. - * @param variable - * @return Substituted variable - * @throws Exception + * Deserialize value + * @param value the value + * @return deserialized value */ - default Object expandParameter(String variable) { - return expandParameter(variable, ""); + default Object deserialize(String value) { + return deserialize(value, null); } /** - * Substitute variable reference with its value. - * @param variable + * Deserialize value + * @param value the value * @param format serialization format - * @return Substituted variable - * @throws Exception + * @return deserialized value */ - Object expandParameter(String variable, String format); + Object deserialize(String value, String format); + + /** + * + * @return Supported serialization formats + */ + List getSerializationFormats(); + + /** + * + * @return Supported deserialization formats + */ + List getDeserializationFormats(); /** * Persists object value to file. - * @param file - * @param object + * @param file file + * @param object object */ - default void persist(Path file, Object object) { - persist(file, object, "JSON"); - } + void persist(Path file, Object object); /** * Persists object value to file. - * @param file - * @param object - * @param format + * @param file the file + * @param object the object + * @param format serialization format */ void persist(Path file, Object object, String format); /** * Executes scriptEngine statement - * @param statement + * @param statement the statement * @return result - * @throws Exception + * @throws Exception in case of error */ Object execute(String statement) throws Exception; /** * Executes scriptEngine script - * @param script + * @param script the script * @return result - * @throws Exception + * @throws Exception in case of error */ default Object execute(File script) throws Exception { return execute(script, null); @@ -150,11 +166,19 @@ default Object execute(File script) throws Exception { /** * Executes scriptEngine script - * @param script - * @param args - * @return - * @throws Exception + * @param script the script + * @param args arguments + * @return result + * @throws Exception in case of error */ Object execute(File script, Object[] args) throws Exception; + /** + * Executes scriptEngine closure + * @param closure closure + * @param args arguments + * @return result + */ + Object execute(Object closure, Object... args); + } diff --git a/builtins/src/main/java/org/jline/builtins/SystemRegistry.java b/console/src/main/java/org/jline/console/SystemRegistry.java similarity index 63% rename from builtins/src/main/java/org/jline/builtins/SystemRegistry.java rename to console/src/main/java/org/jline/console/SystemRegistry.java index 737b9bf2d..09e538796 100644 --- a/builtins/src/main/java/org/jline/builtins/SystemRegistry.java +++ b/console/src/main/java/org/jline/console/SystemRegistry.java @@ -6,31 +6,38 @@ * * https://opensource.org/licenses/BSD-3-Clause */ -package org.jline.builtins; +package org.jline.console; import java.io.File; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import org.jline.builtins.ConsoleOptionGetter; import org.jline.reader.Completer; +import org.jline.reader.ParsedLine; import org.jline.terminal.Terminal; -import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; /** * Aggregate command registries and dispatch command executions. * * @author Matti Rinta-Nikkola */ -public interface SystemRegistry extends CommandRegistry { +public interface SystemRegistry extends CommandRegistry, ConsoleOptionGetter { /** - * Set command registeries - * @param commandRegistries command registeries used by the application + * Set command registries + * @param commandRegistries command registries used by the application */ void setCommandRegistries(CommandRegistry... commandRegistries); + /** + * Register subcommand registry + * @param command main command + * @param subcommandRegistry subcommand registry + */ + void register(String command, CommandRegistry subcommandRegistry); + /** * Initialize consoleEngine environment by executing console script * @param script initialization script @@ -55,7 +62,7 @@ public interface SystemRegistry extends CommandRegistry { * @return command description for JLine TailTipWidgets to be displayed * in the terminal status bar. */ - Widgets.CmdDesc commandDescription(Widgets.CmdLine line); + CmdDesc commandDescription(CmdLine line); /** * Execute a command, script or evaluate scriptEngine statement @@ -83,6 +90,13 @@ public interface SystemRegistry extends CommandRegistry { */ void trace(boolean stack, Exception exception); + /** + * Return console option value + * @param name the option name + * @return option value + */ + Object consoleOption(String name); + /** * @return terminal */ @@ -95,17 +109,33 @@ public interface SystemRegistry extends CommandRegistry { * @return command execution result * @throws Exception in case of error */ - Object execute(String command, String[] args) throws Exception; + Object invoke(String command, Object... args) throws Exception; /** - * Execute command with arguments - * @param command command to be executed - * @param args arguments of the command - * @return command execution result - * @throws Exception in case of error + * Returns whether a line contains command/script that is known to this registry. + * @param line the parsed command line to test + * @return true if the specified line has a command registered */ - Object invoke(String command, Object... args) throws Exception; + boolean isCommandOrScript(ParsedLine line); + + /** + * Returns whether command is known to this registry. + * @param command the command to test + * @return true if the specified command is known + */ + boolean isCommandOrScript(String command); + /** + * Returns whether alias is known command alias. + * @param alias the alias to test + * @return true if the alias is known command alias + */ + boolean isCommandAlias(String alias); + + /** + * Orderly close SystemRegistry. + */ + void close(); /** * @return systemRegistry of the current thread */ @@ -115,14 +145,14 @@ static SystemRegistry get() { /** * Add systemRegistry to the thread map - * @param systemRegistry + * @param systemRegistry the systemRegistry */ static void add(SystemRegistry systemRegistry) { Registeries.getInstance().addRegistry(systemRegistry); } /** - * Remove systemRegistry from the thread map + * Remove systemRegistry of the current thread from the thread map */ static void remove() { Registeries.getInstance().removeRegistry(); @@ -131,25 +161,25 @@ static void remove() { /** * Manage systemRegistry store */ - public class Registeries { - private static Registeries instance = new Registeries(); - private Map systemRegisteries = new HashMap<>(); + class Registeries { + private static final Registeries instance = new Registeries(); + private final Map systemRegisteries = new HashMap<>(); private Registeries () {} - public static Registeries getInstance() { + protected static Registeries getInstance() { return instance; } - public void addRegistry(SystemRegistry systemRegistry) { + protected void addRegistry(SystemRegistry systemRegistry) { systemRegisteries.put(Thread.currentThread().getId(), systemRegistry); } - public SystemRegistry getSystemRegistry() { + protected SystemRegistry getSystemRegistry() { return systemRegisteries.getOrDefault(Thread.currentThread().getId(), null); } - public void removeRegistry() { + protected void removeRegistry() { systemRegisteries.remove(Thread.currentThread().getId()); } diff --git a/console/src/main/java/org/jline/console/impl/AbstractCommandRegistry.java b/console/src/main/java/org/jline/console/impl/AbstractCommandRegistry.java new file mode 100644 index 000000000..08306c851 --- /dev/null +++ b/console/src/main/java/org/jline/console/impl/AbstractCommandRegistry.java @@ -0,0 +1,263 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.util.*; +import java.util.stream.Collectors; + +import org.jline.console.CmdDesc; +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.reader.impl.completer.SystemCompleter; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; + +/** + * CommandRegistry common methods. + * + * @author Matti Rinta-Nikkola + */ +public abstract class AbstractCommandRegistry implements CommandRegistry { + private CmdRegistry cmdRegistry; + private Exception exception; + + public AbstractCommandRegistry() {} + + public CmdDesc doHelpDesc(String command, List info, CmdDesc cmdDesc) { + List mainDesc = new ArrayList<>(); + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(command.toLowerCase()).append(" - "); + for (String s : info) { + if (asb.length() == 0) { + asb.append("\t"); + } + asb.append(s); + mainDesc.add(asb.toAttributedString()); + asb = new AttributedStringBuilder(); + asb.tabs(2); + } + asb = new AttributedStringBuilder(); + asb.tabs(7); + asb.append("Usage:"); + for(AttributedString as : cmdDesc.getMainDesc()) { + asb.append("\t"); + asb.append(as); + mainDesc.add(asb.toAttributedString()); + asb = new AttributedStringBuilder(); + asb.tabs(7); + } + return new CmdDesc(mainDesc, new ArrayList<>(), cmdDesc.getOptsDesc()); + } + + public > void registerCommands(Map commandName, Map commandExecute) { + cmdRegistry = new EnumCmdRegistry<>(commandName, commandExecute); + } + + public void registerCommands(Map commandExecute) { + cmdRegistry = new NameCmdRegistry(commandExecute); + } + + @Override + public Object invoke(CommandSession session, String command, Object... args) throws Exception { + exception = null; + CommandMethods methods = getCommandMethods(command); + Object out = methods.execute().apply(new CommandInput(command, args, session)); + if (exception != null) { + throw exception; + } + return out; + } + + public void saveException(Exception exception) { + this.exception = exception; + } + + @Override + public boolean hasCommand(String command) { + return cmdRegistry.hasCommand(command); + } + + @Override + public Set commandNames() { + return cmdRegistry.commandNames(); + } + + @Override + public Map commandAliases() { + return cmdRegistry.commandAliases(); + } + + public > void rename(V command, String newName) { + cmdRegistry.rename(command, newName); + } + + public void alias(String alias, String command) { + cmdRegistry.alias(alias, command); + } + + @Override + public SystemCompleter compileCompleters() { + return cmdRegistry.compileCompleters(); + } + + public CommandMethods getCommandMethods(String command) { + return cmdRegistry.getCommandMethods(command); + } + + public Object registeredCommand(String command) { + return cmdRegistry.command(command); + } + + private interface CmdRegistry { + boolean hasCommand(String command); + Set commandNames(); + Map commandAliases(); + Object command(String command); + > void rename(V command, String newName); + void alias(String alias, String command); + SystemCompleter compileCompleters(); + CommandMethods getCommandMethods(String command); + } + + private static class EnumCmdRegistry> implements CmdRegistry { + private final Map commandName; + private Map nameCommand = new HashMap<>(); + private final Map commandExecute; + private final Map aliasCommand = new HashMap<>(); + + public EnumCmdRegistry(Map commandName, Map commandExecute) { + this.commandName = commandName; + this.commandExecute = commandExecute; + doNameCommand(); + } + + private void doNameCommand() { + nameCommand = commandName.entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); + } + + public Set commandNames() { + return nameCommand.keySet(); + } + + public Map commandAliases() { + return aliasCommand; + } + + @SuppressWarnings("unchecked") + public > void rename(V command, String newName) { + if (nameCommand.containsKey(newName)) { + throw new IllegalArgumentException("Duplicate command name!"); + } else if (!commandName.containsKey(command)) { + throw new IllegalArgumentException("Command does not exists!"); + } + commandName.put((T)command, newName); + doNameCommand(); + } + + public void alias(String alias, String command) { + if (!nameCommand.containsKey(command)) { + throw new IllegalArgumentException("Command does not exists!"); + } + aliasCommand.put(alias, command); + } + + public boolean hasCommand(String name) { + return nameCommand.containsKey(name) || aliasCommand.containsKey(name); + } + + public SystemCompleter compileCompleters() { + SystemCompleter out = new SystemCompleter(); + for (Map.Entry entry: commandName.entrySet()) { + out.add(entry.getValue(), commandExecute.get(entry.getKey()).compileCompleter().apply(entry.getValue())); + } + out.addAliases(aliasCommand); + return out; + } + + public T command(String name) { + T out; + if (!hasCommand(name)) { + throw new IllegalArgumentException("Command does not exists!"); + } + if (aliasCommand.containsKey(name)) { + name = aliasCommand.get(name); + } + if (nameCommand.containsKey(name)) { + out = nameCommand.get(name); + } else { + throw new IllegalArgumentException("Command does not exists!"); + } + return out; + } + + public CommandMethods getCommandMethods(String command) { + return commandExecute.get(command(command)); + } + + } + + private static class NameCmdRegistry implements CmdRegistry { + private final Map commandExecute; + private final Map aliasCommand = new HashMap<>(); + + public NameCmdRegistry(Map commandExecute) { + this.commandExecute = commandExecute; + } + + public Set commandNames() { + return commandExecute.keySet(); + } + + public Map commandAliases() { + return aliasCommand; + } + + public > void rename(V command, String newName) { + throw new IllegalArgumentException(); + } + + public void alias(String alias, String command) { + if (!commandExecute.containsKey(command)) { + throw new IllegalArgumentException("Command does not exists!"); + } + aliasCommand.put(alias, command); + } + + public boolean hasCommand(String name) { + return commandExecute.containsKey(name) || aliasCommand.containsKey(name); + } + + public SystemCompleter compileCompleters() { + SystemCompleter out = new SystemCompleter(); + for (String c : commandExecute.keySet()) { + out.add(c, commandExecute.get(c).compileCompleter().apply(c)); + } + out.addAliases(aliasCommand); + return out; + } + + public String command(String name) { + if (commandExecute.containsKey(name)) { + return name; + } else if (aliasCommand.containsKey(name)) { + return aliasCommand.get(name); + } + return null; + } + + public CommandMethods getCommandMethods(String command) { + return commandExecute.get(command(command)); + } + + } + +} diff --git a/console/src/main/java/org/jline/console/impl/Builtins.java b/console/src/main/java/org/jline/console/impl/Builtins.java new file mode 100644 index 000000000..dcd6cb1a9 --- /dev/null +++ b/console/src/main/java/org/jline/console/impl/Builtins.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.nio.file.Path; +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.jline.builtins.Commands; +import org.jline.builtins.TTop; +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.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.Option; +import org.jline.reader.Widget; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; + +/** + * Builtins: create tab completers, execute and create descriptions for builtins commands. + * + * @author Matti Rinta-Nikkola + */ +public class Builtins extends JlineCommandRegistry implements CommandRegistry { + public enum Command {NANO + , LESS + , HISTORY + , WIDGET + , KEYMAP + , SETOPT + , SETVAR + , UNSETOPT + , TTOP + , COLORS + } + + private final ConfigurationPath configPath; + private final Function widgetCreator; + private final Supplier workDir; + private LineReader reader; + + public Builtins(Path workDir, ConfigurationPath configPath, Function widgetCreator) { + this(null, () -> workDir, configPath, widgetCreator); + } + + public Builtins(Set commands, Path workDir, ConfigurationPath configpath, Function widgetCreator) { + this(commands, () -> workDir, configpath, widgetCreator); + } + + public Builtins(Supplier workDir, ConfigurationPath configPath, Function widgetCreator) { + this(null, workDir, configPath, widgetCreator); + } + + public Builtins(Set commands, Supplier workDir, ConfigurationPath configpath, Function widgetCreator) { + super(); + this.configPath = configpath; + this.widgetCreator = widgetCreator; + this.workDir = workDir; + Set cmds; + Map commandName = new HashMap<>(); + Map commandExecute = new HashMap<>(); + if (commands == null) { + cmds = new HashSet<>(EnumSet.allOf(Command.class)); + } else { + cmds = new HashSet<>(commands); + } + for (Command c: cmds) { + commandName.put(c, c.name().toLowerCase()); + } + commandExecute.put(Command.NANO, new CommandMethods(this::nano, this::nanoCompleter)); + commandExecute.put(Command.LESS, new CommandMethods(this::less, this::lessCompleter)); + commandExecute.put(Command.HISTORY, new CommandMethods(this::history, this::historyCompleter)); + commandExecute.put(Command.WIDGET, new CommandMethods(this::widget, this::widgetCompleter)); + commandExecute.put(Command.KEYMAP, new CommandMethods(this::keymap, this::defaultCompleter)); + commandExecute.put(Command.SETOPT, new CommandMethods(this::setopt, this::setoptCompleter)); + commandExecute.put(Command.SETVAR, new CommandMethods(this::setvar, this::setvarCompleter)); + commandExecute.put(Command.UNSETOPT, new CommandMethods(this::unsetopt, this::unsetoptCompleter)); + commandExecute.put(Command.TTOP, new CommandMethods(this::ttop, this::defaultCompleter)); + commandExecute.put(Command.COLORS, new CommandMethods(this::colors, this::defaultCompleter)); + registerCommands(commandName, commandExecute); + } + + public void setLineReader(LineReader reader) { + this.reader = reader; + } + + private void less(CommandInput input) { + try { + Commands.less(input.terminal(), input.in(), input.out(), input.err(), workDir.get(), input.args(), configPath); + } catch (Exception e) { + saveException(e); + } + } + + private void nano(CommandInput input) { + try { + Commands.nano(input.terminal(), input.out(), input.err(), workDir.get(), input.args(), configPath); + } catch (Exception e) { + saveException(e); + } + } + + private void history(CommandInput input) { + try { + Commands.history(reader, input.out(), input.err(), workDir.get(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void widget(CommandInput input) { + try { + Commands.widget(reader, input.out(), input.err(), widgetCreator, input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void keymap(CommandInput input) { + try { + Commands.keymap(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void setopt(CommandInput input) { + try { + Commands.setopt(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void setvar(CommandInput input) { + try { + Commands.setvar(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void unsetopt(CommandInput input) { + try { + Commands.unsetopt(reader, input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void ttop(CommandInput input) { + try { + TTop.ttop(input.terminal(), input.out(), input.err(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private void colors(CommandInput input) { + try { + Commands.colors(input.terminal(), input.out(), input.args()); + } catch (Exception e) { + saveException(e); + } + } + + private List unsetOptions(boolean set) { + List out = new ArrayList<>(); + for (Option option : Option.values()) { + if (set == (reader.isSet(option) == option.isDef())) { + out.add((option.isDef() ? "no-" : "") + option.toString().toLowerCase().replace('_', '-')); + } + } + return out; + } + + private Set allWidgets() { + Set out = new HashSet<>(); + for (String s: reader.getWidgets().keySet()) { + out.add(s); + out.add(reader.getWidgets().get(s).toString()); + } + return out; + } + + private List nanoCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(new FilesCompleter(workDir) + , this::commandOptions + , 1) + )); + return completers; + } + + private List lessCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(new FilesCompleter(workDir) + , this::commandOptions + , 1) + )); + return completers; + } + + private List historyCompleter(String name) { + List completers = new ArrayList<>(); + List optDescs = commandOptions(name); + for (OptDesc o : optDescs) { + if (o.shortOption() != null && (o.shortOption().equals("-A") || o.shortOption().equals("-W") + || o.shortOption().equals("-R"))) { + o.setValueCompleter(new FilesCompleter(workDir)); + } + } + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(NullCompleter.INSTANCE + , optDescs + , 1) + )); + return completers; + } + + private List widgetCompleter(String name) { + List completers = new ArrayList<>(); + List optDescs = commandOptions(name); + Candidate aliasOption = new Candidate("-A", "-A", null, null, null, null, true); + Iterator i = optDescs.iterator(); + while (i.hasNext()) { + OptDesc o = i.next(); + if (o.shortOption() != null) { + if (o.shortOption().equals("-D")) { + o.setValueCompleter(new StringsCompleter(() -> reader.getWidgets().keySet())); + } else if (o.shortOption().equals("-A")) { + aliasOption = new Candidate(o.shortOption(), o.shortOption(), null, o.description(), null, null, true); + i.remove(); + } + } + } + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(NullCompleter.INSTANCE + , optDescs + , 1) + )); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new StringsCompleter(aliasOption), new StringsCompleter(this::allWidgets) + , new StringsCompleter(() -> reader.getWidgets().keySet()), NullCompleter.INSTANCE)); + return completers; + } + + private List setvarCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new StringsCompleter(() -> reader.getVariables().keySet()), NullCompleter.INSTANCE)); + return completers; + } + + private List setoptCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new StringsCompleter(() -> unsetOptions(true)))); + return completers; + } + + private List unsetoptCompleter(String name) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new StringsCompleter(() -> unsetOptions(false)))); + return completers; + } + +} diff --git a/builtins/src/main/java/org/jline/builtins/ConsoleEngineImpl.java b/console/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java similarity index 58% rename from builtins/src/main/java/org/jline/builtins/ConsoleEngineImpl.java rename to console/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java index bee503257..69e56f109 100644 --- a/builtins/src/main/java/org/jline/builtins/ConsoleEngineImpl.java +++ b/console/src/main/java/org/jline/console/impl/ConsoleEngineImpl.java @@ -6,12 +6,16 @@ * * https://opensource.org/licenses/BSD-3-Clause */ -package org.jline.builtins; +package org.jline.console.impl; +import java.awt.Desktop; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.*; @@ -21,86 +25,96 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.jline.builtins.Builtins; -import org.jline.builtins.Builtins.CommandMethods; import org.jline.builtins.Completers.FilesCompleter; import org.jline.builtins.Completers.OptDesc; import org.jline.builtins.Completers.OptionCompleter; -import org.jline.builtins.Completers.SystemCompleter; -import org.jline.builtins.Nano.SyntaxHighlighter; +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.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.terminal.Terminal; import org.jline.utils.AttributedString; import org.jline.utils.AttributedStringBuilder; -import org.jline.utils.AttributedStyle; /** * Manage console variables, commands and script execution. * * @author Matti Rinta-Nikkola */ -public class ConsoleEngineImpl implements ConsoleEngine { +public class ConsoleEngineImpl extends JlineCommandRegistry implements ConsoleEngine { public enum Command {SHOW , DEL , PRNT , ALIAS , PIPE , UNALIAS - , SLURP}; + , DOC + , SLURP} + private static final String VAR_CONSOLE_OPTIONS = "CONSOLE_OPTIONS"; - private static final String VAR_PRNT_OPTIONS = "PRNT_OPTIONS"; private static final String VAR_PATH = "PATH"; - private static final String VAR_NANORC = "NANORC"; private static final String[] OPTION_HELP = {"-?", "--help"}; private static final String OPTION_VERBOSE = "-v"; private static final String END_HELP = "END_HELP"; private static final int HELP_MAX_SIZE = 30; private final ScriptEngine engine; - private Map commandName = new HashMap<>(); - private Map nameCommand = new HashMap<>(); - private Map aliasCommand = new HashMap<>(); - private final Map commandExecute = new HashMap<>(); private Exception exception; private SystemRegistry systemRegistry; private String scriptExtension = "jline"; private final Supplier workDir; - private final ConfigurationPath configPath; private final Map aliases = new HashMap<>(); private final Map> pipes = new HashMap<>(); private Path aliasFile; private LineReader reader; private boolean executing = false; + private final Printer printer; + + public ConsoleEngineImpl(ScriptEngine engine, Printer printer + , Supplier workDir, ConfigurationPath configPath) throws IOException { + this(null, engine, printer, workDir, configPath); + } @SuppressWarnings("unchecked") - public ConsoleEngineImpl(ScriptEngine engine + public ConsoleEngineImpl(Set commands, ScriptEngine engine, Printer printer , Supplier workDir, ConfigurationPath configPath) throws IOException { + super(); this.engine = engine; this.workDir = workDir; - this.configPath = configPath; - Set cmds = new HashSet<>(EnumSet.allOf(Command.class)); + this.printer = printer; + Map commandName = new HashMap<>(); + Map commandExecute = new HashMap<>(); + Set cmds; + if (commands == null) { + cmds = new HashSet<>(EnumSet.allOf(Command.class)); + } else { + cmds = new HashSet<>(commands); + } for (Command c: cmds) { commandName.put(c, c.name().toLowerCase()); } - doNameCommand(); commandExecute.put(Command.DEL, new CommandMethods(this::del, this::variableCompleter)); commandExecute.put(Command.SHOW, new CommandMethods(this::show, this::variableCompleter)); commandExecute.put(Command.PRNT, new CommandMethods(this::prnt, this::prntCompleter)); commandExecute.put(Command.SLURP, new CommandMethods(this::slurpcmd, this::slurpCompleter)); commandExecute.put(Command.ALIAS, new CommandMethods(this::aliascmd, this::aliasCompleter)); commandExecute.put(Command.UNALIAS, new CommandMethods(this::unalias, this::unaliasCompleter)); - commandExecute.put(Command.PIPE, new CommandMethods(this::pipe, this::pipeCompleter)); + commandExecute.put(Command.DOC, new CommandMethods(this::doc, this::docCompleter)); + commandExecute.put(Command.PIPE, new CommandMethods(this::pipe, this::defaultCompleter)); aliasFile = configPath.getUserConfig("aliases.json"); if (aliasFile == null) { aliasFile = configPath.getUserConfig("aliases.json", true); - engine.persist(aliasFile, aliases); + persist(aliasFile, aliases); } else { aliases.putAll((Map)slurp(aliasFile)); } + registerCommands(commandName, commandExecute); } @Override @@ -130,23 +144,6 @@ public void setScriptExtension(String extension) { this.scriptExtension = extension; } - @Override - public Set commandNames() { - return nameCommand.keySet(); - } - - public Map commandAliases() { - return aliasCommand; - } - - @Override - public boolean hasCommand(String name) { - if (nameCommand.containsKey(name) || aliasCommand.containsKey(name)) { - return true; - } - return false; - } - @Override public boolean hasAlias(String name) { return aliases.containsKey(name); @@ -162,72 +159,47 @@ public Map> getPipes() { return pipes; } - private void doNameCommand() { - nameCommand = commandName.entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); - } - - private Command command(String name) { - Command out = null; - if (!hasCommand(name)) { - throw new IllegalArgumentException("Command does not exists!"); - } - if (aliasCommand.containsKey(name)) { - name = aliasCommand.get(name); - } - if (nameCommand.containsKey(name)) { - out = nameCommand.get(name); - } else { - throw new IllegalArgumentException("Command does not exists!"); - } - return out; - } - - public void rename(Command command, String newName) { - if (nameCommand.containsKey(newName)) { - throw new IllegalArgumentException("Duplicate command name!"); - } else if (!commandName.containsKey(command)) { - throw new IllegalArgumentException("Command does not exists!"); - } - commandName.put(command, newName); - doNameCommand(); - } - - public void alias(String alias, String command) { - if (!nameCommand.keySet().contains(command)) { - throw new IllegalArgumentException("Command does not exists!"); - } - aliasCommand.put(alias, command); - } - @Override - public Completers.SystemCompleter compileCompleters() { - SystemCompleter out = new SystemCompleter(); - for (Map.Entry entry: commandName.entrySet()) { - out.add(entry.getValue(), commandExecute.get(entry.getKey()).compileCompleter().apply(entry.getValue())); + public List getNamedPipes() { + List out = new ArrayList<>(); + List opers = new ArrayList<>(); + for (String p : pipes.keySet()) { + if (p.matches("[a-zA-Z0-9]+")) { + out.add(p); + } else { + opers.add(p); + } + } + opers.addAll(systemRegistry.getPipeNames()); + for (Map.Entry entry : aliases.entrySet()) { + if (opers.contains(entry.getValue().split(" ")[0])) { + out.add(entry.getKey()); + } } - out.addAliases(aliasCommand); return out; } - private Set variables() { - return engine.find().keySet(); - } - @Override public List scriptCompleters() { List out = new ArrayList<>(); - out.add(new ArgumentCompleter(new StringsCompleter(this::variables), NullCompleter.INSTANCE)); out.add(new ArgumentCompleter(new StringsCompleter(this::scriptNames) , new OptionCompleter(NullCompleter.INSTANCE , this::commandOptions , 1) )); - out.add(new ArgumentCompleter(new StringsCompleter(aliases::keySet), NullCompleter.INSTANCE)); + out.add(new ArgumentCompleter(new StringsCompleter(this::commandAliasNames) + , NullCompleter.INSTANCE)); return out; } + private Set commandAliasNames() { + Set opers = pipes.keySet().stream().filter(p -> !p.matches("\\w+")).collect(Collectors.toSet()); + opers.addAll(systemRegistry.getPipeNames()); + return aliases.entrySet().stream() + .filter(e -> !opers.contains(e.getValue().split(" ")[0])) + .map(Map.Entry::getKey).collect(Collectors.toSet()); + } + private Set scriptNames() { return scripts().keySet(); } @@ -239,12 +211,20 @@ public Map scripts() { try { List scripts = new ArrayList<>(); if (engine.hasVariable(VAR_PATH)) { - for (String pp : (List) engine.get(VAR_PATH)) { + List dirs = new ArrayList<>(); + for (String file : (List) engine.get(VAR_PATH)) { + file = file.startsWith("~") ? file.replace("~", System.getProperty("user.home")) : file; + File dir = new File(file); + if (dir.exists() && dir.isDirectory()) { + dirs.add(file); + } + } + for (String pp : dirs) { for (String e : scriptExtensions()) { String regex = pp + "/*." + e; PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + regex); Files.find(Paths.get(new File(regex).getParent()), Integer.MAX_VALUE, - (path, f) -> pathMatcher.matches(path)).forEach(p -> scripts.add(p)); + (path, f) -> pathMatcher.matches(path)).forEach(scripts::add); } } } @@ -269,25 +249,59 @@ public Map scripts() { @Override public Object[] expandParameters(String[] args) throws Exception { Object[] out = new Object[args.length]; + String regexPath = "(.*)\\$\\{(.*?)}(/.*)"; for (int i = 0; i < args.length; i++) { - if (args[i].startsWith("${")) { + if (args[i].matches(regexPath)) { + Matcher matcher = Pattern.compile(regexPath).matcher(args[i]); + if (matcher.find()) { + out[i] = matcher.group(1) + engine.get(matcher.group(2)) + matcher.group(3); + } else { + throw new IllegalArgumentException(); + } + } else if (args[i].startsWith("${")) { out[i] = engine.execute(expandName(args[i])); } else if (args[i].startsWith("$")) { out[i] = engine.get(expandName(args[i])); } else { - out[i] = engine.expandParameter(args[i]); + out[i] = engine.deserialize(args[i]); } } return out; } + private String expandToList(String[] args) { + return expandToList(Arrays.asList(args)); + } + + @Override + public String expandToList(List params) { + StringBuilder sb = new StringBuilder(); + sb.append("["); + boolean first = true; + for (String param : params) { + if (!first) { + sb.append(","); + } + if (param.equalsIgnoreCase("true") || param.equalsIgnoreCase("false") || param.equalsIgnoreCase("null")) { + sb.append(param.toLowerCase()); + } else if (isNumber(param)) { + sb.append(param); + } else { + sb.append(param.startsWith("$") ? param.substring(1) : quote(param)); + } + first = false; + } + sb.append("]"); + return sb.toString(); + } + private String expandName(String name) { - String regexVar = "[a-zA-Z_]{1,}[a-zA-Z0-9_-]*"; + String regexVar = "[a-zA-Z_]+[a-zA-Z0-9_-]*"; String out = name; if (name.matches("^\\$" + regexVar)) { out = name.substring(1); - } else if (name.matches("^\\$\\{" + regexVar + "\\}.*")) { - Matcher matcher = Pattern.compile("^\\$\\{(" + regexVar + ")\\}(.*)").matcher(name); + } else if (name.matches("^\\$\\{" + regexVar + "}.*")) { + Matcher matcher = Pattern.compile("^\\$\\{(" + regexVar + ")}(.*)").matcher(name); if (matcher.find()) { out = matcher.group(1) + matcher.group(2); } else { @@ -297,6 +311,10 @@ private String expandName(String name) { return out; } + private boolean isNumber(String str) { + return str.matches("-?\\d+(\\.\\d+)?"); + } + private boolean isCodeBlock(String line) { return line.contains("\n") && line.trim().endsWith("}"); } @@ -333,8 +351,7 @@ private String quote(String var) { } private List scriptExtensions() { - List extensions = new ArrayList<>(); - extensions.addAll(engine.getExtensions()); + List extensions = new ArrayList<>(engine.getExtensions()); extensions.add(scriptExtension); return extensions; } @@ -377,7 +394,7 @@ public ScriptFile(String command, String cmdLine, String[] args) { } doArgs(args); } catch (Exception e) { - + // ignore } } @@ -477,6 +494,15 @@ public boolean execute() throws Exception { return true; } + private String expandParameterName(String parameter) { + if (parameter.startsWith("$")) { + return expandName(parameter); + } else if (isNumber(parameter)) { + return parameter; + } + return quote(parameter); + } + private void internalExecute() throws Exception { if (isEngineScript()) { result = engine.execute(script, expandParameters(args)); @@ -492,25 +518,25 @@ private void internalExecute() throws Exception { } try { line += l; - done = false; parser().parse(line, line.length() + 1, ParseContext.ACCEPT_LINE); done = true; for (int i = 1; i < args.length; i++) { line = line.replaceAll("\\s\\$" + i + "\\b", - args[i].startsWith("$") ? (" " + expandName(args[i]) + " ") - : (" " + quote(args[i]) + " ")); - line = line.replaceAll("\\$\\{" + i + "(|:-.*)\\}", - args[i].startsWith("$") ? expandName(args[i]) : quote(args[i])); + (" " + expandParameterName(args[i]) + " ")); + line = line.replaceAll("\\$\\{" + i + "(|:-.*)}", + expandParameterName(args[i])); } + line = line.replaceAll("\\$\\{@}", expandToList(args)); + line = line.replaceAll("\\$@", expandToList(args)); line = line.replaceAll("\\s\\$\\d\\b", ""); - line = line.replaceAll("\\$\\{\\d+\\}", ""); - Matcher matcher=Pattern.compile("\\$\\{\\d+:-(.*?)\\}").matcher(line); + line = line.replaceAll("\\$\\{\\d+}", ""); + Matcher matcher = Pattern.compile("\\$\\{\\d+:-(.*?)}").matcher(line); if (matcher.find()) { - line = matcher.replaceAll("'$1'"); + line = matcher.replaceAll(expandParameterName(matcher.group(1))); } if (verbose) { AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append(line, AttributedStyle.DEFAULT.foreground(AttributedStyle.GREEN)); + asb.styled(Styles.prntStyle().resolve(".vs"), line); asb.toAttributedString().println(terminal()); terminal().flush(); } @@ -577,7 +603,7 @@ public Object execute(File script, String cmdLine, String[] args) throws Excepti @Override public String expandCommandLine(String line) { - String out = null; + String out; if (isCommandLine(line)) { StringBuilder sb = new StringBuilder(); List ws = parser().parse(line, 0, ParseContext.COMPLETE).words(); @@ -599,9 +625,9 @@ public String expandCommandLine(String line) { argv[i] = quote(argv[i]); } } - String cmd = hasAlias(ws.get(0).substring(idx + 1)) ? getAlias(ws.get(0).substring(idx + 1)) + String cmd = hasAlias(ws.get(0).substring(idx + 1)) ? getAlias(ws.get(0).substring(idx + 1)) : ws.get(0).substring(idx + 1); - sb.append("org.jline.builtins.SystemRegistry.get().invoke('" + cmd + "'"); + sb.append(SystemRegistry.class.getCanonicalName()).append(".get().invoke('").append(cmd).append("'"); for (int i = 1; i < argv.length; i++) { sb.append(", "); sb.append(argv[i]); @@ -696,12 +722,27 @@ public boolean executeWidget(Object function) { } @SuppressWarnings("unchecked") - private T consoleOption(String option, T defval) { + private Map consoleOptions() { + return engine.hasVariable(VAR_CONSOLE_OPTIONS) ? (Map) engine.get(VAR_CONSOLE_OPTIONS) + : new HashMap<>(); + } + + @SuppressWarnings("unchecked") + @Override + public T consoleOption(String option, T defval) { T out = defval; try { - if (engine.hasVariable(VAR_CONSOLE_OPTIONS)) { - out = (T) ((Map) engine.get(VAR_CONSOLE_OPTIONS)).getOrDefault(option, defval); - } + out = (T) consoleOptions().getOrDefault(option, defval); + } catch (Exception e) { + trace(new Exception("Bad CONSOLE_OPTION value: " + e.getMessage())); + } + return out; + } + + private boolean consoleOption(String option) { + boolean out = false; + try { + out = consoleOptions().containsKey(option); } catch (Exception e) { trace(new Exception("Bad CONSOLE_OPTION value: " + e.getMessage())); } @@ -710,8 +751,8 @@ private T consoleOption(String option, T defval) { @Override public ExecutionResult postProcess(String line, Object result, String output) { - ExecutionResult out = new ExecutionResult(1, null); - Object _output = output != null && !output.trim().isEmpty() && consoleOption("splitOutput", true) + ExecutionResult out; + Object _output = output != null && !output.trim().isEmpty() && !consoleOption("no-splittedOutput") ? output.split("\\r?\\n") : output; String consoleVar = parser().getVariable(line); if (consoleVar != null && result != null) { @@ -729,13 +770,11 @@ public ExecutionResult postProcess(String line, Object result, String output) { private ExecutionResult postProcess(String line, Object result) { int status = 0; - Object out = result != null && result instanceof String && ((String)result).trim().isEmpty() ? null : result; + Object out = result instanceof String && ((String)result).trim().isEmpty() ? null : result; String consoleVar = parser().getVariable(line); if (consoleVar != null) { status = saveResult(consoleVar, result); - if (!consoleVar.startsWith("_")) { - out = null; - } + out = null; } else if (!parser().getCommand(line).equals("show")) { if (result != null) { status = saveResult("_", result); @@ -752,7 +791,7 @@ public ExecutionResult postProcess(Object result) { } private int saveResult(String var, Object result) { - int out = 0; + int out; try { engine.put("_executionResult", result); if (var != null) { @@ -775,7 +814,7 @@ public Object invoke(CommandRegistry.CommandSession session, String command, Obj exception = null; Object out = null; if (hasCommand(command)) { - out = commandExecute.get(command(command)).executeFunction().apply(new Builtins.CommandInput(null, args, session)); + out = getCommandMethods(command).execute().apply(new CommandInput(command, args, session)); } else { String[] _args = new String[args.length]; for (int i = 0; i < args.length; i++) { @@ -795,16 +834,6 @@ public Object invoke(CommandRegistry.CommandSession session, String command, Obj return out; } - @SuppressWarnings("unchecked") - private Map defaultPrntOptions() { - Map out = new HashMap<>(); - if (engine.hasVariable(VAR_PRNT_OPTIONS)) { - out.putAll((Map)engine.get(VAR_PRNT_OPTIONS)); - } - return out; - } - - @Override public void trace(final Object object) { Object toPrint = object; int level = consoleOption("trace", 0); @@ -822,335 +851,92 @@ public void trace(final Object object) { } } else if (level > 1) { if (object instanceof SystemRegistryImpl.CommandData) { - toPrint = ((SystemRegistryImpl.CommandData)object).toString(); + toPrint = object.toString(); } } - println(options, toPrint); + printer.println(options, toPrint); } private void error(String message) { - highlight(AttributedStyle.RED, message).println(terminal()); + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.styled(Styles.prntStyle().resolve(".em"), message); + asb.println(terminal()); } @Override public void println(Object object) { - Map options = defaultPrntOptions(); - println(options, object); + printer.println(object); } - @Override - public void println(Map options, Object object) { - if (object == null) { - return; - } - options.putIfAbsent("width", terminal().getSize().getColumns()); - String style = (String) options.getOrDefault("style", ""); - int width = (int) options.get("width"); - if (style.equalsIgnoreCase("JSON")) { - highlightAndPrint(width, style, engine.toJson(object)); - } else if (!style.isEmpty() && object instanceof String) { - highlightAndPrint(width, style, (String) object); - } else if (object instanceof Exception) { - systemRegistry.trace(options.getOrDefault("exception", "stack").equals("stack"), (Exception)object); - } else if (object instanceof String) { - highlight(AttributedStyle.YELLOW + AttributedStyle.BRIGHT, object).println(terminal()); - } else if (object instanceof Number) { - highlight(AttributedStyle.BLUE + AttributedStyle.BRIGHT, object).println(terminal()); - } else { - for (AttributedString as : highlight(options, object)) { - as.println(terminal()); - } - } - terminal().flush(); - } - - private AttributedString highlight(int attrStyle, Object obj) { - AttributedString out = new AttributedString(""); - AttributedStringBuilder asb = new AttributedStringBuilder(); - String tp = obj.toString(); - if (!tp.isEmpty()) { - asb.append(tp, AttributedStyle.DEFAULT.foreground(attrStyle)); - out = asb.toAttributedString(); - } - return out; - } - - private void highlightAndPrint(int width, String style, String object) { - Path nanorc = configPath != null ? configPath.getConfig("jnanorc") : null; - if (engine.hasVariable(VAR_NANORC)) { - nanorc = Paths.get((String)engine.get(VAR_NANORC)); - } - if (nanorc == null) { - nanorc = Paths.get("/etc/nanorc"); - } - SyntaxHighlighter highlighter = nanorc != null ? SyntaxHighlighter.build(nanorc, style) - : null; - for (String s: object.split("\\r?\\n")) { - AttributedStringBuilder asb = new AttributedStringBuilder(); - asb.append(s); - if (highlighter != null) { - highlighter.highlight(asb).println(terminal()); - } else { - asb.subSequence(0, width).println(terminal()); - } - } - } - - private Map keyToString(Map map) { - return map.entrySet().stream() - .collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue)); - } - - @SuppressWarnings("unchecked") - private List highlight(Map options, Object obj) { - List out = new ArrayList<>(); - int width = (int)options.getOrDefault("width", Integer.MAX_VALUE); - boolean rownum = options.containsKey("rownum"); - if (obj == null) { - // do nothing - } else if (obj instanceof Map) { - out = highlightMap(keyToString((Map)obj), width); - } else if (obj instanceof Collection || obj instanceof Object[]) { - Collection collection = obj instanceof Collection ? (Collection)obj - : Arrays.asList((Object[])obj); - if (!collection.isEmpty()) { - if (collection.size() == 1) { - Object elem = collection.iterator().next(); - if (elem instanceof Map) { - out = highlightMap(keyToString((Map)elem), width); - } else if (canConvert(elem)){ - out = highlightMap(engine.toMap(elem), width); - } else { - out.add(new AttributedString(engine.toString(obj))); - } - } else { - Object elem = collection.iterator().next(); - boolean convert = canConvert(elem); - if (elem instanceof Map || convert) { - Map map = convert ? engine.toMap(elem): keyToString((Map)elem); - List header = map.keySet().stream().collect(Collectors.toList()); - List columns = new ArrayList<>(); - for (int i = 0; i < header.size(); i++) { - columns.add(header.get(i).length() + 1); - } - for (Object o : collection) { - for (int i = 0; i < header.size(); i++) { - Map m = convert ? engine.toMap(o) : keyToString((Map)o); - if (engine.toString(m.get(header.get(i))).length() > columns.get(i) - 1) { - columns.set(i, engine.toString(m.get(header.get(i))).length() + 1); - } - } - } - columns.add(0, 0); - toTabStops(columns, rownum); - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns); - if (rownum) { - asb.append("\t"); - } - for (int i = 0; i < header.size(); i++) { - asb.append(header.get(i), AttributedStyle.DEFAULT.foreground(AttributedStyle.BLUE + AttributedStyle.BRIGHT)); - asb.append("\t"); - } - out.add(truncate(asb, width)); - Integer row = 0; - for (Object o : collection) { - AttributedStringBuilder asb2 = new AttributedStringBuilder().tabs(columns); - if (rownum) { - asb2.append(row.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.BLUE + AttributedStyle.BRIGHT)); - asb2.append("\t"); - row++; - } - for (int i = 0; i < header.size(); i++) { - Map m = convert ? engine.toMap(o) : keyToString((Map)o); - asb2.append(engine.toString(m.get(header.get(i)))); - asb2.append("\t"); - } - out.add(asb2.subSequence(0, width)); - } - } else if (elem instanceof Collection || elem instanceof Object[]) { - boolean isCollection = elem instanceof Collection; - List columns = new ArrayList<>(); - for (Object o : collection) { - List inner = new ArrayList<>(); - inner.addAll(isCollection ? (Collection)o : Arrays.asList((Object[])o)); - for (int i = 0; i < inner.size(); i++) { - int len1 = engine.toString(inner.get(i)).length() + 1; - if (columns.size() <= i) { - columns.add(len1); - } else if (len1 > columns.get(i)) { - columns.set(i, len1); - } - } - } - toTabStops(columns, rownum); - Integer row = 0; - for (Object o : collection) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns); - if (rownum) { - asb.append(row.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.BLUE + AttributedStyle.BRIGHT)); - asb.append("\t"); - row++; - } - List inner = new ArrayList<>(); - inner.addAll(isCollection ? (Collection)o : Arrays.asList((Object[])o)); - for (int i = 0; i < inner.size(); i++) { - asb.append(engine.toString(inner.get(i))); - asb.append("\t"); - } - out.add(truncate(asb, width)); - } - } else { - Integer row = 0; - Integer tabsize = ((Integer)collection.size()).toString().length() + 1; - for (Object o: collection) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabsize); - if (rownum) { - asb.append(row.toString(), AttributedStyle.DEFAULT.foreground(AttributedStyle.BLUE + AttributedStyle.BRIGHT)); - asb.append("\t"); - row++; - } - asb.append(engine.toString(o)); - out.add(truncate(asb, width)); - } - } - } - } - } else if (canConvert(obj)) { - out = highlightMap(engine.toMap(obj), width); - } else { - out.add(new AttributedString(engine.toString(obj))); - } - return out; - } - - private boolean canConvert(Object obj) { - if (obj instanceof Number || obj instanceof Map || obj instanceof Iterable || obj instanceof String - || obj instanceof Date || obj instanceof File || obj instanceof Boolean || obj instanceof Object[] - || obj instanceof Collection || obj instanceof Enum) { - return false; - } - return true; - } - - private AttributedString truncate(AttributedStringBuilder asb, int width) { - return asb.columnLength() > width ? asb.subSequence(0, width) : asb.toAttributedString(); - } - - private void toTabStops(List columns, boolean rownum) { - if (rownum) { - columns.add(0, 5); - } - for (int i = 1; i < columns.size(); i++) { - columns.set(i, columns.get(i - 1) + columns.get(i)); - } - } - - private List highlightMap(Map map, int width) { - List out = new ArrayList<>(); - int max = map.keySet().stream().map(String::length).max(Integer::compareTo).get(); - for (Map.Entry entry : map.entrySet()) { - AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(0, max + 1)); - asb.append(entry.getKey(), AttributedStyle.DEFAULT.foreground(AttributedStyle.BLUE + AttributedStyle.BRIGHT)); - if (map.size() == 1) { - for (String v : engine.toString(entry.getValue()).split("\\r?\\n")) { - asb.append("\t"); - asb.append(v, AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); - out.add(truncate(asb, width)); - asb = new AttributedStringBuilder().tabs(Arrays.asList(0, max + 1)); - } - } else { - String v = engine.toString(entry.getValue()); - if (v.contains("\n")) { - v = Arrays.asList(v.split("\\r?\\n")).toString(); - } - asb.append("\t"); - asb.append(v, AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)); - out.add(truncate(asb, width)); - } - } - return out; - } - - private Object show(Builtins.CommandInput input) { + private Object show(CommandInput input) { final String[] usage = { "show - list console variables", "Usage: show [VARIABLE]", " -? --help Displays command help", }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; + try { + parseOptions(usage, input.args()); + Map options = new HashMap<>(); + options.put(Printer.MAX_DEPTH, 0); + printer.println(options, engine.find(input.args().length > 0 ? input.args()[0] : null)); + } catch (Exception e) { + exception = e; } - return engine.find(input.args().length > 0 ? input.args()[0] : null); + return null; } - private Object del(Builtins.CommandInput input) { + private Object del(CommandInput input) { final String[] usage = { - "del - delete console variables", + "del - delete console variables, methods, classes and imports", "Usage: del [var1] ...", " -? --help Displays command help", }; - Options opt = Options.compile(usage).parse(input.xargs()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; + try { + parseOptions(usage, input.args()); + engine.del(input.args()); + } catch (Exception e) { + exception = e; } - engine.del(input.args()); return null; } - private Object prnt(Builtins.CommandInput input) { - final String[] usage = { - "prnt - print object", - "Usage: prnt [OPTIONS] object", - " -? --help Displays command help", - " -r --rownum Display table row numbers", - " -s --style=STYLE Use nanorc STYLE", - " -w --width=WIDTH Display width (default terminal width)" - }; - Options opt = Options.compile(usage).parse(input.xargs()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; - } - Map options = defaultPrntOptions(); - if (opt.isSet("style")) { - options.put("style", opt.get("style")); - } - if (opt.isSet("width")) { - options.put("width", opt.getNumber("width")); - } - if (opt.isSet("rownum")) { - options.put("rownum", true); - } - options.put("exception", "stack"); - List args = opt.argObjects(); - if (args.size() > 0) { - println(options, args.get(0)); + private Object prnt(CommandInput input) { + Exception result = printer.prntCommand(input); + if (result != null) { + exception = result; } return null; } - private Object slurpcmd(Builtins.CommandInput input) { + private Object slurpcmd(CommandInput input) { final String[] usage = { - "slurp - slurp file context to string/object", - "Usage: slurp [OPTIONS] file", + "slurp - slurp file or string variable context to object", + "Usage: slurp [OPTIONS] file|variable", " -? --help Displays command help", " -e --encoding=ENCODING Encoding (default UTF-8)", " -f --format=FORMAT Serialization format" }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; - } Object out = null; - try { + try { + Options opt = parseOptions(usage, input.xargs()); if (!opt.args().isEmpty()){ + Object _arg = opt.argObjects().get(0); + if (!(_arg instanceof String)) { + throw new IllegalArgumentException("Invalid parameter type: " + _arg.getClass().getSimpleName()); + } + String arg = (String)_arg; Charset encoding = opt.isSet("encoding") ? Charset.forName(opt.get("encoding")): StandardCharsets.UTF_8; - String format = opt.isSet("format") ? opt.get("format") : ""; - out = slurp(Paths.get(opt.args().get(0)), encoding, format); + String format = opt.isSet("format") ? opt.get("format") : engine.getSerializationFormats().get(0); + try { + Path path = Paths.get(arg); + if (path.toFile().exists()) { + out = slurp(path, encoding, format); + } else { + out = engine.deserialize(arg, format); + } + } catch (Exception e) { + out = engine.deserialize(arg, format); + } } } catch (Exception e) { exception = e; @@ -1158,49 +944,46 @@ private Object slurpcmd(Builtins.CommandInput input) { return out; } - private Object slurp(Path file) throws IOException { - return slurp(file, StandardCharsets.UTF_8, "JSON"); + @Override + public void persist(Path file, Object object) { + engine.persist(file, object); + } + + @Override + public Object slurp(Path file) throws IOException { + return slurp(file, StandardCharsets.UTF_8, engine.getSerializationFormats().get(0)); } private Object slurp(Path file, Charset encoding, String format) throws IOException { - Object out = null; byte[] encoded = Files.readAllBytes(file); - if (format.equalsIgnoreCase("TXT")) { - out = new String(encoded, encoding); - } else { - out = engine.expandParameter(new String(encoded, encoding), format); - } - return out; + return engine.deserialize(new String(encoded, encoding), format); } - private Object aliascmd(Builtins.CommandInput input) { + private Object aliascmd(CommandInput input) { final String[] usage = { "alias - create command alias", "Usage: alias [ALIAS] [COMMANDLINE]", " -? --help Displays command help" }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; - } Object out = null; - try { + try { + Options opt = parseOptions(usage, input.args()); List args = opt.args(); if (args.isEmpty()) { out = aliases; } else if (args.size() == 1) { out = aliases.getOrDefault(args.get(0), null); } else { - for (int i = 1; i < args.size(); i++) { - for (int j = 0; j < 10; j++) { - args.set(i, args.get(i).replaceAll("%" + j , "\\$" + j)); - args.set(i, args.get(i).replaceAll("%\\{" + j + "\\}", "\\$\\{" + j + "\\}")); - args.set(i, args.get(i).replaceAll("%\\{" + j + ":-", "\\$\\{" + j + ":-")); - } + String alias = String.join(" ", args.subList(1, args.size())); + for (int j = 0; j < 10; j++) { + alias = alias.replaceAll("%" + j , "\\$" + j); + alias = alias.replaceAll("%\\{" + j + "}", "\\$\\{" + j + "\\}"); + alias = alias.replaceAll("%\\{" + j + ":-", "\\$\\{" + j + ":-"); } - aliases.put(args.get(0), String.join(" ", args.subList(1, args.size()))); - engine.persist(aliasFile, aliases); + alias = alias.replaceAll("%@" , "\\$@"); + alias = alias.replaceAll("%\\{@}", "\\$\\{@\\}"); + aliases.put(args.get(0), alias); + persist(aliasFile, aliases); } } catch (Exception e) { exception = e; @@ -1208,31 +991,25 @@ private Object aliascmd(Builtins.CommandInput input) { return out; } - private Object unalias(Builtins.CommandInput input) { + private Object unalias(CommandInput input) { final String[] usage = { "unalias - remove command alias", "Usage: unalias [ALIAS...]", " -? --help Displays command help" }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; - } - Object out = null; - try { + try { + Options opt = parseOptions(usage, input.args()); for (String a : opt.args()) { aliases.remove(a); } + persist(aliasFile, aliases); } catch (Exception e) { exception = e; - } finally { - engine.persist(aliasFile, aliases); } - return out; + return null; } - private Object pipe(Builtins.CommandInput input) { + private Object pipe(CommandInput input) { final String[] usage = { "pipe - create/delete pipe operator", "Usage: pipe [OPERATOR] [PREFIX] [POSTFIX]", @@ -1242,51 +1019,134 @@ private Object pipe(Builtins.CommandInput input) { " -d --delete Delete pipe operators", " -l --list List pipe operators", }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return null; - } - Object out = null; - if (opt.isSet("delete")) { - if ( opt.args().size() == 1 && opt.args().get(0).equals("*")) { - pipes.clear(); - } else { - for (String p: opt.args()) { - pipes.remove(p.trim()); + try { + Options opt = parseOptions(usage, input.args()); + Map options = new HashMap<>(); + if (opt.isSet("delete")) { + if ( opt.args().size() == 1 && opt.args().get(0).equals("*")) { + pipes.clear(); + } else { + for (String p: opt.args()) { + pipes.remove(p.trim()); + } } + } else if (opt.isSet("list") || opt.args().size() == 0) { + options.put(Printer.MAX_DEPTH, 0); + printer.println(options, pipes); + } else if (opt.args().size() != 3) { + exception = new IllegalArgumentException("Bad number of arguments!"); + } else if (systemRegistry.getPipeNames().contains(opt.args().get(0))) { + exception = new IllegalArgumentException("Reserved pipe operator"); + } else { + List fixes = new ArrayList<>(); + fixes.add(opt.args().get(1)); + fixes.add(opt.args().get(2)); + pipes.put(opt.args().get(0), fixes); } - } else if (opt.isSet("list") || opt.args().size() == 0) { - out = pipes; - } else if (opt.args().size() != 3) { - exception = new IllegalArgumentException("Bad number of arguments!"); - } else if (systemRegistry.getPipeNames().contains(opt.args().get(0))) { - exception = new IllegalArgumentException("Reserved pipe operator"); - } else { - List fixes = new ArrayList<>(); - fixes.add(opt.args().get(1)); - fixes.add(opt.args().get(2)); - pipes.put(opt.args().get(0), fixes); + } catch (Exception e) { + exception = e; } - return out; + return null; } - private List commandOptions(String command) { + private Object doc(CommandInput input) { + final String[] usage = { + "doc - open document on browser", + "Usage: doc [OBJECT]", + " -? --help Displays command help" + }; try { - invoke(new CommandSession(), command, "--help"); - } catch (HelpException e) { - return Builtins.compileCommandOptions(e.getMessage()); + parseOptions(usage, input.xargs()); + if (input.xargs().length == 0) { + return null; + } + if (!Desktop.isDesktopSupported()) { + throw new IllegalStateException("Desktop is not supported!"); + } + Map docs = consoleOption("docs", null); + boolean done = false; + Object arg = input.xargs()[0]; + if (arg instanceof String) { + String address = docs != null ? (String)docs.get(input.args()[0]) : null; + if (address != null) { + done = true; + if (urlExists(address)) { + Desktop.getDesktop().browse(new URI(address)); + } else { + throw new IllegalArgumentException("Document not found: " + address); + } + } + } + if (!done) { + String name; + if (arg instanceof String && ((String)arg).matches("([a-z]+\\.)+[A-Z][a-zA-Z]+")) { + name = (String)arg; + } else { + name = arg.getClass().getCanonicalName(); + } + name = name.replaceAll("\\.", "/") + ".html"; + Object doc = null; + assert docs != null; + for (Map.Entry entry : docs.entrySet()) { + if (name.matches(entry.getKey())) { + doc = entry.getValue(); + break; + } + } + boolean docFound = false; + if (doc != null) { + if (doc instanceof Collection) { + for (Object o : (Collection) doc) { + String url = o + name; + if (urlExists(url)) { + Desktop.getDesktop().browse(new URI(url)); + docFound = true; + } + } + } else { + String url = doc + name; + if (urlExists(url)) { + Desktop.getDesktop().browse(new URI(url)); + docFound = true; + } + } + } + if (!docFound) { + throw new IllegalArgumentException("Document not found: " + name); + } + } } catch (Exception e) { - trace(e); + exception = e; } return null; } + private boolean urlExists(String weburl) { + try { + URL url = new URL(weburl); + HttpURLConnection huc = (HttpURLConnection) url.openConnection(); + huc.setRequestMethod("HEAD"); + return huc.getResponseCode() == HttpURLConnection.HTTP_OK; + } catch (Exception e) { + return false; + } + } + private List slurpCompleter(String command) { List completers = new ArrayList<>(); + List optDescs = commandOptions("slurp"); + for (OptDesc o : optDescs) { + if (o.shortOption() != null && o.shortOption().equals("-f")) { + o.setValueCompleter(new StringsCompleter(engine.getDeserializationFormats())); + break; + } + } + AggregateCompleter argCompleter = new AggregateCompleter(new FilesCompleter(workDir) + , new StringsCompleter(this::variableReferences)); completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(new FilesCompleter(workDir) - , this::commandOptions + , new OptionCompleter(Arrays.asList(argCompleter + , NullCompleter.INSTANCE) + , optDescs , 1) )); return completers; @@ -1309,7 +1169,8 @@ private List variableReferences() { private List prntCompleter(String command) { List completers = new ArrayList<>(); completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(new StringsCompleter(this::variableReferences) + , new OptionCompleter(Arrays.asList(new StringsCompleter(this::variableReferences) + , NullCompleter.INSTANCE) , this::commandOptions , 1) )); @@ -1343,25 +1204,51 @@ public void complete(LineReader reader, ParsedLine commandLine, List private List aliasCompleter(String command) { List completers = new ArrayList<>(); + List params = new ArrayList<>(); + params.add(new StringsCompleter(aliases::keySet)); + params.add(new AliasValueCompleter(aliases)); completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new StringsCompleter(aliases::keySet), new AliasValueCompleter(aliases), NullCompleter.INSTANCE)); + , new OptionCompleter(params + , this::commandOptions + , 1) + )); return completers; } private List unaliasCompleter(String command) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE, new StringsCompleter(aliases::keySet))); - return completers; - } - - private List pipeCompleter(String command) { List completers = new ArrayList<>(); completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(NullCompleter.INSTANCE + , new OptionCompleter(new StringsCompleter(aliases::keySet) , this::commandOptions , 1) )); - return completers; + return completers; + } + + private List docs() { + List out = new ArrayList<>(); + for (String v : engine.find().keySet()) { + out.add("$" + v); + } + Map docs = consoleOption("docs", null); + if (!docs.isEmpty()) { + for (String d : docs.keySet()) { + if (d.matches("\\w+")) { + out.add(d); + } + } + } + return out; } + private List docCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(Arrays.asList(new StringsCompleter(this::docs) + , NullCompleter.INSTANCE) + , this::commandOptions + , 1) + )); + return completers; + } } diff --git a/console/src/main/java/org/jline/console/impl/DefaultPrinter.java b/console/src/main/java/org/jline/console/impl/DefaultPrinter.java new file mode 100644 index 000000000..e094aac04 --- /dev/null +++ b/console/src/main/java/org/jline/console/impl/DefaultPrinter.java @@ -0,0 +1,984 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.jline.builtins.ConfigurationPath; +import org.jline.builtins.Options; +import org.jline.builtins.Styles; +import org.jline.builtins.Nano.SyntaxHighlighter; +import org.jline.console.CmdDesc; +import org.jline.console.CommandInput; +import org.jline.console.Printer; +import org.jline.console.ScriptEngine; +import org.jline.console.SystemRegistry; +import org.jline.terminal.Terminal; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.Log; +import org.jline.utils.StyleResolver; + +/** + * Print highlighted objects to console. + * + * @author Matti Rinta-Nikkola + */ +public class DefaultPrinter extends JlineCommandRegistry implements Printer { + private static final String VAR_PRNT_OPTIONS = "PRNT_OPTIONS"; + private static final String VAR_NANORC = "NANORC"; + private static final int PRNT_MAX_ROWS = 100000; + private static final int PRNT_MAX_DEPTH = 1; + private static final int PRNT_INDENTION = 4; + private static final int NANORC_MAX_STRING_LENGTH = 400; + + private Map, Function>> objectToMap = new HashMap<>(); + private Map, Function> objectToString = new HashMap<>(); + private Map> highlightValue = new HashMap<>(); + private int totLines; + + private final ScriptEngine engine; + private final ConfigurationPath configPath; + private StyleResolver prntStyle; + + public DefaultPrinter(ConfigurationPath configPath) { + this(null, configPath); + } + + public DefaultPrinter(ScriptEngine engine, ConfigurationPath configPath) { + this.engine = engine; + this.configPath = configPath; + } + + @Override + public void println(Object object) { + internalPrintln(defaultPrntOptions(false), object); + } + + @Override + public void println(Map optionsIn, Object object) { + Map options = new HashMap<>(optionsIn); + for (Map.Entry entry + : defaultPrntOptions(options.containsKey(Printer.SKIP_DEFAULT_OPTIONS)).entrySet()) { + options.putIfAbsent(entry.getKey(), entry.getValue()); + } + manageBooleanOptions(options); + internalPrintln(options, object); + } + + public String[] appendUsage(String[] customUsage) { + final String[] usage = { + "prnt - print object", + "Usage: prnt [OPTIONS] object", + " -? --help Displays command help", + " -a --all Ignore columnsOut configuration", + " -c --columns=COLUMNS,... Display given columns on table", + " -e --exclude=COLUMNS,... Exclude given columns on table", + " -i --include=COLUMNS,... Include given columns on table", + " --indention=IDENTION Indention size", + " --maxColumnWidth=WIDTH Maximum column width", + " -d --maxDepth=DEPTH Maximum depth objects are resolved", + " --maxrows=ROWS Maximum number of lines to display", + " --oneRowTable Display one row data on table", + " -r --rownum Display table row numbers", + " --shortNames Truncate table column names (property.field -> field)", + " --skipDefaultOptions Ignore all options defined in PRNT_OPTIONS", + " --structsOnTable Display structs and lists on table", + " -s --style=STYLE Use nanorc STYLE", + " --toString use object's toString() method to get print value", + " DEFAULT: object's fields are put to property map before printing", + " --valueStyle=STYLE Use nanorc style to highlight column/map values", + " -w --width=WIDTH Display width (default terminal width)" + }; + String[] out; + if (customUsage == null || customUsage.length == 0) { + out = usage; + } else { + out = new String[usage.length + customUsage.length]; + System.arraycopy(usage, 0, out, 0, usage.length); + System.arraycopy(customUsage, 0, out, usage.length, customUsage.length); + } + return out; + } + + public Map compileOptions(Options opt) { + Map options = new HashMap<>(); + if (opt.isSet(Printer.SKIP_DEFAULT_OPTIONS)) { + options.put(Printer.SKIP_DEFAULT_OPTIONS, true); + } else if (opt.isSet(Printer.STYLE)) { + options.put(Printer.STYLE, opt.get(Printer.STYLE)); + } + if (opt.isSet(Printer.TO_STRING)) { + options.put(Printer.TO_STRING, true); + } + if (opt.isSet(Printer.WIDTH)) { + options.put(Printer.WIDTH, opt.getNumber(Printer.WIDTH)); + } + if (opt.isSet(Printer.ROWNUM)) { + options.put(Printer.ROWNUM, true); + } + if (opt.isSet(Printer.ONE_ROW_TABLE)) { + options.put(Printer.ONE_ROW_TABLE, true); + } + if (opt.isSet(Printer.SHORT_NAMES)) { + options.put(Printer.SHORT_NAMES, true); + } + if (opt.isSet(Printer.STRUCT_ON_TABLE)) { + options.put(Printer.STRUCT_ON_TABLE, true); + } + if (opt.isSet(Printer.COLUMNS)) { + options.put(Printer.COLUMNS, Arrays.asList(opt.get(Printer.COLUMNS).split(","))); + } + if (opt.isSet(Printer.EXCLUDE)) { + options.put(Printer.EXCLUDE, Arrays.asList(opt.get(Printer.EXCLUDE).split(","))); + } + if (opt.isSet(Printer.INCLUDE)) { + options.put(Printer.INCLUDE, Arrays.asList(opt.get(Printer.INCLUDE).split(","))); + } + if (opt.isSet(Printer.ALL)) { + options.put(Printer.ALL, true); + } + if (opt.isSet(Printer.MAXROWS)) { + options.put(Printer.MAXROWS, opt.getNumber(Printer.MAXROWS)); + } + if (opt.isSet(Printer.MAX_COLUMN_WIDTH)) { + options.put(Printer.MAX_COLUMN_WIDTH, opt.getNumber(Printer.MAX_COLUMN_WIDTH)); + } + if (opt.isSet(Printer.MAX_DEPTH)) { + options.put(Printer.MAX_DEPTH, opt.getNumber(Printer.MAX_DEPTH)); + } + if (opt.isSet(Printer.INDENTION)) { + options.put(Printer.INDENTION, opt.getNumber(Printer.INDENTION)); + } + if (opt.isSet(Printer.VALUE_STYLE)) { + options.put(Printer.VALUE_STYLE, opt.get(Printer.VALUE_STYLE)); + } + options.put("exception", "stack"); + return options; + } + + @Override + public Exception prntCommand(CommandInput input) { + Exception out = null; + String[] usage = appendUsage(null); + try { + Options opt = parseOptions(usage, input.xargs()); + Map options = compileOptions(opt); + List args = opt.argObjects(); + if (args.size() > 0) { + println(options, args.get(0)); + } + } catch (Exception e) { + out = e; + } + return out; + } + + /** + * Override ScriptEngine toMap() method + * @param objectToMap key: object class, value: toMap function + */ + public void setObjectToMap(Map, Function>> objectToMap) { + this.objectToMap = objectToMap; + } + + /** + * Override ScriptEngine toString() method + * @param objectToString key: object class, value: toString function + */ + public void setObjectToString(Map, Function> objectToString) { + this.objectToString = objectToString; + } + + /** + * Highlight column value + * @param highlightValue key: regex for column name, value: highlight function + */ + public void setHighlightValue(Map> highlightValue) { + this.highlightValue = highlightValue; + } + + private Terminal terminal() { + return SystemRegistry.get().terminal(); + } + + private void manageBooleanOptions(Map options) { + for (String key : Printer.BOOLEAN_KEYS) { + if (options.containsKey(key)) { + boolean value = options.get(key) instanceof Boolean && (boolean) options.get(key); + if (!value) { + options.remove(key); + } + } + } + } + + @SuppressWarnings("unchecked") + private Map defaultPrntOptions(boolean skipDefault) { + Map out = new HashMap<>(); + if (engine != null && !skipDefault && engine.hasVariable(VAR_PRNT_OPTIONS)) { + out.putAll((Map)engine.get(VAR_PRNT_OPTIONS)); + out.remove(Printer.SKIP_DEFAULT_OPTIONS); + manageBooleanOptions(out); + } + out.putIfAbsent(Printer.MAXROWS, PRNT_MAX_ROWS); + out.putIfAbsent(Printer.MAX_DEPTH, PRNT_MAX_DEPTH); + out.putIfAbsent(Printer.INDENTION, PRNT_INDENTION); + out.putIfAbsent(Printer.COLUMNS_OUT, new ArrayList()); + out.putIfAbsent(Printer.COLUMNS_IN, new ArrayList()); + if (engine == null) { + out.remove(Printer.OBJECT_TO_MAP); + out.remove(Printer.OBJECT_TO_STRING); + out.remove(Printer.HIGHLIGHT_VALUE); + } + return out; + } + + @SuppressWarnings("unchecked") + private void internalPrintln(Map options, Object object) { + if (object == null) { + return; + } + long start = new Date().getTime(); + if (options.containsKey(Printer.EXCLUDE)) { + List colOut = optionList(Printer.EXCLUDE, options); + List colIn = optionList(Printer.COLUMNS_IN, options); + colIn.removeAll(colOut); + colOut.addAll((List)options.get(Printer.COLUMNS_OUT)); + options.put(Printer.COLUMNS_IN, colIn); + options.put(Printer.COLUMNS_OUT, colOut); + } + if (options.containsKey(Printer.INCLUDE)) { + List colIn = optionList(Printer.INCLUDE, options); + colIn.addAll((List)options.get(Printer.COLUMNS_IN)); + options.put(Printer.COLUMNS_IN, colIn); + } + String valueStyle = (String)options.get(Printer.VALUE_STYLE); + if (options.containsKey(Printer.VALUE_STYLE)) { + options.put(Printer.VALUE_STYLE, valueHighlighter(valueStyle)); + } + prntStyle = Styles.prntStyle(); + options.putIfAbsent(Printer.WIDTH, terminal().getSize().getColumns()); + String style = (String) options.getOrDefault(Printer.STYLE, ""); + int width = (int) options.get(Printer.WIDTH); + if (!style.isEmpty() && object instanceof String) { + highlightAndPrint(width, style, (String) object); + } else if (style.equalsIgnoreCase("JSON")) { + if (engine == null) { + throw new IllegalArgumentException("JSON style not supported!"); + } + highlightAndPrint(width, style, engine.toJson(object)); + } else if (options.containsKey(Printer.SKIP_DEFAULT_OPTIONS)) { + highlightAndPrint(options, object); + } else if (object instanceof Exception) { + SystemRegistry.get().trace(options.getOrDefault("exception", "stack").equals("stack"), (Exception)object); + } else if (object instanceof CmdDesc) { + highlight((CmdDesc)object).println(terminal()); + } else if (object instanceof String || object instanceof Number) { + highlightAndPrint(width, valueStyle, object.toString()); + } else { + highlightAndPrint(options, object); + } + terminal().flush(); + Log.debug("println: ", new Date().getTime() - start, " msec"); + } + + private AttributedString highlight(CmdDesc cmdDesc) { + StringBuilder sb = new StringBuilder(); + for (AttributedString as : cmdDesc.getMainDesc()) { + sb.append(as.toString()); + sb.append("\n"); + } + List tabs = Arrays.asList(0, 2, 33); + for (Map.Entry> entry : cmdDesc.getOptsDesc().entrySet()) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.tabs(tabs); + asb.append("\t"); + asb.append(entry.getKey()); + asb.append("\t"); + boolean first = true; + for (AttributedString as : entry.getValue()) { + if (!first) { + asb.append("\t"); + asb.append("\t"); + } + asb.append(as); + asb.append("\n"); + first = false; + } + sb.append(asb.toString()); + } + return Options.HelpException.highlight(sb.toString(), Styles.helpStyle()); + } + + private SyntaxHighlighter valueHighlighter(String style) { + SyntaxHighlighter out; + if (style == null) { + out = null; + } else if (style.matches("[a-z]+:.*")) { + out = SyntaxHighlighter.build(style); + } else { + Path nanorc = configPath != null ? configPath.getConfig("jnanorc") : null; + if (engine != null && engine.hasVariable(VAR_NANORC)) { + nanorc = Paths.get((String)engine.get(VAR_NANORC)); + } + if (nanorc == null) { + nanorc = Paths.get("/etc/nanorc"); + } + out = SyntaxHighlighter.build(nanorc, style); + } + return out; + } + + private String truncate4nanorc(String obj) { + String val = obj; + if (val.length() > NANORC_MAX_STRING_LENGTH && !val.contains("\n")) { + val = val.substring(0, NANORC_MAX_STRING_LENGTH - 1); + } + return val; + } + + private AttributedString highlight(Integer width, SyntaxHighlighter highlighter, String object) { + return highlight(width, highlighter, object, isValue(object)); + } + + private AttributedString highlight(Integer width, SyntaxHighlighter highlighter, String object, boolean doValueHighlight) { + AttributedString out; + AttributedStringBuilder asb = new AttributedStringBuilder(); + String val = object; + if (highlighter != null && doValueHighlight) { + val = truncate4nanorc(object); + } + asb.append(val); + if (highlighter != null && val.length() < NANORC_MAX_STRING_LENGTH && doValueHighlight) { + out = highlighter.highlight(asb); + } else { + out = asb.toAttributedString(); + } + if (width != null) { + out = out.columnSubSequence(0, width); + } + return out; + } + + private boolean isValue(String value) { + if(value.matches("\"(\\.|[^\"])*\"|'(\\.|[^'])*'") + || (value.startsWith("[") && value.endsWith("]")) + || (value.startsWith("(") && value.endsWith(")")) + || (value.startsWith("{") && value.endsWith("}")) + || (value.startsWith("<") && value.endsWith(">")) + ) { + return true; + } else { + return !value.contains(" ") && !value.contains("\t"); + } + } + + private void highlightAndPrint(int width, String style, String object) { + SyntaxHighlighter highlighter = valueHighlighter(style); + boolean doValueHighlight = isValue(object); + for (String s: object.split("\\r?\\n")) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + List sas = asb.append(s).columnSplitLength(width); + for (AttributedString as : sas) { + highlight(width, highlighter, as.toString(), doValueHighlight).println(terminal()); + } + } + } + + private Map keysToString(Map map) { + Map out = new HashMap<>(); + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() instanceof String) { + out.put((String)entry.getKey(), entry.getValue()); + } else if (entry.getKey() != null) { + out.put(entry.getKey().toString(), entry.getValue()); + } else { + out.put("null", entry.getValue()); + } + } + return out; + } + + @SuppressWarnings("unchecked") + private Object mapValue(Map options, String key, Map map) { + Object out = null; + if (map.containsKey(key)) { + out = map.get(key); + } else if (key.contains(".")) { + String[] keys = key.split("\\."); + out = map.get(keys[0]); + for (int i = 1; i < keys.length; i++) { + if (out instanceof Map) { + Map m = keysToString((Map) out); + out = m.get(keys[i]); + } else if (canConvert(out)) { + out = engine.toMap(out).get(keys[i]); + } else { + break; + } + } + } + if (!(out instanceof Map) && canConvert(out)){ + out = objectToMap(options, out); + } + return out; + } + + @SuppressWarnings("unchecked") + private List optionList(String key, Map options) { + List out = new ArrayList<>(); + if (options.containsKey(key)) { + if (options.get(key) instanceof String) { + out.addAll(Arrays.asList(((String)options.get(key)).split(","))); + } else if (options.get(key) instanceof Collection) { + out.addAll((Collection)options.get(key)); + } else { + throw new IllegalArgumentException("Unsupported option list: {key: " + key + + ", type: " + options.get(key).getClass() + "}"); + } + } + return out; + } + + private boolean hasMatch(List regexes, String value) { + for (String r: regexes) { + if (value.matches(r)) { + return true; + } + } + return false; + } + + private AttributedString addPadding(AttributedString str, int width) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + for (int i = str.columnLength(); i < width; i++) { + sb.append(" "); + } + sb.append(str); + return sb.toAttributedString(); + } + + private String columnValue(String value) { + return value.replaceAll("\r","CR").replaceAll("\n", "LF"); + } + + @SuppressWarnings("unchecked") + private Map objectToMap(Map options, Object obj) { + if (obj != null) { + Map, Object> toMap = options.containsKey(Printer.OBJECT_TO_MAP) + ? (Map, Object>)options.get(Printer.OBJECT_TO_MAP) + : new HashMap<>(); + if (toMap.containsKey(obj.getClass())) { + return (Map)engine.execute(toMap.get(obj.getClass()), obj); + } else if (objectToMap.containsKey(obj.getClass())) { + return objectToMap.get(obj.getClass()).apply(obj); + } + } + return engine.toMap(obj); + } + + @SuppressWarnings("unchecked") + private String objectToString(Map options, Object obj) { + String out = "null"; + if (obj != null) { + Map, Object> toString = options.containsKey(Printer.OBJECT_TO_STRING) + ? (Map, Object>)options.get(Printer.OBJECT_TO_STRING) + : new HashMap<>(); + if (toString.containsKey(obj.getClass())) { + out = (String) engine.execute(toString.get(obj.getClass()), obj); + } else if (objectToString.containsKey(obj.getClass())) { + out = objectToString.get(obj.getClass()).apply(obj); + } else if (obj instanceof Class) { + out = ((Class) obj).getName(); + } else if (engine != null) { + out = engine.toString(obj); + } else { + out = obj.toString(); + } + } + return out; + } + + private AttributedString highlightMapValue(Map options, String key, Map map) { + return highlightValue(options, key, mapValue(options, key, map)); + } + + private boolean isHighlighted(AttributedString value) { + for (int i = 0; i < value.length(); i++) { + if (value.styleAt(i).getStyle() != AttributedStyle.DEFAULT.getStyle()) { + return true; + } + } + return false; + } + + @SuppressWarnings("unchecked") + private AttributedString highlightValue(Map options, String column, Object obj) { + AttributedString out = null; + Object raw = options.containsKey(Printer.TO_STRING) && obj != null ? objectToString(options, obj) : obj; + Map hv = options.containsKey(Printer.HIGHLIGHT_VALUE) + ? (Map) options.get(Printer.HIGHLIGHT_VALUE) + : new HashMap<>(); + if (column != null && simpleObject(raw)) { + for (Map.Entry entry : hv.entrySet()) { + if (!entry.getKey().equals("*") && column.matches(entry.getKey())) { + out = (AttributedString) engine.execute(hv.get(entry.getKey()), raw); + break; + } + } + if (out == null) { + for (Map.Entry> entry : highlightValue.entrySet()) { + if (!entry.getKey().equals("*") && column.matches(entry.getKey())) { + out = highlightValue.get(entry.getKey()).apply(raw); + break; + } + } + } + } + if (out == null) { + if (raw instanceof String) { + out = new AttributedString(columnValue((String)raw)); + } else { + out = new AttributedString(columnValue(objectToString(options, raw))); + } + } + if ((simpleObject(raw) || raw == null) && (hv.containsKey("*") || highlightValue.containsKey("*")) + && !isHighlighted(out)) { + if (hv.containsKey("*")) { + out = (AttributedString) engine.execute(hv.get("*"), out); + } + if (highlightValue.containsKey("*")) { + out = highlightValue.get("*").apply(out); + } + } + if (options.containsKey(Printer.VALUE_STYLE) && !isHighlighted(out)) { + out = highlight(null, (SyntaxHighlighter)options.get(Printer.VALUE_STYLE), out.toString()); + } + return truncateValue(options, out); + } + + private AttributedString truncateValue(Map options, AttributedString value) { + if (value.columnLength() > (int)options.getOrDefault(Printer.MAX_COLUMN_WIDTH, Integer.MAX_VALUE)) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(value.subSequence(0, (int)options.get(Printer.MAX_COLUMN_WIDTH) - 3)); + asb.append("..."); + return asb.toAttributedString(); + } + return value; + } + + private String truncateValue(int maxWidth, String value) { + if (value.length() > maxWidth) { + return value.subSequence(0, maxWidth - 3) + "..."; + } + return value; + } + + @SuppressWarnings("unchecked") + private List objectToList(Object obj) { + List out = new ArrayList<>(); + if (obj instanceof List) { + out = (List)obj; + } else if (obj instanceof Collection) { + out.addAll((Collection) obj); + } else if (obj instanceof Object[]) { + out.addAll(Arrays.asList((Object[]) obj)); + } else if (obj instanceof Iterator) { + ((Iterator) obj).forEachRemaining(out::add); + } else if (obj instanceof Iterable) { + ((Iterable) obj).forEach(out::add); + } else { + out.add(obj); + } + return out; + } + + private boolean similarSets(Set ref, Set c2, double threshold) { + boolean out = c2.containsAll(ref); + if (!out) { + int matches = 0; + for (String s : ref) { + if (c2.contains(s)) { + matches += 1; + } + } + double r = (1.0*matches)/ref.size(); + out = r > threshold; + } + return out; + } + + @SuppressWarnings("serial") + private static class TruncatedOutputException extends RuntimeException { + public TruncatedOutputException(String message) { + super(message); + } + } + + private void println(AttributedString line, int maxrows) { + line.println(terminal()); + totLines++; + if (totLines > maxrows) { + totLines = 0; + throw new TruncatedOutputException("Truncated output: " + maxrows); + } + } + + private String columnName(String name, boolean shortName) { + String out = name; + if (shortName) { + String[] p = name.split("\\."); + out = p[p.length - 1]; + } + return out; + } + + private boolean isNumber(String str) { + return str.matches("-?\\d+(\\.\\d+)?"); + } + + @SuppressWarnings("unchecked") + private void highlightAndPrint(Map options, Object obj) { + int width = (int)options.get(Printer.WIDTH); + boolean rownum = options.containsKey(Printer.ROWNUM); + totLines = 0; + String message = null; + if (obj == null) { + // do nothing + } else if (obj instanceof Map) { + highlightMap(options, keysToString((Map)obj), width); + } else if (collectionObject(obj)) { + List collection = objectToList(obj); + if (collection.size() > (int)options.get(Printer.MAXROWS)) { + message = "Truncated output: " + options.get(Printer.MAXROWS) + "/" + collection.size(); + collection = collection.subList(collection.size() - (int)options.get(Printer.MAXROWS), collection.size()); + } + if (!collection.isEmpty()) { + if (collection.size() == 1 && !options.containsKey(Printer.ONE_ROW_TABLE)) { + Object elem = collection.iterator().next(); + if (elem instanceof Map) { + highlightMap(options, keysToString((Map)elem), width); + } else if (canConvert(elem) && !options.containsKey(Printer.TO_STRING)){ + highlightMap(options, objectToMap(options, elem), width); + } else { + highlightValue(options, null, objectToString(options, obj)).println(terminal()); + } + } else { + try { + Object elem = collection.iterator().next(); + boolean convert = canConvert(elem); + if ((elem instanceof Map || convert) && !options.containsKey(Printer.TO_STRING)) { + Map map = convert ? objectToMap(options, elem) + : keysToString((Map) elem); + List _header; + List columnsIn = optionList(Printer.COLUMNS_IN, options); + List columnsOut = !options.containsKey("all") ? optionList(Printer.COLUMNS_OUT, options) + : new ArrayList<>(); + if (options.containsKey(Printer.COLUMNS)) { + _header = (List) options.get(Printer.COLUMNS); + } else { + _header = columnsIn; + _header.addAll(map.keySet().stream() + .filter(k -> !columnsIn.contains(k) && !hasMatch(columnsOut, k)) + .collect(Collectors.toList())); + } + List header = new ArrayList<>(); + List columns = new ArrayList<>(); + int headerWidth = 0; + Set refKeys = new HashSet<>(); + for (String value : _header) { + if (!map.containsKey(value.split("\\.")[0]) && !map.containsKey(value)) { + continue; + } + if (options.containsKey(Printer.COLUMNS)) { + // do nothing + } else if (!options.containsKey(Printer.STRUCT_ON_TABLE)) { + Object val = mapValue(options, value, map); + if (!simpleObject(val)) { + continue; + } + } + String rk = map.containsKey(value) ? value : value.split("\\.")[0]; + refKeys.add(rk); + header.add(value); + String cn = columnName(value, options.containsKey(Printer.SHORT_NAMES)); + columns.add(cn.length() + 1); + headerWidth += cn.length() + 1; + if (headerWidth > width) { + break; + } + } + if (header.size() == 0) { + throw new Exception("No columns for table!"); + } + double mapSimilarity = 0.8; + if (options.containsKey(Printer.MAP_SIMILARITY)) { + mapSimilarity = ((java.math.BigDecimal)options.get(Printer.MAP_SIMILARITY)).doubleValue(); + } + for (Object o : collection) { + Map m = convert ? objectToMap(options, o) + : keysToString((Map) o); + if (o instanceof Map && !similarSets(refKeys, m.keySet(), mapSimilarity)) { + throw new Exception("Not homogenous list!"); + } + for (int i = 0; i < header.size(); i++) { + int cw = highlightMapValue(options, header.get(i), m).columnLength(); + if (cw > columns.get(i) - 1) { + columns.set(i, cw + 1); + } + } + } + columns.add(0, 0); + toTabStops(columns, collection.size(), rownum); + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns); + int firstColumn = 0; + if (rownum) { + asb.append("\t"); + firstColumn = 1; + } + for (String s : header) { + asb.styled(prntStyle.resolve(".th") + , columnName(s, options.containsKey(Printer.SHORT_NAMES))); + asb.append("\t"); + } + truncate(asb, width).println(terminal()); + int row = 0; + for (Object o : collection) { + AttributedStringBuilder asb2 = new AttributedStringBuilder().tabs(columns); + if (rownum) { + asb2.styled(prntStyle.resolve(".rn"), Integer.toString(row)).append(":"); + asb2.append("\t"); + row++; + } + Map m = convert ? objectToMap(options, o) + : keysToString((Map) o); + for (int i = 0; i < header.size(); i++) { + AttributedString v = highlightMapValue(options, header.get(i), m); + if (isNumber(v.toString())) { + v = addPadding(v, + columns.get(firstColumn + i + 1) - columns.get(firstColumn + i) - 1); + } + asb2.append(v); + asb2.append("\t"); + } + asb2.subSequence(0, width).println(terminal()); + } + } else if (collectionObject(elem) && !options.containsKey(Printer.TO_STRING)) { + List columns = new ArrayList<>(); + for (Object o : collection) { + List inner = objectToList(o); + for (int i = 0; i < inner.size(); i++) { + int len1 = objectToString(options, inner.get(i)).length() + 1; + if (columns.size() <= i) { + columns.add(len1); + } else if (len1 > columns.get(i)) { + columns.set(i, len1); + } + } + } + toTabStops(columns, collection.size(), rownum); + int row = 0; + int firstColumn = rownum ? 1 : 0; + for (Object o : collection) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns); + if (rownum) { + asb.styled(prntStyle.resolve(".rn"), Integer.toString(row)).append(":"); + asb.append("\t"); + row++; + } + List inner = objectToList(o); + for (int i = 0; i < inner.size(); i++) { + AttributedString v = highlightValue(options, null, inner.get(i)); + if (isNumber(v.toString())) { + v = addPadding(v, + columns.get(firstColumn + i + 1) - columns.get(firstColumn + i) - 1); + } + asb.append(v); + asb.append("\t"); + } + truncate(asb, width).println(terminal()); + } + } else { + highlightList(options, collection, width); + } + } catch (Exception e) { + Log.debug("Stack: ", e); + highlightList(options, collection, width); + } + } + } else { + highlightValue(options, null, objectToString(options, obj)).println(terminal()); + } + } else if (canConvert(obj) && !options.containsKey(Printer.TO_STRING)) { + highlightMap(options, objectToMap(options, obj), width); + } else { + highlightValue(options, null, objectToString(options, obj)).println(terminal()); + } + if (message != null) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.styled(prntStyle.resolve(".em"), message); + asb.println(terminal()); + } + } + + private void highlightList(Map options + , List collection, int width) { + highlightList(options, collection, width, 0); + } + + private void highlightList(Map options + , List collection, int width, int depth) { + int row = 0; + int maxrows = (int)options.get(Printer.MAXROWS); + int indent = (int)options.get(Printer.INDENTION); + List tabs = new ArrayList<>(); + tabs.add(indent*depth); + if (options.containsKey(Printer.ROWNUM)) { + tabs.add(indent*depth + digits(collection.size()) + 2); + } + options.remove(Printer.MAX_COLUMN_WIDTH); + for (Object o : collection) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); + if (depth > 0) { + asb.append("\t"); + } + if (options.containsKey(Printer.ROWNUM)) { + asb.styled(prntStyle.resolve(".rn"), Integer.toString(row)).append(":"); + asb.append("\t"); + row++; + } + asb.append(highlightValue(options, null, o)); + println(truncate(asb, width), maxrows); + } + } + + private boolean collectionObject(Object obj) { + return obj instanceof Iterator || obj instanceof Iterable || obj instanceof Object[]; + } + + private boolean simpleObject(Object obj) { + return obj instanceof Number || obj instanceof String || obj instanceof Date || obj instanceof File + || obj instanceof Boolean || obj instanceof Enum; + } + + private boolean canConvert(Object obj) { + return engine != null && obj != null && !(obj instanceof Class) && !(obj instanceof Map) && !simpleObject(obj) + && !collectionObject(obj); + } + + private AttributedString truncate(AttributedStringBuilder asb, int width) { + return asb.columnLength() > width ? asb.subSequence(0, width) : asb.toAttributedString(); + } + + private int digits(int number) { + if (number < 100) { + return number < 10 ? 1 : 2; + } else if (number < 1000) { + return 3; + } else { + return number < 10000 ? 4 : 5; + } + } + + private void toTabStops(List columns, int rows, boolean rownum) { + int delta = 5; + if (rownum) { + delta = digits(rows) + 2; + columns.add(0, delta); + } + for (int i = 1; i < columns.size(); i++) { + delta = columns.get(i); + columns.set(i, columns.get(i - 1) + columns.get(i)); + } + columns.add(columns.get(columns.size() - 1) + delta); + } + + private void highlightMap(Map options, Map map, int width) { + if (!map.isEmpty()) { + highlightMap(options, map, width, 0); + } else { + highlightValue(options, null, objectToString(options, map)).println(terminal()); + } + } + + @SuppressWarnings("unchecked") + private void highlightMap(Map options + , Map map, int width, int depth) { + int maxrows = (int)options.get(Printer.MAXROWS); + int max = map.keySet().stream().map(String::length).max(Integer::compareTo).get(); + if (max > (int)options.getOrDefault(Printer.MAX_COLUMN_WIDTH, Integer.MAX_VALUE)) { + max = (int)options.get(Printer.MAX_COLUMN_WIDTH); + } + Map mapOptions = new HashMap<>(options); + mapOptions.remove(Printer.MAX_COLUMN_WIDTH); + int indent = (int)options.get(Printer.INDENTION); + int maxDepth = (int)options.get(Printer.MAX_DEPTH); + for (Map.Entry entry : map.entrySet()) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(0, depth*indent, depth*indent + max + 1)); + if (depth != 0) { + asb.append("\t"); + } + asb.styled(prntStyle.resolve(".mk"), truncateValue(max, entry.getKey())); + Object elem = entry.getValue(); + boolean convert = canConvert(elem); + boolean highlightValue = true; + if (depth < maxDepth && !options.containsKey(Printer.TO_STRING)) { + if (elem instanceof Map || convert) { + Map childMap = convert ? objectToMap(options, elem) + : keysToString((Map) elem); + if (!childMap.isEmpty()) { + println(truncate(asb, width), maxrows); + highlightMap(options, childMap, width, depth + 1); + highlightValue = false; + } + } else if (collectionObject(elem)) { + List collection = objectToList(elem); + if (!collection.isEmpty()) { + println(truncate(asb, width), maxrows); + Map listOptions = new HashMap<>(options); + listOptions.put(Printer.TO_STRING, true); + highlightList(listOptions, collection, width, depth + 1); + highlightValue = false; + } + } + } + if (highlightValue) { + AttributedString val = highlightMapValue(mapOptions, entry.getKey(), map); + asb.append("\t"); + if (map.size() == 1) { + if (val.contains('\n')) { + for (String v : val.toString().split("\\r?\\n")) { + asb.append(highlightValue(options, entry.getKey(), v)); + println(truncate(asb, width), maxrows); + asb = new AttributedStringBuilder().tabs(Arrays.asList(0, max + 1)); + } + } else { + asb.append(val); + println(truncate(asb, width), maxrows); + } + } else { + if (val.contains('\n')) { + val = new AttributedString(Arrays.asList(val.toString().split("\\r?\\n")).toString()); + asb.append(highlightValue(options, entry.getKey(), val.toString())); + } else { + asb.append(val); + } + println(truncate(asb, width), maxrows); + } + } + } + } +} diff --git a/console/src/main/java/org/jline/console/impl/JlineCommandRegistry.java b/console/src/main/java/org/jline/console/impl/JlineCommandRegistry.java new file mode 100644 index 000000000..6009b92fe --- /dev/null +++ b/console/src/main/java/org/jline/console/impl/JlineCommandRegistry.java @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.jline.utils.AttributedString; +import org.jline.utils.Log; +import org.jline.builtins.Options; +import org.jline.builtins.Completers.AnyCompleter; +import org.jline.builtins.Completers.OptDesc; +import org.jline.builtins.Completers.OptionCompleter; +import org.jline.builtins.Options.HelpException; +import org.jline.console.ArgDesc; +import org.jline.console.CmdDesc; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; + +/** + * CommandRegistry common methods for JLine commands that are using HelpException. + * + * @author Matti Rinta-Nikkola + */ +public abstract class JlineCommandRegistry extends AbstractCommandRegistry { + + public JlineCommandRegistry() { + super(); + } + + public List commandInfo(String command) { + try { + Object[] args = {"--help"}; + if (command.equals("help")) { + args = new Object[] {}; + } + invoke(new CommandSession(), command, args); + } catch (HelpException e) { + return compileCommandInfo(e.getMessage()); + } catch (Exception e) { + Log.info("Error while getting command info", e); + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + return new ArrayList<>(); + } + throw new IllegalArgumentException("JlineCommandRegistry.commandInfo() method must be overridden in class " + + this.getClass().getCanonicalName()); + } + + public CmdDesc commandDescription(List args) { + String command = args != null && !args.isEmpty() ? args.get(0) : ""; + try { + invoke(new CommandSession(), command, "--help"); + } catch (HelpException e) { + return compileCommandDescription(e.getMessage()); + } catch (Exception e) { + // ignore + } + throw new IllegalArgumentException("JlineCommandRegistry.commandDescription() method must be overridden in class " + + this.getClass().getCanonicalName()); + } + + public List commandOptions(String command) { + try { + invoke(new CommandSession(), command, "--help"); + } catch (HelpException e) { + return compileCommandOptions(e.getMessage()); + } catch (Exception e) { + // ignore + } + return null; + } + + public List defaultCompleter(String command) { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(NullCompleter.INSTANCE + , this::commandOptions + , 1) + )); + return completers; + } + + public 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; + } + + // + // Utils for helpMessage parsing + // + private static AttributedString highlightComment(String comment) { + return HelpException.highlightComment(comment, HelpException.defaultStyle()); + } + + private static String[] helpLines(String helpMessage, boolean body) { + return new HelpLines(helpMessage, body).lines(); + } + + private static class HelpLines { + private final String helpMessage; + private final boolean body; + private boolean subcommands; + + public HelpLines(String helpMessage, boolean body) { + this.helpMessage = helpMessage; + this.body = body; + } + + public String[] lines() { + String out = ""; + Matcher tm = Pattern.compile("(^|\\n)(Usage|Summary)(:)").matcher(helpMessage); + if (tm.find()) { + subcommands = tm.group(2).matches("Summary"); + if (body) { + out = helpMessage.substring(tm.end(3)); + } else { + out = helpMessage.substring(0,tm.start(1)); + } + } else if (!body) { + out = helpMessage; + } + return out.split("\\r?\\n"); + } + + public boolean subcommands() { + return subcommands; + } + } + + public static CmdDesc compileCommandDescription(String helpMessage) { + List main = new ArrayList<>(); + Map> options = new HashMap<>(); + String prevOpt = null; + boolean mainDone = false; + HelpLines hl = new HelpLines(helpMessage, true); + for (String s : hl.lines()) { + if (s.matches("^\\s+-.*$")) { + mainDone = true; + int ind = s.lastIndexOf(" "); + if (ind > 0) { + String o = s.substring(0, ind); + String d = s.substring(ind); + if (o.trim().length() > 0) { + prevOpt = o.trim(); + options.put(prevOpt, new ArrayList<>(Collections.singletonList(highlightComment(d.trim())))); + } + } + } else if (s.matches("^[\\s]{20}.*$") && prevOpt != null && options.containsKey(prevOpt)) { + int ind = s.lastIndexOf(" "); + if (ind > 0) { + options.get(prevOpt).add(highlightComment(s.substring(ind).trim())); + } + } else { + prevOpt = null; + } + if (!mainDone) { + main.add(HelpException.highlightSyntax(s.trim(), HelpException.defaultStyle(), hl.subcommands())); + } + } + return new CmdDesc(main, ArgDesc.doArgNames(Collections.singletonList("")), options); + } + + public static List compileCommandOptions(String helpMessage) { + List out = new ArrayList<>(); + for (String s : helpLines(helpMessage, true)) { + if (s.matches("^\\s+-.*$")) { + int ind = s.lastIndexOf(" "); + if (ind > 0) { + String[] op = s.substring(0, ind).trim().split("\\s+"); + String d = s.substring(ind).trim(); + String so = null; + String lo = null; + if (op.length == 1) { + if (op[0].startsWith("--")) { + lo = op[0]; + } else { + so = op[0]; + } + } else { + so = op[0]; + lo = op[1]; + } + boolean hasValue = false; + if (lo != null && lo.contains("=")) { + hasValue = true; + lo = lo.split("=")[0]; + } + out.add(new OptDesc(so, lo, d, hasValue ? AnyCompleter.INSTANCE : null)); + } + } + } + return out; + } + + public static List compileCommandInfo(String helpMessage) { + List out = new ArrayList<>(); + boolean first = true; + for (String s : helpLines(helpMessage, false)) { + if (first && s.contains(" - ")) { + out.add(s.substring(s.indexOf(" - ") + 3).trim()); + } else { + out.add(s.trim()); + } + first = false; + } + return out; + } + +} diff --git a/console/src/main/java/org/jline/console/impl/SystemHighlighter.java b/console/src/main/java/org/jline/console/impl/SystemHighlighter.java new file mode 100644 index 000000000..0b926bd94 --- /dev/null +++ b/console/src/main/java/org/jline/console/impl/SystemHighlighter.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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 org.jline.builtins.Nano.SyntaxHighlighter; +import org.jline.console.SystemRegistry; +import org.jline.reader.LineReader; +import org.jline.reader.Parser; +import org.jline.reader.impl.DefaultHighlighter; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; + +import java.util.regex.Pattern; + +/** + * Highlight command and language syntax using nanorc highlighter. + * + * @author Matti Rinta-Nikkola + */ +public class SystemHighlighter extends DefaultHighlighter { + private final SyntaxHighlighter commandHighlighter; + private final SyntaxHighlighter argsHighlighter; + private final SyntaxHighlighter langHighlighter; + private final SystemRegistry systemRegistry; + private Pattern errorPattern; + private int errorIndex = -1; + + public SystemHighlighter(SyntaxHighlighter commandHighlighter, SyntaxHighlighter argsHighlighter + , SyntaxHighlighter langHighlighter) { + this.commandHighlighter = commandHighlighter; + this.argsHighlighter = argsHighlighter; + this.langHighlighter = langHighlighter; + this.systemRegistry = SystemRegistry.get(); + } + + @Override + public void setErrorPattern(Pattern errorPattern) { + this.errorPattern = errorPattern; + super.setErrorPattern(errorPattern); + } + + @Override + public void setErrorIndex(int errorIndex) { + this.errorIndex = errorIndex; + super.setErrorIndex(errorIndex); + } + + @Override + public AttributedString highlight(LineReader reader, String buffer) { + return doDefaultHighlight(reader) ? super.highlight(reader, buffer) : systemHighlight(reader.getParser(), buffer); + } + + private boolean doDefaultHighlight(LineReader reader) { + String search = reader.getSearchTerm(); + return ((search != null && search.length() > 0) || reader.getRegionActive() != LineReader.RegionType.NONE + || errorIndex > -1 || errorPattern != null); + } + + private AttributedString systemHighlight(Parser parser, String buffer) { + AttributedString out; + String command = parser.getCommand(buffer.trim().split("\\s+")[0]); + if (buffer.trim().isEmpty()) { + out = new AttributedStringBuilder().append(buffer).toAttributedString(); + } else if (systemRegistry.isCommandOrScript(command) || systemRegistry.isCommandAlias(command)) { + if (commandHighlighter != null || argsHighlighter != null) { + int idx = -1; + boolean cmdFound = false; + for (int i = 0; i < buffer.length(); i++) { + char c = buffer.charAt(i); + if (c != ' ') { + cmdFound = true; + } else if (cmdFound) { + idx = i; + break; + } + } + AttributedStringBuilder asb = new AttributedStringBuilder(); + if (idx < 0) { + highlightCommand(buffer, asb); + } else { + highlightCommand(buffer.substring(0, idx), asb); + if (argsHighlighter != null) { + asb.append(argsHighlighter.highlight(buffer.substring(idx))); + } else { + asb.append(buffer.substring(idx)); + } + } + out = asb.toAttributedString(); + } else { + out = new AttributedStringBuilder().append(buffer).toAttributedString(); + } + } else if (langHighlighter != null) { + out = langHighlighter.highlight(buffer); + } else { + out = new AttributedStringBuilder().append(buffer).toAttributedString(); + } + return out; + } + + private void highlightCommand(String command, AttributedStringBuilder asb) { + if (commandHighlighter != null) { + asb.append(commandHighlighter.highlight(command)); + } else { + asb.append(command); + } + } +} diff --git a/console/src/main/java/org/jline/console/impl/SystemRegistryImpl.java b/console/src/main/java/org/jline/console/impl/SystemRegistryImpl.java new file mode 100644 index 000000000..266494057 --- /dev/null +++ b/console/src/main/java/org/jline/console/impl/SystemRegistryImpl.java @@ -0,0 +1,1927 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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 static org.jline.keymap.KeyMap.ctrl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.File; +import java.io.FileOutputStream; +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.Styles; +import org.jline.builtins.Options.HelpException; +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.utils.*; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; + +/** + * 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; + private final Parser parser; + private final ConfigurationPath configPath; + 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 Supplier workDir; + private final SystemCompleter customSystemCompleter = new SystemCompleter(); + private final AggregateCompleter customAggregateCompleter = new AggregateCompleter(new ArrayList<>()); + private boolean commandGroups = true; + private Function scriptDescription; + + 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) { + Object out = null; + if (consoleId != null) { + out = consoleEngine().consoleOption(name, null); + } + return out; + } + + /** + * 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 { + if (subcommands.containsKey(command)) { + registryHelp(subcommands.get(command)); + } 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()) { + if (subcommands.containsKey(command)) { + for(Map.Entry> entry : subcommands.get(command).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(); + if (subcommands.containsKey(cmd)) { + String c = args.size() > 1 ? args.get(1) : null; + if (c == null || subcommands.get(cmd).hasCommand(c)) { + if (c != null && c.equals("help")) { + out = null; + } else if (c != null) { + out = subcommands.get(cmd).commandDescription(Collections.singletonList(c)); + } else { + out = commandDescription(subcommands.get(cmd)); + } + } else { + out = commandDescription(subcommands.get(cmd)); + } + if (out != null) { + out.setSubcommand(true); + } + } else { + args.set(0, cmd); + out = commandDescription(args); + } + } + break; + case METHOD: + case SYNTAX: + if (!isCommandOrScript(cmd)) { + 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() 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) + .jna(false) + .jansi(false) + .type(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()); + this.command = parser.getCommand(args.get(0)); + if (!parser.validCommandName(command)) { + this.command = ""; + } + this.variable = parser.getVariable(args.get(0)); + } + + 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; + } + + } + + 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.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") + private 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(); + } + 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(Exception exception) { + outputStream.close(); + ConsoleEngine consoleEngine = consoleEngine(); + if (consoleEngine != null) { + consoleEngine.putVariable("exception", exception); + consoleEngine.trace(exception); + } else { + trace(false, exception); + } + } + + @Override + public void trace(boolean stack, Exception 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(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + asb.toAttributedString().println(terminal()); + } else if (stack) { + exception.printStackTrace(); + } else { + String message = exception.getMessage(); + AttributedStringBuilder asb = new AttributedStringBuilder(); + if (message != null) { + String m = exception.getClass().getSimpleName() + ": " + message; + asb.append(m, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + } else { + asb.append("Caught exception: ", AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + asb.append(exception.getClass().getCanonicalName(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED)); + } + 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.args()); + ConsoleEngine consoleEngine = consoleEngine(); + if (!opt.args().isEmpty() && consoleEngine != null) { + try { + Object[] ret = consoleEngine.expandParameters(opt.args().toArray(new String[0])); + consoleEngine.putVariable("_return", ret.length == 1 ? ret[0] : ret); + } 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); + } + } + } + +} diff --git a/console/src/main/java/org/jline/widget/AutopairWidgets.java b/console/src/main/java/org/jline/widget/AutopairWidgets.java new file mode 100644 index 000000000..bcbee4e2c --- /dev/null +++ b/console/src/main/java/org/jline/widget/AutopairWidgets.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.widget; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jline.keymap.KeyMap; +import org.jline.reader.Binding; +import org.jline.reader.Buffer; +import org.jline.reader.LineReader; +import org.jline.reader.Reference; + +/** + * Creates and manages widgets that auto-closes, deletes and skips over matching delimiters intelligently. + * + * @author Matti Rinta-Nikkola + */ +public class AutopairWidgets extends Widgets { + /* + * Inspired by zsh-autopair + * https://github.com/hlissner/zsh-autopair + */ + private static final Map LBOUNDS; + private static final Map RBOUNDS; + private final Map pairs; + private final Map defaultBindings = new HashMap<>(); + private boolean enabled; + + { + pairs = new HashMap<>(); + pairs.put("`", "`"); + pairs.put("'", "'"); + pairs.put("\"", "\""); + pairs.put("[", "]"); + pairs.put("(", ")"); + pairs.put(" ", " "); + } + static { + LBOUNDS = new HashMap<>(); + LBOUNDS.put("all", "[.:/\\!]"); + LBOUNDS.put("quotes", "[\\]})a-zA-Z0-9]"); + LBOUNDS.put("spaces", "[^{(\\[]"); + LBOUNDS.put("braces", ""); + LBOUNDS.put("`", "`"); + LBOUNDS.put("\"", "\""); + LBOUNDS.put("'", "'"); + RBOUNDS = new HashMap<>(); + RBOUNDS.put("all", "[\\[{(<,.:?/%$!a-zA-Z0-9]"); + RBOUNDS.put("quotes", "[a-zA-Z0-9]"); + RBOUNDS.put("spaces", "[^\\]})]"); + RBOUNDS.put("braces", ""); + RBOUNDS.put("`", ""); + RBOUNDS.put("\"", ""); + RBOUNDS.put("'", ""); + } + + public AutopairWidgets(LineReader reader) { + this(reader, false); + } + + public AutopairWidgets(LineReader reader, boolean addCurlyBrackets) { + super(reader); + if (existsWidget(AP_INSERT)) { + throw new IllegalStateException("AutopairWidgets already created!"); + } + if (addCurlyBrackets) { + pairs.put("{", "}"); + } + addWidget(AP_INSERT, this::autopairInsert); + addWidget("_autopair-close", this::autopairClose); + addWidget(AP_BACKWARD_DELETE_CHAR, this::autopairDelete); + addWidget(AUTOPAIR_TOGGLE, this::toggleKeyBindings); + + KeyMap map = getKeyMap(); + for (Map.Entry p: pairs.entrySet()) { + defaultBindings.put(p.getKey(), map.getBound(p.getKey())); + if (!p.getKey().equals(p.getValue())) { + defaultBindings.put(p.getValue(), map.getBound(p.getValue())); + } + } + } + + public void enable() { + if (!enabled) { + toggle(); + } + } + + public void disable() { + if (enabled) { + toggle(); + } + } + + public boolean toggle() { + boolean before = enabled; + toggleKeyBindings(); + return !before; + } + + /* + * Widgets + */ + public boolean autopairInsert() { + if (pairs.containsKey(lastBinding())) { + if (canSkip(lastBinding())) { + callWidget(LineReader.FORWARD_CHAR); + } else if (canPair(lastBinding())) { + callWidget(LineReader.SELF_INSERT); + putString(pairs.get(lastBinding())); + callWidget(LineReader.BACKWARD_CHAR); + } else { + callWidget(LineReader.SELF_INSERT); + } + } else { + callWidget(LineReader.SELF_INSERT); + } + return true; + } + + public boolean autopairClose() { + if (pairs.containsValue(lastBinding()) + && currChar().equals(lastBinding())) { + callWidget(LineReader.FORWARD_CHAR); + } else { + callWidget(LineReader.SELF_INSERT); + } + return true; + } + + public boolean autopairDelete() { + if (pairs.containsKey(prevChar()) && pairs.get(prevChar()).equals(currChar()) + && canDelete(prevChar())) { + callWidget(LineReader.DELETE_CHAR); + } + callWidget(LineReader.BACKWARD_DELETE_CHAR); + return true; + } + + public boolean toggleKeyBindings() { + if (enabled) { + defaultBindings(); + } else { + customBindings(); + } + return enabled; + } + /* + * key bindings... + * + */ + private void customBindings() { + boolean ttActive = tailtipEnabled(); + if (ttActive) { + callWidget(TAILTIP_TOGGLE); + } + KeyMap map = getKeyMap(); + for (Map.Entry p: pairs.entrySet()) { + map.bind(new Reference(AP_INSERT), p.getKey()); + if (!p.getKey().equals(p.getValue())) { + map.bind(new Reference("_autopair-close"), p.getValue()); + } + } + aliasWidget(AP_BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR); + if (ttActive) { + callWidget(TAILTIP_TOGGLE); + } + enabled = true; + } + + private void defaultBindings() { + KeyMap map = getKeyMap(); + for (Map.Entry p: pairs.entrySet()) { + map.bind(defaultBindings.get(p.getKey()), p.getKey()); + if (!p.getKey().equals(p.getValue())) { + map.bind(defaultBindings.get(p.getValue()), p.getValue()); + } + } + aliasWidget("." + LineReader.BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR); + if (tailtipEnabled()) { + callWidget(TAILTIP_TOGGLE); + callWidget(TAILTIP_TOGGLE); + } + enabled = false; + } + /* + * helpers + */ + private boolean tailtipEnabled() { + return getWidget(LineReader.ACCEPT_LINE).equals(TT_ACCEPT_LINE); + } + + private boolean canPair(String d) { + if (balanced(d) && !nexToBoundary(d)) { + return !d.equals(" ") || (!prevChar().equals(" ") && !currChar().equals(" ")); + } + return false; + } + + private boolean canSkip(String d) { + return pairs.get(d).equals(d) && d.charAt(0) != ' ' && currChar().equals(d) + && balanced(d); + } + + private boolean canDelete(String d) { + return balanced(d); + } + + private boolean balanced(String d) { + boolean out = false; + Buffer buf = buffer(); + String lbuf = buf.upToCursor(); + String rbuf = buf.substring(lbuf.length()); + String regx1 = pairs.get(d).equals(d)? d : "\\" + d; + String regx2 = pairs.get(d).equals(d)? pairs.get(d) : "\\" + pairs.get(d); + int llen = lbuf.length() - lbuf.replaceAll(regx1, "").length(); + int rlen = rbuf.length() - rbuf.replaceAll(regx2, "").length(); + if (llen == 0 && rlen == 0) { + out = true; + } else if (d.charAt(0) == ' ') { + out = true; + } else if (pairs.get(d).equals(d)) { + if ( llen == rlen || (llen + rlen) % 2 == 0 ) { + out = true; + } + } else { + int l2len = lbuf.length() - lbuf.replaceAll(regx2, "").length(); + int r2len = rbuf.length() - rbuf.replaceAll(regx1, "").length(); + int ltotal = llen - l2len; + int rtotal = rlen - r2len; + if (ltotal < 0) { + ltotal = 0; + } + if (ltotal >= rtotal) { + out = true; + } + } + return out; + } + + private boolean boundary(String lb, String rb) { + return (lb.length() > 0 && prevChar().matches(lb)) + || + (rb.length() > 0 && currChar().matches(rb)); + } + + private boolean nexToBoundary(String d) { + List bk = new ArrayList<>(); + bk.add("all"); + if (d.matches("['\"`]")) { + bk.add("quotes"); + } else if (d.matches("[{\\[(<]")) { + bk.add("braces"); + } else if (d.charAt(0) == ' ') { + bk.add("spaces"); + } + if (LBOUNDS.containsKey(d) && RBOUNDS.containsKey(d)) { + bk.add(d); + } + for (String k: bk) { + if (boundary(LBOUNDS.get(k), RBOUNDS.get(k))) { + return true; + } + } + return false; + } +} diff --git a/console/src/main/java/org/jline/widget/AutosuggestionWidgets.java b/console/src/main/java/org/jline/widget/AutosuggestionWidgets.java new file mode 100644 index 000000000..1417da2df --- /dev/null +++ b/console/src/main/java/org/jline/widget/AutosuggestionWidgets.java @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.widget; + +import org.jline.reader.Buffer; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.SuggestionType; +import org.jline.reader.impl.BufferImpl; + +/** + * Creates and manages widgets for as you type command line suggestions. + * Suggestions are created using a using command history. + * + * @author Matti Rinta-Nikkola + */ +public class AutosuggestionWidgets extends Widgets { + private boolean enabled = false; + + public AutosuggestionWidgets(LineReader reader) { + super(reader); + if (existsWidget("_autosuggest-forward-char")) { + throw new IllegalStateException("AutosuggestionWidgets already created!"); + } + addWidget("_autosuggest-forward-char", this::autosuggestForwardChar); + addWidget("_autosuggest-end-of-line", this::autosuggestEndOfLine); + addWidget("_autosuggest-forward-word", this::partialAccept); + addWidget(AUTOSUGGEST_TOGGLE, this::toggleKeyBindings); + } + + public void disable() { + defaultBindings(); + } + + public void enable() { + customBindings(); + } + /* + * Widgets + */ + public boolean partialAccept() { + Buffer buffer = buffer(); + if (buffer.cursor() == buffer.length()) { + int curPos = buffer.cursor(); + buffer.write(tailTip()); + buffer.cursor(curPos); + replaceBuffer(buffer); + callWidget(LineReader.FORWARD_WORD); + Buffer newBuf = new BufferImpl(); + newBuf.write(buffer().substring(0, buffer().cursor())); + replaceBuffer(newBuf); + } else { + callWidget(LineReader.FORWARD_WORD); + } + return true; + } + + public boolean autosuggestForwardChar() { + return accept(LineReader.FORWARD_CHAR); + } + + public boolean autosuggestEndOfLine() { + return accept(LineReader.END_OF_LINE); + } + + public boolean toggleKeyBindings() { + if (enabled) { + defaultBindings(); + } else { + customBindings(); + } + return enabled; + } + + + private boolean accept(String widget) { + Buffer buffer = buffer(); + if (buffer.cursor() == buffer.length()) { + putString(tailTip()); + } else { + callWidget(widget); + } + return true; + } + /* + * key bindings... + * + */ + private void customBindings() { + if (enabled) { + return; + } + aliasWidget("_autosuggest-forward-char", LineReader.FORWARD_CHAR); + aliasWidget("_autosuggest-end-of-line", LineReader.END_OF_LINE); + aliasWidget("_autosuggest-forward-word", LineReader.FORWARD_WORD); + enabled = true; + setSuggestionType(SuggestionType.HISTORY); + } + + private void defaultBindings() { + if (!enabled) { + return; + } + aliasWidget("." + LineReader.FORWARD_CHAR, LineReader.FORWARD_CHAR); + aliasWidget("." + LineReader.END_OF_LINE, LineReader.END_OF_LINE); + aliasWidget("." + LineReader.FORWARD_WORD, LineReader.FORWARD_WORD); + enabled = false; + setSuggestionType(SuggestionType.NONE); + } +} diff --git a/console/src/main/java/org/jline/widget/TailTipWidgets.java b/console/src/main/java/org/jline/widget/TailTipWidgets.java new file mode 100644 index 000000000..25d523e22 --- /dev/null +++ b/console/src/main/java/org/jline/widget/TailTipWidgets.java @@ -0,0 +1,774 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.widget; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.jline.builtins.Options.HelpException; +import org.jline.console.ArgDesc; +import org.jline.console.CmdDesc; +import org.jline.console.CmdLine; +import org.jline.keymap.KeyMap; +import org.jline.reader.Binding; +import org.jline.reader.Buffer; +import org.jline.reader.LineReader; +import org.jline.reader.Reference; +import org.jline.reader.LineReader.SuggestionType; +import org.jline.utils.*; + +import static org.jline.keymap.KeyMap.key; + +/** + * Creates and manages widgets for as you type command line suggestions. + * Suggestions are created using a command completer data and/or positional argument descriptions. + * + * @author Matti Rinta-Nikkola + */ +public class TailTipWidgets extends Widgets { + public enum TipType { + /** + * Prepare command line suggestions using a command positional argument descriptions. + */ + TAIL_TIP, + /** + * Prepare command line suggestions using a command completer data. + */ + COMPLETER, + /** + * Prepare command line suggestions using either a command positional argument descriptions if exists + * or command completers data. + */ + COMBINED + } + private boolean enabled = false; + private final CommandDescriptions cmdDescs; + private TipType tipType; + private int descriptionSize; + private boolean descriptionEnabled = true; + private boolean descriptionCache = false; + private Object readerErrors; + + /** + * Creates tailtip widgets used in command line suggestions. Suggestions are created using a command + * positional argument names. If argument descriptions do not exists command completer data will be used. + * Status bar for argument descriptions will not be created. + * + * @param reader LineReader. + * @param tailTips Commands options and positional argument descriptions. + * @throws IllegalStateException If widgets are already created. + */ + public TailTipWidgets(LineReader reader, Map tailTips) { + this(reader, tailTips, 0, TipType.COMBINED); + } + + /** + * Creates tailtip widgets used in command line suggestions. + * Status bar for argument descriptions will not be created. + * + * @param reader LineReader. + * @param tailTips Commands options and positional argument descriptions. + * @param tipType Defines which data will be used for suggestions. + * @throws IllegalStateException If widgets are already created. + */ + public TailTipWidgets(LineReader reader, Map tailTips, TipType tipType) { + this(reader, tailTips, 0, tipType); + } + + /** + * Creates tailtip widgets used in command line suggestions. Suggestions are created using a command + * positional argument names. If argument descriptions do not exists command completer data will be used. + * + * @param reader LineReader. + * @param tailTips Commands options and positional argument descriptions. + * @param descriptionSize Size of the status bar. + * @throws IllegalStateException If widgets are already created. + */ + public TailTipWidgets(LineReader reader, Map tailTips, int descriptionSize) { + this(reader, tailTips, descriptionSize, TipType.COMBINED); + } + + /** + * Creates tailtip widgets used in command line suggestions. + * + * @param reader LineReader. + * @param tailTips Commands options and positional argument descriptions. + * @param descriptionSize Size of the status bar. + * @param tipType Defines which data will be used for suggestions. + * @throws IllegalStateException If widgets are already created. + */ + public TailTipWidgets(LineReader reader, Map tailTips, int descriptionSize, TipType tipType) { + this(reader, tailTips, descriptionSize, tipType, null); + } + + /** + * Creates tailtip widgets used in command line suggestions. + * + * @param reader LineReader. + * @param descFun Function that returns command description. + * @param descriptionSize Size of the status bar. + * @param tipType Defines which data will be used for suggestions. + * @throws IllegalStateException If widgets are already created. + */ + public TailTipWidgets(LineReader reader, Function descFun, int descriptionSize, TipType tipType) { + this(reader, null, descriptionSize, tipType, descFun); + } + + private TailTipWidgets(LineReader reader + , Map tailTips + , int descriptionSize, TipType tipType, Function descFun) { + super(reader); + if (existsWidget(TT_ACCEPT_LINE)) { + throw new IllegalStateException("TailTipWidgets already created!"); + } + this.cmdDescs = tailTips != null ? new CommandDescriptions(tailTips) : new CommandDescriptions(descFun); + this.descriptionSize = descriptionSize; + this.tipType = tipType; + addWidget(TT_ACCEPT_LINE, this::tailtipAcceptLine); + addWidget("_tailtip-self-insert", this::tailtipInsert); + addWidget("_tailtip-backward-delete-char", this::tailtipBackwardDelete); + addWidget("_tailtip-delete-char", this::tailtipDelete); + addWidget("_tailtip-expand-or-complete", this::tailtipComplete); + addWidget("_tailtip-redisplay", this::tailtipUpdateStatus); + addWidget("_tailtip-kill-line", this::tailtipKillLine); + addWidget("_tailtip-kill-whole-line", this::tailtipKillWholeLine); + addWidget("tailtip-window", this::toggleWindow); + addWidget(TAILTIP_TOGGLE, this::toggleKeyBindings); + } + + public void setTailTips(Map tailTips) { + cmdDescs.setDescriptions(tailTips); + } + + public void setDescriptionSize(int descriptionSize) { + this.descriptionSize = descriptionSize; + initDescription(descriptionSize); + } + + public int getDescriptionSize() { + return descriptionSize; + } + + public void setTipType(TipType type) { + this.tipType = type; + if (tipType == TipType.TAIL_TIP) { + setSuggestionType(SuggestionType.TAIL_TIP); + } else { + setSuggestionType(SuggestionType.COMPLETER); + } + } + + public TipType getTipType() { + return tipType; + } + + public boolean isEnabled() { + return enabled; + } + + public void disable() { + if (enabled) { + toggleKeyBindings(); + } + } + + public void enable() { + if (!enabled) { + toggleKeyBindings(); + } + } + + public void setDescriptionCache(boolean cache) { + this.descriptionCache = cache; + } + + /* + * widgets + */ + public boolean tailtipComplete() { + if (doTailTip(LineReader.EXPAND_OR_COMPLETE)) { + if (lastBinding().equals("\t")) { + callWidget(LineReader.BACKWARD_CHAR); + reader.runMacro(key(reader.getTerminal(), InfoCmp.Capability.key_right)); + } + return true; + } + return false; + } + + public boolean tailtipAcceptLine() { + if (tipType != TipType.TAIL_TIP) { + setSuggestionType(SuggestionType.COMPLETER); + } + clearDescription(); + setErrorPattern(null); + setErrorIndex(-1); + cmdDescs.clearTemporaryDescs(); + return clearTailTip(LineReader.ACCEPT_LINE); + } + + public boolean tailtipBackwardDelete() { + return doTailTip(autopairEnabled() ? AP_BACKWARD_DELETE_CHAR : LineReader.BACKWARD_DELETE_CHAR); + } + + private boolean clearTailTip(String widget) { + clearTailTip(); + callWidget(widget); + return true; + } + + public boolean tailtipDelete() { + clearTailTip(); + return doTailTip(LineReader.DELETE_CHAR); + } + + public boolean tailtipKillLine() { + clearTailTip(); + return doTailTip(LineReader.KILL_LINE); + } + + public boolean tailtipKillWholeLine() { + callWidget(LineReader.KILL_WHOLE_LINE); + return doTailTip(LineReader.REDISPLAY); + } + + public boolean tailtipInsert() { + return doTailTip(autopairEnabled() ? AP_INSERT : LineReader.SELF_INSERT); + } + + public boolean tailtipUpdateStatus() { + return doTailTip(LineReader.REDISPLAY); + } + + private boolean doTailTip(String widget) { + Buffer buffer = buffer(); + callWidget(widget); + Pair cmdkey; + List args = args(); + if (buffer.length() == buffer.cursor()) { + cmdkey = cmdDescs.evaluateCommandLine(buffer.toString(), args); + } else { + cmdkey = cmdDescs.evaluateCommandLine(buffer.toString(), buffer.cursor()); + } + CmdDesc cmdDesc = cmdDescs.getDescription(cmdkey.getU()); + if (cmdDesc == null) { + setErrorPattern(null); + setErrorIndex(-1); + clearDescription(); + resetTailTip(); + } else if (cmdDesc.isValid()) { + if (cmdkey.getV()) { + if (cmdDesc.isCommand() && buffer.length() == buffer.cursor()) { + doCommandTailTip(widget, cmdDesc, args); + } + } else { + doDescription(compileMainDescription(cmdDesc, descriptionSize)); + setErrorPattern(cmdDesc.getErrorPattern()); + setErrorIndex(cmdDesc.getErrorIndex()); + } + } + return true; + } + + private void doCommandTailTip(String widget, CmdDesc cmdDesc, List args) { + int argnum = 0; + String prevArg = ""; + for (String a : args) { + if (!a.startsWith("-")) { + if (!prevArg.matches("-[a-zA-Z]") || !cmdDesc.optionWithValue(prevArg)) { + argnum++; + } + } + prevArg = a; + } + String lastArg = ""; + prevArg = args.get(args.size() - 1); + if (!prevChar().equals(" ") && args.size() > 1) { + lastArg = args.get(args.size() - 1); + prevArg = args.get(args.size() - 2); + } + int bpsize = argnum; + boolean doTailTip = true; + boolean noCompleters = false; + if (widget.endsWith(LineReader.BACKWARD_DELETE_CHAR)) { + setSuggestionType(SuggestionType.TAIL_TIP); + noCompleters = true; + if (!lastArg.startsWith("-")) { + if (!prevArg.matches("-[a-zA-Z]") || !cmdDesc.optionWithValue(prevArg)) { + bpsize--; + } + } + if (prevChar().equals(" ")) { + bpsize++; + } + } else if (!prevChar().equals(" ")) { + doTailTip = false; + doDescription(compileMainDescription(cmdDesc, descriptionSize, cmdDesc.isSubcommand() ? lastArg : null)); + } else if (cmdDesc != null) { + doDescription(compileMainDescription(cmdDesc, descriptionSize)); + } + if (cmdDesc != null) { + if (lastArg.startsWith("-")) { + if (lastArg.matches("-[a-zA-Z][a-zA-Z0-9]+")) { + if (cmdDesc.optionWithValue(lastArg.substring(0,2))) { + doDescription(compileOptionDescription(cmdDesc, lastArg.substring(0,2), descriptionSize)); + setTipType(tipType); + } else { + doDescription(compileOptionDescription(cmdDesc, "-" + lastArg.substring(lastArg.length() - 1), descriptionSize)); + setSuggestionType(SuggestionType.TAIL_TIP); + noCompleters = true; + } + } else { + doDescription(compileOptionDescription(cmdDesc, lastArg, descriptionSize)); + if (!lastArg.contains("=")) { + setSuggestionType(SuggestionType.TAIL_TIP); + noCompleters = true; + } else { + setTipType(tipType); + } + } + } else if (!widget.endsWith(LineReader.BACKWARD_DELETE_CHAR)){ + setTipType(tipType); + } + if (bpsize > 0 && doTailTip) { + List params = cmdDesc.getArgsDesc(); + if (!noCompleters) { + setSuggestionType(tipType == TipType.COMPLETER ? SuggestionType.COMPLETER : SuggestionType.TAIL_TIP); + } + if (bpsize - 1 < params.size()) { + if (!lastArg.startsWith("-")) { + List d; + if (!prevArg.matches("-[a-zA-Z]") || !cmdDesc.optionWithValue(prevArg)) { + d = params.get(bpsize - 1).getDescription(); + } else { + d = compileOptionDescription(cmdDesc, prevArg, descriptionSize); + } + if (d == null || d.isEmpty()) { + d = compileMainDescription(cmdDesc, descriptionSize, cmdDesc.isSubcommand() ? lastArg : null); + } + doDescription(d); + } + StringBuilder tip = new StringBuilder(); + for (int i = bpsize - 1; i < params.size(); i++) { + tip.append(params.get(i).getName()); + tip.append(" "); + } + setTailTip(tip.toString()); + } else if (!params.isEmpty() && params.get(params.size() - 1).getName().startsWith("[")) { + setTailTip(params.get(params.size() - 1).getName()); + doDescription(params.get(params.size() - 1).getDescription()); + } + } else if (doTailTip) { + resetTailTip(); + } + } else { + clearDescription(); + resetTailTip(); + } + } + + private void resetTailTip() { + setTailTip(""); + if (tipType != TipType.TAIL_TIP) { + setSuggestionType(SuggestionType.COMPLETER); + } + } + + private void doDescription(List desc) { + if (descriptionSize == 0 || !descriptionEnabled) { + return; + } + if (desc.isEmpty()) { + clearDescription(); + } else if (desc.size() == descriptionSize) { + addDescription(desc); + } else if (desc.size() > descriptionSize) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(desc.get(descriptionSize - 1)).append("...", new AttributedStyle(AttributedStyle.INVERSE)); + List mod = new ArrayList<>(desc.subList(0, descriptionSize-1)); + mod.add(asb.toAttributedString()); + addDescription(mod); + } else { + while (desc.size() != descriptionSize) { + desc.add(new AttributedString("")); + } + addDescription(desc); + } + } + + private boolean autopairEnabled() { + Binding binding = getKeyMap().getBound("("); + return binding instanceof Reference && ((Reference) binding).name().equals(AP_INSERT); + } + + public boolean toggleWindow() { + descriptionEnabled = !descriptionEnabled; + if (descriptionEnabled) { + initDescription(descriptionSize); + } else { + destroyDescription(); + } + callWidget(LineReader.REDRAW_LINE); + return true; + } + + public boolean toggleKeyBindings() { + if (enabled) { + defaultBindings(); + destroyDescription(); + reader.setVariable(LineReader.ERRORS, readerErrors); + } else { + customBindings(); + if (descriptionEnabled) { + initDescription(descriptionSize); + } + readerErrors = reader.getVariable(LineReader.ERRORS); + reader.setVariable(LineReader.ERRORS, 0); + } + try { + callWidget(LineReader.REDRAW_LINE); + } catch (Exception e) { + // ignore + } + return enabled; + } + + /* + * key bindings... + * + */ + private boolean defaultBindings() { + if (!enabled) { + return false; + } + aliasWidget("." + LineReader.ACCEPT_LINE, LineReader.ACCEPT_LINE); + aliasWidget("." + LineReader.BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR); + aliasWidget("." + LineReader.DELETE_CHAR, LineReader.DELETE_CHAR); + aliasWidget("." + LineReader.EXPAND_OR_COMPLETE, LineReader.EXPAND_OR_COMPLETE); + aliasWidget("." + LineReader.SELF_INSERT, LineReader.SELF_INSERT); + aliasWidget("." + LineReader.REDISPLAY, LineReader.REDISPLAY); + aliasWidget("." + LineReader.KILL_LINE, LineReader.KILL_LINE); + aliasWidget("." + LineReader.KILL_WHOLE_LINE, LineReader.KILL_WHOLE_LINE); + KeyMap map = getKeyMap(); + map.bind(new Reference(LineReader.INSERT_CLOSE_PAREN), ")"); + + setSuggestionType(SuggestionType.NONE); + if (autopairEnabled()) { + callWidget(AUTOPAIR_TOGGLE); + callWidget(AUTOPAIR_TOGGLE); + } + enabled = false; + return true; + } + + private void customBindings() { + if (enabled) { + return; + } + aliasWidget(TT_ACCEPT_LINE, LineReader.ACCEPT_LINE); + aliasWidget("_tailtip-backward-delete-char", LineReader.BACKWARD_DELETE_CHAR); + aliasWidget("_tailtip-delete-char", LineReader.DELETE_CHAR); + aliasWidget("_tailtip-expand-or-complete", LineReader.EXPAND_OR_COMPLETE); + aliasWidget("_tailtip-self-insert", LineReader.SELF_INSERT); + aliasWidget("_tailtip-redisplay", LineReader.REDISPLAY); + aliasWidget("_tailtip-kill-line", LineReader.KILL_LINE); + aliasWidget("_tailtip-kill-whole-line", LineReader.KILL_WHOLE_LINE); + KeyMap map = getKeyMap(); + map.bind(new Reference("_tailtip-self-insert"), ")"); + + if (tipType != TipType.TAIL_TIP) { + setSuggestionType(SuggestionType.COMPLETER); + } else { + setSuggestionType(SuggestionType.TAIL_TIP); + } + enabled = true; + } + + private List compileMainDescription(CmdDesc cmdDesc, int descriptionSize) { + return compileMainDescription(cmdDesc, descriptionSize, null); + } + + private List compileMainDescription(CmdDesc cmdDesc, int descriptionSize, String lastArg) { + List out = new ArrayList<>(); + List mainDesc = cmdDesc.getMainDesc(); + if (mainDesc == null) { + return out; + } + if (cmdDesc.isCommand() && cmdDesc.isValid() && !cmdDesc.isHighlighted()) { + mainDesc = new ArrayList<>(); + StyleResolver resolver = HelpException.defaultStyle(); + for (AttributedString as : cmdDesc.getMainDesc()) { + mainDesc.add(HelpException.highlightSyntax(as.toString(), resolver)); + } + } + if (mainDesc.size() <= descriptionSize && lastArg == null) { + out.addAll(mainDesc); + } else { + int tabs = 0; + for (AttributedString as: mainDesc) { + if (as.columnLength() >= tabs) { + tabs = as.columnLength() + 2; + } + } + int row = 0; + int col = 0; + List descList = new ArrayList<>(); + for (int i = 0; i < descriptionSize; i++) { + descList.add(new AttributedString("")); + } + for (AttributedString as: mainDesc) { + if (lastArg != null && !as.toString().startsWith(lastArg)) { + continue; + } + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); + if (col > 0) { + asb.append(descList.get(row)); + asb.append("\t"); + } + asb.append(as); + descList.remove(row); + descList.add(row, asb.toAttributedString()); + row++; + if (row >= descriptionSize) { + row = 0; + col++; + } + } + out = new ArrayList<>(descList); + } + return out; + } + + private List compileOptionDescription(CmdDesc cmdDesc, String opt, int descriptionSize) { + List out = new ArrayList<>(); + Map> optsDesc = cmdDesc.getOptsDesc(); + StyleResolver resolver = HelpException.defaultStyle(); + + if (!opt.startsWith("-")) { + return out; + } else { + int ind = opt.indexOf("="); + if (ind > 0) { + opt = opt.substring(0, ind); + } + } + List matched = new ArrayList<>(); + int tabs = 0; + for (String key: optsDesc.keySet()) { + for (String k: key.split("\\s+")) { + if (k.trim().startsWith(opt)) { + matched.add(key); + if (key.length() >= tabs) { + tabs = key.length() + 2; + } + break; + } + } + } + if (matched.size() == 1) { + out.add(HelpException.highlightSyntax(matched.get(0), resolver)); + for (AttributedString as: optsDesc.get(matched.get(0))) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(8); + asb.append("\t"); + asb.append(as); + out.add(asb.toAttributedString()); + } + } else if (matched.size() <= descriptionSize) { + for (String key: matched) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); + asb.append(HelpException.highlightSyntax(key, resolver)); + asb.append("\t"); + asb.append(cmdDesc.optionDescription(key)); + out.add(asb.toAttributedString()); + } + } else if (matched.size() <= 2*descriptionSize) { + List keyList = new ArrayList<>(); + int row = 0; + int columnWidth = 2*tabs; + while (columnWidth < 50) { + columnWidth += tabs; + } + for (String key: matched) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); + if (row < descriptionSize) { + asb.append(HelpException.highlightSyntax(key, resolver)); + asb.append("\t"); + asb.append(cmdDesc.optionDescription(key)); + if (asb.columnLength() > columnWidth - 2) { + AttributedString trunc = asb.columnSubSequence(0, columnWidth - 5); + asb = new AttributedStringBuilder().tabs(tabs); + asb.append(trunc); + asb.append("...", new AttributedStyle(AttributedStyle.INVERSE)); + asb.append(" "); + } else { + for (int i = asb.columnLength(); i < columnWidth; i++) { + asb.append(" "); + } + } + keyList.add(asb.toAttributedString().columnSubSequence(0, columnWidth)); + } else { + asb.append(keyList.get(row - descriptionSize)); + asb.append(HelpException.highlightSyntax(key, resolver)); + asb.append("\t"); + asb.append(cmdDesc.optionDescription(key)); + keyList.remove(row - descriptionSize); + keyList.add(row - descriptionSize, asb.toAttributedString()); + + } + row++; + } + out = new ArrayList<>(keyList); + } else { + List keyList = new ArrayList<>(); + for (int i = 0; i < descriptionSize; i++) { + keyList.add(new AttributedString("")); + } + int row = 0; + for (String key: matched) { + AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs); + asb.append(keyList.get(row)); + asb.append(HelpException.highlightSyntax(key, resolver)); + asb.append("\t"); + keyList.remove(row); + keyList.add(row, asb.toAttributedString()); + row++; + if (row >= descriptionSize) { + row = 0; + } + } + out = new ArrayList<>(keyList); + } + return out; + } + + private class CommandDescriptions { + Map descriptions = new HashMap<>(); + Map temporaryDescs = new HashMap<>(); + Map volatileDescs = new HashMap<>(); + Function descFun; + + public CommandDescriptions(Map descriptions) { + this.descriptions = new HashMap<>(descriptions); + } + + public CommandDescriptions(Function descFun) { + this.descFun = descFun; + } + + public void setDescriptions(Map descriptions) { + this.descriptions = new HashMap<>(descriptions); + } + + public Pair evaluateCommandLine(String line, int curPos) { + return evaluateCommandLine(line, args(), curPos); + } + + public Pair evaluateCommandLine(String line, List args) { + return evaluateCommandLine(line, args, line.length()); + } + + private Pair evaluateCommandLine(String line, List args, int curPos) { + String cmd = null; + CmdLine.DescriptionType descType = CmdLine.DescriptionType.METHOD; + String head = line.substring(0, curPos); + String tail = line.substring(curPos); + if (prevChar().equals(")")) { + descType = CmdLine.DescriptionType.SYNTAX; + cmd = head; + } else { + if (line.length() == curPos) { + cmd = args != null && (args.size() > 1 || (args.size() == 1 + && line.endsWith(" "))) ? parser().getCommand(args.get(0)) : null; + descType = CmdLine.DescriptionType.COMMAND; + } + int brackets = 0; + for (int i = head.length() - 1; i >= 0; i--) { + if (head.charAt(i) == ')') { + brackets++; + } else if (head.charAt(i) == '(') { + brackets--; + } + if (brackets < 0) { + descType = CmdLine.DescriptionType.METHOD; + head = head.substring(0, i); + cmd = head; + break; + } + } + if (descType == CmdLine.DescriptionType.METHOD) { + brackets = 0; + for (int i = 0; i < tail.length(); i++) { + if (tail.charAt(i) == ')') { + brackets++; + } else if (tail.charAt(i) == '(') { + brackets--; + } + if (brackets > 0) { + tail = tail.substring(i + 1); + break; + } + } + } + } + if (cmd != null && descFun != null + && !descriptions.containsKey(cmd) && !temporaryDescs.containsKey(cmd)) { + CmdDesc c = descFun.apply(new CmdLine(line, head, tail, args, descType)); + if (descType == CmdLine.DescriptionType.COMMAND) { + if (!descriptionCache) { + volatileDescs.put(cmd, c); + } else if (c != null) { + descriptions.put(cmd, c); + } else { + temporaryDescs.put(cmd, null); + } + } else { + temporaryDescs.put(cmd, c); + } + } + return new Pair<>(cmd, descType == CmdLine.DescriptionType.COMMAND); + } + + public CmdDesc getDescription(String command) { + CmdDesc out = null; + if (descriptions.containsKey(command)) { + out = descriptions.get(command); + } else if (temporaryDescs.containsKey(command)) { + out = temporaryDescs.get(command); + } else if (volatileDescs.containsKey(command)) { + out = volatileDescs.get(command); + volatileDescs.remove(command); + } + return out; + } + + public void clearTemporaryDescs() { + temporaryDescs.clear(); + } + + } + + static class Pair { + final U u; final V v; + public Pair(U u, V v) { + this.u = u; + this.v = v; + } + public U getU() { + return u; + } + public V getV() { + return v; + } + } + +} + diff --git a/console/src/main/java/org/jline/widget/Widgets.java b/console/src/main/java/org/jline/widget/Widgets.java new file mode 100644 index 000000000..b13bc7660 --- /dev/null +++ b/console/src/main/java/org/jline/widget/Widgets.java @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.widget; + +import static org.jline.keymap.KeyMap.alt; +import static org.jline.keymap.KeyMap.ctrl; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import org.jline.keymap.KeyMap; +import org.jline.reader.Binding; +import org.jline.reader.Buffer; +import org.jline.reader.LineReader; +import org.jline.reader.Parser; +import org.jline.reader.Reference; +import org.jline.reader.Widget; +import org.jline.reader.LineReader.SuggestionType; +import org.jline.reader.Parser.ParseContext; +import org.jline.utils.AttributedString; +import org.jline.utils.Status; + +/** + * Create custom widgets by extending Widgets class + * + * @author Matti Rinta-Nikkola + */ +public abstract class Widgets { + public static final String TAILTIP_TOGGLE = "tailtip-toggle"; + public static final String TAILTIP_PANE = "tailtip-window"; + public static final String AUTOPAIR_TOGGLE = "autopair-toggle"; + public static final String AUTOSUGGEST_TOGGLE = "autosuggest-toggle"; + protected static final String AP_INSERT = "_autopair-insert"; + protected static final String AP_BACKWARD_DELETE_CHAR = "_autopair-backward-delete-char"; + protected static final String TT_ACCEPT_LINE = "_tailtip-accept-line"; + + protected final LineReader reader; + + public Widgets(LineReader reader) { + this.reader = reader; + } + + /** + * Add widget to the LineReader + * @param name the name of widget + * @param widget widget + */ + public void addWidget(String name, Widget widget) { + reader.getWidgets().put(name, namedWidget(name, widget)); + } + + private Widget namedWidget(final String name, final Widget widget) { + return new Widget() { + @Override + public String toString() { + return name; + } + @Override + public boolean apply() { + return widget.apply(); + } + }; + } + + /** + * Call widget. System widget will be call if the name does not start with '_' or ends with '-toggle' + * i.e. '.' will be added at the beginning of the name. + * @param name widget name + */ + public void callWidget(String name) { + if (!name.startsWith("_") && !name.endsWith("-toggle")) { + name = "." + name; + } + reader.callWidget(name); + } + + /** + * Bind widget to ctrl-alt-x and execute it + * @param name widget name + */ + public void executeWidget(String name) { + Binding ref = getKeyMap().getBoundKeys().get(alt(ctrl('X'))); + getKeyMap().bind(new Reference(name), alt(ctrl('X'))); + reader.runMacro(alt(ctrl('X'))); + if (ref != null) { + getKeyMap().bind(ref, alt(ctrl('X'))); + } + } + + /** + * Create alias to widget + * @param orig widget original name + * @param alias alias name + */ + public void aliasWidget(String orig, String alias) { + reader.getWidgets().put(alias, widget(orig)); + } + + /** + * + * @param name widget name or alias + * @return widget name + */ + public String getWidget(String name) { + return widget(name).toString(); + } + + /** + * + * @param name widget name or alias + * @return true if widget exists + */ + public boolean existsWidget(String name) { + try { + widget(name); + return true; + } catch(Exception e) { + // ignore + } + return false; + } + + private Widget widget(String name) { + Widget out; + if (name.startsWith(".")) { + out = reader.getBuiltinWidgets().get(name.substring(1)); + } else { + out = reader.getWidgets().get(name); + } + if (out == null) { + throw new InvalidParameterException("widget: no such widget " + name); + } + return out; + } + + /** + * + * @return The LineRearer Parser + */ + public Parser parser() { + return reader.getParser(); + } + + /** + * + * @return The LineReader Main KeyMap + */ + public KeyMap getKeyMap() { + return reader.getKeyMaps().get(LineReader.MAIN); + } + + /** + * + * @return The LineReader Buffer + */ + public Buffer buffer() { + return reader.getBuffer(); + } + + /** + * + * @param buffer buffer that will be copied to the LineReader Buffer + */ + public void replaceBuffer(Buffer buffer) { + reader.getBuffer().copyFrom(buffer); + } + + /** + * + * @return command line arguments + */ + public List args() { + return reader.getParser().parse(buffer().toString(), 0, ParseContext.COMPLETE).words(); + } + + /** + * + * @return Buffer's previous character + */ + public String prevChar() { + return String.valueOf((char)reader.getBuffer().prevChar()); + } + + /** + * + * @return Buffer's current character + */ + public String currChar() { + return String.valueOf((char)reader.getBuffer().currChar()); + } + + /** + * + * @return LineReader's last binding + */ + public String lastBinding() { + return reader.getLastBinding(); + } + + /** + * + * @param string string to be written into LineReader Buffer + */ + public void putString(String string) { + reader.getBuffer().write(string); + } + + /** + * + * @return Command line tail tip. + */ + public String tailTip() { + return reader.getTailTip(); + } + + /** + * + * @param tailTip tail tip to be added to the command line + */ + public void setTailTip(String tailTip) { + reader.setTailTip(tailTip); + } + + /** + * + * @param errorPattern error pattern to be set LineReader Highlighter + */ + public void setErrorPattern(Pattern errorPattern) { + reader.getHighlighter().setErrorPattern(errorPattern); + } + + /** + * + * @param errorIndex error index to be set LineReader Highlighter + */ + public void setErrorIndex(int errorIndex) { + reader.getHighlighter().setErrorIndex(errorIndex); + } + + /** + * Clears command line tail tip + */ + public void clearTailTip() { + reader.setTailTip(""); + } + + /** + * + * @param type type to be set to the LineReader autosuggestion + */ + public void setSuggestionType(SuggestionType type) { + reader.setAutosuggestion(type); + } + + /** + * + * @param desc Text to be displayed on terminal status bar + */ + public void addDescription(List desc) { + Status.getStatus(reader.getTerminal()).update(desc); + } + + /** + * Clears terminal status bar + */ + public void clearDescription() { + initDescription(0); + } + + /** + * Initialize terminal status bar + * @param size Terminal status bar size in rows + */ + public void initDescription(int size) { + Status status = Status.getStatus(reader.getTerminal(), false); + if (size > 0) { + if (status == null) { + status = Status.getStatus(reader.getTerminal()); + } + status.setBorder(true); + List as = new ArrayList<>(); + for (int i = 0; i < size; i++) { + as.add(new AttributedString("")); + } + addDescription(as); + } else if (status != null) { + if (size < 0) { + status.update(null); + } else { + status.clear(); + } + } + } + + /** + * Remove terminal status bar + */ + public void destroyDescription() { + initDescription(-1); + } +} diff --git a/console/src/test/java/org/jline/example/Console.java b/console/src/test/java/org/jline/example/Console.java new file mode 100644 index 000000000..671aa8d12 --- /dev/null +++ b/console/src/test/java/org/jline/example/Console.java @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.example; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +import org.jline.console.ArgDesc; +import org.jline.console.CmdDesc; +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.console.Printer; +import org.jline.widget.AutopairWidgets; +import org.jline.widget.AutosuggestionWidgets; +import org.jline.widget.TailTipWidgets; +import org.jline.widget.TailTipWidgets.TipType; +import org.jline.console.impl.Builtins; +import org.jline.console.impl.DefaultPrinter; +import org.jline.console.impl.SystemRegistryImpl; +import org.jline.keymap.KeyMap; +import org.jline.reader.*; +import org.jline.reader.LineReader.Option; +import org.jline.reader.LineReader.SuggestionType; +import org.jline.reader.impl.DefaultParser; +import org.jline.reader.impl.LineReaderImpl; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.reader.impl.completer.SystemCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.utils.AttributedString; +import org.jline.utils.InfoCmp.Capability; + +public class Console +{ + private static Map compileTailTips() { + Map tailTips = new HashMap<>(); + Map> optDesc = new HashMap<>(); + optDesc.put("--optionA", Arrays.asList(new AttributedString("optionA description..."))); + optDesc.put("--noitpoB", Arrays.asList(new AttributedString("noitpoB description..."))); + optDesc.put("--optionC", Arrays.asList(new AttributedString("optionC description...") + , new AttributedString("line2"))); + Map> widgetOpts = new HashMap<>(); + List mainDesc = Arrays.asList(new AttributedString("widget -N new-widget [function-name]") + , new AttributedString("widget -D widget ...") + , new AttributedString("widget -A old-widget new-widget") + , new AttributedString("widget -U string ...") + , new AttributedString("widget -l [options]") + ); + widgetOpts.put("-N", Arrays.asList(new AttributedString("Create new widget"))); + widgetOpts.put("-D", Arrays.asList(new AttributedString("Delete widgets"))); + widgetOpts.put("-A", Arrays.asList(new AttributedString("Create alias to widget"))); + widgetOpts.put("-U", Arrays.asList(new AttributedString("Push characters to the stack"))); + widgetOpts.put("-l", Arrays.asList(new AttributedString("List user-defined widgets"))); + + tailTips.put("widget", new CmdDesc(mainDesc, ArgDesc.doArgNames(Arrays.asList("[pN...]")), widgetOpts)); + tailTips.put("foo12", new CmdDesc(ArgDesc.doArgNames(Arrays.asList("param1", "param2", "[paramN...]")))); + tailTips.put("foo11", new CmdDesc(Arrays.asList( + new ArgDesc("param1",Arrays.asList(new AttributedString("Param1 description...") + , new AttributedString("line 2: This is a very long line that does exceed the terminal width." + +" The line will be truncated automatically (by Status class) before printing out.") + , new AttributedString("line 3") + , new AttributedString("line 4") + , new AttributedString("line 5") + , new AttributedString("line 6") + )) + , new ArgDesc("param2",Arrays.asList(new AttributedString("Param2 description...") + , new AttributedString("line 2") + )) + , new ArgDesc("param3", new ArrayList<>()) + ), optDesc)); + return tailTips; + } + + + private static class ExampleCommands implements CommandRegistry { + private LineReader reader; + private AutosuggestionWidgets autosuggestionWidgets; + private TailTipWidgets tailtipWidgets; + private AutopairWidgets autopairWidgets; + private final Map commandExecute = new HashMap<>(); + private final Map> commandInfo = new HashMap<>(); + private Map aliasCommand = new HashMap<>(); + private Exception exception; + private Printer printer; + + public ExampleCommands(Printer printer) { + this.printer = printer; + commandExecute.put("testprint", new CommandMethods(this::testprint, this::defaultCompleter)); + commandExecute.put("testkey", new CommandMethods(this::testkey, this::defaultCompleter)); + commandExecute.put("clear", new CommandMethods(this::clear, this::defaultCompleter)); + commandExecute.put("autopair", new CommandMethods(this::autopair, this::defaultCompleter)); + commandExecute.put("autosuggestion", new CommandMethods(this::autosuggestion, this::autosuggestionCompleter)); + + commandInfo.put("testkey", Arrays.asList("display key events")); + commandInfo.put("clear", Arrays.asList("clear screen")); + commandInfo.put("autopair", Arrays.asList("toggle brackets/quotes autopair key bindings")); + commandInfo.put("autosuggestion", Arrays.asList("set autosuggestion modality: history, completer, tailtip or none")); + } + + public void setLineReader(LineReader reader) { + this.reader = reader; + } + + public void setAutosuggestionWidgets(AutosuggestionWidgets autosuggestionWidgets) { + this.autosuggestionWidgets = autosuggestionWidgets; + } + + public void setTailTipWidgets(TailTipWidgets tailtipWidgets) { + this.tailtipWidgets = tailtipWidgets; + } + + public void setAutopairWidgets(AutopairWidgets autopairWidgets) { + this.autopairWidgets = autopairWidgets; + } + + private Terminal terminal() { + return reader.getTerminal(); + } + + public Set commandNames() { + return commandExecute.keySet(); + } + + public Map commandAliases() { + return aliasCommand; + } + + public List commandInfo(String command) { + return commandInfo.get(command(command)); + } + + public boolean hasCommand(String command) { + if (commandExecute.containsKey(command) || aliasCommand.containsKey(command)) { + return true; + } + return false; + } + + private String command(String name) { + if (commandExecute.containsKey(name)) { + return name; + } else if (aliasCommand.containsKey(name)) { + return aliasCommand.get(name); + } + return null; + } + + public SystemCompleter compileCompleters() { + SystemCompleter out = new SystemCompleter(); + for (String c : commandExecute.keySet()) { + out.add(c, commandExecute.get(c).compileCompleter().apply(c)); + } + out.addAliases(aliasCommand); + return out; + } + + public Object invoke(CommandSession session, String command, Object... args) throws Exception { + exception = null; + Object out = commandExecute.get(command(command)).execute().apply(new CommandInput(command, args, session)); + if (exception != null) { + throw exception; + } + return out; + } + + public CmdDesc commandDescription(List args) { + // TODO + return new CmdDesc(false); + } + + private Map fillMap(String name, Integer age, String country, String town) { + Map out = new HashMap<>(); + Map address = new HashMap<>(); + address.put("country", country); + address.put("town", town); + out.put("name", name); + out.put("age", age); + out.put("address", address); + return out; + } + + private void testprint(CommandInput input) { + List> data = new ArrayList<>(); + data.add(fillMap("heikki", 10, "finland", "helsinki")); + data.add(fillMap("pietro", 11, "italy", "milano")); + data.add(fillMap("john", 12, "england", "london")); + printer.println(data); + Map options = new HashMap<>(); + options.put(Printer.STRUCT_ON_TABLE, true); + options.put(Printer.VALUE_STYLE, "classpath:/org/jline/example/gron.nanorc"); + printer.println(options,data); + options.clear(); + options.put(Printer.COLUMNS, Arrays.asList("name", "age", "address.country", "address.town")); + options.put(Printer.SHORT_NAMES, true); + options.put(Printer.VALUE_STYLE, "classpath:/org/jline/example/gron.nanorc"); + printer.println(options,data); + } + + private void testkey(CommandInput input) { + try { + terminal().writer().write("Input the key event(Enter to complete): "); + terminal().writer().flush(); + StringBuilder sb = new StringBuilder(); + while (true) { + int c = ((LineReaderImpl) reader).readCharacter(); + if (c == 10 || c == 13) break; + sb.append(new String(Character.toChars(c))); + } + terminal().writer().println(KeyMap.display(sb.toString())); + terminal().writer().flush(); + } catch (Exception e) { + exception = e; + } + } + + private void clear(CommandInput input) { + try { + terminal().puts(Capability.clear_screen); + terminal().flush(); + } catch (Exception e) { + exception = e; + } + } + + private void autopair(CommandInput input) { + try { + if (tailtipWidgets.isEnabled()) { + tailtipWidgets.disable(); + } + terminal().writer().print("Autopair widgets are "); + if (autopairWidgets.toggle()) { + terminal().writer().println("enabled."); + } else { + terminal().writer().println("disabled."); + } + } catch (Exception e) { + exception = e; + } + } + + private void autosuggestion(CommandInput input) { + String[] argv = input.args(); + try { + if (argv.length > 0) { + String type = argv[0].toLowerCase(); + if (type.startsWith("his")) { + tailtipWidgets.disable(); + autosuggestionWidgets.enable(); + } else if (type.startsWith("tai")) { + autosuggestionWidgets.disable(); + autopairWidgets.disable(); + if (argv.length > 1) { + String mode = argv[1].toLowerCase(); + if (mode.startsWith("tai")) { + tailtipWidgets.setTipType(TipType.TAIL_TIP); + } else if (mode.startsWith("comp")) { + tailtipWidgets.setTipType(TipType.COMPLETER); + } else if (mode.startsWith("comb")) { + tailtipWidgets.setTipType(TipType.COMBINED); + } + } + tailtipWidgets.enable(); + } else if (type.startsWith("com")) { + autosuggestionWidgets.disable(); + tailtipWidgets.disable(); + reader.setAutosuggestion(SuggestionType.COMPLETER); + } else if (type.startsWith("non")) { + autosuggestionWidgets.disable(); + tailtipWidgets.disable(); + reader.setAutosuggestion(SuggestionType.NONE); + } else { + terminal().writer().println("Usage: autosuggestion history|completer|tailtip|none"); + } + } else { + if (tailtipWidgets.isEnabled()) { + terminal().writer().println("Autosuggestion: tailtip/" + tailtipWidgets.getTipType()); + } else { + terminal().writer().println("Autosuggestion: " + reader.getAutosuggestion()); + } + } + } catch (Exception e) { + exception = e; + } + } + + private List defaultCompleter(String command) { + return Arrays.asList(NullCompleter.INSTANCE); + } + + private List autosuggestionCompleter(String command) { + List out = new ArrayList<>(); + out.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new StringsCompleter("history", "completer", "none") + , NullCompleter.INSTANCE)); + out.add(new ArgumentCompleter(NullCompleter.INSTANCE + , new StringsCompleter("tailtip") + , new StringsCompleter("tailtip", "completer", "combined") + , NullCompleter.INSTANCE)); + return out; + } + } + + private static Path workDir() { + return Paths.get(System.getProperty("user.dir")); + } + + public static void main(String[] args) throws IOException { + try { + String prompt = "prompt> "; + String rightPrompt = null; + + boolean argument = true; + Completer completer = new ArgumentCompleter(new Completer() { + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + candidates.add(new Candidate("foo11", "foo11", null, "complete cmdDesc", null, null, true)); + candidates.add(new Candidate("foo12", "foo12", null, "cmdDesc -names only", null, null, true)); + candidates.add(new Candidate("foo13", "foo13", null, "-", null, null, true)); + candidates.add( + new Candidate("widget", "widget", null, "cmdDesc with short options", null, null, true)); + } + }, new StringsCompleter("foo21", "foo22", "foo23"), new Completer() { + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + candidates.add(new Candidate("", "", null, "frequency in MHz", null, null, false)); + } + }); + + Terminal terminal = TerminalBuilder.builder().build(); + Parser parser = new DefaultParser(); + // + // Command registeries + // + Builtins builtins = new Builtins(Console::workDir, null, null); + builtins.rename(Builtins.Command.TTOP, "top"); + builtins.alias("zle", "widget"); + builtins.alias("bindkey", "keymap"); + DefaultPrinter printer = new DefaultPrinter(null); + ExampleCommands exampleCommands = new ExampleCommands(printer); + SystemRegistryImpl masterRegistry = new SystemRegistryImpl(parser, terminal, Console::workDir, null); + masterRegistry.setCommandRegistries(exampleCommands, builtins); + masterRegistry.addCompleter(completer); + // + // Terminal & LineReader + // + System.out.println(terminal.getName() + ": " + terminal.getType()); + LineReader reader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(masterRegistry.completer()) + .parser(parser) + .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M%P > ") + .variable(LineReader.INDENTATION, 2) + .option(Option.INSERT_BRACKET, true) + .option(Option.EMPTY_WORD_OPTIONS, false) + .build(); + // + // widgets + // + AutopairWidgets autopairWidgets = new AutopairWidgets(reader); + AutosuggestionWidgets autosuggestionWidgets = new AutosuggestionWidgets(reader); + TailTipWidgets tailtipWidgets = null; + if (argument) { + tailtipWidgets = new TailTipWidgets(reader, compileTailTips(), 5, TipType.COMPLETER); + } else { + tailtipWidgets = new TailTipWidgets(reader, masterRegistry::commandDescription, 5, TipType.COMPLETER); + } + // + // complete command registeries + // + builtins.setLineReader(reader); + exampleCommands.setLineReader(reader); + exampleCommands.setAutosuggestionWidgets(autosuggestionWidgets); + exampleCommands.setTailTipWidgets(tailtipWidgets); + exampleCommands.setAutopairWidgets(autopairWidgets); + // + // REPL-loop + // + while (true) { + try { + masterRegistry.cleanUp(); + String line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null); + masterRegistry.execute(line); + } + catch (UserInterruptException e) { + // Ignore + } + catch (EndOfFileException e) { + break; + } + catch (Exception e) { + masterRegistry.trace(true, e); + } + } + masterRegistry.close(); + } + catch (Throwable t) { + t.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/console/src/test/resources/org/jline/example/gron.nanorc b/console/src/test/resources/org/jline/example/gron.nanorc new file mode 100644 index 000000000..c9b8110ef --- /dev/null +++ b/console/src/test/resources/org/jline/example/gron.nanorc @@ -0,0 +1,12 @@ +syntax "GRON" "\.gron$" +header "^\[$" + +color brightblue "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>" +color yellow ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*" +color cyan "\" +color brightcyan "\<(true|false)\>" +color brightyellow "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:" +color white "(:|\[|,|\])" +color magenta "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]" +color ,green "[[:space:]]+$" +color ,red " + +| + +" \ No newline at end of file diff --git a/demo/jline-repl.bat b/demo/jline-repl.bat index ef52de050..c68fdbd03 100644 --- a/demo/jline-repl.bat +++ b/demo/jline-repl.bat @@ -24,6 +24,7 @@ pushd %TARGETDIR%\lib for %%G in (jline-*.jar) do call:APPEND_TO_CLASSPATH %%G rem Groovy for %%G in (groovy-*.jar) do call:APPEND_TO_CLASSPATH %%G +for %%G in (ivy-*.jar) do call:APPEND_TO_CLASSPATH %%G set "opts=%JLINE_OPTS%" set "logconf=%DIRNAME%etc\logging.properties" diff --git a/demo/jline-repl.sh b/demo/jline-repl.sh index e17a044c8..28570bbfc 100755 --- a/demo/jline-repl.sh +++ b/demo/jline-repl.sh @@ -28,6 +28,7 @@ cp=${TARGETDIR}/classes # JLINE cp=${cp}$(find ${TARGETDIR}/lib -name "jline-*.jar" -exec printf :{} ';') cp=${cp}$(find ${TARGETDIR}/lib -name "groovy-*.jar" -exec printf :{} ';') +cp=${cp}$(find ${TARGETDIR}/lib -name "ivy-*.jar" -exec printf :{} ';') opts="${JLINE_OPTS}" logconf="${DIRNAME}/etc/logging.properties" diff --git a/demo/pom.xml b/demo/pom.xml index b9f3110f1..898329901 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -16,18 +16,49 @@ org.jline jline-parent - 3.14.1-SNAPSHOT + 3.18.1-SNAPSHOT jline-demo JLine Demo + + org.jline.demo + + org.jline - jline + jline-terminal-jansi + + + org.jline + jline-terminal-jna + + + org.jline + jline-reader + + + org.jline + jline-builtins + + + org.jline + jline-console + + + org.jline + jline-remote-ssh + + + org.jline + jline-remote-telnet + + + org.jline + jline-style - org.jline jline-groovy @@ -41,6 +72,12 @@ org.apache.felix org.apache.felix.gogo.jline + + + org.jline + jline + + @@ -83,6 +120,16 @@ groovy-json + + org.codehaus.groovy + groovy-console + + + + org.apache.ivy + ivy + + org.slf4j slf4j-api @@ -165,6 +212,7 @@ src/main/scripts init.jline + data.json @@ -172,6 +220,15 @@ + + maven-javadoc-plugin + + + javadoc + none + + + diff --git a/demo/src/main/java/org/jline/demo/Repl.java b/demo/src/main/java/org/jline/demo/Repl.java index a9e474489..8d8927366 100644 --- a/demo/src/main/java/org/jline/demo/Repl.java +++ b/demo/src/main/java/org/jline/demo/Repl.java @@ -8,10 +8,7 @@ */ package org.jline.demo; -import java.io.BufferedReader; -import java.io.File; -import java.io.InputStream; -import java.io.InputStreamReader; +import java.io.*; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -22,31 +19,18 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; -import java.util.stream.Collectors; -import org.jline.builtins.Builtins; -import org.jline.builtins.CommandRegistry; -import org.jline.builtins.Completers; -import org.jline.builtins.ConsoleEngine; -import org.jline.builtins.ConsoleEngineImpl; -import org.jline.builtins.Options; -import org.jline.builtins.Completers.OptDesc; +import org.jline.builtins.*; +import org.jline.builtins.Nano.SyntaxHighlighter; import org.jline.builtins.Completers.OptionCompleter; -import org.jline.builtins.Completers.SystemCompleter; -import org.jline.builtins.Options.HelpException; -import org.jline.builtins.SystemRegistry; -import org.jline.builtins.SystemRegistryImpl; -import org.jline.builtins.Widgets.TailTipWidgets; -import org.jline.builtins.Widgets.TailTipWidgets.TipType; +import org.jline.console.impl.*; +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.CommandRegistry; +import org.jline.console.ConsoleEngine; +import org.jline.console.Printer; import org.jline.keymap.KeyMap; -import org.jline.reader.Binding; -import org.jline.reader.Completer; -import org.jline.reader.ConfigurationPath; -import org.jline.reader.EndOfFileException; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; -import org.jline.reader.Reference; -import org.jline.reader.UserInterruptException; +import org.jline.reader.*; import org.jline.reader.LineReader.Option; import org.jline.reader.impl.DefaultParser; import org.jline.reader.impl.LineReaderImpl; @@ -54,12 +38,18 @@ import org.jline.reader.impl.completer.ArgumentCompleter; import org.jline.reader.impl.completer.NullCompleter; import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.script.GroovyCommand; import org.jline.script.GroovyEngine; +import org.jline.terminal.Size; import org.jline.terminal.Terminal; import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.Terminal.Signal; import org.jline.utils.InfoCmp; import org.jline.utils.InfoCmp.Capability; import org.jline.utils.OSUtils; +import org.jline.widget.TailTipWidgets; +import org.jline.widget.TailTipWidgets.TipType; +import org.jline.widget.Widgets; /** * Demo how to create REPL app with JLine. @@ -68,19 +58,20 @@ */ public class Repl { - private static class MyCommands implements CommandRegistry { + protected static class MyCommands extends JlineCommandRegistry implements CommandRegistry { private LineReader reader; - private final Map commandExecute = new HashMap<>(); - private Map aliasCommand = new HashMap<>(); - private Exception exception; - private Supplier workDir; + private final Supplier workDir; public MyCommands(Supplier workDir) { + super(); this.workDir = workDir; - commandExecute.put("tput", new Builtins.CommandMethods(this::tput, this::tputCompleter)); - commandExecute.put("testkey", new Builtins.CommandMethods(this::testkey, this::defaultCompleter)); - commandExecute.put("clear", new Builtins.CommandMethods(this::clear, this::defaultCompleter)); - commandExecute.put("!", new Builtins.CommandMethods(this::shell, this::defaultCompleter)); + Map commandExecute = new HashMap<>(); + commandExecute.put("tput", new CommandMethods(this::tput, this::tputCompleter)); + commandExecute.put("testkey", new CommandMethods(this::testkey, this::defaultCompleter)); + commandExecute.put("clear", new CommandMethods(this::clear, this::defaultCompleter)); + commandExecute.put("!", new CommandMethods(this::shell, this::defaultCompleter)); + commandExecute.put("objarg", new CommandMethods(this::objarg, this::defaultCompleter)); + registerCommands(commandExecute); } public void setLineReader(LineReader reader) { @@ -91,62 +82,33 @@ private Terminal terminal() { return reader.getTerminal(); } - public Set commandNames() { - return commandExecute.keySet(); - } - - public Map commandAliases() { - return aliasCommand; - } - - public boolean hasCommand(String command) { - if (commandExecute.containsKey(command) || aliasCommand.containsKey(command)) { - return true; - } - return false; - } - - private String command(String name) { - if (commandExecute.containsKey(name)) { - return name; - } else if (aliasCommand.containsKey(name)) { - return aliasCommand.get(name); - } - return null; - } - - public Completers.SystemCompleter compileCompleters() { - SystemCompleter out = new SystemCompleter(); - for (String c : commandExecute.keySet()) { - out.add(c, commandExecute.get(c).compileCompleter().apply(c)); + private Object objarg(CommandInput input) { + final String[] usage = { + "objarg - manage correctly object parameters", + " parse input.xargs, return opt.argObjects[0]", + "Usage: objarg [OBJECT]", + " -? --help Displays command help" + }; + Object out = null; + try { + Options opt = parseOptions(usage, input.xargs()); + List xargs = opt.argObjects(); + out = xargs.size() > 0 ? xargs.get(0) : null; + } catch (Exception e) { + saveException(e); } - out.addAliases(aliasCommand); return out; } - public Object execute(CommandRegistry.CommandSession session, String command, String[] args) throws Exception { - exception = null; - commandExecute.get(command(command)).execute().accept(new Builtins.CommandInput(args, session)); - if (exception != null) { - throw exception; - } - return null; - } - - private void tput(Builtins.CommandInput input) { + private void tput(CommandInput input) { final String[] usage = { "tput - put terminal capability", "Usage: tput [CAPABILITY]", " -? --help Displays command help" }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return; - } - - List argv = opt.args(); try { + Options opt = parseOptions(usage, input.args()); + List argv = opt.args(); if (argv.size() == 1) { Capability vcap = Capability.byName(argv.get(0)); if (vcap != null) { @@ -158,22 +120,18 @@ private void tput(Builtins.CommandInput input) { terminal().writer().println("Usage: tput [CAPABILITY]"); } } catch (Exception e) { - exception = e; + saveException(e); } } - private void testkey(Builtins.CommandInput input) { + private void testkey(CommandInput input) { final String[] usage = { "testkey - display the key events", "Usage: testkey", " -? --help Displays command help" }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return; - } try { + parseOptions(usage, input.args()); terminal().writer().write("Input the key event(Enter to complete): "); terminal().writer().flush(); StringBuilder sb = new StringBuilder(); @@ -185,26 +143,22 @@ private void testkey(Builtins.CommandInput input) { terminal().writer().println(KeyMap.display(sb.toString())); terminal().writer().flush(); } catch (Exception e) { - exception = e; + saveException(e); } } - private void clear(Builtins.CommandInput input) { + private void clear(CommandInput input) { final String[] usage = { "clear - clear terminal", "Usage: clear", " -? --help Displays command help" }; - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help")) { - exception = new HelpException(opt.usage()); - return; - } try { + parseOptions(usage, input.args()); terminal().puts(Capability.clear_screen); terminal().flush(); } catch (Exception e) { - exception = e; + saveException(e); } } @@ -218,7 +172,7 @@ private void executeCmnd(List args) throws Exception { _args.add("sh"); _args.add("-c"); } - _args.add(args.stream().collect(Collectors.joining(" "))); + _args.add(String.join(" ", args)); builder.command(_args); builder.directory(workDir.get().toFile()); Process process = builder.start(); @@ -230,51 +184,28 @@ private void executeCmnd(List args) throws Exception { } } - private void shell(Builtins.CommandInput input) { + private void shell(CommandInput input) { final String[] usage = { "! - execute shell command" , "Usage: !" , " -? --help Displays command help" }; - try { - Options opt = Options.compile(usage).parse(input.args()); - if (opt.isSet("help") && opt.args().isEmpty()) { - exception = new HelpException(opt.usage()); - return; - } - } catch (Exception e) { - // ignore - } - List argv = new ArrayList<>(); - argv.addAll(Arrays.asList(input.args())); - if (!argv.isEmpty()) { + if (input.args().length == 1 && (input.args()[0].equals("-?") || input.args()[0].equals("--help"))) { try { - executeCmnd(argv); + parseOptions(usage, input.args()); } catch (Exception e) { - exception = e; + saveException(e); + } + } else { + List argv = new ArrayList<>(Arrays.asList(input.args())); + if (!argv.isEmpty()) { + try { + executeCmnd(argv); + } catch (Exception e) { + saveException(e); + } } } } - private List commandOptions(String command) { - try { - execute(new CommandRegistry.CommandSession(), command, new String[] {"--help"}); - } catch (HelpException e) { - return Builtins.compileCommandOptions(e.getMessage()); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - private List defaultCompleter(String command) { - List completers = new ArrayList<>(); - completers.add(new ArgumentCompleter(NullCompleter.INSTANCE - , new OptionCompleter(NullCompleter.INSTANCE - , this::commandOptions - , 1) - )); - return completers; - } - private Set capabilities() { return InfoCmp.getCapabilitiesByName().keySet(); } @@ -292,8 +223,8 @@ private List tputCompleter(String command) { } private static class StreamGobbler implements Runnable { - private InputStream inputStream; - private Consumer consumer; + private final InputStream inputStream; + private final Consumer consumer; public StreamGobbler(InputStream inputStream, Consumer consumer) { this.inputStream = inputStream; @@ -322,26 +253,51 @@ public static void main(String[] args) { parser.setEscapeChars(null); parser.setRegexCommand("[:]{0,1}[a-zA-Z!]{1,}\\S*"); // change default regex to support shell commands Terminal terminal = TerminalBuilder.builder().build(); + if (terminal.getWidth() == 0 || terminal.getHeight() == 0) { + terminal.setSize(new Size(120, 40)); // hard coded terminal size when redirecting + } + Thread executeThread = Thread.currentThread(); + terminal.handle(Signal.INT, signal -> executeThread.interrupt()); // - // ScriptEngine and command registeries + // Create jnanorc config file for demo // File file = new File(Repl.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); String root = file.getCanonicalPath().replace("classes", "").replaceAll("\\\\", "/"); // forward slashes works better also in windows! + File jnanorcFile = Paths.get(root, "jnanorc").toFile(); + if (!jnanorcFile.exists()) { + FileWriter fw = new FileWriter(jnanorcFile); + fw.write("include " + root + "nanorc/*.nanorc\n"); + fw.close(); + } + // + // ScriptEngine and command registries + // GroovyEngine scriptEngine = new GroovyEngine(); scriptEngine.put("ROOT", root); ConfigurationPath configPath = new ConfigurationPath(Paths.get(root), Paths.get(root)); - ConsoleEngine consoleEngine = new ConsoleEngineImpl(scriptEngine, Repl::workDir, configPath); - Builtins builtins = new Builtins(Repl::workDir, configPath, (String fun)-> {return new ConsoleEngine.WidgetCreator(consoleEngine, fun);}); + Printer printer = new DefaultPrinter(scriptEngine, configPath); + ConsoleEngineImpl consoleEngine = new ConsoleEngineImpl(scriptEngine + , printer + , Repl::workDir, configPath); + Builtins builtins = new Builtins(Repl::workDir, configPath, (String fun)-> new ConsoleEngine.WidgetCreator(consoleEngine, fun)); MyCommands myCommands = new MyCommands(Repl::workDir); - SystemRegistry systemRegistry = new SystemRegistryImpl(parser, terminal, configPath); + SystemRegistryImpl systemRegistry = new SystemRegistryImpl(parser, terminal, Repl::workDir, configPath); + systemRegistry.register("groovy", new GroovyCommand(scriptEngine, printer)); systemRegistry.setCommandRegistries(consoleEngine, builtins, myCommands); + systemRegistry.addCompleter(scriptEngine.getScriptCompleter()); + systemRegistry.setScriptDescription(scriptEngine::scriptDescription); // // LineReader // + Path jnanorc = configPath.getConfig("jnanorc"); + SyntaxHighlighter commandHighlighter = SyntaxHighlighter.build(jnanorc,"COMMAND"); + SyntaxHighlighter argsHighlighter = SyntaxHighlighter.build(jnanorc,"ARGS"); + SyntaxHighlighter groovyHighlighter = SyntaxHighlighter.build(jnanorc,"Groovy"); LineReader reader = LineReaderBuilder.builder() .terminal(terminal) .completer(systemRegistry.completer()) .parser(parser) + .highlighter(new SystemHighlighter(commandHighlighter, argsHighlighter, groovyHighlighter)) .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M%P > ") .variable(LineReader.INDENTATION, 2) .variable(LineReader.LIST_MAX, 100) @@ -355,7 +311,7 @@ public static void main(String[] args) { reader.setVariable(LineReader.BLINK_MATCHING_PAREN, 0); // if enabled cursor remains in begin parenthesis (gitbash) } // - // complete command registeries + // complete command registries // consoleEngine.setLineReader(reader); builtins.setLineReader(reader); @@ -363,15 +319,14 @@ public static void main(String[] args) { // // widgets and console initialization // - TailTipWidgets ttw = new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TipType.COMPLETER); - ttw.setDescriptionCache(false); + new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TipType.COMPLETER); KeyMap keyMap = reader.getKeyMaps().get("main"); - keyMap.bind(new Reference("tailtip-toggle"), KeyMap.alt("s")); + keyMap.bind(new Reference(Widgets.TAILTIP_TOGGLE), KeyMap.alt("s")); systemRegistry.initialize(Paths.get(root, "init.jline").toFile()); // // REPL-loop // - consoleEngine.println(terminal.getName()+": "+terminal.getType()); + System.out.println(terminal.getName() + ": " + terminal.getType()); while (true) { try { systemRegistry.cleanUp(); // delete temporary variables and reset output streams @@ -384,12 +339,33 @@ public static void main(String[] args) { // Ignore } catch (EndOfFileException e) { + String pl = e.getPartialLine(); + if (pl != null) { // execute last line from redirected file (required for Windows) + try { + consoleEngine.println(systemRegistry.execute(pl)); + } catch (Exception e2) { + systemRegistry.trace(e2); + } + } break; } catch (Exception e) { systemRegistry.trace(e); // print exception and save it to console variable } } + systemRegistry.close(); // persist pipeline completer names etc + + Set threadSet = Thread.getAllStackTraces().keySet(); + boolean groovyRunning=false; // check Groovy GUI apps + for (Thread t : threadSet) { + if (t.getName().startsWith("AWT-Shut")) { + groovyRunning = true; + break; + } + } + if (groovyRunning) { + consoleEngine.println("Please, close Groovy Consoles/Object Browsers!"); + } } catch (Throwable t) { t.printStackTrace(); diff --git a/demo/src/main/scripts/args.nanorc b/demo/src/main/scripts/args.nanorc new file mode 100644 index 000000000..e94e6c5b5 --- /dev/null +++ b/demo/src/main/scripts/args.nanorc @@ -0,0 +1,11 @@ +syntax "ARGS" + +color brightblue "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>" +color yellow ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*" +color green "\<(console|grab|inspect)\>" +color cyan "\" +color brightcyan "\<(true|false)\>" +color brightyellow "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:" +color white "(:|\[|,|\])" +color magenta "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]" +color ,red " + +| + +" \ No newline at end of file diff --git a/demo/src/main/scripts/command.nanorc b/demo/src/main/scripts/command.nanorc new file mode 100644 index 000000000..2f48d612b --- /dev/null +++ b/demo/src/main/scripts/command.nanorc @@ -0,0 +1,5 @@ +syntax "COMMAND" + +color green "[a-zA-Z]+[a-zA-Z0-9]*" +color yellow ".*=" +color white "(\"|'|\.|=|:|\[|,|\])" diff --git a/demo/src/main/scripts/completerTest.jline b/demo/src/main/scripts/completerTest.jline index bb8c1ba09..90a236315 100644 --- a/demo/src/main/scripts/completerTest.jline +++ b/demo/src/main/scripts/completerTest.jline @@ -1,7 +1,7 @@ import org.jline.reader.impl.completer.* import org.jline.reader.* import org.jline.builtins.Completers.OptionCompleter -import org.jline.builtins.SystemRegistry +import org.jline.console.SystemRegistry def complete(commandLine) { candidates = [] diff --git a/demo/src/main/scripts/data.json b/demo/src/main/scripts/data.json new file mode 100644 index 000000000..6b8ae485a --- /dev/null +++ b/demo/src/main/scripts/data.json @@ -0,0 +1 @@ +{"responseHeader":{"status":0,"QTime":1,"params":{"q":"a:jline","core":"gav","indent":"off","fl":"id,g,a,v,p,ec,timestamp,tags","start":"","sort":"score desc,timestamp desc,g asc,a asc,v desc","rows":"30","wt":"json","version":"2.2"}},"response":{"numFound":139,"start":0,"docs":[{"id":"org.jline:jline:3.13.3","g":"org.jline","a":"jline","v":"3.13.3","p":"jar","timestamp":1578390706000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.13.2","g":"org.jline","a":"jline","v":"3.13.2","p":"jar","timestamp":1574846327000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.13.1","g":"org.jline","a":"jline","v":"3.13.1","p":"jar","timestamp":1571995758000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.13.0","g":"org.jline","a":"jline","v":"3.13.0","p":"jar","timestamp":1571243077000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.12.1","g":"org.jline","a":"jline","v":"3.12.1","p":"jar","timestamp":1562570172000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.12.0","g":"org.jline","a":"jline","v":"3.12.0","p":"jar","timestamp":1562252029000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.11.0","g":"org.jline","a":"jline","v":"3.11.0","p":"jar","timestamp":1554406548000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.10.0","g":"org.jline","a":"jline","v":"3.10.0","p":"jar","timestamp":1549961984000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.9.0","g":"org.jline","a":"jline","v":"3.9.0","p":"jar","timestamp":1531854452000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.8.2","g":"org.jline","a":"jline","v":"3.8.2","p":"jar","timestamp":1531853039000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.8.1","g":"org.jline","a":"jline","v":"3.8.1","p":"jar","timestamp":1531770227000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.8.0","g":"org.jline","a":"jline","v":"3.8.0","p":"jar","timestamp":1529487955000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.7.1","g":"org.jline","a":"jline","v":"3.7.1","p":"jar","timestamp":1527581950000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.7.0","g":"org.jline","a":"jline","v":"3.7.0","p":"jar","timestamp":1523864387000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"jline:jline:2.14.6","g":"jline","a":"jline","v":"2.14.6","p":"jar","timestamp":1522055920000,"ec":["-javadoc.jar","-sources.jar",".jar","-tests.jar",".pom"],"tags":["https","open","projects","maven","sonatype","repositories","source","helps"]},{"id":"org.jline:jline:3.6.2","g":"org.jline","a":"jline","v":"3.6.2","p":"jar","timestamp":1521117841000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.6.1","g":"org.jline","a":"jline","v":"3.6.1","p":"jar","timestamp":1518685024000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.6.0","g":"org.jline","a":"jline","v":"3.6.0","p":"jar","timestamp":1517574347000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.5.2","g":"org.jline","a":"jline","v":"3.5.2","p":"jar","timestamp":1513681163000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.scala-lang:jline:2.10.7","g":"org.scala-lang","a":"jline","v":"2.10.7","p":"jar","timestamp":1509739624000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["readline","better","like"]},{"id":"org.jline:jline:3.5.1","g":"org.jline","a":"jline","v":"3.5.1","p":"jar","timestamp":1506080801000,"ec":["-sources.jar","-javadoc.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.5.0","g":"org.jline","a":"jline","v":"3.5.0","p":"jar","timestamp":1505236425000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.4.0","g":"org.jline","a":"jline","v":"3.4.0","p":"jar","timestamp":1501763894000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"jline:jline:2.14.5","g":"jline","a":"jline","v":"2.14.5","p":"jar","timestamp":1498507667000,"ec":["-sources.jar","-javadoc.jar","-tests.jar",".jar",".pom"],"tags":["https","open","projects","maven","sonatype","repositories","source","helps"]},{"id":"org.jline:jline:3.3.1","g":"org.jline","a":"jline","v":"3.3.1","p":"jar","timestamp":1496731372000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"jline:jline:2.14.4","g":"jline","a":"jline","v":"2.14.4","p":"jar","timestamp":1496731057000,"ec":["-sources.jar","-javadoc.jar","-tests.jar",".jar",".pom"],"tags":["https","open","projects","maven","sonatype","repositories","source","helps"]},{"id":"org.jline:jline:3.3.0","g":"org.jline","a":"jline","v":"3.3.0","p":"jar","timestamp":1494632386000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.2.0","g":"org.jline","a":"jline","v":"3.2.0","p":"jar","timestamp":1489399766000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["jline"]},{"id":"org.jline:jline:3.1.3","g":"org.jline","a":"jline","v":"3.1.3","p":"jar","timestamp":1485945557000,"ec":["-javadoc.jar","-sources.jar",".jar",".pom"],"tags":["https","open","projects","maven","sonatype","repositories","source","helps"]},{"id":"jline:jline:2.14.3","g":"jline","a":"jline","v":"2.14.3","p":"jar","timestamp":1483696017000,"ec":["-sources.jar","-javadoc.jar","-tests.jar",".jar",".pom"],"tags":["https","open","projects","maven","sonatype","repositories","source","helps"]}]}} diff --git a/demo/src/main/scripts/gron.nanorc b/demo/src/main/scripts/gron.nanorc new file mode 100644 index 000000000..c9b8110ef --- /dev/null +++ b/demo/src/main/scripts/gron.nanorc @@ -0,0 +1,12 @@ +syntax "GRON" "\.gron$" +header "^\[$" + +color brightblue "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>" +color yellow ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*" +color cyan "\" +color brightcyan "\<(true|false)\>" +color brightyellow "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:" +color white "(:|\[|,|\])" +color magenta "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]" +color ,green "[[:space:]]+$" +color ,red " + +| + +" \ No newline at end of file diff --git a/demo/src/main/scripts/groovy.nanorc b/demo/src/main/scripts/groovy.nanorc new file mode 100644 index 000000000..175942388 --- /dev/null +++ b/demo/src/main/scripts/groovy.nanorc @@ -0,0 +1,17 @@ +## Here is an example for Groovy. +## +syntax "Groovy" "\.groovy$" +color green "\<(boolean|byte|char|double|float|int|long|new|short|this|transient|void|def|it)\>" +color red "\<(break|case|catch|continue|default|do|else|finally|for|if|return|switch|throw|try|while)\>" +color green,,faint "(([a-z]{2,}[.]{1}){2,10}([a-z]{2,}){0,1})" +color green,,faint "\<(print|println|sleep)\>" +color green "\<[A-Z]{0,2}([A-Z]{1}[a-z]+){1,}\>" +color cyan "\<(abstract|class|extends|final|implements|import|instanceof|interface|native|package|private|protected|public|static|strictfp|super|synchronized|throws|volatile)\>" +color red ""[^"]*"" +color yellow "\<(true|false|null)\>" +color yellow "\<[A-Z]+([_]{1}[A-Z]+){0,}\>" +icolor yellow "\b(([1-9][0-9]+)|0+)\.[0-9]+\b" "\b[1-9][0-9]*\b" "\b0[0-7]*\b" "\b0x[1-9a-f][0-9a-f]*\b" +color blue "//.*" +color blue start="/\*" end="\*/" +color brightblue start="/\*\*" end="\*/" +color brightwhite,yellow "(FIXME|TODO|XXX)" diff --git a/demo/src/main/scripts/hello.groovy b/demo/src/main/scripts/hello.groovy index 705398166..789aed527 100644 --- a/demo/src/main/scripts/hello.groovy +++ b/demo/src/main/scripts/hello.groovy @@ -1,5 +1,5 @@ // END_HELP -import org.jline.builtins.SystemRegistry +import org.jline.console.SystemRegistry import org.jline.builtins.Options; class HelloWorld { diff --git a/demo/src/main/scripts/hello2.jline b/demo/src/main/scripts/hello2.jline index 8393e72e1..4f96fa373 100644 --- a/demo/src/main/scripts/hello2.jline +++ b/demo/src/main/scripts/hello2.jline @@ -8,11 +8,16 @@ array=[0,1] array.each { println it - def resp - resp=:show - :prnt $resp + :prnt $ROOT }.identity{} +_reader = org.jline.console.SystemRegistry.get().consoleEngine().reader +prnt $_reader +prnt --toString $_reader + +params = $@ +prnt -s JSON $params + println('hello ' + ${1:-world} + '!') exit 'ok' \ No newline at end of file diff --git a/demo/src/main/scripts/init.jline b/demo/src/main/scripts/init.jline index 973b284ba..34866ca27 100644 --- a/demo/src/main/scripts/init.jline +++ b/demo/src/main/scripts/init.jline @@ -1,59 +1,184 @@ -# -# All imports added here are imported also to your groovy shell -# -import java.nio.file.*; - -PATH = [ROOT + 'scripts'] - -# -# create jnanorc configuration file -# -_jnanorc = Paths.get(ROOT, 'jnanorc').toFile() -_jnanorc << 'include ' + ROOT + 'nanorc/*.nanorc\n' > null -# -# console options -# -CONSOLE_OPTIONS = [:] -CONSOLE_OPTIONS.trace = 1 -CONSOLE_OPTIONS.splitOutput = true -# -# custom Groovy pipes -# -pipe --delete * -pipe |. '.collect{' '}' -pipe |: '.collectEntries{' '}' -pipe |:: '.collectMany{' '}' -pipe |? '.findAll{' '}' -pipe |?1 '.find{' '}' -pipe |! '.each{' '}' -pipe |# '.take(' ')' -pipe |& '.' ' ' -pipe grep '.collect{it.toString()}.findAll{it=~/' '/}' -alias null '|& identity{}' -alias xargs '|; %{0} %{1} %{2} %{3} %{4} %{5} %{6} %{7} %{8} %{9}' -alias to '|; %{0} =' -# -# create test-widget and bind it to ctrl-alt-x -# It will read widget name from buffer and execute it -# -def _testWidget() { - def name = _buffer().toString().split('\\s+')[0] - _widget "$name" -} -widget -N test-widget _testWidget -keymap '^[^x' test-widget -# -# create _tailtip-toggle widget that changes also maximum candidates to display -# -def _tailTipToggle() { - def al = _reader.getWidgets().get('accept-line').toString() - def enabled = al == '_tailtip-accept-line' ? true : false - if (enabled) { - _reader.setVariable('LIST_MAX',100) - } else { - _reader.setVariable('LIST_MAX',50) - } - _widget 'tailtip-toggle' -} -widget -N _tailtip-toggle _tailTipToggle -keymap '^[s' _tailtip-toggle +# +# All imports added here are imported also to your groovy shell +# +import java.nio.file.* +import java.util.regex.* +import org.jline.utils.* + +PATH = [ROOT + 'scripts'] +# +# console options +# +CONSOLE_OPTIONS = [:] +CONSOLE_OPTIONS.docs = [:] +CONSOLE_OPTIONS['docs'].put('jline','https://github.com/jline/jline3/wiki') +CONSOLE_OPTIONS['docs'].put('groovy','http://groovy-lang.org/documentation.html') +CONSOLE_OPTIONS['docs'].put('.*groovy/.*','http://docs.groovy-lang.org/latest/html/gapi/') +CONSOLE_OPTIONS['docs'].put('java.*',['https://docs.oracle.com/javase/8/docs/api/', + 'http://docs.groovy-lang.org/latest/html/groovy-jdk/']) +CONSOLE_OPTIONS['docs'].put('.*/java.*','https://docs.oracle.com/javase/8/docs/api/') +CONSOLE_OPTIONS['docs'].put('org/jline/.*','https://www.javadoc.io/doc/org.jline/jline/latest/') +# +# customize prnt command +# +def _reader2map(reader){ + def out = [:] + out['othersGroupName'] = reader.othersGroupName + out['keyMap'] = reader.keyMap + out['autosuggestion'] = reader.autosuggestion + out['terminal.kind'] = reader.terminal.kind + out +} + +def _reader2string(reader){ + def out = "[" + out += 'othersGroupName:' + reader.othersGroupName + ', ' + out += 'keyMap:' + reader.keyMap + ', ' + out += 'autosuggestion:' + reader.autosuggestion + ', ' + out += 'terminal.kind:' + reader.terminal.kind + out += ']' + out +} + +def _number2date(number){ + def out = null + try { + out = number instanceof Long ? new Date(number) : number + } catch (Exception e) { + out = number + } + new AttributedString(out.toString()) +} + +def _orgJline(p) { + def out = p + if (p.toString().startsWith('org.jline')) { + def m = Pattern.compile('(org\\.jline(\\.[a-z]+)*)(.*)').matcher(p.toString()) + if (m.find()) { + def asb = new AttributedStringBuilder() + asb.append(m.group(1),AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW + AttributedStyle.BRIGHT)) + asb.append(m.group(3)) + out = asb.toAttributedString() + } + } + out +} + +PRNT_OPTIONS = [:] +PRNT_OPTIONS.objectToMap = [:] +PRNT_OPTIONS.objectToString = [:] +PRNT_OPTIONS.highlightValue = [:] +PRNT_OPTIONS['objectToMap'].put(org.jline.reader.impl.LineReaderImpl, _reader2map) +PRNT_OPTIONS['objectToString'].put(org.jline.reader.impl.LineReaderImpl, _reader2string) +PRNT_OPTIONS['highlightValue'].put('time.*', _number2date) +PRNT_OPTIONS['highlightValue'].put('*', _orgJline) +PRNT_OPTIONS.valueStyle = 'GRON' + +GROOVY_OPTIONS = [:] +GROOVY_OPTIONS.identifiersCompletion = true +GROOVY_OPTIONS.allFieldsCompletion = false +GROOVY_OPTIONS.allMethodsCompletion = false +GROOVY_OPTIONS.metaMethodsCompletion = false +GROOVY_OPTIONS.allClassesCompletion = false +GROOVY_OPTIONS.allConstructorsCompletion = false + +# +# custom Groovy pipes +# +pipe --delete * +pipe |. '.collect{' '}' +pipe |: '.collectEntries{' '}' +pipe |:: '.collectMany{' '}' +pipe |? '.findAll{' '}' +pipe |?1 '.find{' '}' +pipe |! '.each{' '}' +pipe |# '.take(' ')' +pipe |& '.' ' ' +pipe grep '.collect{it.toString()}.findAll{it=~/' '/}' +alias null '|& identity{}' +alias xargs '|; %{0} %{1} %{2} %{3} %{4} %{5} %{6} %{7} %{8} %{9}' +alias to '|; %{0} =' +alias select '|. def m = it; def out = [:]; %{@}.each{def v = m; it.split("\\.").each{v=v[it]}; out.put(it,v)}; out' +alias sort '|& sort{def p = %{@}; p[0].startsWith("-") ? -it[p[0].substring(1)] : it[p[0]]}' +alias distinct '|& unique()' +# +# test-widget reads widget name from buffer and execute it +# +def _testWidget() { + def name = _buffer().toString().split('\\s+')[0] + _widget "$name" +} +# +# create _tailtip-toggle widget that changes also maximum candidates to display +# +def _tailTipToggle() { + def al = _reader.getWidgets().get('accept-line').toString() + def enabled = al == '_tailtip-accept-line' + if (enabled) { + _reader.setVariable('list-max',100) + _reader.option(org.jline.reader.LineReader.Option.INSERT_BRACKET, true) + } else { + _reader.setVariable('list-max',50) + _reader.option(org.jline.reader.LineReader.Option.INSERT_BRACKET, false) + } + _widget 'tailtip-toggle' +} +# +# widget functions to open web doc of jline and groovy +# +def _docJline() { + :doc jline +} + +def _docGroovy() { + :doc groovy +} +# +# widget functions for groovy autosuggestion completion +# +def _toggleFields() { + GROOVY_OPTIONS.allFieldsCompletion = !GROOVY_OPTIONS.allFieldsCompletion +} + +def _toggleMethods() { + GROOVY_OPTIONS.allMethodsCompletion = !GROOVY_OPTIONS.allMethodsCompletion +} + +def _toggleMetaMethods() { + GROOVY_OPTIONS.metaMethodsCompletion = !GROOVY_OPTIONS.metaMethodsCompletion +} + +def _toggleMenuList() { + def optAutoMenu = org.jline.reader.LineReader.Option.AUTO_MENU_LIST + _reader.option(optAutoMenu, !_reader.isSet(optAutoMenu)) +} +# +# resolve function keys +# +def _getkey(name) { + def key + key=:tget $name + key.values()[0] +} +_f1 = _getkey('key_f1') +_f2 = _getkey('key_f2') + +widget -N _test-widget _testWidget +widget -N _tailtip-toggle _tailTipToggle +widget -N _doc-jline _docJline +widget -N _doc-groovy _docGroovy +widget -N _toggle-fields _toggleFields +widget -N _toggle-methods _toggleMethods +widget -N _toggle-meta-methods _toggleMetaMethods +widget -N _toggle-menu-list _toggleMenuList + +keymap '^[^x' _test-widget +keymap '^[s' _tailtip-toggle +keymap '^[f' _toggle-fields +keymap '^[g' _toggle-meta-methods +keymap '^[m' _toggle-methods +keymap '^[l' _toggle-menu-list +if (_f1 && _f2) { + :keymap $_f1 _doc-jline + :keymap $_f2 _doc-groovy +} diff --git a/demo/src/main/scripts/tget.groovy b/demo/src/main/scripts/tget.groovy index f1c653eeb..f40bace32 100644 --- a/demo/src/main/scripts/tget.groovy +++ b/demo/src/main/scripts/tget.groovy @@ -51,7 +51,7 @@ class TerminalCapability { } def static main(def _args){ - def terminal = org.jline.builtins.SystemRegistry.get().consoleEngine().reader.getTerminal() + def terminal = org.jline.console.SystemRegistry.get().consoleEngine().reader.getTerminal() def capability = new TerminalCapability(terminal) capability.get(_args) } diff --git a/demo/src/main/scripts/trace.groovy b/demo/src/main/scripts/trace.groovy new file mode 100644 index 000000000..eacc85c68 --- /dev/null +++ b/demo/src/main/scripts/trace.groovy @@ -0,0 +1,57 @@ +// END_HELP +import java.util.logging.* +import org.jline.utils.* +import org.jline.builtins.Options + +class Trace { + + static def set(def args) { + String[] usage = [ + "trace - set JLine REPL console trace level", + "Usage: trace [LEVEL]", + " -? --help Displays command help" + ] + Options opt = Options.compile(usage).parse(args) + if (opt.isSet("help")) { + throw new Options.HelpException(opt.usage()) + } + def logger = LogManager.getLogManager().getLogger("") + def console = org.jline.console.SystemRegistry.get().consoleEngine() + def out + def CONSOLE_OPTIONS = console.getVariable('CONSOLE_OPTIONS'); + def handlers = logger.getHandlers() + if (!opt.args() || !opt.args().get(0).isInteger()) { + out = [:] + out['CONSOLE_OPTIONS.trace'] = CONSOLE_OPTIONS.trace + int i = 0 + for (def h : handlers) { + out['Log.handler' + i++] = h + } + } else { + def level = opt.args().get(0).toInteger() + if (!handlers) { + def handler = new ConsoleHandler() + System.setProperty("java.util.logging.SimpleFormatter.format",'%5$s%n') + SimpleFormatter formatter = new SimpleFormatter() + handler.setFormatter(formatter) + logger.addHandler(handler) + handlers = logger.getHandlers() + } + def tl = Level.OFF + CONSOLE_OPTIONS.trace = level + if (level == 2) { + tl = Level.FINE + } else if (level > 2) { + tl = Level.ALL + } + console.putVariable('CONSOLE_OPTIONS', CONSOLE_OPTIONS) + logger.setLevel(tl) + handlers.each { it.setLevel(tl) } + } + out + } +} + +def static main(def _args){ + Trace.set(_args) +} diff --git a/graal/pom.xml b/graal/pom.xml new file mode 100644 index 000000000..7f6e36f2c --- /dev/null +++ b/graal/pom.xml @@ -0,0 +1,138 @@ + + + + + 4.0.0 + + + org.jline + jline-parent + 3.18.1-SNAPSHOT + + + jline-graal + JLine Graal Demo + + + org.jline.graal + + + + + org.jline + jline-reader + + + org.jline + jline-console + + + + net.java.dev.jna + jna + + + + org.fusesource.jansi + jansi + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-jdk14 + + + org.graalvm.sdk + graal-sdk + provided + + + + + + + maven-dependency-plugin + + + copy + + copy-dependencies + + + ${project.build.directory}/lib + + + + + + maven-resources-plugin + + + copy-root + validate + + copy-resources + + + ${basedir}/target + + + src/main/resources + + reflection-config.json + + + + + + + + + org.graalvm.nativeimage + native-image-maven-plugin + + + + native-image + + package + + + + ${native.image.skip} + graal + org.jline.demo.graal.Graal + + --no-fallback + --report-unsupported-elements-at-runtime + --allow-incomplete-classpath + -H:ReflectionConfigurationFiles=reflection-config.json + + + + + maven-javadoc-plugin + + + javadoc + none + + + + + + + diff --git a/graal/src/main/java/org/jline/demo/graal/Graal.java b/graal/src/main/java/org/jline/demo/graal/Graal.java new file mode 100644 index 000000000..17e3c8517 --- /dev/null +++ b/graal/src/main/java/org/jline/demo/graal/Graal.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.demo.graal; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Supplier; + +import org.jline.console.impl.Builtins; +import org.jline.console.impl.Builtins.Command; +import org.jline.console.impl.SystemRegistryImpl; +import org.jline.builtins.ConfigurationPath; +import org.jline.keymap.KeyMap; +import org.jline.reader.*; +import org.jline.reader.LineReader.Option; +import org.jline.reader.impl.DefaultParser; +import org.jline.terminal.Terminal; +import org.jline.terminal.TerminalBuilder; +import org.jline.terminal.Terminal.Signal; +import org.jline.utils.OSUtils; +import org.jline.widget.TailTipWidgets; +import org.jline.widget.TailTipWidgets.TipType; +import org.jline.widget.Widgets; + +public class Graal { + + public static void main(String[] args) { + try { + Supplier workDir = () -> Paths.get(System.getProperty("user.dir")); + // + // Parser & Terminal + // + DefaultParser parser = new DefaultParser(); + parser.setEofOnUnclosedQuote(true); + parser.setEscapeChars(null); + parser.setRegexVariable(null); // we do not have console variables! + Terminal terminal = TerminalBuilder.builder().build(); + Thread executeThread = Thread.currentThread(); + terminal.handle(Signal.INT, signal -> executeThread.interrupt()); + // + // Command registries + // + File file = new File(Graal.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath()); + String root = file.getCanonicalPath(); + root = root.substring(0, root.length() - 6); + ConfigurationPath configPath = new ConfigurationPath(Paths.get(root), Paths.get(root)); + Set commands = new HashSet<>(Arrays.asList(Builtins.Command.values())); + commands.remove(Command.TTOP); // ttop command is not supported in GraalVM + Builtins builtins = new Builtins(commands, workDir, configPath, null); + SystemRegistryImpl systemRegistry = new SystemRegistryImpl(parser, terminal, workDir, configPath); + systemRegistry.setCommandRegistries(builtins); + // + // LineReader + // + LineReader reader = LineReaderBuilder.builder() + .terminal(terminal) + .completer(systemRegistry.completer()) + .parser(parser) + .variable(LineReader.SECONDARY_PROMPT_PATTERN, "%M%P > ") + .variable(LineReader.INDENTATION, 2) + .variable(LineReader.LIST_MAX, 100) + .variable(LineReader.HISTORY_FILE, Paths.get(root, "history")) + .option(Option.INSERT_BRACKET, true) + .option(Option.EMPTY_WORD_OPTIONS, false) + .option(Option.USE_FORWARD_SLASH, true) // use forward slash in directory separator + .option(Option.DISABLE_EVENT_EXPANSION, true) + .build(); + if (OSUtils.IS_WINDOWS) { + reader.setVariable(LineReader.BLINK_MATCHING_PAREN, 0); // if enabled cursor remains in begin parenthesis (gitbash) + } + // + // complete command registries + // + builtins.setLineReader(reader); + // + // widgets and console initialization + // + new TailTipWidgets(reader, systemRegistry::commandDescription, 5, TipType.COMPLETER); + KeyMap keyMap = reader.getKeyMaps().get("main"); + keyMap.bind(new Reference(Widgets.TAILTIP_TOGGLE), KeyMap.alt("s")); + // + // REPL-loop + // + System.out.println(terminal.getName() + ": " + terminal.getType()); + while (true) { + try { + systemRegistry.cleanUp(); // reset output streams + String line = reader.readLine("graal> "); + Object result = systemRegistry.execute(line); + if (result != null) { + System.out.println(result); + } + } + catch (UserInterruptException e) { + // Ignore + } + catch (EndOfFileException e) { + break; + } + catch (Exception e) { + systemRegistry.trace(true, e); // print exception + } + } + systemRegistry.close(); + } + catch (Throwable t) { + t.printStackTrace(); + } + } + +} diff --git a/graal/src/main/resources/native-image/resource-config.json b/graal/src/main/resources/native-image/resource-config.json new file mode 100644 index 000000000..d3a079e3e --- /dev/null +++ b/graal/src/main/resources/native-image/resource-config.json @@ -0,0 +1,6 @@ +{ + "resources": [ + {"pattern": "org/jline/utils/.*caps$"}, + {"pattern": "org/jline/utils/capabilities\\.txt$"} + ] +} \ No newline at end of file diff --git a/graal/src/main/resources/reflection-config.json b/graal/src/main/resources/reflection-config.json new file mode 100644 index 000000000..e5136889a --- /dev/null +++ b/graal/src/main/resources/reflection-config.json @@ -0,0 +1,11 @@ +[ + { + "name" : "sun.misc.SignalHandler", + "allDeclaredConstructors" : true, + "allPublicConstructors" : true, + "allDeclaredMethods" : true, + "allPublicMethods" : true, + "allDeclaredClasses" : true, + "allPublicClasses" : true + } +] \ No newline at end of file diff --git a/groovy/pom.xml b/groovy/pom.xml index 805a638d8..0ca889b17 100644 --- a/groovy/pom.xml +++ b/groovy/pom.xml @@ -14,15 +14,20 @@ org.jline jline-parent - 3.14.1-SNAPSHOT + 3.18.1-SNAPSHOT jline-groovy JLine Groovy http://maven.apache.org + + + org.jline.groovy + + org.jline - jline-reader + jline-console org.codehaus.groovy @@ -32,6 +37,16 @@ org.codehaus.groovy groovy-json + + org.codehaus.groovy + groovy-console + true + + + org.apache.ivy + ivy + true + junit junit @@ -48,8 +63,6 @@ -Xlint:all,-options -Werror - -profile - compact1 true diff --git a/groovy/src/main/groovy/org/jline/groovy/ObjectInspector.groovy b/groovy/src/main/groovy/org/jline/groovy/ObjectInspector.groovy new file mode 100644 index 000000000..124a86a52 --- /dev/null +++ b/groovy/src/main/groovy/org/jline/groovy/ObjectInspector.groovy @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.groovy + +import groovy.inspect.Inspector + +class ObjectInspector { + public static final String FIELD_NAME = 'name' + public static final String FIELD_PARAMETERS = 'parameters' + public static final String FIELD_MODIFIERS = 'modifiers' + public static final String FIELD_RETURN = 'return' + public static final List METHOD_COLUMNS = ['language', FIELD_MODIFIERS, 'this', FIELD_RETURN, FIELD_NAME + , FIELD_PARAMETERS,'exception','8'] + public static final List GLOBAL_META_METHODS = ['print', 'println', 'printf', 'sprintf', 'sleep'] + private static final List OPERATOR_META_METHODS = ['plus', 'minus', 'multiply', 'div', 'mod', 'power', 'or', 'and' + , 'xor', 'asType', 'call', 'getAt', 'putAt', 'isCase', 'leftShift' + , 'rightShift', 'rightShiftUnsigned', 'next', 'previous', 'positive' + , 'negative', 'bitwiseNegate' + /* other methods to exclude from completion */ , 'asBoolean' , 'toBoolean' , 'addShutdownHook'] + def obj + def inspector + def types = [] + + ObjectInspector(def obj) { + this.obj = obj + types.add(obj.getClass().getSimpleName()) + addInterfaces(obj.getClass()) + def superClass = obj.getClass().getSuperclass() + while (superClass) { + types.add(superClass.getSimpleName()) + addInterfaces(superClass) + superClass = superClass.getSuperclass() + } + this.inspector = new Inspector(obj) + } + + private void addInterfaces(def clazz) { + clazz.interfaces.each{types.add(it.getSimpleName())} + } + + List> methods() { + def out = [] + inspector.methods.each { + def mdef = [:] + for (int i = 0; i < it.size(); i++) { + mdef.put(METHOD_COLUMNS.get(i), it[i]) + } + out.add(mdef) + } + out + } + + List> metaMethods() { + metaMethods(true) + } + + List> metaMethods(boolean includeOperatorMethods) { + def out = [] + def added = [] + types.each { type -> + inspector.metaMethods.each { + def mdef = [:] + for (int i = 0; i < it.size(); i++) { + mdef.put(METHOD_COLUMNS.get(i), it[i]) + } + if (type == mdef.this && !added.contains(mdef.name + mdef.parameters)) { + if (!GLOBAL_META_METHODS.contains(mdef.name) + && (includeOperatorMethods || !OPERATOR_META_METHODS.contains(mdef.name))) { + added.add(mdef.name + mdef.parameters) + out.add(mdef) + } + } + } + } + out + } + + def properties() { + def out = [:] + def props = ['propertyInfo', 'publicFields', 'classProps'] + props.each { + def val = inspector.properties.get(it) + out.put(it, val) + } + out + } +} diff --git a/groovy/src/main/groovy/org/jline/groovy/Utils.groovy b/groovy/src/main/groovy/org/jline/groovy/Utils.groovy index f8baecdf9..0965de495 100644 --- a/groovy/src/main/groovy/org/jline/groovy/Utils.groovy +++ b/groovy/src/main/groovy/org/jline/groovy/Utils.groovy @@ -6,20 +6,28 @@ * * https://opensource.org/licenses/BSD-3-Clause */ -package org.jline.groovy; +package org.jline.groovy -import java.nio.charset.StandardCharsets -import java.nio.file.Path; +import org.codehaus.groovy.runtime.HandleMetaClass +import java.nio.file.Path +import org.jline.script.GroovyEngine.Format import groovy.json.JsonOutput import groovy.json.JsonSlurper import groovy.json.JsonParserType -public class Utils { +class Utils { private Utils() {} static String toString(Object object) { + if (object == null) { + return 'null' + } else if (object instanceof Collection) { + return object.toListString() + } else if (object instanceof Map) { + return object.toMapString() + } object.toString() } @@ -29,7 +37,15 @@ public class Utils { } static Map toMap(Object object) { - object.properties + def out = [:] + if (object instanceof Closure) { + out['closure'] = object.getClass().getName() + } else if (object instanceof HandleMetaClass) { + out['object'] = object.toString() + } else { + out = object != null ? object.properties : null + } + out } static String toJson(Object object) { @@ -38,8 +54,14 @@ public class Utils { || (json.startsWith("[") && json.endsWith("]"))) && json.length() > 5) ? JsonOutput.prettyPrint(json) : json } - static void persist(Path file, Object object) { - file.toFile().write(JsonOutput.toJson(object)) + static void persist(Path file, Object object, Format format) { + if (format == Format.JSON) { + file.toFile().write(JsonOutput.toJson(object)) + } else if (format == Format.NONE) { + file.toFile().write(toString(object)) + } else { + throw new IllegalArgumentException() + } } } diff --git a/groovy/src/main/java/org/jline/script/GroovyCommand.java b/groovy/src/main/java/org/jline/script/GroovyCommand.java new file mode 100644 index 000000000..d631203fb --- /dev/null +++ b/groovy/src/main/java/org/jline/script/GroovyCommand.java @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.script; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jline.console.CommandInput; +import org.jline.console.CommandMethods; +import org.jline.console.impl.AbstractCommandRegistry; +import org.jline.builtins.Completers.OptDesc; +import org.jline.builtins.Completers.OptionCompleter; +import org.jline.console.CmdDesc; +import org.jline.console.CommandRegistry; +import org.jline.console.Printer; +import org.jline.groovy.ObjectInspector; +import org.jline.reader.Completer; +import org.jline.reader.impl.completer.ArgumentCompleter; +import org.jline.reader.impl.completer.NullCompleter; +import org.jline.reader.impl.completer.StringsCompleter; +import org.jline.utils.AttributedString; + +import groovy.console.ui.Console; +import groovy.console.ui.ObjectBrowser; + +public class GroovyCommand extends AbstractCommandRegistry implements CommandRegistry { + public enum Command {INSPECT, CONSOLE, GRAB} + private static final String DEFAULT_NANORC_VALUE = "classpath:/org/jline/groovy/gron.nanorc"; + private final GroovyEngine engine; + private final Printer printer; + private final Map commandDescs = new HashMap<>(); + private final Map> commandInfos = new HashMap<>(); + private boolean consoleUi; + private boolean ivy; + + public GroovyCommand(GroovyEngine engine, Printer printer) { + this(null, engine, printer); + } + + public GroovyCommand(Set commands, GroovyEngine engine, Printer printer) { + this.engine = engine; + this.printer = printer; + try { + Class.forName("groovy.console.ui.ObjectBrowser"); + consoleUi = true; + } catch (Exception e) { + // ignore + } + try { + Class.forName("org.apache.ivy.util.Message"); + System.setProperty("groovy.grape.report.downloads","false"); + ivy = true; + } catch (Exception e) { + // ignore + } + Set cmds; + Map commandName = new HashMap<>(); + Map commandExecute = new HashMap<>(); + if (commands == null) { + cmds = new HashSet<>(EnumSet.allOf(Command.class)); + } else { + cmds = new HashSet<>(commands); + } + if (!consoleUi) { + cmds.remove(Command.CONSOLE); + } + if (!ivy) { + cmds.remove(Command.GRAB); + } + for (Command c: cmds) { + commandName.put(c, c.name().toLowerCase()); + } + commandExecute.put(Command.INSPECT, new CommandMethods(this::inspect, this::inspectCompleter)); + commandExecute.put(Command.CONSOLE, new CommandMethods(this::console, this::defaultCompleter)); + commandExecute.put(Command.GRAB, new CommandMethods(this::grab, this::defaultCompleter)); + registerCommands(commandName, commandExecute); + commandDescs.put(Command.INSPECT, inspectCmdDesc()); + commandDescs.put(Command.CONSOLE, consoleCmdDesc()); + commandDescs.put(Command.GRAB, grabCmdDesc()); + } + + @Override + public List commandInfo(String command) { + Command cmd = (Command)registeredCommand(command); + return commandInfos.get(cmd); + } + + @Override + public CmdDesc commandDescription(List args) { + String command = args != null && !args.isEmpty() ? args.get(0) : ""; + Command cmd = (Command)registeredCommand(command); + return commandDescs.get(cmd); + } + + @SuppressWarnings("unchecked") + public Object grab(CommandInput input) { + if (input.xargs().length == 0) { + return null; + } + if (input.args().length > 2) { + throw new IllegalArgumentException("Wrong number of command parameters: " + input.args().length); + } + try { + String arg = input.args()[0]; + if (arg.equals("-?") || arg.equals("--help")) { + printer.println(helpDesc(Command.GRAB)); + } else if (arg.equals("-l") || arg.equals("--list")) { + Object resp = engine.execute("groovy.grape.Grape.getInstance().enumerateGrapes()"); + Map options = new HashMap<>(); + options.put(Printer.SKIP_DEFAULT_OPTIONS, true); + options.put(Printer.MAX_DEPTH, 1); + options.put(Printer.INDENTION, 4); + options.put(Printer.VALUE_STYLE, engine.groovyOption(GroovyEngine.NANORC_VALUE, DEFAULT_NANORC_VALUE)); + printer.println(options, resp); + } else { + int artifactId = 0; + if (input.args().length == 2) { + if (input.args()[0].equals("-v") || input.args()[0].equals("--verbose")) { + System.setProperty("groovy.grape.report.downloads","true"); + artifactId = 1; + } else if (input.args()[1].equals("-v") || input.args()[1].equals("--verbose")) { + System.setProperty("groovy.grape.report.downloads","true"); + } else { + throw new IllegalArgumentException("Unknown command parameters!"); + } + } + Map artifact = new HashMap<>(); + Object xarg = input.xargs()[artifactId]; + if (xarg instanceof String) { + String[] vals = input.args()[artifactId].split(":"); + if (vals.length != 3) { + throw new IllegalArgumentException("Invalid command parameter: " + input.args()[artifactId]); + } + artifact.put("group", vals[0]); + artifact.put("module", vals[1]); + artifact.put("version", vals[2]); + } else if (xarg instanceof Map) { + artifact = (Map) xarg; + } else { + throw new IllegalArgumentException("Unknown command parameter: " + xarg); + } + engine.put("_artifact", artifact); + engine.execute("groovy.grape.Grape.grab(_artifact)"); + } + } catch (Exception e) { + saveException(e); + } finally { + System.setProperty("groovy.grape.report.downloads","false"); + } + return null; + } + + public void console(CommandInput input) { + if (input.args().length > 1) { + throw new IllegalArgumentException("Wrong number of command parameters: " + input.args().length); + } + if (input.args().length == 1) { + String arg = input.args()[0]; + if (arg.equals("-?") || arg.equals("--help")) { + printer.println(helpDesc(Command.CONSOLE)); + return; + } else { + throw new IllegalArgumentException("Unknown command parameter: " + input.args()[0]); + } + } + Console c = new Console(engine.sharedData); + c.run(); + } + + public Object inspect(CommandInput input) { + if (input.xargs().length == 0) { + return null; + } + if (input.args().length > 2) { + throw new IllegalArgumentException("Wrong number of command parameters: " + input.args().length); + } + int idx = optionIdx(input.args()); + String option = idx < 0 ? "--info" : input.args()[idx]; + if (option.equals("-?") || option.equals("--help")) { + printer.println(helpDesc(Command.INSPECT)); + return null; + } + int id = 0; + if (idx >= 0) { + id = idx == 0 ? 1 : 0; + } + if (input.args().length < id + 1) { + throw new IllegalArgumentException("Wrong number of command parameters: " + input.args().length); + } + try { + Object obj = input.xargs()[id]; + ObjectInspector inspector = new ObjectInspector(obj); + Object out = null; + Map options = new HashMap<>(); + if (option.equals("-m") || option.equals("--methods")) { + out = inspector.methods(); + } else if (option.equals("-n") || option.equals("--metaMethods")) { + out = inspector.metaMethods(); + } else if (option.equals("-i") || option.equals("--info")) { + out = inspector.properties(); + options.put(Printer.VALUE_STYLE, engine.groovyOption(GroovyEngine.NANORC_VALUE, DEFAULT_NANORC_VALUE)); + } else if (consoleUi && (option.equals("-g") || option.equals("--gui"))) { + ObjectBrowser.inspect(obj); + } else { + throw new IllegalArgumentException("Unknown option: " + option); + } + options.put(Printer.SKIP_DEFAULT_OPTIONS, true); + options.put(Printer.COLUMNS, ObjectInspector.METHOD_COLUMNS); + options.put(Printer.MAX_DEPTH, 1); + options.put(Printer.INDENTION, 4); + printer.println(options, out); + } catch (Exception e) { + saveException(e); + } + return null; + } + + private CmdDesc helpDesc(Command command) { + return doHelpDesc(command.toString().toLowerCase(), commandInfos.get(command), commandDescs.get(command)); + } + + private CmdDesc grabCmdDesc() { + Map> optDescs = new HashMap<>(); + optDescs.put("-? --help", doDescription ("Displays command help")); + optDescs.put("-l --list", doDescription ("List the modules in the cache")); + optDescs.put("-v --verbose", doDescription ("Report downloads")); + CmdDesc out = new CmdDesc(new ArrayList<>(), optDescs); + List mainDesc = new ArrayList<>(); + List info = new ArrayList<>(); + info.add("Add maven repository dependencies to classpath"); + commandInfos.put(Command.GRAB, info); + mainDesc.add(new AttributedString("grab [OPTIONS] ::")); + mainDesc.add(new AttributedString("grab --list")); + out.setMainDesc(mainDesc); + out.setHighlighted(false); + return out; + } + + private CmdDesc consoleCmdDesc() { + Map> optDescs = new HashMap<>(); + optDescs.put("-? --help", doDescription ("Displays command help")); + CmdDesc out = new CmdDesc(new ArrayList<>(), optDescs); + List mainDesc = new ArrayList<>(); + List info = new ArrayList<>(); + info.add("Launch Groovy console"); + commandInfos.put(Command.CONSOLE, info); + mainDesc.add(new AttributedString("console")); + out.setMainDesc(mainDesc); + out.setHighlighted(false); + return out; + } + + private CmdDesc inspectCmdDesc() { + Map> optDescs = new HashMap<>(); + optDescs.put("-? --help", doDescription ("Displays command help")); + if (consoleUi) { + optDescs.put("-g --gui", doDescription ("Launch object browser")); + } + optDescs.put("-i --info", doDescription ("Object class info")); + optDescs.put("-m --methods", doDescription ("List object methods")); + optDescs.put("-n --metaMethods", doDescription ("List object metaMethods")); + CmdDesc out = new CmdDesc(new ArrayList<>(), optDescs); + List mainDesc = new ArrayList<>(); + List info = new ArrayList<>(); + info.add("Display object info on terminal"); + commandInfos.put(Command.INSPECT, info); + mainDesc.add(new AttributedString("inspect [OPTION] OBJECT")); + out.setMainDesc(mainDesc); + out.setHighlighted(false); + return out; + } + + private List doDescription(String description) { + List out = new ArrayList<>(); + out.add(new AttributedString(description)); + return out; + } + + private int optionIdx(String[] args) { + int out = 0; + for (String a : args) { + if (a.startsWith("-")) { + return out; + } + out++; + } + return -1; + } + + private List variables() { + List out = new ArrayList<>(); + for (String v : engine.find(null).keySet()) { + out.add("$" + v); + } + return out; + } + + private List compileOptDescs(String command) { + List out = new ArrayList<>(); + Command cmd = Command.valueOf(command.toUpperCase()); + for (Map.Entry> entry : commandDescs.get(cmd).getOptsDesc().entrySet()) { + String[] option = entry.getKey().split("\\s+"); + String desc = entry.getValue().get(0).toString(); + if (option.length == 2) { + out.add(new OptDesc(option[0], option[1], desc)); + } else if (option[0].charAt(1) == '-') { + out.add(new OptDesc(null, option[0], desc)); + } else { + out.add(new OptDesc(option[0], null, desc)); + } + } + return out; + } + + private List inspectCompleter(String command) { + List out = new ArrayList<>(); + ArgumentCompleter ac = new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(Arrays.asList(new StringsCompleter(this::variables), NullCompleter.INSTANCE) + , this::compileOptDescs, 1) + ); + out.add(ac); + return out; + } + + private List defaultCompleter(String command) { + List out = new ArrayList<>(); + ArgumentCompleter ac = new ArgumentCompleter(NullCompleter.INSTANCE + , new OptionCompleter(NullCompleter.INSTANCE + , this::compileOptDescs, 1) + ); + out.add(ac); + return out; + } +} diff --git a/groovy/src/main/java/org/jline/script/GroovyEngine.java b/groovy/src/main/java/org/jline/script/GroovyEngine.java index 15e1ae723..f2e921c09 100644 --- a/groovy/src/main/java/org/jline/script/GroovyEngine.java +++ b/groovy/src/main/java/org/jline/script/GroovyEngine.java @@ -1,255 +1,1817 @@ -/* - * Copyright (c) 2002-2020, the original author or authors. - * - * 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.script; - -import java.io.File; -import java.nio.file.Path; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jline.groovy.Utils; -import org.jline.reader.ScriptEngine; - -import groovy.lang.Binding; -import groovy.lang.GroovyShell; -import groovy.lang.Script; - -/** - * Implements Groovy ScriptEngine. - * You must be very careful when using GroovyEngine in a multithreaded environment. The Binding instance is not - * thread safe, and it is shared by all scripts. - * - * @author Matti Rinta-Nikkola - */ -public class GroovyEngine implements ScriptEngine { - private static final String REGEX_SYSTEM_VAR = "[A-Z]+[A-Z_]*"; - private static final String REGEX_VAR = "[a-zA-Z_]+[a-zA-Z0-9_]*"; - private static final Pattern PATTERN_FUNCTION_DEF=Pattern.compile("^def\\s+(" + REGEX_VAR + ")\\s*\\(([a-zA-Z0-9_ ,]*)\\)\\s*\\{(.*)?\\}(|\n)$" - , Pattern.DOTALL); - private static final Pattern PATTERN_CLASS_DEF=Pattern.compile("^class\\s+(" + REGEX_VAR + ")\\ .*?\\{.*?\\}(|\n)$" - , Pattern.DOTALL); - private GroovyShell shell; - private Binding sharedData; - private Map imports = new HashMap<>(); - private Map methods = new HashMap<>(); - - public GroovyEngine() { - this.sharedData = new Binding(); - shell = new GroovyShell(sharedData); - } - - @Override - public boolean hasVariable(String name) { - return sharedData.hasVariable(name); - } - - @Override - public void put(String name, Object value) { - sharedData.setProperty(name, value); - } - - @Override - public Object get(String name) { - return sharedData.hasVariable(name) ? sharedData.getVariable(name) : null; - } - - @SuppressWarnings("unchecked") - @Override - public Map find(String name) { - Map out = new HashMap<>(); - if (name == null) { - out = sharedData.getVariables(); - } else { - for (String v : internalFind(name)) { - out.put(v, get(v)); - } - } - return out; - } - - @Override - public Object expandParameter(String variable, String format) { - Object out = variable; - if (format.equalsIgnoreCase("TXT")) { - // do nothing - } else if (format.equalsIgnoreCase("JSON")) { - out = Utils.toObject(variable); - } else if (format.equalsIgnoreCase("GROOVY")) { - try { - out = execute(variable); - } catch (Exception e) { - throw new IllegalArgumentException(e.getMessage()); - } - } else { - variable = variable.trim(); - boolean hasCurly = variable.contains("{") && variable.contains("}"); - try { - if (variable.startsWith("[") && variable.endsWith("]")) { - try { - if (hasCurly) { - out = Utils.toObject(variable); // try json - } else { - out = execute(variable); - } - } catch (Exception e) { - if (hasCurly) { - try { - out = execute(variable); - } catch (Exception e2) { - - } - } else { - out = Utils.toObject(variable); // try json - } - } - } else if (variable.startsWith("{") && variable.endsWith("}")) { - out = Utils.toObject(variable); - } - } catch (Exception e) { - } - } - return out; - } - - @Override - public void persist(Path file, Object object, String format) { - if (!format.equalsIgnoreCase("JSON")) { - throw new IllegalArgumentException(); - } - Utils.persist(file, object); - } - - @Override - public Object execute(File script, Object[] args) throws Exception { - sharedData.setProperty("_args", args); - Script s = shell.parse(script); - return s.run(); - } - - @Override - public Object execute(String statement) throws Exception { - Object out = null; - if (statement.startsWith("import ")) { - shell.evaluate(statement); - String[] p = statement.split("\\s+", 2); - imports.put(p[1].replaceAll(";", ""), statement); - } else if (statement.equals("import")) { - out = new ArrayList<>(imports.keySet()); - } else if (functionDef(statement)) { - // do nothing - } else if (statement.equals("def")) { - out = methods; - } else if (statement.matches("def\\s+" + REGEX_VAR)) { - String name = statement.split("\\s+")[1]; - if (methods.containsKey(name)) { - out = "def " + name + methods.get(name); - } - } else { - String e = ""; - for (Map.Entry entry : imports.entrySet()) { - e += entry.getValue()+"\n"; - } - e += statement; - if (classDef(statement)) { - e += "; null"; - } - out = shell.evaluate(e); - } - return out; - } - - @Override - public String getEngineName() { - return this.getClass().getSimpleName(); - } - - @Override - public List getExtensions() { - return Arrays.asList("groovy"); - } - - @SuppressWarnings("unchecked") - private List internalFind(String var) { - List out = new ArrayList<>(); - if(!var.contains(".") && var.contains("*")) { - var = var.replaceAll("\\*", ".*"); - } - for (String v : (Set)sharedData.getVariables().keySet()) { - if (v.matches(var)) { - out.add(v); - } - } - return out; - } - - private boolean functionDef (String statement) throws Exception{ - boolean out = false; - Matcher m = PATTERN_FUNCTION_DEF.matcher(statement); - if(m.matches()){ - out = true; - put(m.group(1), execute("{" + m.group(2) + "->" + m.group(3) + "}")); - methods.put(m.group(1), "(" + m.group(2) + ")" + "{" + m.group(3) + "}"); - } - return out; - } - - private boolean classDef (String statement) throws Exception{ - return PATTERN_CLASS_DEF.matcher(statement).matches(); - } - - private void del(String var) { - if (var == null) { - return; - } - if (imports.containsKey(var)) { - imports.remove(var); - } else if (sharedData.hasVariable(var)) { - sharedData.getVariables().remove(var); - if (methods.containsKey(var)) { - methods.remove(var); - } - } else if (!var.contains(".") && var.contains("*")) { - for (String v : internalFind(var)){ - if (sharedData.hasVariable(v) && !v.equals("_") && !v.matches(REGEX_SYSTEM_VAR)) { - sharedData.getVariables().remove(v); - if (methods.containsKey(v)) { - methods.remove(v); - } - } - } - } - } - - @Override - public void del(String... vars) { - if (vars == null) { - return; - } - for (String s: vars) { - del(s); - } - } - - @Override - public String toJson(Object obj) { - return Utils.toJson(obj); - } - - @Override - public String toString(Object obj) { - return Utils.toString(obj); - } - - @Override - public Map toMap(Object obj) { - return Utils.toMap(obj); - } - -} +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.script; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.file.Path; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import groovy.lang.*; +import org.apache.groovy.ast.tools.ImmutablePropertyUtils; +import org.codehaus.groovy.control.messages.SyntaxErrorMessage; +import org.codehaus.groovy.syntax.SyntaxException; +import org.jline.builtins.Nano.SyntaxHighlighter; +import org.jline.builtins.Styles; +import org.jline.console.CmdDesc; +import org.jline.console.CmdLine; +import org.jline.console.ScriptEngine; +import org.jline.console.SystemRegistry; +import org.jline.groovy.ObjectInspector; +import org.jline.groovy.Utils; +import org.jline.reader.Candidate; +import org.jline.reader.Completer; +import org.jline.reader.LineReader; +import org.jline.reader.ParsedLine; +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.utils.AttributedString; +import org.jline.utils.Log; +import org.jline.utils.OSUtils; +import org.jline.utils.StyleResolver; + +/** + * Implements Groovy ScriptEngine. + * You must be very careful when using GroovyEngine in a multithreaded environment. The Binding instance is not + * thread safe, and it is shared by all scripts. + * + * @author Matti Rinta-Nikkola + */ +public class GroovyEngine implements ScriptEngine { + public enum Format {JSON, GROOVY, NONE} + + public static final String CANONICAL_NAMES = "canonicalNames"; + public static final String NANORC_SYNTAX = "nanorcSyntax"; + public static final String NANORC_VALUE = "nanorcValue"; + public static final String GROOVY_COLORS = "GROOVY_COLORS"; + public static final String NO_SYNTAX_CHECK = "noSyntaxCheck"; + public static final String RESTRICTED_COMPLETION = "restrictedCompletion"; + public static final String ALL_FIELDS_COMPLETION = "allFieldsCompletion"; + public static final String ALL_METHODS_COMPLETION = "allMethodsCompletion"; + public static final String ALL_CONSTRUCTORS_COMPLETION = "allConstructorsCompletion"; + public static final String ALL_CLASSES_COMPLETION = "allClassesCompletion"; + public static final String IDENTIFIERS_COMPLETION = "identifiersCompletion"; + public static final String META_METHODS_COMPLETION = "metaMethodsCompletion"; + + private static final String VAR_GROOVY_OPTIONS = "GROOVY_OPTIONS"; + private static final String REGEX_SYSTEM_VAR = "[A-Z]+[A-Z_]*"; + private static final String REGEX_VAR = "[a-zA-Z_]+[a-zA-Z0-9_]*"; + private static final Pattern PATTERN_FUNCTION_DEF = Pattern.compile( + "^def\\s+(" + REGEX_VAR + ")\\s*\\(([a-zA-Z0-9_ ,]*)\\)\\s*\\{(.*)?}(|\n)$" + , Pattern.DOTALL); + private static final Pattern PATTERN_CLASS_DEF = Pattern.compile("^class\\s+(" + REGEX_VAR + ") .*?\\{.*?}(|\n)$" + , Pattern.DOTALL); + private static final Pattern PATTERN_CLASS_NAME = Pattern.compile("(.*?)\\.([A-Z].*)"); + private static final List DEFAULT_IMPORTS = Arrays.asList("java.lang.*", "java.util.*", "java.io.*" + , "java.net.*", "groovy.lang.*", "groovy.util.*" + , "java.math.BigInteger", "java.math.BigDecimal"); + private final Map> defaultNameClass = new HashMap<>(); + private final GroovyShell shell; + protected Binding sharedData; + private final Map imports = new HashMap<>(); + private final Map methods = new HashMap<>(); + private final Map> nameClass; + private Cloner objectCloner = new ObjectCloner(); + + public interface Cloner { + Object clone(Object obj); + void markCache(); + void purgeCache(); + } + + public GroovyEngine() { + this.sharedData = new Binding(); + shell = new GroovyShell(sharedData); + for (String s : DEFAULT_IMPORTS) { + addToNameClass(s, defaultNameClass); + } + nameClass = new HashMap<>(defaultNameClass); + } + + @Override + public Completer getScriptCompleter() { + return compileCompleter(); + } + + @Override + public boolean hasVariable(String name) { + return sharedData.hasVariable(name); + } + + @Override + public void put(String name, Object value) { + sharedData.setProperty(name, value); + } + + @Override + public Object get(String name) { + return sharedData.hasVariable(name) ? sharedData.getVariable(name) : null; + } + + @SuppressWarnings("unchecked") + @Override + public Map find(String name) { + Map out = new HashMap<>(); + if (name == null) { + out = sharedData.getVariables(); + } else { + for (String v : internalFind(name)) { + out.put(v, get(v)); + } + } + return out; + } + + @Override + public List getSerializationFormats() { + return Arrays.asList(Format.JSON.toString(), Format.NONE.toString()); + } + + @Override + public List getDeserializationFormats() { + return Arrays.asList(Format.JSON.toString(), Format.GROOVY.toString(), Format.NONE.toString()); + } + + @Override + public Object deserialize(String value, String formatStr) { + Object out = value; + Format format = formatStr != null && !formatStr.isEmpty() ? Format.valueOf(formatStr.toUpperCase()) : null; + if (format == Format.NONE) { + // do nothing + } else if (format == Format.JSON) { + out = Utils.toObject(value); + } else if (format == Format.GROOVY) { + try { + out = execute(value); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage()); + } + } else { + value = value.trim(); + boolean hasCurly = value.contains("{") && value.contains("}"); + try { + if (value.startsWith("[") && value.endsWith("]")) { + try { + if (hasCurly) { + out = Utils.toObject(value); // try json + } else { + out = execute(value); + } + } catch (Exception e) { + if (hasCurly) { + try { + out = execute(value); + } catch (Exception e2) { + // ignore + } + } else { + out = Utils.toObject(value); // try json + } + } + } else if (value.startsWith("{") && value.endsWith("}")) { + out = Utils.toObject(value); + } + } catch (Exception e) { + // ignore + } + } + return out; + } + + @Override + public void persist(Path file, Object object) { + persist(file, object, getSerializationFormats().get(0)); + } + + @Override + public void persist(Path file, Object object, String format) { + Utils.persist(file, object, Format.valueOf(format.toUpperCase())); + } + + @Override + public Object execute(File script, Object[] args) throws Exception { + sharedData.setProperty("_args", args); + Script s = shell.parse(script); + return s.run(); + } + + private static Set> classesForPackage(String pckgname) throws ClassNotFoundException { + String name = pckgname; + Matcher matcher = PATTERN_CLASS_NAME.matcher(name); + if (matcher.matches()) { + name = matcher.group(1) + ".**"; + } + Set> out = new HashSet<>(PackageHelper.getClassesForPackage(name)); + if (out.isEmpty()) { + out.addAll(JrtJavaBasePackages.getClassesForPackage(name)); + } + return out; + } + + private void addToNameClass(String name) { + addToNameClass(name, nameClass); + } + + private void addToNameClass(String name, Map> nameClass) { + try { + if (name.endsWith(".*")) { + for (Class c : classesForPackage(name)) { + nameClass.put(c.getSimpleName(), c); + } + } else { + Class clazz = classResolver(name); + if (clazz != null) { + nameClass.put(clazz.getSimpleName(), clazz); + } + } + } catch (Exception e) { + // ignore + } + } + + @Override + public Object execute(String statement) throws Exception { + Object out = null; + if (statement.startsWith("import ")) { + shell.evaluate(statement); + String[] p = statement.split("\\s+", 2); + String classname = p[1].replaceAll(";", ""); + imports.put(classname, statement); + addToNameClass(classname); + } else if (statement.equals("import")) { + out = new ArrayList<>(imports.keySet()); + } else if (functionDef(statement)) { + // do nothing + } else if (statement.equals("def")) { + out = methods; + } else if (statement.matches("def\\s+" + REGEX_VAR)) { + String name = statement.split("\\s+")[1]; + if (methods.containsKey(name)) { + out = "def " + name + methods.get(name); + } + } else { + StringBuilder e = new StringBuilder(); + for (Map.Entry entry : imports.entrySet()) { + e.append(entry.getValue()).append("\n"); + } + e.append(statement); + if (classDef(statement)) { + e.append("; null"); + } + out = shell.evaluate(e.toString()); + } + return out; + } + + @Override + public Object execute(Object closure, Object... args) { + if (!(closure instanceof Closure)) { + throw new IllegalArgumentException(); + } + return ((Closure)closure).call(args); + } + + @Override + public String getEngineName() { + return this.getClass().getSimpleName(); + } + + @Override + public List getExtensions() { + return Collections.singletonList("groovy"); + } + + @SuppressWarnings("unchecked") + private List internalFind(String var) { + List out = new ArrayList<>(); + if(!var.contains(".") && var.contains("*")) { + var = var.replaceAll("\\*", ".*"); + } + for (String v : (Set)sharedData.getVariables().keySet()) { + if (v.matches(var)) { + out.add(v); + } + } + return out; + } + + private boolean functionDef(String statement) throws Exception{ + boolean out = false; + Matcher m = PATTERN_FUNCTION_DEF.matcher(statement); + if (m.matches()) { + out = true; + put(m.group(1), execute("{" + m.group(2) + "->" + m.group(3) + "}")); + methods.put(m.group(1), "(" + m.group(2) + ")" + "{" + m.group(3) + "}"); + } + return out; + } + + private boolean classDef(String statement) { + return PATTERN_CLASS_DEF.matcher(statement).matches(); + } + + private void refreshNameClass() { + nameClass.clear(); + nameClass.putAll(defaultNameClass); + for (String name : imports.keySet()) { + addToNameClass(name); + } + } + + private void del(String var) { + if (var == null) { + return; + } + if (imports.containsKey(var)) { + imports.remove(var); + if (var.endsWith(".*")) { + refreshNameClass(); + } else { + nameClass.remove(var.substring(var.lastIndexOf('.') + 1)); + } + } else if (sharedData.hasVariable(var)) { + sharedData.getVariables().remove(var); + methods.remove(var); + } else if (!var.contains(".") && var.contains("*")) { + for (String v : internalFind(var)){ + if (sharedData.hasVariable(v) && !v.equals("_") && !v.matches(REGEX_SYSTEM_VAR)) { + sharedData.getVariables().remove(v); + methods.remove(v); + } + } + } + } + + @Override + public void del(String... vars) { + if (vars == null) { + return; + } + for (String s: vars) { + del(s); + } + } + + @Override + public String toJson(Object obj) { + return Utils.toJson(obj); + } + + @Override + public String toString(Object obj) { + return Utils.toString(obj); + } + + @Override + public Map toMap(Object obj) { + return Utils.toMap(obj); + } + + public void setObjectCloner(Cloner objectCloner) { + this.objectCloner = objectCloner; + } + + public Cloner getObjectCloner() { + return objectCloner; + } + + public CmdDesc scriptDescription(CmdLine line) { + return new Inspector(this).scriptDescription(line); + } + + @SuppressWarnings("unchecked") + protected Map groovyOptions() { + return hasVariable(VAR_GROOVY_OPTIONS) ? (Map) get(VAR_GROOVY_OPTIONS) + : new HashMap<>(); + } + + protected T groovyOption(String option, T defval) { + return groovyOption(groovyOptions(), option, defval); + } + + @SuppressWarnings("unchecked") + protected static T groovyOption(Map options, String option, T defval) { + T out = defval; + try { + out = (T) options.getOrDefault(option, defval); + } catch (Exception e) { + // ignore + } + return out; + } + + private Completer compileCompleter() { + List completers = new ArrayList<>(); + completers.add(new ArgumentCompleter(new StringsCompleter("class", "print", "println"), NullCompleter.INSTANCE)); + completers.add(new ArgumentCompleter(new StringsCompleter("def"), new StringsCompleter(methods::keySet) + , NullCompleter.INSTANCE)); + completers.add(new ArgumentCompleter(new StringsCompleter("import") + , new PackageCompleter(CandidateType.PACKAGE, this), NullCompleter.INSTANCE)); + completers.add(new MethodCompleter(this)); + return new AggregateCompleter(completers); + } + + private enum CandidateType {CONSTRUCTOR, STATIC_METHOD, PACKAGE, METHOD, FIELD, IDENTIFIER, META_METHOD, OTHER} + + private static Class classResolver(String classDotName) { + Class out = null; + Matcher matcher = PATTERN_CLASS_NAME.matcher(classDotName); + if (matcher.matches()) { + String classname = matcher.group(2).replaceAll("\\.", "\\$"); + try { + out = Class.forName(matcher.group(1) + "." + classname); + } catch (ClassNotFoundException ex) { + if (Log.isDebugEnabled()) { + ex.printStackTrace(); + } + } + } + return out; + } + + protected static class AccessRules { + protected final boolean allMethods; + protected final boolean allFields; + protected final boolean allConstructors; + protected final boolean allClasses; + + public AccessRules() { + this(new HashMap<>()); + } + + public AccessRules(Map options) { + this.allMethods = groovyOption(options, ALL_METHODS_COMPLETION, false); + this.allFields = groovyOption(options, ALL_FIELDS_COMPLETION, false); + this.allConstructors = groovyOption(options, ALL_CONSTRUCTORS_COMPLETION, false); + this.allClasses = groovyOption(options, ALL_CLASSES_COMPLETION, false); + } + } + + private static class Helpers { + + private static Set loadedPackages() { + Set out = new HashSet<>(); + for (Package p : Package.getPackages()) { + out.add(p.getName()); + } + return out; + } + + private static Set names(String domain) { + Set out = new HashSet<>(); + for (String p : loadedPackages()) { + if (p.startsWith(domain)) { + int idx = p.indexOf('.', domain.length()); + if (idx < 0) { + idx = p.length(); + } + out.add(p.substring(domain.length(), idx)); + } + } + return out; + } + + public static Set getClassMethods(Class clazz, boolean all) { + Set out = new HashSet<>(Arrays.asList(clazz.getMethods())); + if (all) { + out.addAll(Arrays.asList(clazz.getDeclaredMethods())); + } + return out; + } + + public static Set getMethods(Class clazz, boolean all) { + return getMethods(clazz, all, false, false); + } + + public static Set getStaticMethods(Class clazz, boolean all) { + return getMethods(clazz, all, true, false); + } + + public static boolean noStaticMethods(Class clazz, boolean all) { + return getMethods(clazz, all, true, true).isEmpty(); + } + + private static Set getMethods(Class clazz, boolean all, boolean statc, boolean firstOnly) { + Set out = new HashSet<>(); + try { + for (Method method : getClassMethods(clazz, all)) { + if ((statc && Modifier.isStatic(method.getModifiers())) + || (!statc && !Modifier.isStatic(method.getModifiers()))) { + out.add(method.getName()); + if (firstOnly) { + break; + } + } + } + } catch (NoClassDefFoundError e) { + // ignore + } + return out; + } + + public static Map getFields(Class clazz, boolean all) { + return getFields(clazz, all, false, false); + } + + public static Map getStaticFields(Class clazz, boolean all) { + return getFields(clazz, all, true, false); + } + + public static boolean noStaticFields(Class clazz, boolean all) { + return getFields(clazz, all, true, true).isEmpty(); + } + + private static Map getFields(Class clazz, boolean all, boolean statc, boolean firstOnly) { + Map out = new HashMap<>(); + for (Field field : all ? clazz.getDeclaredFields() : clazz.getFields()) { + if ((statc && Modifier.isStatic(field.getModifiers())) + || (!statc && !Modifier.isStatic(field.getModifiers()))) { + out.put(field.getName(), field.getType().getSimpleName()); + if (firstOnly) { + break; + } + } + } + return out; + } + + public static Set nextDomain(String domain, CandidateType type) { + return nextDomain(domain, new AccessRules(), type); + } + + public static Set nextDomain(String domain, AccessRules access, CandidateType type) { + Set out = new HashSet<>(); + if (domain.isEmpty()) { + for (String p : loadedPackages()) { + out.add(p.split("\\.")[0]); + } + } else if ((domain.split("\\.")).length < 2) { + out = names(domain); + } else { + try { + for (Class c : classesForPackage(domain)) { + try { + if ((!Modifier.isPublic(c.getModifiers()) && !access.allClasses) || c.getCanonicalName() == null) { + continue; + } + if ((type == CandidateType.CONSTRUCTOR && (c.getConstructors().length == 0 + || Modifier.isAbstract(c.getModifiers()))) + || (type == CandidateType.STATIC_METHOD && noStaticMethods(c, access.allMethods) + && noStaticFields(c, access.allFields))) { + continue; + } + String name = c.getCanonicalName(); + Log.debug(name); + if (name.startsWith(domain)) { + int idx = name.indexOf('.', domain.length()); + if (idx < 0) { + idx = name.length(); + } + out.add(name.substring(domain.length(), idx)); + } + } catch (NoClassDefFoundError e) { + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + } + } + } catch (ClassNotFoundException e) { + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + out = names(domain); + } + } + return out; + } + + private static Map listToMap(Collection list) { + return list.stream() + .collect(Collectors.toMap(it -> it, it -> "")); + } + + public static void doCandidates(List candidates, Collection fields, String curBuf, String hint, + CandidateType type) { + doCandidates(candidates, listToMap(fields), curBuf, hint, type); + } + + public static void doCandidates(List candidates, Map fields, String curBuf, String hint + , CandidateType type) { + if (fields == null) { + return; + } + for (Map.Entry entry : fields.entrySet()) { + String group = null; + String desc = entry.getValue().isEmpty() ? null : entry.getValue(); + String s = entry.getKey(); + if (s == null || !s.startsWith(hint)) { + continue; + } + String postFix = ""; + if (type == CandidateType.CONSTRUCTOR) { + if (s.matches("[a-z]+.*")) { + postFix = "."; + } else if (s.matches("[A-Z]+.*")) { + postFix = "("; + } + } else if (type == CandidateType.PACKAGE) { + if (s.matches("[a-z]+.*")) { + postFix = "."; + } + } else if (type == CandidateType.METHOD) { + postFix = "("; + group = "Methods"; + } else if (type == CandidateType.FIELD) { + group = "Fields"; + } else if (type == CandidateType.IDENTIFIER) { + group = "Identifiers"; + if (s.contains("-") || s.contains("+") || s.contains(" ") || s.contains("#") + || !s.matches("[a-zA-Z$_].*")){ + continue; + } + } else if (type == CandidateType.META_METHOD) { + postFix = "("; + group = "MetaMethods"; + } + candidates.add(new Candidate(AttributedString.stripAnsi(curBuf + s + postFix), s, group, desc, null + ,null, false)); + } + } + + public static int statementBegin(String buffer) { + String buf = buffer; + while (buf.matches(".*\\)\\.\\w+$")) { + int idx = buf.lastIndexOf("."); + int openingRound = Brackets.indexOfOpeningRound(buf.substring(0,idx)); + buf = buf.substring(0,openingRound); + } + return statementBegin(new Brackets(buf)); + } + + public static int statementBegin(String buffer, String wordbuffer, Brackets brackets) { + int out = -1; + int idx = buffer.lastIndexOf(wordbuffer); + if (idx > -1) { + out = statementBegin(brackets.lastDelim() - idx + , brackets.lastOpenRound() - idx + , brackets.lastComma() - idx + , brackets.lastOpenCurly() - idx + , brackets.lastCloseCurly() - idx + , brackets.lastSemicolon() - idx); + } + return out; + } + + private static int statementBegin(Brackets brackets) { + return statementBegin(brackets.lastDelim() + , brackets.lastOpenRound() + , brackets.lastComma() + , brackets.lastOpenCurly(), brackets.lastCloseCurly(), brackets.lastSemicolon()); + } + + private static int statementBegin(int lastDelim, int openRound, int comma, int openCurly, int closeCurly, int semicolon) { + int out = lastDelim; + if (openRound > out) { + out = openRound; + } + if (comma > out) { + out = comma; + } + if (openCurly > out) { + out = openCurly; + } + if (closeCurly > out) { + out = closeCurly; + } + if (semicolon > out) { + out = semicolon; + } + return Math.max(out, -1); + } + + public static boolean constructorStatement(String fragment) { + return fragment.matches("(.*\\s+new|.*\\(new|.*\\{new|.*=new|.*,new|new)"); + } + + } + + private static class PackageCompleter implements Completer { + private final CandidateType type; + private final GroovyEngine groovyEngine; + + public PackageCompleter(CandidateType type, GroovyEngine groovyEngine) { + this.type = type; + this.groovyEngine = groovyEngine; + } + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + String buffer = commandLine.word().substring(0, commandLine.wordCursor()); + String param = buffer; + String curBuf = ""; + int lastDelim = buffer.lastIndexOf('.'); + if (lastDelim > -1) { + param = buffer.substring(lastDelim + 1); + curBuf = buffer.substring(0, lastDelim + 1); + } + Helpers.doCandidates(candidates + , Helpers.nextDomain(curBuf, new AccessRules(groovyEngine.groovyOptions()), type) + , curBuf, param, type); + } + + } + + private static class MethodCompleter implements Completer { + private static final List VALUES = Arrays.asList("true", "false"); + private final GroovyEngine groovyEngine; + private final SystemRegistry systemRegistry = SystemRegistry.get(); + private Inspector inspector; + private AccessRules access; + private boolean metaMethodCompletion; + private boolean identifierCompletion; + + public MethodCompleter(GroovyEngine engine){ + this.groovyEngine = engine; + } + + @Override + public void complete(LineReader reader, ParsedLine commandLine, List candidates) { + assert commandLine != null; + assert candidates != null; + if (systemRegistry.isCommandOrScript(commandLine) + || (commandLine.wordIndex() > 0 && commandLine.words().get(0).equals("import"))) { + return; + } + String wordbuffer = commandLine.word(); + String buffer = commandLine.line().substring(0, commandLine.cursor()); + Brackets brackets; + try { + brackets = new Brackets(buffer); + } catch (Exception e) { + return; + } + if (brackets.openQuote()) { + return; + } + boolean restrictedCompletion = groovyEngine.groovyOption(RESTRICTED_COMPLETION, false); + metaMethodCompletion = groovyEngine.groovyOption(META_METHODS_COMPLETION, false); + identifierCompletion = groovyEngine.groovyOption(IDENTIFIERS_COMPLETION, false); + access = new AccessRules(groovyEngine.groovyOptions()); + inspector = new Inspector(groovyEngine); + inspector.loadStatementVars(buffer); + int eqsep = Helpers.statementBegin(buffer); + if (brackets.numberOfRounds() > 0 && brackets.lastCloseRound() > eqsep) { + int varsep = buffer.lastIndexOf('.'); + if (varsep > 0 && varsep > brackets.lastCloseRound() && !restrictedCompletion) { + Class clazz = inspector.evaluateClass(buffer.substring(eqsep + 1, varsep)); + Object involvedObject = inspector.getInvolvedObject(); + int vs = wordbuffer.lastIndexOf('.'); + String curBuf = wordbuffer.substring(0, vs + 1); + String hint = wordbuffer.substring(vs + 1); + doMethodCandidates(candidates, involvedObject == null ? clazz : involvedObject, curBuf, hint); + } + } else if (completingConstructor(commandLine)) { + if (wordbuffer.matches("[a-z]+.*")) { + int idx = wordbuffer.lastIndexOf('.'); + if (idx > 0 && wordbuffer.substring(idx + 1).matches("[A-Z]+.*")) { + try { + Class.forName(wordbuffer); + Helpers.doCandidates(candidates, Collections.singletonList("("), wordbuffer, "(" + , CandidateType.OTHER); + } catch (Exception e) { + String param = wordbuffer.substring(0, idx + 1); + Helpers.doCandidates(candidates + , Helpers.nextDomain(param, CandidateType.CONSTRUCTOR) + , param, wordbuffer.substring(idx + 1), CandidateType.CONSTRUCTOR); + } + } else { + new PackageCompleter(CandidateType.CONSTRUCTOR, groovyEngine).complete(reader, commandLine, candidates); + } + } else { + Helpers.doCandidates(candidates, retrieveConstructors(access.allConstructors), "", wordbuffer + , CandidateType.CONSTRUCTOR); + } + } else { + boolean addKeyWords = eqsep == brackets.lastSemicolon() || eqsep == brackets.lastOpenCurly(); + int varsep = wordbuffer.lastIndexOf('.'); + eqsep = Helpers.statementBegin(buffer, wordbuffer, brackets); + String param = wordbuffer.substring(eqsep + 1); + if (param.trim().length() == 0) { + // do nothing + } else if (varsep < 0 || varsep < eqsep) { + String curBuf = wordbuffer.substring(0, eqsep + 1); + if (addKeyWords) { + Helpers.doCandidates(candidates, ObjectInspector.GLOBAL_META_METHODS, curBuf, param + , CandidateType.METHOD); + } else { + Helpers.doCandidates(candidates, VALUES, curBuf, param, CandidateType.OTHER); + } + Helpers.doCandidates(candidates, inspector.variables(), curBuf, param, CandidateType.OTHER); + Helpers.doCandidates(candidates, retrieveClassesWithStaticMethods(), curBuf, param, CandidateType.PACKAGE); + } else { + boolean firstMethod = param.indexOf('.') == param.lastIndexOf('.'); + String var = param.substring(0, param.indexOf('.')); + String curBuf = wordbuffer.substring(0, varsep + 1); + String p = wordbuffer.substring(varsep + 1); + if (inspector.nameClass().containsKey(var)) { + if (firstMethod) { + doStaticMethodCandidates(candidates, inspector.nameClass().get(var), curBuf, p); + } else if (!restrictedCompletion) { + Class clazz = inspector.evaluateClass(wordbuffer.substring(eqsep + 1, varsep)); + Object involvedObject = inspector.getInvolvedObject(); + doMethodCandidates(candidates, involvedObject == null ? clazz : involvedObject, curBuf, p); + } + } else if (inspector.hasVariable(var)) { + if (firstMethod) { + doMethodCandidates(candidates, inspector.getVariable(var), curBuf, p); + } else if (!restrictedCompletion) { + Class clazz = inspector.evaluateClass(wordbuffer.substring(eqsep + 1, varsep)); + Object involvedObject = inspector.getInvolvedObject(); + doMethodCandidates(candidates, involvedObject == null ? clazz : involvedObject, curBuf, p); + } + } else { + try { + param = wordbuffer.substring(eqsep + 1, varsep); + Class clazz = classResolver(param); + if (clazz != null) { + doStaticMethodCandidates(candidates, clazz, curBuf, p); + } + } catch (Exception e) { + // ignore + } finally { + param = wordbuffer.substring(eqsep + 1, varsep + 1); + Helpers.doCandidates(candidates + , Helpers.nextDomain(param, CandidateType.STATIC_METHOD) + , curBuf, p, CandidateType.PACKAGE); + } + } + } + } + } + + private boolean completingConstructor(ParsedLine commandLine) { + return !commandLine.word().contains("(") && ( + (commandLine.wordIndex() == 1 && commandLine.words().get(0).matches("(new|\\w+=[{]?new)")) + || + (commandLine.wordIndex() > 1 + && Helpers.constructorStatement(commandLine.words().get(commandLine.wordIndex() - 1))) + ); + } + + @SuppressWarnings("unchecked") + private void doIdentifierCandidates(List candidates, Object object, String curBuf, String hint) { + if (!(object instanceof Map)) { + return; + } + Map map = (Map)object; + if (map.isEmpty() || !(map.keySet().iterator().next() instanceof String)) { + return; + } + Helpers.doCandidates(candidates, (Set)map.keySet(), curBuf, hint, CandidateType.IDENTIFIER); + } + + private Set doMetaMethodCandidates(List candidates, Object object, String curBuf, String hint) { + ObjectInspector inspector = new ObjectInspector(object); + List> mms = inspector.metaMethods(false); + Set metaMethods = new HashSet<>(); + for (Map mm : mms) { + metaMethods.add(mm.get(ObjectInspector.FIELD_NAME)); + } + Helpers.doCandidates(candidates, metaMethods, curBuf, hint, CandidateType.META_METHOD); + return metaMethods; + } + + private void doMethodCandidates(List candidates, Object object, String curBuf, String hint) { + if (object == null) { + return; + } + Set metaMethods = null; + if (identifierCompletion) { + doIdentifierCandidates(candidates, object, curBuf, hint); + } + if (metaMethodCompletion) { + metaMethods = doMetaMethodCandidates(candidates, object, curBuf, hint); + } + doMethodCandidates(candidates, object.getClass(), curBuf, hint + , identifierCompletion && !(object instanceof Map), metaMethods); + } + + private void doMethodCandidates(List candidates, Class clazz, String curBuf, String hint + , boolean addIdentifiers, Set metaMethods) { + if (clazz == null) { + return; + } + Set methods = Helpers.getMethods(clazz, access.allMethods); + if (addIdentifiers) { + Set identifiers = new HashSet<>(); + for (String m : methods) { + if (m.matches("get[A-Z].*")) { + Class cc = clazz; + while (cc != null) { + try { + try { + cc.getMethod(m); + } catch (NoSuchMethodException exp) { + cc.getDeclaredMethod(m); + } + char[] c = m.substring(3).toCharArray(); + c[0] = Character.toLowerCase(c[0]); + identifiers.add(new String(c)); + break; + } catch (NoSuchMethodException e) { + cc = cc.getSuperclass(); + } + } + } + } + Helpers.doCandidates(candidates, identifiers, curBuf, hint, CandidateType.IDENTIFIER); + } + if (metaMethods != null) { + for (String mm : metaMethods) { + methods.remove(mm); + } + } + Helpers.doCandidates(candidates, methods, curBuf, hint, CandidateType.METHOD); + Helpers.doCandidates(candidates, Helpers.getFields(clazz, access.allFields), curBuf, hint, CandidateType.FIELD); + } + + private void doStaticMethodCandidates(List candidates, Class clazz, String curBuf, String hint) { + if (clazz == null) { + return; + } + Helpers.doCandidates(candidates, Helpers.getStaticMethods(clazz, access.allMethods), curBuf, hint + , CandidateType.METHOD); + Helpers.doCandidates(candidates, Helpers.getStaticFields(clazz, access.allFields), curBuf, hint + , CandidateType.FIELD); + } + + private Set retrieveConstructors(boolean all) { + Set out = new HashSet<>(); + for (Iterator>> it = inspector.nameClass().entrySet().iterator(); it.hasNext(); ) { + Map.Entry> entry = it.next(); + Class c = entry.getValue(); + try { + if ((!all && c.getConstructors().length == 0) || (all && c.getDeclaredConstructors().length == 0) + || Modifier.isAbstract(c.getModifiers())) { + continue; + } + out.add(entry.getKey()); + } catch (NoClassDefFoundError e) { + it.remove(); + } + } + return out; + } + + private Set retrieveClassesWithStaticMethods() { + Set out = new HashSet<>(); + for (Iterator>> it = inspector.nameClass().entrySet().iterator(); it.hasNext(); ) { + Map.Entry> entry = it.next(); + Class c = entry.getValue(); + try { + if (Helpers.noStaticMethods(c, access.allMethods) && Helpers.noStaticFields(c, access.allFields)) { + continue; + } + out.add(entry.getKey()); + } catch (NoClassDefFoundError e) { + it.remove(); + } + } + return out; + } + } + + private static class Inspector { + static final Pattern PATTERN_FOR = Pattern.compile("^for\\s*\\((.*?)"); + static final Pattern PATTERN_FOR_EACH = Pattern.compile("^for\\s*\\((.*?):(.*?)\\).*"); + static final Pattern PATTERN_LAMBDA = Pattern.compile(".*\\([(]*(.*?)[)]*->.*"); + static final Pattern PATTERN_FUNCTION_BODY = Pattern.compile("^\\s*\\(([a-zA-Z0-9_ ,]*)\\)\\s*\\{(.*)?}(|\n)$" + , Pattern.DOTALL); + static final Pattern PATTERN_FUNCTION = Pattern.compile("\\s*def\\s+\\w+\\s*\\((.*?)\\).*"); + static final Pattern PATTERN_CLOSURE = Pattern.compile(".*\\{(.*?)->.*"); + static final Pattern PATTERN_TYPE_VAR = Pattern.compile("(\\w+)\\s+(\\w+)"); + static final String DEFAULT_NANORC_SYNTAX = "classpath:/org/jline/groovy/java.nanorc"; + static final String DEFAULT_GROOVY_COLORS = "ti=1;34:me=31"; + + private final GroovyShell shell; + protected Binding sharedData = new Binding(); + private final Map imports; + private final Map> nameClass; + private PrintStream nullstream; + private boolean canonicalNames = false; + private final boolean noSyntaxCheck; + private final boolean restrictedCompletion; + private final boolean metaMethodsCompletion; + private final AccessRules access; + private String[] equationLines; + private int cuttedSize; + private final String nanorcSyntax; + private final String groovyColors; + private Object involvedObject = null; + + public Inspector(GroovyEngine groovyEngine) { + this.imports = groovyEngine.imports; + this.nameClass = groovyEngine.nameClass; + this.canonicalNames = groovyEngine.groovyOption(CANONICAL_NAMES, canonicalNames); + this.nanorcSyntax = groovyEngine.groovyOption(NANORC_SYNTAX, DEFAULT_NANORC_SYNTAX); + this.noSyntaxCheck = groovyEngine.groovyOption(NO_SYNTAX_CHECK, false); + this.restrictedCompletion = groovyEngine.groovyOption(RESTRICTED_COMPLETION, false); + this.metaMethodsCompletion = groovyEngine.groovyOption(META_METHODS_COMPLETION, false); + this.access = new AccessRules(groovyEngine.groovyOptions()); + String gc = groovyEngine.groovyOption(GROOVY_COLORS, null); + groovyColors = gc != null && Styles.isAnsiStylePattern(gc) ? gc : DEFAULT_GROOVY_COLORS; + groovyEngine.getObjectCloner().markCache(); + for (Map.Entry entry : groovyEngine.find().entrySet()) { + Object obj = groovyEngine.getObjectCloner().clone(entry.getValue()); + sharedData.setVariable(entry.getKey(), obj); + } + groovyEngine.getObjectCloner().purgeCache(); + shell = new GroovyShell(sharedData); + try { + File file = OSUtils.IS_WINDOWS ? new File("NUL") : new File("/dev/null"); + OutputStream outputStream = new FileOutputStream(file); + nullstream = new PrintStream(outputStream); + } catch (Exception e) { + // ignore + } + for (Map.Entry entry : groovyEngine.methods.entrySet()) { + Matcher m = PATTERN_FUNCTION_BODY.matcher(entry.getValue()); + if (m.matches() && sharedData.hasVariable(entry.getKey()) + && sharedData.getVariable(entry.getKey()) instanceof Closure) { + sharedData.setVariable(entry.getKey(), execute("{" + m.group(1) + "->" + m.group(2) + "}")); + } + } + } + + public Object getInvolvedObject() { + return involvedObject; + } + + public Class evaluateClass(String objectStatement) { + Class out = null; + try { + involvedObject = execute(objectStatement); + out = involvedObject.getClass(); + } catch (Exception e) { + // ignore + } + try { + if (out == null || out == Class.class) { + if (!objectStatement.contains(".") ) { + out = (Class)execute(objectStatement + ".class"); + } else { + out = Class.forName(objectStatement); + } + } + } catch (Exception e) { + // ignore + } + return out; + } + + private Object execute(String statement) { + PrintStream origOut = System.out; + PrintStream origErr = System.err; + if (nullstream != null) { + System.setOut(nullstream); + System.setErr(nullstream); + } + Object out; + try { + StringBuilder e = new StringBuilder(); + for (Map.Entry entry : imports.entrySet()) { + e.append(entry.getValue()).append("\n"); + } + e.append(statement); + out = shell.evaluate(e.toString()); + } finally { + System.setOut(origOut); + System.setErr(origErr); + } + return out; + } + + private String stripVarType(String statement) { + if (statement.matches("\\w+\\s+\\w+.*")) { + int idx = statement.indexOf(' '); + return statement.substring(idx + 1); + } + return statement; + } + + private String defineArgs(String[] args) { + StringBuilder out = new StringBuilder(); + for (String v : args) { + Matcher matcher = PATTERN_TYPE_VAR.matcher(v.trim()); + if (matcher.matches()) { + out.append(constructVariable(matcher.group(1), matcher.group(2))); + } else { + out.append(v).append(" = null; "); + } + } + return out.toString(); + } + + private String constructVariable(String type, String name) { + String out = ""; + if (type.matches("[B|b]yte") || type.matches("[S|s]hort") + || type.equals("int") || type.equals("Integer") || type.matches("[L|l]ong") + || type.matches("[F|f]loat") || type.matches("[D|d]ouble") + || type.matches("[B|b]oolean") || type.equals("char") || type.equals("Character")) { + out = name + " = (" + type + ")0; "; + } else if (type.matches("[A-Z].*")) { + out = "try {" + name + " = new " + type + "() } catch (Exception e) {" + name + " = null}; "; + } + return out; + } + + public void loadStatementVars(String line) { + if (!new Brackets(line).openCurly()) { + return; + } + for (String s : line.split("\\r?\\n|;")) { + String statement = s.trim(); + boolean constructedStatement = true; + try { + Matcher forEachMatcher = PATTERN_FOR_EACH.matcher(statement); + Matcher forMatcher = PATTERN_FOR.matcher(statement); + Matcher lambdaMatcher = PATTERN_LAMBDA.matcher(statement); + Matcher functionMatcher = PATTERN_FUNCTION.matcher(statement); + Matcher closureMatcher = PATTERN_CLOSURE.matcher(statement); + Matcher typeVarMatcher = PATTERN_TYPE_VAR.matcher(statement); + if (statement.matches("^(if|while)\\s*\\(.*") || statement.matches("(}\\s*|^)else(\\s*\\{|$)") + || statement.matches("(}\\s*|^)else\\s+if\\s*\\(.*") || statement.matches("^break[;]+") + || statement.matches("^case\\s+.*:") || statement.matches("^default\\s+:") + || statement.matches("([{}])") || statement.length() == 0) { + continue; + } else if (forEachMatcher.matches()) { + statement = stripVarType(forEachMatcher.group(1).trim()); + String cc = forEachMatcher.group(2); + statement += "=" + cc + " instanceof Map ? " + cc + ".entrySet()[0] : " + cc + "[0]"; + } else if (forMatcher.matches()) { + statement = stripVarType(forMatcher.group(1).trim()); + if (!statement.contains("=")) { + statement += " = null"; + } + } else if (closureMatcher.matches()) { + statement = defineArgs(closureMatcher.group(1).split(",")); + } else if (functionMatcher.matches()) { + statement = defineArgs(functionMatcher.group(1).split(",")); + } else if (lambdaMatcher.matches()) { + statement = defineArgs(lambdaMatcher.group(1).split(",")); + } else if (statement.contains("=")) { + statement = stripVarType(statement); + constructedStatement = false; + } else if (typeVarMatcher.matches()) { + statement = constructVariable(typeVarMatcher.group(1), typeVarMatcher.group(2)); + } + Brackets br = new Brackets(statement); + if (statement.contains("=") && !br.openRound() && !br.openCurly() && !br.openSquare()) { + int idx = statement.indexOf('='); + String st = "null"; + if (restrictedCompletion && !constructedStatement && br.numberOfRounds() > 0) { + statement = statement.substring(0, idx + 1) + "null"; + } else { + st = statement.substring(idx + 1).trim(); + } + if (!st.isEmpty() && !st.equals("new") ) { + execute(statement); + } + } + } catch (Exception e) { + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + } + } + } + + public Map> nameClass() { + return nameClass; + } + + @SuppressWarnings("unchecked") + public Set variables() { + return sharedData.getVariables().keySet(); + } + + public boolean hasVariable(String name) { + return sharedData.hasVariable(name); + } + + public Object getVariable(String name) { + return sharedData.hasVariable(name) ? sharedData.getVariable(name) : null; + } + + public CmdDesc scriptDescription(CmdLine line) { + CmdDesc out = null; + try { + switch (line.getDescriptionType()) { + case COMMAND: + break; + case METHOD: + out = methodDescription(line); + break; + case SYNTAX: + if (!noSyntaxCheck) { + out = checkSyntax(line); + } + break; + } + } catch (Throwable e) { + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + } + return out; + } + + private String trimName(String name) { + String out = name; + int idx = name.lastIndexOf('('); + if (idx > 0) { + out = name.substring(0, idx); + } + return out; + } + + private String accessModifier(int modifier, boolean all) { + String out = ""; + if (!all) { + return out; + } + if (Modifier.isPrivate(modifier)) { + out = "private "; + } else if (Modifier.isProtected(modifier)) { + out = "protected "; + } else if (Modifier.isPublic(modifier)) { + out = "public "; + } + return out; + } + + private CmdDesc methodDescription(CmdLine line) { + CmdDesc out = new CmdDesc(); + List args = line.getArgs(); + boolean constructor = false; + Class clazz = null; + String methodName = null; + String buffer = line.getHead(); + int eqsep = Helpers.statementBegin(buffer); + int varsep = buffer.lastIndexOf('.'); + if (varsep > 0 && varsep > eqsep) { + loadStatementVars(buffer); + methodName = buffer.substring(varsep + 1); + int ior = Brackets.indexOfOpeningRound(buffer.substring(0, varsep)); + if (ior > 0 && ior < eqsep) { + eqsep = ior; + } + String st = buffer.substring(eqsep + 1, varsep); + if (st.matches("[A-Z]+\\w+\\s*\\(.*")) { + st = "new " + st; + } + int nb = new Brackets(st).numberOfRounds(); + if (!restrictedCompletion || nb == 0) { + clazz = evaluateClass(st); + } + } else if (args.size() > 1 && Helpers.constructorStatement(args.get(args.size() - 2)) + && args.get(args.size() - 1).matches("[A-Z]+\\w+\\s*\\(.*") + && new Brackets(args.get(args.size() - 1)).openRound()) { + constructor = true; + clazz = evaluateClass(trimName(args.get(args.size() - 1))); + } + List mainDesc = new ArrayList<>(); + if (clazz != null) { + SyntaxHighlighter java = SyntaxHighlighter.build(nanorcSyntax); + mainDesc.add(java.highlight(clazz.toString())); + if (constructor) { + for (Constructor m : access.allConstructors ? clazz.getDeclaredConstructors() + : clazz.getConstructors()) { + StringBuilder sb = new StringBuilder(); + String name = m.getName(); + if (!canonicalNames) { + int idx = name.lastIndexOf('.'); + name = name.substring(idx + 1); + } + sb.append(accessModifier(m.getModifiers(), access.allConstructors)); + sb.append(name); + sb.append("("); + boolean first = true; + for (Class p : m.getParameterTypes()) { + if (!first) { + sb.append(", "); + } + sb.append(canonicalNames ? p.getTypeName() : p.getSimpleName()); + first = false; + } + sb.append(")"); + first = true; + for (Class e : m.getExceptionTypes()) { + if (first) { + sb.append(" throws "); + } else { + sb.append(", "); + } + sb.append(canonicalNames ? e.getCanonicalName() : e.getSimpleName()); + first = false; + } + mainDesc.add(java.highlight(trimMethodDescription(sb))); + } + } else { + List addedMethods = new ArrayList<>(); + if (metaMethodsCompletion && involvedObject != null) { + for (Map mm : new ObjectInspector(involvedObject).metaMethods(false)) { + if (!mm.get(ObjectInspector.FIELD_NAME).equals(methodName)) { + continue; + } + StringBuilder sb = new StringBuilder(); + String modifiers = mm.get(ObjectInspector.FIELD_MODIFIERS); + if (!access.allMethods) { + if (modifiers.equals("public")) { + modifiers = ""; + } else if (modifiers.startsWith("public ")) { + modifiers = modifiers.substring(7); + } + } + if (!modifiers.isEmpty()) { + sb.append(modifiers).append(" "); + } + sb.append(convertArrayParams(mm.get(ObjectInspector.FIELD_RETURN))).append(" "); + sb.append(methodName).append("("); + sb.append(convertArrayParams(mm.get(ObjectInspector.FIELD_PARAMETERS))); + sb.append(")"); + if (!addedMethods.contains(sb.toString())) { + addedMethods.add(sb.toString()); + mainDesc.add(java.highlight(trimMethodDescription(sb))); + } + } + } + do { + for (Method m : Helpers.getClassMethods(clazz, access.allMethods)) { + if (!m.getName().equals(methodName)) { + continue; + } + StringBuilder sb = new StringBuilder(); + sb.append(accessModifier(m.getModifiers(), access.allMethods)); + if (Modifier.isFinal(m.getModifiers())) { + sb.append("final "); + } + if (Modifier.isStatic(m.getModifiers())) { + sb.append("static "); + } + sb.append(canonicalNames ? m.getReturnType().getCanonicalName() : m.getReturnType().getSimpleName()); + sb.append(" "); + sb.append(methodName); + sb.append("("); + boolean first = true; + for (Class p : m.getParameterTypes()) { + if (!first) { + sb.append(", "); + } + sb.append(canonicalNames ? p.getTypeName() : p.getSimpleName()); + first = false; + } + sb.append(")"); + first = true; + for (Class e : m.getExceptionTypes()) { + if (first) { + sb.append(" throws "); + } else { + sb.append(", "); + } + sb.append(canonicalNames ? e.getCanonicalName() : e.getSimpleName()); + first = false; + } + if (!addedMethods.contains(sb.toString())) { + addedMethods.add(sb.toString()); + mainDesc.add(java.highlight(trimMethodDescription(sb))); + } + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + out.setMainDesc(mainDesc); + } + return out; + } + + private String convertArrayParams(String value) { + String out = value.replaceAll("\\[B", "byte[]"); + Pattern arrayPattern = Pattern.compile("(.*)\\[L.*\\.([A-Z].*?);(.*)"); + Matcher matcher = arrayPattern.matcher(value); + while (matcher.matches()) { + out = matcher.group(1) + matcher.group(2) + "[]" + matcher.group(3); + matcher = arrayPattern.matcher(out); + } + return out; + } + + private String trimMethodDescription(StringBuilder sb) { + String out = sb.toString(); + if (canonicalNames) { + out = out.replaceAll("java\\.lang\\.", ""); + } + return out; + } + + private CmdDesc checkSyntax(CmdLine line) { + CmdDesc out = new CmdDesc(); + int openingRound = Brackets.indexOfOpeningRound(line.getHead()); + if (openingRound == -1) { + return out; + } + String cuttedLine = line.getHead().substring(0, openingRound); + if (new Brackets(cuttedLine).openQuote()) { + return out; + } + loadStatementVars(line.getHead()); + int eqsep = Helpers.statementBegin(cuttedLine); + int end = line.getHead().length(); + if (eqsep > 0 && Helpers.constructorStatement(line.getHead().substring(0, eqsep))) { + eqsep = line.getHead().substring(0, eqsep).lastIndexOf("new") - 1; + } else if (line.getHead().substring(eqsep + 1).matches("\\s*for\\s*\\(.*") + || line.getHead().substring(eqsep + 1).matches("\\s*while\\s*\\(.*") + || line.getHead().substring(eqsep + 1).matches("\\s*else\\s+if\\s*\\(.*") + || line.getHead().substring(eqsep + 1).matches("\\s*if\\s*\\(.*")) { + eqsep = openingRound; + end = end - 1; + } else if (line.getHead().substring(eqsep + 1).matches("\\s*switch\\s*\\(.*") + || line.getHead().substring(eqsep + 1).matches("\\s*def\\s+\\w+\\s*\\(.*") + || line.getHead().substring(eqsep + 1).matches("\\s*catch\\s*\\(.*")) { + return out; + } + List mainDesc = new ArrayList<>(); + String objEquation = line.getHead().substring(eqsep + 1, end).trim(); + equationLines = objEquation.split("\\r?\\n"); + cuttedSize = eqsep + 1; + if (objEquation.matches("\\(\\s*\\w+\\s*[,\\s*\\w+]*\\)") + || objEquation.matches("\\(\\s*\\)")) { + // do nothing + } else { + try { + execute(objEquation); + } catch (groovy.lang.MissingPropertyException e) { + mainDesc.addAll(doExceptionMessage(e)); + out.setErrorPattern(Pattern.compile("\\b" + e.getProperty() + "\\b")); + } catch (java.util.regex.PatternSyntaxException e) { + mainDesc.addAll(doExceptionMessage(e)); + int idx = line.getHead().lastIndexOf(e.getPattern()); + if (idx >= 0) { + out.setErrorIndex(idx + e.getIndex()); + } + } catch (org.codehaus.groovy.control.MultipleCompilationErrorsException e) { + if (e.getErrorCollector().getErrors() != null) { + for (Object o : e.getErrorCollector().getErrors()) { + if (o instanceof SyntaxErrorMessage) { + SyntaxErrorMessage sem = (SyntaxErrorMessage) o; + out.setErrorIndex(errorIndex(e.getMessage(), sem.getCause())); + } + } + } + if (e.getErrorCollector().getWarnings() != null) { + for (Object o : e.getErrorCollector().getWarnings()) { + if (o instanceof SyntaxErrorMessage) { + SyntaxErrorMessage sem = (SyntaxErrorMessage) o; + out.setErrorIndex(errorIndex(e.getMessage(), sem.getCause())); + } + } + } + mainDesc.addAll(doExceptionMessage(e)); + } catch (MissingMethodException e) { + if (!e.getMessage().split("\r?\n")[0].matches(".*types:\\s+\\(.*null.*\\).*")) { + mainDesc.addAll(doExceptionMessage(e)); + } + } catch (NullPointerException e) { + // do nothing + } catch (Exception e) { + mainDesc.addAll(doExceptionMessage(e)); + } + } + out.setMainDesc(mainDesc); + return out; + } + + private List doExceptionMessage(Exception exception) { + List out = new ArrayList<>(); + SyntaxHighlighter java = SyntaxHighlighter.build(nanorcSyntax); + StyleResolver resolver = style(groovyColors); + Pattern header = Pattern.compile("^[a-zA-Z() ]{3,}:(\\s+|$)"); + out.add(java.highlight(exception.getClass().getCanonicalName())); + if (exception.getMessage() != null) { + for (String s: exception.getMessage().split("\\r?\\n")) { + if (s.trim().length() == 0) { + // do nothing + } else if (s.length() > 80) { + boolean doHeader = true; + int start = 0; + for (int i = 80; i < s.length(); i++) { + if ((s.charAt(i) == ' ' && i - start > 80 ) || i - start > 100) { + AttributedString as = new AttributedString(s.substring(start, i), resolver.resolve(".me")); + if (doHeader) { + as = as.styleMatches(header, resolver.resolve(".ti")); + doHeader = false; + } + out.add(as); + start = i; + if (s.length() - start < 80) { + out.add(new AttributedString(s.substring(start), resolver.resolve(".me"))); + break; + } + } + } + if (doHeader) { + AttributedString as = new AttributedString(s, resolver.resolve(".me")); + as = as.styleMatches(header, resolver.resolve(".ti")); + out.add(as); + } + } else { + AttributedString as = new AttributedString(s, resolver.resolve(".me")); + as = as.styleMatches(header, resolver.resolve(".ti")); + out.add(as); + } + } + } + return out; + } + + private int errorIndex(String message, SyntaxException se) { + int out; + String line = null; + String[] mlines = message.split("\n"); + for (int i = 0; i < mlines.length; i++) { + if (mlines[i].matches(".*Script[0-9]+\\.groovy: .*")) { + line = mlines[i + 1].trim(); + break; + } + } + int tot = 0; + if (line != null) { + for (String l: equationLines) { + if (l.contains(line)) { + break; + } + tot += l.length() + 1; + } + } + out = cuttedSize + tot + se.getStartColumn() - 1; + return out; + } + + private static StyleResolver style(String style) { + Map colors = Arrays.stream(style.split(":")) + .collect(Collectors.toMap(s -> s.substring(0, s.indexOf('=')), + s -> s.substring(s.indexOf('=') + 1))); + return new StyleResolver(colors::get); + } + + } + + private static class ObjectCloner implements Cloner { + Map cache = new HashMap<>(); + Set marked = new HashSet<>(); + + public ObjectCloner() { + + } + + /** + * Shallow copy of the object using java Cloneable clone() method. + */ + public Object clone(Object obj) { + if (obj == null || ImmutablePropertyUtils.builtinOrMarkedImmutableClass(obj.getClass()) + || obj instanceof Exception || obj instanceof Closure) { + return obj; + } + Object out; + String key = cacheKey(obj); + try { + if (cache.containsKey(key)) { + marked.remove(key); + out = cache.get(key); + } else { + Class clazz = obj.getClass(); + Method clone = clazz.getDeclaredMethod("clone"); + out = clone.invoke(obj); + cache.put(key, out); + } + } catch (Exception e) { + out = obj; + cache.put(key, out); + } + return out; + } + + public void markCache() { + marked = new HashSet<>(cache.keySet()); + } + + public void purgeCache() { + for (String k : marked) { + cache.remove(k); + } + } + + private String cacheKey(Object obj) { + return obj.getClass().getCanonicalName() + ":" + obj.hashCode(); + } + + } + + private static class Brackets { + static final List DELIMS = Arrays.asList('+', '-', '*', '=', '/'); + static char[] quote = {'"', '\''}; + Deque roundOpen = new ArrayDeque<>(); + Deque curlyOpen = new ArrayDeque<>(); + Map lastComma = new HashMap<>(); + int lastRoundClose = -1; + int lastCurlyClose = -1; + int lastSemicolon = -1; + int lastBlanck = -1; + int lastDelim = -1; + int quoteId = -1; + int round = 0; + int curly = 0; + int square = 0; + int rounds = 0; + int curlies = 0; + + public Brackets(String line) { + int pos = -1; + char prevChar = ' '; + for (char ch : line.toCharArray()) { + pos++; + if (quoteId < 0) { + for (int i = 0; i < quote.length; i++) { + if (ch == quote[i]) { + quoteId = i; + break; + } + } + } else { + if (ch == quote[quoteId]) { + quoteId = -1; + } + continue; + } + if (quoteId >= 0) { + continue; + } + if (ch == '(') { + round++; + roundOpen.add(pos); + } else if (ch == ')') { + rounds++; + round--; + lastComma.remove(roundOpen.getLast()); + roundOpen.removeLast(); + lastRoundClose = pos; + } else if (ch == '{') { + curly++; + curlyOpen.add(pos); + } else if (ch == '}') { + curlies++; + curly--; + curlyOpen.removeLast(); + lastCurlyClose = pos; + } else if (ch == '[') { + square++; + } else if (ch == ']') { + square--; + } else if (ch == ',' && !roundOpen.isEmpty()) { + lastComma.put(roundOpen.getLast(), pos); + } else if (ch == ';' || ch == '\n' || (ch == '>' && prevChar == '-')) { + lastSemicolon = pos; + } else if (ch == ' ' && round == 0 && String.valueOf(prevChar).matches("\\w")) { + lastBlanck = pos; + } else if (DELIMS.contains(ch)) { + lastDelim = pos; + } + prevChar = ch; + if (round < 0 || curly < 0 || square < 0) { + throw new IllegalArgumentException(); + } + } + } + + public static int indexOfOpeningRound(String line) { + int out = -1; + if (!line.endsWith(")")) { + return out; + } + int quoteId = -1; + int round = 0; + int curly = 0; + char[] chars = line.toCharArray(); + for (int i = line.length() - 1; i >= 0; i--) { + char ch = chars[i]; + if (quoteId < 0) { + for (int j = 0; j < quote.length; j++) { + if (ch == quote[j]) { + quoteId = j; + break; + } + } + } else { + if (ch == quote[quoteId]) { + quoteId = -1; + } + continue; + } + if (quoteId >= 0) { + continue; + } + if (ch == '(') { + round++; + } else if (ch == ')') { + round--; + } else if (ch == '{') { + curly++; + } else if (ch == '}') { + curly--; + } + if (curly == 0 && round == 0) { + out = i; + break; + } + } + return out; + } + + public boolean openRound() { + return round > 0; + } + + public boolean openCurly() { + return curly > 0; + } + + public boolean openSquare() { + return square > 0; + } + + public int numberOfRounds() { + return rounds; + } + + public int lastOpenRound() { + return !roundOpen.isEmpty() ? roundOpen.getLast() : -1; + } + + public int lastCloseRound() { + return lastRoundClose; + } + + public int lastOpenCurly() { + return !curlyOpen.isEmpty() ? curlyOpen.getLast() : -1; + } + + public int lastCloseCurly() { + return lastCurlyClose; + } + + public int lastComma() { + int last = lastOpenRound(); + return lastComma.getOrDefault(last, -1); + } + + public int lastSemicolon() { + return lastSemicolon; + } + + public int lastDelim() { + return lastDelim; + } + + public boolean openQuote() { + return quoteId != -1; + } + + public String toString() { + return "rounds: " + rounds + "\n" + + "curlies: " + curlies + "\n" + + "lastOpenRound: " + lastOpenRound() + "\n" + + "lastCloseRound: " + lastRoundClose + "\n" + + "lastComma: " + lastComma() + "\n"; + } + } + +} diff --git a/groovy/src/main/java/org/jline/script/JrtJavaBasePackages.java b/groovy/src/main/java/org/jline/script/JrtJavaBasePackages.java new file mode 100644 index 000000000..9205c599d --- /dev/null +++ b/groovy/src/main/java/org/jline/script/JrtJavaBasePackages.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.script; + +import org.jline.utils.Log; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static java.nio.file.FileVisitResult.CONTINUE; + +/** + * + * @author Matti Rinta-Nikkola + */ +public class JrtJavaBasePackages { + public static List> getClassesForPackage(String pckgname) { + FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/")); + List dirs = new ArrayList<>(); + dirs.add("java.base"); + boolean nestedClasses = true; + boolean onlyCurrent = false; + if (pckgname.endsWith(".*")) { + onlyCurrent = true; + nestedClasses = false; + pckgname = pckgname.substring(0, pckgname.length() - 2); + } else if (pckgname.endsWith(".**")) { + onlyCurrent = true; + pckgname = pckgname.substring(0, pckgname.length() - 3); + } + dirs.addAll(Arrays.asList(pckgname.split("\\."))); + Path path = fs.getPath("modules", dirs.toArray(new String[0])); + FileVisitor fv = new FileVisitor(nestedClasses); + try { + if (onlyCurrent) { + Files.walkFileTree(path, new HashSet<>(),1, fv); + } else { + Files.walkFileTree(path, fv); + } + } catch (IOException e) { + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + } + return fv.getClasses(); + } + + private static class FileVisitor extends SimpleFileVisitor { + private final List> classes = new ArrayList<>(); + private final boolean nestedClasses; + + public FileVisitor(boolean nestedClasses) { + super(); + this.nestedClasses = nestedClasses; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attr) { + try { + String name = file.toString().substring(18); + if (name.endsWith(".class") && (nestedClasses || !name.contains("$"))) { + classes.add(Class.forName(name.substring(0, name.length() - 6).replaceAll("/", "."))); + } + } catch (Exception|Error e) { + if (Log.isDebugEnabled()) { + e.printStackTrace(); + } + } + return CONTINUE; + } + + private List> getClasses() { + return classes; + } + } +} \ No newline at end of file diff --git a/groovy/src/main/java/org/jline/script/PackageHelper.java b/groovy/src/main/java/org/jline/script/PackageHelper.java new file mode 100644 index 000000000..d307b5287 --- /dev/null +++ b/groovy/src/main/java/org/jline/script/PackageHelper.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2002-2020, the original author or authors. + * + * 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.script; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +/** + * + * https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection/22462785#22462785 + * + */ +public class PackageHelper { + private enum ClassesToScann {ALL, PACKAGE_ALL, PACKAGE_CLASS}; + /** + * Private helper method + * + * @param directory + * The directory to start with + * @param pckgname + * The package name to search for. Will be needed for getting the + * Class object. + * @param classes + * if a file isn't loaded but still is in the directory + * @param scann + * determinate which classes will be added + */ + private static void checkDirectory(File directory, String pckgname, + List> classes, ClassesToScann scann) { + File tmpDirectory; + + if (directory.exists() && directory.isDirectory()) { + final String[] files = directory.list(); + + for (final String file : files != null ? files : new String[0]) { + if (file.endsWith(".class")) { + addClass(pckgname + '.' + + file.substring(0, file.length() - 6), classes); + } else if (scann == ClassesToScann.ALL && (tmpDirectory = new File(directory, file)).isDirectory()) { + checkDirectory(tmpDirectory, pckgname + "." + file, classes, ClassesToScann.ALL); + } + } + } + } + + /** + * Private helper method. + * + * @param connection + * the connection to the jar + * @param pckgname + * the package name to search for + * @param classes + * the current ArrayList of all classes. This method will simply + * add new classes. + * @param scann + * determinate which classes will be added + * @throws IOException + * if it can't correctly read from the jar file. + */ + private static void checkJarFile(JarURLConnection connection, + String pckgname, List> classes, ClassesToScann scann) + throws IOException { + final JarFile jarFile = connection.getJarFile(); + final Enumeration entries = jarFile.entries(); + String name; + + for (JarEntry jarEntry; entries.hasMoreElements() && ((jarEntry = entries.nextElement()) != null);) { + name = jarEntry.getName(); + if (name.contains(".class")) { + name = name.substring(0, name.length() - 6).replace('/', '.'); + if (scann != ClassesToScann.ALL) { + String namepckg = name.substring(0, name.lastIndexOf(".")); + if (pckgname.equals(namepckg) && ((scann == ClassesToScann.PACKAGE_CLASS && !name.contains("$")) + || scann == ClassesToScann.PACKAGE_ALL)) { + addClass(name, classes); + } + } else if (name.contains(pckgname)) { + addClass(name, classes); + } + } + } + } + + private static void addClass(String className, List> classes) { + try { + classes.add(Class.forName(className)); + } catch (Exception|Error e) { + // ignore + } + } + + /** + * Attempts to list all the classes in the specified package as determined + * by the context class loader + * + * @param pckgname + * the package name to search + * @return a list of classes that exist within that package + * @throws ClassNotFoundException + * if something went wrong + */ + public static List> getClassesForPackage(String pckgname) throws ClassNotFoundException { + List> classes = new ArrayList<>(); + ClassesToScann scann = ClassesToScann.ALL; + if (pckgname.endsWith(".*")) { + scann = ClassesToScann.PACKAGE_CLASS; + pckgname = pckgname.substring(0, pckgname.length() - 2); + } else if (pckgname.endsWith(".**")) { + pckgname = pckgname.substring(0, pckgname.length() - 3); + scann = ClassesToScann.PACKAGE_ALL; + } + + try { + final ClassLoader cld = Thread.currentThread().getContextClassLoader(); + if (cld == null) { + throw new ClassNotFoundException("Can't get class loader."); + } + final Enumeration resources = cld.getResources(pckgname.replace('.', '/')); + URLConnection connection; + + for (URL url; resources.hasMoreElements() + && ((url = resources.nextElement()) != null);) { + try { + connection = url.openConnection(); + + if (connection instanceof JarURLConnection) { + checkJarFile((JarURLConnection) connection, pckgname, classes, scann); + } else if (connection.getClass().getCanonicalName().equals("sun.net.www.protocol.file.FileURLConnection")) { + try { + checkDirectory( + new File(URLDecoder.decode(url.getPath(), "UTF-8")), pckgname, classes, scann); + } catch (final UnsupportedEncodingException ex) { + throw new ClassNotFoundException( + pckgname + + " does not appear to be a valid package (Unsupported encoding)", + ex); + } + } else { + throw new ClassNotFoundException(pckgname + " (" + + url.getPath() + + ") does not appear to be a valid package"); + } + } catch (final IOException ioex) { + throw new ClassNotFoundException( + "IOException was thrown when trying to get all resources for " + + pckgname, ioex); + } + } + } catch (final NullPointerException ex) { + throw new ClassNotFoundException( + pckgname + + " does not appear to be a valid package (Null pointer exception)", + ex); + } catch (final IOException ioex) { + throw new ClassNotFoundException( + "IOException was thrown when trying to get all resources for " + + pckgname, ioex); + } + return classes; + } +} diff --git a/groovy/src/main/resources/org/jline/groovy/gron.nanorc b/groovy/src/main/resources/org/jline/groovy/gron.nanorc new file mode 100644 index 000000000..c9b8110ef --- /dev/null +++ b/groovy/src/main/resources/org/jline/groovy/gron.nanorc @@ -0,0 +1,12 @@ +syntax "GRON" "\.gron$" +header "^\[$" + +color brightblue "\<[-]?[0-9]*([Ee][+-]?[0-9]+)?\>" "\<[-]?[0](\.[0-9]+)?\>" +color yellow ""(\\.|[^"])*"|'(\\.|[^'])*'|[a-zA-Z]+[a-zA-Z0-9]*" +color cyan "\" +color brightcyan "\<(true|false)\>" +color brightyellow "\"(\\"|[^"])*\"\s*:" "'(\'|[^'])*'\s*:" "(\[|,)\s*[a-zA-Z0-9]*\s*:" +color white "(:|\[|,|\])" +color magenta "\\u[0-9a-fA-F]{4}|\\[bfnrt'"/\\]" +color ,green "[[:space:]]+$" +color ,red " + +| + +" \ No newline at end of file diff --git a/groovy/src/main/resources/org/jline/groovy/java.nanorc b/groovy/src/main/resources/org/jline/groovy/java.nanorc new file mode 100644 index 000000000..744dc838e --- /dev/null +++ b/groovy/src/main/resources/org/jline/groovy/java.nanorc @@ -0,0 +1,17 @@ +## Here is an example for Java. +## +syntax "Java" "\.java$" +color green "\<(boolean|byte|char|double|float|int|long|new|short|this|transient|void)\>" +color red "\<(break|case|catch|continue|default|do|else|finally|for|if|return|switch|throw|try|while)\>" +color green,,faint "(([a-z]{2,}[.]{1}){2,10}([a-z]{2,}){0,1})" +color green "\<[A-Z]{0,2}([A-Z]{1}[a-z]+){1,}\>" +color cyan "\<(abstract|class|extends|final|implements|import|instanceof|interface|native|package|private|protected|public|static|strictfp|super|synchronized|throws|volatile)\>" +color red ""[^"]*"" +color yellow "\<(true|false|null)\>" +color yellow "\<[A-Z]+([_]{1}[A-Z]+){0,}\>" +icolor yellow "\b(([1-9][0-9]+)|0+)\.[0-9]+\b" "\b[1-9][0-9]*\b" "\b0[0-7]*\b" "\b0x[1-9a-f][0-9a-f]*\b" +color blue "//.*" +color blue start="/\*" end="\*/" +color brightblue start="/\*\*" end="\*/" +color brightwhite,yellow "(FIXME|TODO|XXX)" +color ,green "[[:space:]]+$" diff --git a/jline/pom.xml b/jline/pom.xml index 5fd899254..b32b5c4c4 100644 --- a/jline/pom.xml +++ b/jline/pom.xml @@ -16,12 +16,16 @@ org.jline jline-parent - 3.14.1-SNAPSHOT + 3.18.1-SNAPSHOT jline JLine Bundle + + org.jline + + org.fusesource.jansi @@ -61,42 +65,47 @@ org.jline jline-terminal - provided + test org.jline jline-terminal-jansi - provided + test org.jline jline-terminal-jna - provided + test org.jline jline-reader - provided + test org.jline jline-builtins - provided + test + + + org.jline + jline-console + test org.jline jline-remote-ssh - provided + test org.jline jline-remote-telnet - provided + test org.jline jline-style - provided + test @@ -153,6 +162,14 @@ false ${project.build.directory}/generated-sources + + org.jline + jline-console + sources + jar + false + ${project.build.directory}/generated-sources + org.jline jline-remote-ssh @@ -292,31 +309,31 @@ default-compile - - **/TTop.java - + + **/TTop.java + **/ConsoleEngineImpl.java + -Xlint:all,-options -Werror - -profile - compact1 - noncompact + compact compile - - **/TTop.java - + + **/TTop.java + **/ConsoleEngineImpl.java + -Xlint:all,-options -Werror -profile - compact3 + compact1 diff --git a/pom.xml b/pom.xml index 8341b1abd..dfa32d0f5 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ jline-parent JLine Parent JLine - 3.14.1-SNAPSHOT + 3.18.1-SNAPSHOT pom @@ -78,17 +78,22 @@ 1.8 1.8 + true + + 5.3.1 - 1.18 + 2.1.0 1.0.3 2.1.0 3.3.1 - 4.12 + 4.13.1 1.1.2 1.1.4 1.7.21 3.0.2 - 3.0.0 + 3.0.7 + 2.5.0 + 19.3.1 @@ -173,6 +178,12 @@ ${project.version} + + org.jline + jline-console + ${project.version} + + org.fusesource.jansi jansi @@ -239,6 +250,24 @@ ${groovy.version} + + org.codehaus.groovy + groovy-console + ${groovy.version} + + + + org.graalvm.sdk + graal-sdk + ${graal.version} + + + + org.apache.ivy + ivy + ${ivy.version} + + org.slf4j slf4j-api @@ -279,7 +308,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.8.1 true @@ -295,7 +324,7 @@ org.apache.maven.plugins maven-deploy-plugin - 2.8.2 + 3.0.0-M1 true @@ -361,6 +390,30 @@ + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + gpg + + --pinentry-mode + loopback + + ${gpg.passphrase} + true + + + + @@ -387,7 +440,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.7.0 + 3.8.1 @@ -399,16 +452,19 @@ org.apache.maven.plugins maven-resources-plugin - 3.1.0 + 3.2.0 org.apache.maven.plugins maven-jar-plugin - 3.1.0 + 3.2.0 ${project.build.outputDirectory}/META-INF/MANIFEST.MF + + ${automatic.module.name} + @@ -416,7 +472,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.21.0 + 3.0.0-M5 true ${surefire.argLine} @@ -427,7 +483,7 @@ org.apache.maven.plugins maven-source-plugin - 3.0.1 + 3.2.1 @@ -448,50 +504,78 @@ - - org.apache.maven.plugins - maven-javadoc-plugin - 3.1.1 - - -Xdoclint:none - -notimestamp - ${javadocOptions} - false - - - - javadoc - - jar - - - - - - org.apache.commons - commons-lang3 - 3.7 - - - - org.apache.maven.plugins maven-release-plugin - 2.5.3 + 3.0.0-M1 true + + org.graalvm.nativeimage + native-image-maven-plugin + ${graal.version} + + + bundle + + + !nobundle + + + + jline + + + + + javadoc + + + !nojavadoc + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + none + true + ${javadocOptions} + false + + + + javadoc + + jar + + + + + + org.apache.commons + commons-lang3 + 3.7 + + + + + + + java9 --add-opens java.base/java.io=ALL-UNNAMED - -html4 [9,) @@ -523,6 +607,13 @@ com.mycila:license-maven-plugin:format + + + native-image + + false + + @@ -530,13 +621,14 @@ terminal-jna terminal-jansi reader - groovy builtins + console + groovy remote-ssh remote-telnet style - jline demo - + graal + diff --git a/reader/pom.xml b/reader/pom.xml index 451f96d38..b0b5d03c6 100644 --- a/reader/pom.xml +++ b/reader/pom.xml @@ -1,7 +1,7 @@