From d5f43704430789eca9ada90d44ab39429ff55b55 Mon Sep 17 00:00:00 2001 From: Anthony Keenan Date: Wed, 1 Aug 2018 14:44:56 +0100 Subject: [PATCH] CORDA-1848 Add example alias and autocomplete for CLI tools (#3700) * Basic alias and autocomplete installation for bash in network bootstrapper * Address review comments * Update completion file if out of date * Refactoring * Some more minor tweaks * Use manifest revision rather than recalculating hash * Add zsh autocomplete compatibility * Actually write .zshrc file * Fix some descriptions * Only rewrite settings files if changes have been made, and make a backup if so. Some refactoring --- .../kotlin/net/corda/bootstrapper/Main.kt | 132 +++++++++++++++++- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index f80699ee65..f89750246f 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -1,12 +1,13 @@ package net.corda.bootstrapper import com.jcabi.manifests.Manifests -import net.corda.core.internal.rootMessage +import net.corda.core.internal.* import net.corda.nodeapi.internal.network.NetworkBootstrapper import picocli.CommandLine import picocli.CommandLine.* import java.nio.file.Path import java.nio.file.Paths +import java.nio.file.StandardCopyOption.REPLACE_EXISTING import kotlin.system.exitProcess fun main(args: Array) { @@ -29,13 +30,13 @@ fun main(args: Array) { versionProvider = CordaVersionProvider::class, mixinStandardHelpOptions = true, showDefaultValues = true, - description = [ "Bootstrap a local test Corda network using a set of node conf files and CorDapp JARs" ] + description = ["Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs"] ) class Main : Runnable { @Option( names = ["--dir"], description = [ - "Root directory containing the node conf files and CorDapp JARs that will form the test network.", + "Root directory containing the node configuration files and CorDapp JARs that will form the test network.", "It may also contain existing node directories." ] ) @@ -47,7 +48,123 @@ class Main : Runnable { @Option(names = ["--verbose"], description = ["Enable verbose output."]) var verbose: Boolean = false + @Option(names = ["--install-shell-extensions"], description = ["Install bootstrapper alias and autocompletion for bash and zsh"]) + var installShellExtensions: Boolean = false + + private class SettingsFile(val filePath: Path) { + private val lines: MutableList by lazy { getFileLines() } + var fileModified: Boolean = false + + // Return the lines in the file if it exists, else return an empty mutable list + private fun getFileLines(): MutableList { + return if (filePath.exists()) { + filePath.toFile().readLines().toMutableList() + } else { + emptyList().toMutableList() + } + } + + fun addOrReplaceIfStartsWith(startsWith: String, replaceWith: String) { + val index = lines.indexOfFirst { it.startsWith(startsWith) } + if (index >= 0) { + if (lines[index] != replaceWith) { + lines[index] = replaceWith + fileModified = true + } + } else { + lines.add(replaceWith) + fileModified = true + } + } + + fun addIfNotExists(line: String) { + if (!lines.contains(line)) { + lines.add(line) + fileModified = true + } + } + + fun updateAndBackupIfNecessary() { + if (fileModified) { + val backupFilePath = filePath.parent / "${filePath.fileName}.backup" + println("Updating settings in ${filePath.fileName} - existing settings file has been backed up to $backupFilePath") + if (filePath.exists()) filePath.copyTo(backupFilePath, REPLACE_EXISTING) + filePath.writeLines(lines) + } + } + } + + private val userHome: Path by lazy { Paths.get(System.getProperty("user.home")) } + private val jarLocation: Path by lazy { this.javaClass.location.toPath() } + + // If on Windows, Path.toString() returns a path with \ instead of /, but for bash Windows users we want to convert those back to /'s + private fun Path.toStringWithDeWindowsfication(): String = this.toAbsolutePath().toString().replace("\\", "/") + private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}" + private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias + + private fun generateAutoCompleteFile(alias: String) { + println("Generating $alias auto completion file") + val autoCompleteFile = getAutoCompleteFileLocation(alias) + autoCompleteFile.parent.createDirectories() + picocli.AutoComplete.main("-f", "-n", alias, this.javaClass.name, "-o", autoCompleteFile.toStringWithDeWindowsfication()) + + // Append hash of file to autocomplete file + autoCompleteFile.toFile().appendText(jarVersion(alias)) + } + + private fun installShellExtensions(alias: String) { + // Get jar location and generate alias command + val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'" + generateAutoCompleteFile(alias) + + // Get bash settings file + val bashSettingsFile = SettingsFile(userHome / ".bashrc") + // Replace any existing bootstrapper alias. There can be only one. + bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command) + val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done" + bashSettingsFile.addIfNotExists(completionFileCommand) + bashSettingsFile.updateAndBackupIfNecessary() + + // Get zsh settings file + val zshSettingsFile = SettingsFile(userHome / ".zshrc") + zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit") + zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit") + zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command) + zshSettingsFile.addIfNotExists(completionFileCommand) + zshSettingsFile.updateAndBackupIfNecessary() + + println("Installation complete, $alias is available in bash with autocompletion. ") + println("Type `$alias ` from the commandline.") + println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now") + } + + private fun checkForAutoCompleteUpdate(alias: String) { + val autoCompleteFile = getAutoCompleteFileLocation(alias) + + // If no autocomplete file, it hasn't been installed, so don't do anything + if (!autoCompleteFile.exists()) return + + var lastLine = "" + autoCompleteFile.toFile().forEachLine { lastLine = it } + + if (lastLine != jarVersion(alias)) { + println("Old auto completion file detected... regenerating") + generateAutoCompleteFile(alias) + println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now") + } + } + + private fun installOrUpdateShellExtensions(alias: String) { + if (installShellExtensions) { + installShellExtensions(alias) + exitProcess(0) + } else { + checkForAutoCompleteUpdate(alias) + } + } + override fun run() { + installOrUpdateShellExtensions("bootstrapper") if (verbose) { System.setProperty("logLevel", "trace") } @@ -56,10 +173,15 @@ class Main : Runnable { } private class CordaVersionProvider : IVersionProvider { + companion object { + val releaseVersion: String by lazy { Manifests.read("Corda-Release-Version") } + val revision: String by lazy { Manifests.read("Corda-Revision") } + } + override fun getVersion(): Array { return arrayOf( - "Version: ${Manifests.read("Corda-Release-Version")}", - "Revision: ${Manifests.read("Corda-Revision")}" + "Version: $releaseVersion", + "Revision: $revision" ) } }