Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

picocli-shell-jline demo does not compile #989

Closed
rdmueller opened this issue Apr 12, 2020 · 4 comments
Closed

picocli-shell-jline demo does not compile #989

rdmueller opened this issue Apr 12, 2020 · 4 comments

Comments

@rdmueller
Copy link

I tried to get the demo in the README up and running, without success so far.

https://github.com/remkop/picocli/blob/master/picocli-shell-jline3/README.md

at least, when I add

    implementation 'info.picocli:picocli-shell-jline3:4.2.0'
    implementation 'org.jline:jline:3.14.1'

as dependencies, it compiles. But I thought I wouldn't need jline as extra dependency, would I?

when I don't add it, I get a

error: cannot find symbol
import org.jline.builtins.CommandRegistry;
                         ^
  symbol:   class CommandRegistry
  location: package org.jline.builtins

So, not that it compiles, I try to start the fat-jar and get a

java.lang.ClassCastException: class picocli.shell.jline3.PicocliCommands cannot be cast to class org.jline.builtins.CommandRegistry (picocli.shell.jline3.PicocliCommands and org.jline.builtins.CommandRegistry are in unn
amed module of loader 'app')
        at picocli.shell.jline3.example.Example.main(Example.java:169)

which referes to the line

systemRegistry.setCommandRegistries(builtins, picocliCommands);

When I try to cast the picocliCommand right in the source, I get a null pointer exception.

Do you have a quick hint? Thanx!

@remkop
Copy link
Owner

remkop commented Apr 12, 2020

Checking...

@remkop
Copy link
Owner

remkop commented Apr 12, 2020

Dependencies

But I thought I wouldn't need jline as extra dependency, would I?

Ok, good point. It may be more user-friendly to make JLine a transitive dependency so it gets pulled in automatically when you have picocli:picocli-shell-jline3 as a dependency. Would you mind raising a separate issue (or PR 🙏 😉 ) for this?

Note that JLine 3 is much more modular than JLine 2, and I think you still need to add one of Jansi or JNA when running your app on Windows. So to be honest I am not sure what would be the best transitive dependency/dependencies to pull in...

Problem with Example

I try to start the fat-jar and get a ClassCastException (...)

Apologies 🙇 . I recently accepted a pull request by one of the JLine authors that fixed an issue with subcommand completion.

The PR fixed the issue but also requires things to be wired up a bit differently than before. So, the example is showing you how to do things with the (unreleased) upcoming version of picocli-shell-jline3. (Kind of the opposite of an example that is out of date. Still not very useful. Hm...)

Solution 1: Use the (Latest) Source, Luke! ⚔️

If you are up for it, I would suggest using a SNAPSHOT build ⚡ of picocli-shell-jline3 for a while until the next version of picocli (4.3.0) is released.

To do this, check out picocli master locally, and build it with publishToMavenLocal. That should publish picocli-shell-jline3-4.2.1-SNAPSHOT to your local .m2 Maven cache.

git clone https://github.com/remkop/picocli.git
cd picocli
gradlew publishToMavenLocal

You can then try this in a project that uses the info.picocli:picocli-shell-jline3:4.2.1-SNAPSHOT dependency.

For reference, I tested the PR with the below command and did not find any issues:

java -cp "picocli-4.2.0.jar:picocli-shell-jline3-4.2.1-SNAPSHOT-tests.jar:picocli-shell-jline3-4.2.1-SNAPSHOT.jar:jline-3.14.1.jar:jansi-1.15.jar" picocli.shell.jline3.example.Example

Solution 2: Use the Previous Example

Here is the old example code that should work with info.picocli:picocli-shell-jline3:4.2.0, the version of picocli-shell-jline3 that is available from Maven Central. The drawback of this solution is that if you have nested sub-subcommands you will run into the known issue with completion. 🐛

package picocli.shell.jline3.example;

import org.fusesource.jansi.AnsiConsole;
import org.jline.builtins.Builtins;
import org.jline.builtins.Completers.SystemCompleter;
import org.jline.builtins.Options.HelpException;
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.reader.*;
import org.jline.reader.impl.DefaultParser;
import org.jline.reader.impl.LineReaderImpl;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import picocli.CommandLine;
import picocli.CommandLine.ArgGroup;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.ParentCommand;
import picocli.shell.jline3.PicocliCommands;

import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * Example that demonstrates how to build an interactive shell with JLine3 and picocli.
 * @since 4.1.2
 */
public class Example {

    /**
     * Top-level command that just prints help.
     */
    @Command(name = "",
            description = {
                    "Example interactive shell with completion. " +
                            "Hit @|magenta <TAB>|@ to see available commands.",
                    "Type `@|bold,yellow keymap ^[s tailtip-toggle|@`, " +
                            "then hit @|magenta ALT-S|@ to toggle tailtips.",
                    ""},
            footer = {"", "Press Ctl-D to exit."},
            subcommands = {
                    MyCommand.class, ClearScreen.class, CommandLine.HelpCommand.class})
    static class CliCommands implements Runnable {
        LineReaderImpl reader;
        PrintWriter out;

        CliCommands() {}

        public void setReader(LineReader reader){
            this.reader = (LineReaderImpl)reader;
            out = reader.getTerminal().writer();
        }

        public void run() {
            out.println(new CommandLine(this).getUsageMessage());
        }
    }

