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
This commit is contained in:
Anthony Keenan 2018-08-01 14:44:56 +01:00 committed by GitHub
parent c23167f08e
commit d5f4370443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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<String>) {
@ -29,13 +30,13 @@ fun main(args: Array<String>) {
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<String> 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<String> {
return if (filePath.exists()) {
filePath.toFile().readLines().toMutableList()
} else {
emptyList<String>().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 <options>` 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<String> {
return arrayOf(
"Version: ${Manifests.read("Corda-Release-Version")}",
"Revision: ${Manifests.read("Corda-Revision")}"
"Version: $releaseVersion",
"Revision: $revision"
)
}
}