交互式命令行

发布时间 2023-06-13 17:39:57作者: 流浪的夜空

市面上有不少交互式命令行工具
picocli\jline\kotlin-REPL\jshell
有些直接可以使用,有些需要部分改造,我使用Picocli-shell-jline项目的基础上实现了一款交互式命令行的LicenseGenerator工具

引入maven依赖

<dependency>
  <groupId>info.picocli</groupId>
      <artifactId>picocli-shell-jline3</artifactId>
      <version>4.7.4</version>
  </dependency>

启动

try {
        val workDir = Supplier { Paths.get(System.getProperty("user.dir")) }
        // set up JLine built-in commands
        val builtins = Builtins(workDir, ConfigurationPath(Path.of(""), Path.of("")), null)
        builtins.rename(Builtins.Command.TTOP, "top")
        // set up picocli commands
        val commands = App()
        val factory = PicocliCommands.PicocliCommandsFactory()
        // Or, if you have your own factory, you can chain them like this:
        // MyCustomFactory customFactory = createCustomFactory(); // your application custom factory
        // PicocliCommandsFactory factory = new PicocliCommandsFactory(customFactory); // chain the factories
        val cmd = CommandLine(commands, factory)
        val picocliCommands = PicocliCommands(cmd)
        val parser: Parser = DefaultParser()
        TerminalBuilder.builder().build().use { terminal ->
            val systemRegistry: SystemRegistry = SystemRegistryImpl(parser, terminal, workDir, null)
            systemRegistry.setCommandRegistries(builtins, picocliCommands)
            systemRegistry.register("help", picocliCommands)
            val reader = LineReaderBuilder.builder()
                .terminal(terminal)
                .completer(systemRegistry.completer())
                .parser(parser)
                .variable(LineReader.LIST_MAX, 50) // max tab completion candidates
                .build()
            builtins.setLineReader(reader)
            commands.setReader(reader)
            factory.setTerminal(terminal)
            val widgets =
                TailTipWidgets(reader, systemRegistry::commandDescription, 5, TailTipWidgets.TipType.COMPLETER)
            widgets.enable()
            val keyMap: KeyMap<Binding> = reader.keyMaps["main"]!!
            keyMap.bind(Reference("tailtip-toggle"), KeyMap.alt("s"))
            val prompt = "License> "
            val rightPrompt: String? = null

            // start the shell and process input until the user quits with Ctrl-D
            var line: String?
            while (true) {
                try {
                    systemRegistry.cleanUp()
                    line = reader.readLine(prompt, rightPrompt, null as MaskingCallback?, null)
                    systemRegistry.execute(line)
                    Thread.sleep(1000)
                } catch (e: UserInterruptException) {
                    // Ignore
                } catch (e: EndOfFileException) {
                    return
                } catch (e: Exception) {
                    systemRegistry.trace(e)
                }
            }
        }
    } catch (t: Throwable) {
        t.printStackTrace()
    }

Command

@Command(name = "", version = ["v1.1.2"],
    description = [
        "Example interactive shell with completion and autosuggestions. " +
                "Hit @|magenta <TAB>|@ to see available commands.",
        "Hit @|magenta ALT-S|@ to toggle tailtips.",
        ""],
    footer = ["", "Press Ctrl-D to exit."],
    subcommands = [Generate::class, Checksum::class, Sign::class, Verify::class, PicocliCommands.ClearScreen::class, CommandLine.HelpCommand::class])
class App : Runnable {
    var out: PrintWriter? = null
    override fun run() {
        out?.println(CommandLine(this).usageMessage);
    }
    fun setReader(reader: LineReader) {
        out = reader.terminal.writer()
    }
}

生成公私钥

@CommandLine.Command(name = "generate", version = ["v1.1.2"],
    description = ["Generate Public and Private Keys"])
class Generate : Runnable {
    @Option(names = ["algorithm", "-a"], defaultValue = "RSA", description = ["Algorithm with RSA DECB PKCS5Padding"])
    var algorithm = "RSA"
    @Option(names = ["size", "-s"], defaultValue = "2048", description = ["Size default with 2048"])
    var size = 2048
    @Option(names = ["public", "-p"], defaultValue = "./pub.cert", description = ["Public Key locate"])
    lateinit var public: File
    @Option(names = ["private", "-r"], defaultValue = "./pri.cert", description = ["Private Key locate"])
    lateinit var private: File
    @OptIn(ExperimentalEncodingApi::class)
    override fun run() {
        val generator = KeyPairGenerator.getInstance(algorithm)
        generator.initialize(size)
        val pair = generator.genKeyPair()
        val albyte = algorithm.toByteArray(Charsets.UTF_8)
        val pubBytes = pair.public.encoded
        val priBytes = pair.private.encoded
        FileOutputStream(public).write(Base64.encodeToByteArray(pubBytes))
        FileOutputStream(private).write(Base64.encodeToByteArray(priBytes))
    }
}

总结

总体来说picocli-shell-jline3项目好像还没有开发完,也好像没人维护了,启动非常繁琐,而且有bug,得自己改动,一些逻辑本身不太清楚,而且用了反射的方式调用java基础库的一些不让调用的功能,需要设置jvm启动参数避免警告,总体来说框架本身功能还是非常快速上手,文档也非常全

--illegal-access=deny --add-opens java.base/java.lang=ALL-UNNAMED