    /**
     * A command with some options to demonstrate completion.
     */
    @Command(name = "cmd", mixinStandardHelpOptions = true, version = "1.0",
            description = "Command with some options to demonstrate TAB-completion" +
                    " (note that enum values also get completed)",
            subcommands = CommandLine.HelpCommand.class)
    static class MyCommand implements Runnable {
        @Option(names = {"-v", "--verbose"},
                description = { "Specify multiple -v options to increase verbosity.",
                        "For example, `-v -v -v` or `-vvv`"})
        private boolean[] verbosity = {};

        @ArgGroup(exclusive = false)
        private MyDuration myDuration = new MyDuration();

        static class MyDuration {
            @Option(names = {"-d", "--duration"},
                    description = "The duration quantity.",
                    required = true)
            private int amount;

            @Option(names = {"-u", "--timeUnit"},
                    description = "The duration time unit.",
                    required = true)
            private TimeUnit unit;
        }

        @ParentCommand CliCommands parent;

        public void run() {
            if (verbosity.length > 0) {
                parent.out.printf("Hi there. You asked for %d %s.%n",
                        myDuration.amount, myDuration.unit);
            } else {
                parent.out.println("hi!");
            }
        }
    }

    /**
     * Command that clears the screen.
     */
    @Command(name = "cls", aliases = "clear", mixinStandardHelpOptions = true,
            description = "Clears the screen", version = "1.0")
    static class ClearScreen implements Callable<Void> {

        @ParentCommand CliCommands parent;

        public Void call() throws IOException {
            parent.reader.clearScreen();
            return null;
        }
    }

    /**
     * Provide command descriptions for JLine TailTipWidgets
     * to be displayed in the status bar.
     */
    private static class DescriptionGenerator {
        Builtins builtins;
        PicocliCommands picocli;

        public DescriptionGenerator(Builtins builtins, PicocliCommands picocli) {
            this.builtins = builtins;
            this.picocli = picocli;
        }

        CmdDesc commandDescription(CmdLine line) {
            CmdDesc out = null;
            switch (line.getDescriptionType()) {
                case COMMAND:
                    String cmd = Parser.getCommand(line.getArgs().get(0));
                    if (builtins.hasCommand(cmd)) {
                        out = builtins.commandDescription(cmd);
                    } else if (picocli.hasCommand(cmd)) {
                        out = picocli.commandDescription(cmd);
                    }
                    break;
                default:
                    break;
            }
            return out;
        }
    }

    public static void main(String[] args) {
        AnsiConsole.systemInstall();
        try {
            // set up JLine built-in commands
            Path workDir = Paths.get("");
            Builtins builtins = new Builtins(workDir, null, null);
            builtins.rename(org.jline.builtins.Builtins.Command.TTOP, "top");
            builtins.alias("zle", "widget");
            builtins.alias("bindkey", "keymap");
            SystemCompleter systemCompleter = builtins.compileCompleters();
            // set up picocli commands
            CliCommands commands = new CliCommands();
            CommandLine cmd = new CommandLine(commands);
            PicocliCommands picocliCommands = new PicocliCommands(workDir, cmd);
            systemCompleter.add(picocliCommands.compileCompleters());
            systemCompleter.compile();
            Terminal terminal = TerminalBuilder.builder().build();
            LineReader reader = LineReaderBuilder.builder()
                    .terminal(terminal)
                    .completer(systemCompleter)
                    .parser(new DefaultParser())
                    .variable(LineReader.LIST_MAX, 50)   // max tab completion candidates
                    .build();
            builtins.setLineReader(reader);
            commands.setReader(reader);
            DescriptionGenerator descriptionGenerator = new DescriptionGenerator(builtins, picocliCommands);
            new TailTipWidgets(reader, descriptionGenerator::commandDescription, 5, TipType.COMPLETER);

            String prompt = "prompt> ";
            String rightPrompt = null;

            // start the shell and process input until the user quits with Ctl-D
            String line;
            while (true) {
                try {
                    line = reader.readLine(prompt, rightPrompt, (MaskingCallback) null, null);
                    if (line.matches("^\\s*#.*")) {
                        continue;
                    }
                    ParsedLine pl = reader.getParser().parse(line, 0);
                    String[] arguments = pl.words().toArray(new String[0]);
                    String command = Parser.getCommand(pl.word());
                    if (builtins.hasCommand(command)) {
                        builtins.execute(command, Arrays.copyOfRange(arguments, 1, arguments.length)
                                , System.in, System.out, System.err);
                    } else {
                        new CommandLine(commands).execute(arguments);
                    }
                } catch (HelpException e) {
                    HelpException.highlight(e.getMessage(), HelpException.defaultStyle()).print(terminal);
                } catch (UserInterruptException e) {
                    // Ignore
                } catch (EndOfFileException e) {
                    return;
                } catch (Exception e) {
                    AttributedStringBuilder asb = new AttributedStringBuilder();
                    asb.append(e.getMessage(), AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
                    asb.toAttributedString().println(terminal);
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

@remkop
Copy link
Owner

remkop commented Apr 13, 2020

I updated the picocli-shell-jline3 readme, thanks for pointing this out! Feedback welcome!

@rdmueller
Copy link
Author

wow. thanx for this detailed answer!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants