mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
CORDA-1833: Create a picocli base class (#3826)
* Add shell extensions to CLI utils class and move into its own module * Fix issue with completion script generation and slight refactor * Fix autocompletion for logging level * Delete uneeded comment * More tidying up * Make run function final * Fixed an issue with the program being run twice. * Address review comments
This commit is contained in:
parent
98eef2f960
commit
bcfadfeebf
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -26,6 +26,8 @@
|
||||
<module name="canonicalizer_test" target="1.8" />
|
||||
<module name="client_main" target="1.8" />
|
||||
<module name="client_test" target="1.8" />
|
||||
<module name="cliutils_main" target="1.8" />
|
||||
<module name="cliutils_test" target="1.8" />
|
||||
<module name="common_main" target="1.8" />
|
||||
<module name="common_test" target="1.8" />
|
||||
<module name="confidential-identities_main" target="1.8" />
|
||||
|
@ -70,7 +70,7 @@ buildscript {
|
||||
ext.snappy_version = '0.4'
|
||||
ext.fast_classpath_scanner_version = '2.12.3'
|
||||
ext.jcabi_manifests_version = '1.1'
|
||||
ext.picocli_version = '3.3.0'
|
||||
ext.picocli_version = '3.5.2'
|
||||
|
||||
// Name of the IntelliJ SDK created for the deterministic Java rt.jar.
|
||||
// ext.deterministic_idea_sdk = '1.8 (Deterministic)'
|
||||
|
@ -43,6 +43,7 @@ include 'tools:blobinspector'
|
||||
include 'tools:shell'
|
||||
include 'tools:shell-cli'
|
||||
include 'tools:network-bootstrapper'
|
||||
include 'tools:cliutils'
|
||||
include 'example-code'
|
||||
project(':example-code').projectDir = file("$settingsDir/docs/source/example-code")
|
||||
include 'samples:attachment-demo'
|
||||
|
@ -7,6 +7,7 @@ description 'Network bootstrapper'
|
||||
|
||||
dependencies {
|
||||
compile project(':node-api')
|
||||
compile project(':tools:cliutils')
|
||||
compile "info.picocli:picocli:$picocli_version"
|
||||
compile "org.slf4j:jul-to-slf4j:$slf4j_version"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
|
@ -1,38 +1,17 @@
|
||||
package net.corda.bootstrapper
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.cliutils.CordaCliWrapper
|
||||
import net.corda.cliutils.start
|
||||
import net.corda.nodeapi.internal.network.NetworkBootstrapper
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.*
|
||||
import picocli.CommandLine.Option
|
||||
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>) {
|
||||
val main = Main()
|
||||
try {
|
||||
CommandLine.run(main, *args)
|
||||
} catch (e: ExecutionException) {
|
||||
val throwable = e.cause ?: e
|
||||
if (main.verbose) {
|
||||
throwable.printStackTrace()
|
||||
} else {
|
||||
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
|
||||
}
|
||||
exitProcess(1)
|
||||
}
|
||||
NetworkBootstrapperRunner().start(*args)
|
||||
}
|
||||
|
||||
@Command(
|
||||
name = "Network Bootstrapper",
|
||||
versionProvider = CordaVersionProvider::class,
|
||||
mixinStandardHelpOptions = true,
|
||||
showDefaultValues = true,
|
||||
description = ["Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs"]
|
||||
)
|
||||
class Main : Runnable {
|
||||
class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a local test Corda network using a set of node configuration files and CorDapp JARs") {
|
||||
@Option(
|
||||
names = ["--dir"],
|
||||
description = [
|
||||
@ -45,143 +24,7 @@ class Main : Runnable {
|
||||
@Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""])
|
||||
private var noCopy: Boolean = false
|
||||
|
||||
@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")
|
||||
}
|
||||
override fun runProgram() {
|
||||
NetworkBootstrapper().bootstrap(dir.toAbsolutePath().normalize(), copyCordapps = !noCopy)
|
||||
}
|
||||
}
|
||||
|
||||
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: $releaseVersion",
|
||||
"Revision: $revision"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
14
tools/cliutils/build.gradle
Normal file
14
tools/cliutils/build.gradle
Normal file
@ -0,0 +1,14 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'kotlin'
|
||||
|
||||
description 'CLI Utilities'
|
||||
|
||||
dependencies {
|
||||
compile project(":core")
|
||||
|
||||
compile "info.picocli:picocli:$picocli_version"
|
||||
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"
|
||||
compile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package net.corda.cliutils
|
||||
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.isReadable
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* When a config file is required as part of setup, use this class to check that it exists and is formatted correctly. Add it as
|
||||
* `@CommandLine.Mixin
|
||||
* lateinit var configParser: ConfigFilePathArgsParser`
|
||||
* in your command class and then call `validate()`
|
||||
*/
|
||||
@CommandLine.Command(description = ["Parse configuration file. Checks if given configuration file exists"])
|
||||
class ConfigFilePathArgsParser : Validated {
|
||||
@CommandLine.Option(names = ["--config-file", "-f"], required = true, paramLabel = "FILE", description = ["The path to the config file"])
|
||||
lateinit var configFile: Path
|
||||
|
||||
override fun validator(): List<String> {
|
||||
val res = mutableListOf<String>()
|
||||
if (!configFile.exists()) res += "Config file ${configFile.toAbsolutePath().normalize()} does not exist!"
|
||||
if (!configFile.isReadable) res += "Config file ${configFile.toAbsolutePath().normalize()} is not readable"
|
||||
return res
|
||||
}
|
||||
}
|
@ -1,54 +1,17 @@
|
||||
package net.corda.node.utilities
|
||||
package net.corda.cliutils
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.isReadable
|
||||
import net.corda.core.internal.rootMessage
|
||||
import net.corda.core.utilities.contextLogger
|
||||
|
||||
import org.apache.logging.log4j.Level
|
||||
import picocli.CommandLine
|
||||
import picocli.CommandLine.*
|
||||
import java.nio.file.Path
|
||||
import kotlin.system.exitProcess
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Something heavily used in network services, I am not sure it's of much use in corda, but who knows. Definitely it was the key to making DevOps happy.
|
||||
* Add it as
|
||||
* `@CommandLine.Mixin
|
||||
* lateinit var configParser: ConfigFilePathArgsParser`
|
||||
*
|
||||
* in your command class and then validate()
|
||||
*/
|
||||
@Command(description = ["Parse configuration file. Checks if given configuration file exists"])
|
||||
class ConfigFilePathArgsParser : Validated {
|
||||
@Option(names = ["--config-file", "-f"], required = true, paramLabel = "FILE", description = ["The path to the config file"])
|
||||
lateinit var configFile: Path
|
||||
|
||||
override fun validator(): List<String> {
|
||||
val res = mutableListOf<String>()
|
||||
if(!configFile.exists()) res += "Config file ${configFile.toAbsolutePath().normalize()} does not exist!"
|
||||
if(!configFile.isReadable) res += "Config file ${configFile.toAbsolutePath().normalize()} is not readable"
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple version printing when command is called with --version or -V flag. Assuming that we reuse Corda-Release-Version and Corda-Revision
|
||||
* in the manifest file.
|
||||
*/
|
||||
class CordaVersionProvider : IVersionProvider {
|
||||
override fun getVersion(): Array<String> {
|
||||
return if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
|
||||
arrayOf("Version: ${Manifests.read("Corda-Release-Version")}", "Revision: ${Manifests.read("Corda-Revision")}")
|
||||
} else {
|
||||
arrayOf("No version data is available in the MANIFEST file.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Usually when we have errors in some command line flags that are not handled by picocli (e.g. non existing file). Error is thrown
|
||||
* and no CommandLine help afterwards. This can be called from run() method.
|
||||
* When we have errors in command line flags that are not handled by picocli (e.g. non existing files), an error is thrown
|
||||
* without any command line help afterwards. This can be called from run() method.
|
||||
*/
|
||||
interface Validated {
|
||||
companion object {
|
||||
@ -56,6 +19,7 @@ interface Validated {
|
||||
const val RED = "\u001B[31m"
|
||||
const val RESET = "\u001B[0m"
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that provided command line parameters are valid, e.g. check file existence. Return list of error strings.
|
||||
*/
|
||||
@ -75,6 +39,25 @@ interface Validated {
|
||||
}
|
||||
}
|
||||
|
||||
fun CordaCliWrapper.start(vararg args: String) {
|
||||
val cmd = CommandLine(this)
|
||||
cmd.commandSpec.name(alias)
|
||||
cmd.commandSpec.usageMessage().description(description)
|
||||
try {
|
||||
cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(Help.Ansi.AUTO),
|
||||
DefaultExceptionHandler<List<Any>>().useErr(System.err).useAnsi(Help.Ansi.AUTO),
|
||||
*args)
|
||||
} catch (e: ExecutionException) {
|
||||
val throwable = e.cause ?: e
|
||||
if (this.verbose) {
|
||||
throwable.printStackTrace()
|
||||
} else {
|
||||
System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}")
|
||||
}
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple base class for handling help, version, verbose and logging-level commands.
|
||||
* As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node)
|
||||
@ -83,30 +66,44 @@ interface Validated {
|
||||
@Command(mixinStandardHelpOptions = true,
|
||||
versionProvider = CordaVersionProvider::class,
|
||||
sortOptions = false,
|
||||
showDefaultValues = true,
|
||||
synopsisHeading = "%n@|bold,underline Usage|@:%n%n",
|
||||
descriptionHeading = "%n@|bold,underline Description|@:%n%n",
|
||||
parameterListHeading = "%n@|bold,underline Parameters|@:%n%n",
|
||||
optionListHeading = "%n@|bold,underline Options|@:%n%n",
|
||||
commandListHeading = "%n@|bold,underline Commands|@:%n%n")
|
||||
abstract class ArgsParser {
|
||||
@Option(names = [ "-v", "--verbose" ], description = ["If set, prints logging to the console as well as to a file."])
|
||||
abstract class CordaCliWrapper(val alias: String, val description: String) : Runnable {
|
||||
@Option(names = ["-v", "--verbose"], description = ["If set, prints logging to the console as well as to a file."])
|
||||
var verbose: Boolean = false
|
||||
|
||||
@Option(names = ["--logging-level"],
|
||||
// TODO For some reason I couldn't make picocli COMPLETION-CANDIDATES work
|
||||
description = ["Enable logging at this level and higher. Defaults to INFO. Possible values: OFF, INFO, WARN, TRACE, DEBUG, ERROR, FATAL, ALL"],
|
||||
converter = [LoggingLevelConverter::class])
|
||||
completionCandidates = LoggingLevelConverter.LoggingLevels::class,
|
||||
description = ["Enable logging at this level and higher. Possible values: \${COMPLETION-CANDIDATES}"],
|
||||
converter = [LoggingLevelConverter::class]
|
||||
)
|
||||
var loggingLevel: Level = Level.INFO
|
||||
|
||||
@Mixin
|
||||
private lateinit var installShellExtensionsParser: InstallShellExtensionsParser
|
||||
|
||||
// This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before).
|
||||
// Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used.
|
||||
protected open fun initLogging() {
|
||||
private fun initLogging() {
|
||||
val loggingLevel = loggingLevel.name().toLowerCase(Locale.ENGLISH)
|
||||
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
||||
if (verbose) {
|
||||
System.setProperty("consoleLogLevel", loggingLevel)
|
||||
}
|
||||
}
|
||||
|
||||
// Override this function with the actual method to be run once all the arguments have been parsed
|
||||
abstract fun runProgram()
|
||||
|
||||
final override fun run() {
|
||||
installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name)
|
||||
initLogging()
|
||||
runProgram()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,6 +111,9 @@ abstract class ArgsParser {
|
||||
*/
|
||||
class LoggingLevelConverter : ITypeConverter<Level> {
|
||||
override fun convert(value: String?): Level {
|
||||
return value?.let { Level.getLevel(it) } ?: throw TypeConversionException("Unknown option for --logging-level: $value")
|
||||
return value?.let { Level.getLevel(it) }
|
||||
?: throw TypeConversionException("Unknown option for --logging-level: $value")
|
||||
}
|
||||
|
||||
class LoggingLevels : ArrayList<String>(Level.values().map { it.toString() })
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package net.corda.cliutils
|
||||
|
||||
import com.jcabi.manifests.Manifests
|
||||
import picocli.CommandLine
|
||||
|
||||
/**
|
||||
* Simple version printing when command is called with --version or -V flag. Assuming that we reuse Corda-Release-Version and Corda-Revision
|
||||
* in the manifest file.
|
||||
*/
|
||||
class CordaVersionProvider : CommandLine.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 if (Manifests.exists("Corda-Release-Version") && Manifests.exists("Corda-Revision")) {
|
||||
arrayOf("Version: $releaseVersion", "Revision: $revision")
|
||||
} else {
|
||||
arrayOf("No version data is available in the MANIFEST file.")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package net.corda.cliutils
|
||||
|
||||
import net.corda.core.internal.*
|
||||
import picocli.CommandLine
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.StandardCopyOption
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private class ShellExtensionsGenerator(val alias: String, val className: String) {
|
||||
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 {
|
||||
Collections.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, StandardCopyOption.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, className: String) {
|
||||
println("Generating $alias auto completion file")
|
||||
val autoCompleteFile = getAutoCompleteFileLocation(alias)
|
||||
autoCompleteFile.parent.createDirectories()
|
||||
picocli.AutoComplete.main("-f", "-n", alias, className, "-o", autoCompleteFile.toStringWithDeWindowsfication())
|
||||
|
||||
// Append hash of file to autocomplete file
|
||||
autoCompleteFile.toFile().appendText(jarVersion(alias))
|
||||
}
|
||||
|
||||
fun installShellExtensions() {
|
||||
// Get jar location and generate alias command
|
||||
val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'"
|
||||
generateAutoCompleteFile(alias, className)
|
||||
|
||||
// Get bash settings file
|
||||
val bashSettingsFile = SettingsFile(userHome / ".bashrc")
|
||||
// Replace any existing 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")
|
||||
}
|
||||
|
||||
fun checkForAutoCompleteUpdate() {
|
||||
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, className)
|
||||
println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CommandLine.Command(description = [""])
|
||||
class InstallShellExtensionsParser {
|
||||
@CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"])
|
||||
var installShellExtensions: Boolean = false
|
||||
|
||||
fun installOrUpdateShellExtensions(alias: String, className: String) {
|
||||
val generator = ShellExtensionsGenerator(alias, className)
|
||||
if (installShellExtensions) {
|
||||
generator.installShellExtensions()
|
||||
exitProcess(0)
|
||||
} else {
|
||||
generator.checkForAutoCompleteUpdate()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user