diff --git a/docs/cli.md b/docs/cli.md index 119c23ea75..211cd70cec 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1285,14 +1285,11 @@ The `run` command is used to execute a local pipeline script or remote pipeline : Disable process execution with Docker. `-without-podman` -: Disable process execution in a Podman container. +: Disable process execution with Podman. `-without-spack` : Disable process execution with Spack. -`-without-wave` -: Disable the use of Wave containers. - `-w, -work-dir` (`work`) : Directory where intermediate result files are stored. @@ -1515,3 +1512,28 @@ When a command line parameter includes one or more glob characters, i.e. wildcar nextflow run --files "*.fasta" ``` ::: + +(cli-v2)= + +## CLI v2 + +:::{versionadded} 24.04.0 +::: + +Nextflow now has an alternative command-line interface called `nf`, which more closely follows conventions for CLI options. In particular, long options for `nf` have two dashes, i.e. `-resume` is now `--resume`. + +Pipeline parameters can be specified alongside CLI options as before, as long as they are named differently. If for some reason you have a conflicting param, you can use a double dash `--` to separate it from the CLI options. + +The `nf` command is a near drop-in replacement, by simply using double dashes for long options. The following minor changes were also introduced: + +- The `-deep` option was renamed to `--depth` for the `clone`, `pull`, and `run` commands + +- The `-without-*` options were removed from the `run` command + +- Pipeline positional args were removed from the `inspect` and `run` commands + +- Clustered short options (e.g. `-xvfShortFile` as a shorthand for `-x -v -f ShortFile`) are not supported. Each option should be specified separately and options with values should be separated by a space or `=`. The following dynamic options are affected by this change: + - `node -cluster.=` -> `node --cluster =` + - `run -e.=` -> `run (-e|--env) =` + - `run -executor.=` -> `run --executor =` + - `run -process.=` -> `run --process =` diff --git a/launch-v1.sh b/launch-v1.sh new file mode 100755 index 0000000000..22bd35411d --- /dev/null +++ b/launch-v1.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright 2013-2023, Seqera Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cwd="$(realpath "$(dirname "$0")")" + +MAIN_CLASS="nextflow.cli.Launcher" "${cwd}/launch.sh" "$@" diff --git a/launch-v2.sh b/launch-v2.sh new file mode 100755 index 0000000000..1943492e73 --- /dev/null +++ b/launch-v2.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Copyright 2013-2023, Seqera Labs +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cwd="$(realpath "$(dirname "$0")")" + +MAIN_CLASS="nextflow.cli.v2.Launcher" "${cwd}/launch.sh" "$@" diff --git a/modules/nextflow/build.gradle b/modules/nextflow/build.gradle index b5e0bfbaaa..ac317f30d2 100644 --- a/modules/nextflow/build.gradle +++ b/modules/nextflow/build.gradle @@ -44,6 +44,7 @@ dependencies { api 'jline:jline:2.9' api 'org.pf4j:pf4j:3.10.0' api 'dev.failsafe:failsafe:3.1.0' + api 'info.picocli:picocli-groovy:4.7.1' testImplementation 'org.subethamail:subethasmtp:3.1.7' diff --git a/modules/nextflow/src/main/groovy/nextflow/Session.groovy b/modules/nextflow/src/main/groovy/nextflow/Session.groovy index 5ca23559d7..6a8104cfe6 100644 --- a/modules/nextflow/src/main/groovy/nextflow/Session.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/Session.groovy @@ -432,10 +432,7 @@ class Session implements ISession { } /** - * Given the `run` command line options creates the required {@link TraceObserver}s - * - * @param runOpts The {@code CmdRun} object holding the run command options - * @return A list of {@link TraceObserver} objects or an empty list + * Creates the required {@link TraceObserver}s for the workflow run. */ @PackageScope List createObservers() { diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CliOptions.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CliOptions.groovy index 172790edfc..8876d4c3a3 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CliOptions.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CliOptions.groovy @@ -28,79 +28,100 @@ import org.fusesource.jansi.Ansi * @author Paolo Di Tommaso */ @Slf4j -class CliOptions { +abstract class CliOptions { + + abstract Boolean getAnsiLogCli() + abstract boolean isBackground() + abstract List getConfig() + abstract List getDebug() + abstract boolean getIgnoreConfigIncludes() + abstract String getLogFile() + abstract boolean isQuiet() + abstract boolean getRemoteDebug() + abstract String getSyslog() + abstract List getTrace() + abstract List getUserConfig() + abstract boolean getVersion() + abstract void setAnsiLog(boolean value) + abstract void setBackground(boolean value) + + static class V1 extends CliOptions { + + /** + * The packages to debug + */ + @Parameter(hidden = true, names='-debug') + List debug + + @Parameter(names=['-log'], description = 'Set nextflow log file path') + String logFile + + @Parameter(names=['-c','-config'], description = 'Add the specified file to configuration set') + List userConfig + + @Parameter(names=['-config-ignore-includes'], description = 'Disable the parsing of config includes') + boolean ignoreConfigIncludes + + @Parameter(names=['-C'], description = 'Use the specified configuration file(s) overriding any defaults') + List config + + /** + * the packages to trace + */ + @Parameter(names='-trace', description = 'Enable trace level logging for the specified package name - multiple packages can be provided separating them with a comma e.g. \'-trace nextflow,io.seqera\'') + List trace + + /** + * Enable syslog appender + */ + @Parameter(names = ['-syslog'], description = 'Send logs to syslog server (eg. localhost:514)' ) + String syslog + + /** + * Print out the version number and exit + */ + @Parameter(names = ['-v','-version'], description = 'Print the program version') + boolean version + + /** + * Print out the 'help' and exit + */ + @Parameter(names = ['-h'], description = 'Print this help', help = true) + boolean help + + @Parameter(names = ['-q','-quiet'], description = 'Do not print information messages' ) + boolean quiet + + @Parameter(names = ['-bg','-background'], description = 'Execute nextflow in background', arity = 0) + boolean background + + @DynamicParameter(names = ['-D'], description = 'Set JVM properties' ) + Map jvmOpts = [:] + + @Parameter(names = ['-self-update'], description = 'Update nextflow to the latest version', arity = 0, hidden = true) + boolean selfUpdate + + @Parameter(names=['-remote-debug'], description = "Enable JVM interactive remote debugging (experimental)") + boolean remoteDebug + + Boolean ansiLogCli + + void setAnsiLog(boolean value) { ansiLogCli = value } - /** - * The packages to debug - */ - @Parameter(hidden = true, names='-debug') - List debug - - @Parameter(names=['-log'], description = 'Set nextflow log file path') - String logFile - - @Parameter(names=['-c','-config'], description = 'Add the specified file to configuration set') - List userConfig - - @Parameter(names=['-config-ignore-includes'], description = 'Disable the parsing of config includes') - boolean ignoreConfigIncludes - - @Parameter(names=['-C'], description = 'Use the specified configuration file(s) overriding any defaults') - List config - - /** - * the packages to trace - */ - @Parameter(names='-trace', description = 'Enable trace level logging for the specified package name - multiple packages can be provided separating them with a comma e.g. \'-trace nextflow,io.seqera\'') - List trace - - /** - * Enable syslog appender - */ - @Parameter(names = ['-syslog'], description = 'Send logs to syslog server (eg. localhost:514)' ) - String syslog - - /** - * Print out the version number and exit - */ - @Parameter(names = ['-v','-version'], description = 'Print the program version') - boolean version - - /** - * Print out the 'help' and exit - */ - @Parameter(names = ['-h'], description = 'Print this help', help = true) - boolean help - - @Parameter(names = ['-q','-quiet'], description = 'Do not print information messages' ) - boolean quiet - - @Parameter(names = ['-bg'], description = 'Execute nextflow in background', arity = 0) - boolean background - - @DynamicParameter(names = ['-D'], description = 'Set JVM properties' ) - Map jvmOpts = [:] - - @Parameter(names = ['-self-update'], description = 'Update nextflow to the latest version', arity = 0, hidden = true) - boolean selfUpdate - - @Parameter(names=['-remote-debug'], description = "Enable JVM interactive remote debugging (experimental)") - boolean remoteDebug - - Boolean ansiLog + } boolean getAnsiLog() { - if( ansiLog && quiet ) + if( ansiLogCli && quiet ) throw new AbortOperationException("Command line options `quiet` and `ansi-log` cannot be used together") - if( ansiLog != null ) - return ansiLog + if( ansiLogCli != null ) + return ansiLogCli if( background ) - return ansiLog = false + return ansiLogCli = false if( quiet ) - return ansiLog = false + return ansiLogCli = false final env = System.getenv('NXF_ANSI_LOG') if( env ) try { @@ -113,7 +134,7 @@ class CliOptions { } boolean hasAnsiLogFlag() { - ansiLog==true || System.getenv('NXF_ANSI_LOG')=='true' + ansiLogCli==true || System.getenv('NXF_ANSI_LOG')=='true' } } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdBase.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdBase.groovy index 75b4cde37b..4abb1c5deb 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdBase.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdBase.groovy @@ -17,12 +17,14 @@ package nextflow.cli import com.beust.jcommander.Parameter +import groovy.transform.CompileStatic /** * Implement command shared methods * * @author Paolo Di Tommaso */ +@CompileStatic abstract class CmdBase implements Runnable { private Launcher launcher diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdClean.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdClean.groovy index bb58edacbb..f8b248e552 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdClean.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdClean.groovy @@ -47,51 +47,76 @@ import nextflow.util.HistoryFile.Record */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Clean up project cache and work directories") -class CmdClean extends CmdBase implements CacheBase { +class CmdClean implements CacheBase { static final public NAME = 'clean' - @Parameter(names=['-q', '-quiet'], description = 'Do not print names of files removed', arity = 0) - boolean quiet + interface Options { + String getAfter() + String getBefore() + String getBut() + boolean getDryRun() + boolean getForce() + boolean getKeepLogs() + boolean getQuiet() + List getArgs() + + CliOptions getLauncherOptions() + } + + @Parameters(commandDescription = 'Clean up project cache and work directories') + static class V1 extends CmdBase implements Options { + + @Parameter(names=['-q', '-quiet'], description = 'Do not print names of files removed', arity = 0) + boolean quiet + + @Parameter(names=['-f', '-force'], description = 'Force clean command', arity = 0) + boolean force - @Parameter(names=['-f', '-force'], description = 'Force clean command', arity = 0) - boolean force + @Parameter(names=['-n', '-dry-run'], description = 'Print names of file to be removed without deleting them' , arity = 0) + boolean dryRun - @Parameter(names=['-n', '-dry-run'], description = 'Print names of file to be removed without deleting them' , arity = 0) - boolean dryRun + @Parameter(names='-after', description = 'Clean up runs executed after the specified one') + String after - @Parameter(names='-after', description = 'Clean up runs executed after the specified one') - String after + @Parameter(names='-before', description = 'Clean up runs executed before the specified one') + String before - @Parameter(names='-before', description = 'Clean up runs executed before the specified one') - String before + @Parameter(names='-but', description = 'Clean up all runs except the specified one') + String but - @Parameter(names='-but', description = 'Clean up all runs except the specified one') - String but + @Parameter(names=['-k', '-keep-logs'], description = 'Removes only temporary files but retains execution log entries and metadata') + boolean keepLogs - @Parameter(names=['-k', '-keep-logs'], description = 'Removes only temporary files but retains execution log entries and metadata') - boolean keepLogs + @Parameter + List args - @Parameter - List args + @Override + CliOptions getLauncherOptions() { + launcher.options + } + + @Override + String getName() { NAME } + + @Override + void run() { + new CmdClean(this).run() + } + + } + + @Delegate + private Options options private CacheDB currentCacheDb private Map dryHash = new HashMap<>() - /** - * @return The name of this command {@code clean} - */ - @Override - String getName() { - return NAME + CmdClean(Options options) { + this.options = options } - /** - * Command entry method - */ - @Override void run() { init() validateOptions() @@ -108,7 +133,7 @@ class CmdClean extends CmdBase implements CacheBase { final builder = new ConfigBuilder() .setShowClosures(true) .showMissingVariables(true) - .setOptions(launcher.options) + .setOptions(launcherOptions) .setBaseDir(Paths.get('.')) final config = builder.buildConfigObject() diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdClone.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdClone.groovy index 9c582aaaf6..9f2a926fc8 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdClone.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdClone.groovy @@ -29,34 +29,61 @@ import nextflow.scm.AssetManager */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Clone a project into a folder") -class CmdClone extends CmdBase implements HubOptions { +class CmdClone { static final public NAME = 'clone' - @Parameter(required=true, description = 'name of the project to clone') - List args + interface Options extends HubOptions { + String getPipeline() + String getTargetName() + Integer getDepth() + String getRevision() + } + + @Parameters(commandDescription = "Clone a project into a folder") + static class V1 extends CmdBase implements Options, HubOptions.V1 { + + @Parameter(required=true, description = 'name of the project to clone') + List args = [] + + @Parameter(names='-r', description = 'Revision to clone - It can be a git branch, tag or revision number') + String revision + + @Parameter(names=['-d','-depth','-deep'], description = 'Create a shallow clone of the specified depth') + Integer depth + + @Override + String getPipeline() { args[0] } - @Parameter(names='-r', description = 'Revision to clone - It can be a git branch, tag or revision number') - String revision + @Override + String getTargetName() { + args.size() > 1 ? args[1] : null + } + + @Override + final String getName() { NAME } + + @Override + void run() { + new CmdClone(this).run() + } + } - @Parameter(names=['-d','-deep'], description = 'Create a shallow clone of the specified depth') - Integer deep + @Delegate + private Options options - @Override - final String getName() { NAME } + CmdClone(Options options) { + this.options = options + } - @Override void run() { // init plugin system Plugins.init() - // the pipeline name - String pipeline = args[0] final manager = new AssetManager(pipeline, this) // the target directory is the second parameter // otherwise default the current pipeline name - def target = new File(args.size()> 1 ? args[1] : manager.getBaseName()) + def target = new File(targetName ?: manager.getBaseName()) if( target.exists() ) { if( target.isFile() ) throw new AbortOperationException("A file with the same name already exists: $target") @@ -69,7 +96,7 @@ class CmdClone extends CmdBase implements HubOptions { manager.checkValidRemoteRepo() print "Cloning ${manager.project}${revision ? ':'+revision:''} ..." - manager.clone(target, revision, deep) + manager.clone(target, revision, depth) print "\r" println "${manager.project} cloned to: $target" } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy index ee898caf90..f49ab6d57b 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdConfig.groovy @@ -36,42 +36,82 @@ import nextflow.util.ConfigHelper */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Print a project configuration") -class CmdConfig extends CmdBase { +class CmdConfig { static final public NAME = 'config' - @Parameter(description = 'project name') - List args = [] + interface Options { + String getPipeline() + boolean getShowAllProfiles() + String getProfile() + boolean getPrintProperties() + boolean getPrintFlatten() + String getPrintValue() + boolean getSort() - @Parameter(names=['-a','-show-profiles'], description = 'Show all configuration profiles') - boolean showAllProfiles + CliOptions getLauncherOptions() + } + + @Parameters(commandDescription = "Print a project configuration") + static class V1 extends CmdBase implements Options { + + @Parameter(description = 'project name') + List args = [] + + @Parameter(names=['-a','-show-profiles'], description = 'Show all configuration profiles') + boolean showAllProfiles + + @Parameter(names=['-profile'], description = 'Choose a configuration profile') + String profile + + @Parameter(names = '-properties', description = 'Prints config using Java properties notation') + boolean printProperties + + @Parameter(names = '-flat', description = 'Print config using flat notation') + boolean printFlatten + + @Parameter(names = '-sort', description = 'Sort config attributes') + boolean sort - @Parameter(names=['-profile'], description = 'Choose a configuration profile') - String profile + @Parameter(names = '-value', description = 'Print the value of a config option, or fail if the option is not defined') + String printValue - @Parameter(names = '-properties', description = 'Prints config using Java properties notation') - boolean printProperties + @Override + String getPipeline() { + args.size() > 0 ? args[0] : null + } - @Parameter(names = '-flat', description = 'Print config using flat notation') - boolean printFlatten + @Override + CliOptions getLauncherOptions() { + launcher.options + } - @Parameter(names = '-sort', description = 'Sort config attributes') - boolean sort + @Override + String getName() { NAME } - @Parameter(names = '-value', description = 'Print the value of a config option, or fail if the option is not defined') - String printValue + @Override + void run() { + new CmdConfig(this).run() + } - @Override - String getName() { NAME } + } private OutputStream stdout = System.out - @Override + @Delegate + private Options options + + CmdConfig(Options options) { + this.options = options + } + + /* For testing purposes only */ + CmdConfig() {} + void run() { Plugins.init() Path base = null - if( args ) base = getBaseDir(args[0]) + if( pipeline ) base = getBaseDir(pipeline) if( !base ) base = Paths.get('.') if( profile && showAllProfiles ) { @@ -90,7 +130,7 @@ class CmdConfig extends CmdBase { final builder = new ConfigBuilder() .setShowClosures(true) .showMissingVariables(true) - .setOptions(launcher.options) + .setOptions(launcherOptions) .setBaseDir(base) .setCmdConfig(this) diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdConsole.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdConsole.groovy index 6cd57aff6d..1bce0dba9f 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdConsole.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdConsole.groovy @@ -28,13 +28,38 @@ import nextflow.ui.console.ConsoleExtension * @author Paolo Di Tommaso */ @CompileStatic -@Parameters(commandDescription = "Launch Nextflow interactive console") -class CmdConsole extends CmdBase { +class CmdConsole { - @Parameter(description = 'Nextflow console arguments') - List args + interface Options { + String getScript() + } + + @Parameters(commandDescription = "Launch Nextflow interactive console") + static class V1 extends CmdBase implements Options { + + @Parameter(description = 'Nextflow console arguments') + List args = [] + + @Override + String getScript() { + args.size() > 0 ? args[0] : null + } + + @Override + String getName() { 'console' } - String getName() { 'console' } + @Override + void run() { + new CmdConsole(this).run() + } + } + + @Delegate + private Options options + + CmdConsole(Options options) { + this.options = options + } void run() { Plugins.init() @@ -42,11 +67,6 @@ class CmdConsole extends CmdBase { final console = Plugins.getExtension(ConsoleExtension) if( !console ) throw new IllegalStateException("Failed to find Nextflow Console extension") - // normalise the console args prepending the `console` command itself - if( args == null ) - args = [] - args.add(0, 'console') - // go ! - console.run(args as String[]) + console.run(script) } } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdDrop.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdDrop.groovy index 9d67190a54..dc1a071938 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdDrop.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdDrop.groovy @@ -31,26 +31,48 @@ import nextflow.scm.AssetManager */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Delete the local copy of a project") -class CmdDrop extends CmdBase { +class CmdDrop { static final public NAME = 'drop' - @Parameter(required=true, description = 'name of the project to drop') - List args + interface Options { + String getPipeline() + boolean getForce() + } + + @Parameters(commandDescription = "Delete the local copy of a project") + static class V1 extends CmdBase implements Options { + + @Parameter(required=true, description = 'name of the project to drop') + List args = [] + + @Parameter(names='-f', description = 'Delete the repository without taking care of local changes') + boolean force + + @Override + String getPipeline() { args[0] } - @Parameter(names='-f', description = 'Delete the repository without taking care of local changes') - boolean force + @Override + final String getName() { NAME } - @Override - final String getName() { NAME } + @Override + void run() { + new CmdDrop(this).run() + } + } + + @Delegate + private Options options + + CmdDrop(Options options) { + this.options = options + } - @Override void run() { Plugins.init() - def manager = new AssetManager(args[0]) + def manager = new AssetManager(pipeline) if( !manager.localPath.exists() ) { - throw new AbortOperationException("No match found for: ${args[0]}") + throw new AbortOperationException("No match found for: ${pipeline}") } if( this.force || manager.isClean() ) { diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdFs.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdFs.groovy index bbf11dfb0f..3ca1257252 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdFs.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdFs.groovy @@ -25,6 +25,7 @@ import java.nio.file.Paths import java.nio.file.attribute.BasicFileAttributes import com.beust.jcommander.Parameter +import com.beust.jcommander.Parameters import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import nextflow.Global @@ -42,183 +43,227 @@ import nextflow.plugin.Plugins */ @CompileStatic @Slf4j -class CmdFs extends CmdBase implements UsageAware { +class CmdFs { static final public NAME = 'fs' - static final List commands = new ArrayList<>() + enum Command { + COPY, + MOVE, + LIST, + CAT, + REMOVE, + STAT + } - static { - commands << new CmdCopy() - commands << new CmdMove() - commands << new CmdList() - commands << new CmdCat() - commands << new CmdRemove() - commands << new CmdStat() + interface Options { + CliOptions getLauncherOptions() } - trait SubCmd { - abstract int getArity() + @Parameters(commandDescription = 'Perform basic filesystem operations') + static class V1 extends CmdBase implements UsageAware, Options { + + trait SubCmd { + abstract int getArity() - abstract String getName() + abstract String getName() - abstract String getDescription() + abstract String getDescription() - abstract void apply(Path source, Path target) + abstract Command getCommand() - String usage() { - "Usage: nextflow fs ${name} " + (arity==1 ? "" : "source_file target_file") + String usage() { + "Usage: nextflow fs ${name} " + (arity==1 ? "" : "source_file target_file") + } } - } - static class CmdCopy implements SubCmd { + static class CmdCopy implements SubCmd { + @Override + int getArity() { 2 } - @Override - int getArity() { 2 } + @Override + String getName() { 'cp' } - @Override - String getName() { 'cp' } - - String getDescription() { 'Copy a file' } + @Override + String getDescription() { 'Copy a file' } - @Override - void apply(Path source, Path target) { - FilesEx.copyTo(source, target) + @Override + Command getCommand() { Command.COPY } } - } + static class CmdMove implements SubCmd { + @Override + int getArity() { 2 } - static class CmdMove implements SubCmd { + @Override + String getName() { 'mv' } - @Override - int getArity() { 2 } + @Override + String getDescription() { 'Move a file' } - @Override - String getName() { 'mv' } + @Override + Command getCommand() { Command.MOVE } + } - String getDescription() { 'Move a file' } + static class CmdList implements SubCmd { + @Override + int getArity() { 1 } - @Override - void apply(Path source, Path target) { - FilesEx.moveTo(source, target) - } + @Override + String getDescription() { 'List the content of a folder' } - } + @Override + String getName() { 'ls' } - static class CmdList implements SubCmd { + @Override + Command getCommand() { Command.LIST } + } - @Override - int getArity() { 1 } + static class CmdCat implements SubCmd { + @Override + int getArity() { 1 } - String getDescription() { 'List the content of a folder' } + @Override + String getName() { 'cat' } - @Override - String getName() { 'ls' } + @Override + String getDescription() { 'Print a file to the stdout' } - @Override - void apply(Path source, Path target) { - println source.name + @Override + Command getCommand() { Command.CAT } } - } + static class CmdRemove implements SubCmd { + @Override + int getArity() { 1 } - static class CmdCat implements SubCmd { + @Override + String getName() { 'rm' } - @Override - int getArity() { 1 } + @Override + String getDescription() { 'Remove a file' } - @Override - String getName() { 'cat' } + @Override + Command getCommand() { Command.REMOVE } + } - @Override - String getDescription() { 'Print a file to the stdout' } + static class CmdStat implements SubCmd { + @Override + int getArity() { 1 } - @Override - void apply(Path source, Path target) { - String line - def reader = Files.newBufferedReader(source, Charset.defaultCharset()) - while( line = reader.readLine() ) - println line + @Override + String getName() { 'stat' } + + @Override + String getDescription() { 'Print file metadata' } + + @Override + Command getCommand() { Command.STAT } } - } + private List commands = (List)[ + new CmdCopy(), + new CmdMove(), + new CmdList(), + new CmdCat(), + new CmdRemove(), + new CmdStat() + ] - static class CmdStat implements SubCmd { - @Override - int getArity() { 1 } + @Parameter + List args = [] @Override - String getName() { 'stat' } + CliOptions getLauncherOptions() { + launcher.options + } @Override - String getDescription() { 'Print file to meta info' } + String getName() { + return NAME + } @Override - void apply(Path source, Path target) { - try { - final attr = Files.readAttributes(source, BasicFileAttributes) - print """\ - name : ${source.name} - size : ${attr.size()} - is directory : ${attr.isDirectory()} - last modified : ${attr.lastModifiedTime() ?: '-'} - creation time : ${attr.creationTime() ?: '-'} - """.stripIndent() + void run() { + if( !args ) { + usage() + return } - catch (IOException e) { - log.warn "Unable to read attributes for file: ${source.toUriString()} - cause: $e.message", e + + final cmd = findCmd(args[0]) + if( !cmd ) { + throw new AbortOperationException("Unknown file system command: `$cmd`") } - } - } - static class CmdRemove implements SubCmd { + if( args.size() - 1 != cmd.getArity() ) + throw new AbortOperationException(cmd.usage()) - @Override - int getArity() { 1 } + new CmdFs(this).run(cmd.getCommand(), args.drop(1)) + } - @Override - String getName() { 'rm' } + private SubCmd findCmd( String name ) { + commands.find { it.name == name } + } - @Override - String getDescription() { 'Remove a file' } + /** + * Print the command usage help + */ + void usage() { + usage(args) + } - @Override - void apply(Path source, Path target) { - Files.isDirectory(source) ? FilesEx.deleteDir(source) : FilesEx.delete(source) + /** + * Print the command usage help + * + * @param args The arguments as entered by the user + */ + void usage(List args) { + + def result = [] + if( !args ) { + result << 'Usage: nextflow fs [args]' + result << '' + result << 'Commands:' + commands.each { + result << " ${it.name}\t${it.description}" + } + result << '' + println result.join('\n').toString() + } + else { + def sub = findCmd(args[0]) + if( sub ) + println sub.usage() + else { + throw new AbortOperationException("Unknown fs sub-command: ${args[0]}") + } + } } } + @Delegate + private Options options - @Parameter - List args - - @Override - String getName() { - return NAME + CmdFs(Options options) { + this.options = options } private Session createSession() { // create the config final config = new ConfigBuilder() - .setOptions(getLauncher().getOptions()) + .setOptions(getLauncherOptions()) .setBaseDir(Paths.get('.')) .build() return new Session(config) } - @Override - void run() { - if( !args ) { - usage() - return - } - + void run(Command command, List args) { Plugins.init() final session = createSession() try { - run0() + run0(command, args) } finally { try { @@ -231,32 +276,27 @@ class CmdFs extends CmdBase implements UsageAware { } } - private void run0() { - final cmd = findCmd(args[0]) - if( !cmd ) { - throw new AbortOperationException("Unknown fs sub-command: `$cmd`") - } - - Path target - String source - if( cmd.arity==1 ) { - if( args.size() < 2 ) - throw new AbortOperationException(cmd.usage()) - source = args[1] - target = null - } - else { - if( args.size() < 3 ) - throw new AbortOperationException(cmd.usage()) - source = args[1] - target = args[2] as Path + private void run0(Command command, List args) { + switch( command ) { + case COPY: + traverse(args[0]) { Path path -> copy(path, args[1] as Path) } + break + case MOVE: + traverse(args[0]) { Path path -> move(path, args[1] as Path) } + break + case LIST: + traverse(args[0]) { Path path -> list(path) } + break + case CAT: + traverse(args[0]) { Path path -> cat(path) } + break + case REMOVE: + traverse(args[0]) { Path path -> remove(path) } + break + case STAT: + traverse(args[0]) { Path path -> stat(path) } + break } - - traverse(source) { Path path -> cmd.apply(path, target) } - } - - private SubCmd findCmd( String name ) { - commands.find { it.name == name } } private void traverse( String source, Closure op ) { @@ -281,40 +321,43 @@ class CmdFs extends CmdBase implements UsageAware { } - /** - * Print the command usage help - */ - void usage() { - usage(args) + void copy(Path source, Path target) { + FilesEx.copyTo(source, target) } - /** - * Print the command usage help - * - * @param args The arguments as entered by the user - */ - void usage(List args) { - - def result = [] - if( !args ) { - result << 'Usage: nextflow fs [arg]' - result << '' - result << 'Commands:' - commands.each { - result << " ${it.name}\t${it.description}" - } - result << '' - println result.join('\n').toString() + void move(Path source, Path target) { + FilesEx.moveTo(source, target) + } + + void list(Path source) { + println source.name + } + + void cat(Path source) { + String line + def reader = Files.newBufferedReader(source, Charset.defaultCharset()) + while( line = reader.readLine() ) + println line + } + + void remove(Path source) { + Files.isDirectory(source) ? FilesEx.deleteDir(source) : FilesEx.delete(source) + } + + void stat(Path source) { + try { + final attr = Files.readAttributes(source, BasicFileAttributes) + print """\ + name : ${source.name} + size : ${attr.size()} + is directory : ${attr.isDirectory()} + last modified : ${attr.lastModifiedTime() ?: '-'} + creation time : ${attr.creationTime() ?: '-'} + """.stripIndent() } - else { - def sub = findCmd(args[0]) - if( sub ) - println sub.usage() - else { - throw new AbortOperationException("Unknown fs sub-command: ${args[0]}") - } + catch (IOException e) { + log.warn "Unable to read attributes for file: ${source.toUriString()} - cause: $e.message", e } - } } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdInfo.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdInfo.groovy index c8c1c31bc0..a061754c68 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdInfo.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdInfo.groovy @@ -40,44 +40,75 @@ import org.yaml.snakeyaml.Yaml */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Print project and system runtime information") -class CmdInfo extends CmdBase { +class CmdInfo { static final public NAME = 'info' - private PrintStream out = System.out + interface Options { + abstract String getPipeline() + abstract boolean getDetailed() + abstract boolean getMoreDetailed() + abstract String getFormat() + abstract boolean getCheckForUpdates() + } + + @Parameters(commandDescription = "Print project and system runtime information") + static class V1 extends CmdBase implements Options { + + @Parameter(description = 'project name') + List args = [] + + @Parameter(names='-d',description = 'Show detailed information', arity = 0) + boolean detailed + + @Parameter(names='-dd', hidden = true, arity = 0) + boolean moreDetailed + + @Parameter(names='-o', description = 'Output format, either: text (default), json, yaml') + String format - @Parameter(description = 'project name') - List args + @Parameter(names=['-u','-check-updates'], description = 'Check for remote updates') + boolean checkForUpdates - @Parameter(names='-d',description = 'Show detailed information', arity = 0) - boolean detailed + @Override + String getPipeline() { + args.size() > 0 ? args[0] : null + } + + @Override + final String getName() { NAME } + + @Override + void run() { + new CmdInfo(this).run() + } - @Parameter(names='-dd', hidden = true, arity = 0) - boolean moreDetailed + } + + private PrintStream out = System.out - @Parameter(names='-o', description = 'Output format, either: text (default), json, yaml') - String format + @Delegate + private Options options - @Parameter(names=['-u','-check-updates'], description = 'Check for remote updates') - boolean checkForUpdates + CmdInfo(Options options) { + this.options = options + } - @Override - final String getName() { NAME } + /* For testing purposes only */ + CmdInfo() {} - @Override void run() { int level = moreDetailed ? 2 : ( detailed ? 1 : 0 ) - if( !args ) { + if( !pipeline ) { println getInfo(level) return } Plugins.init() - final manager = new AssetManager(args[0]) + final manager = new AssetManager(pipeline) if( !manager.isLocal() ) - throw new AbortOperationException("Unknown project `${args[0]}`") + throw new AbortOperationException("Unknown project `${pipeline}`") if( !format || format == 'text' ) { printText(manager,level) diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdInspect.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdInspect.groovy index 64ff33d86a..dc9a5094d8 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdInspect.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdInspect.groovy @@ -33,58 +33,115 @@ import nextflow.util.LoggerHelper */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Inspect process settings in a pipeline project") -class CmdInspect extends CmdBase { +class CmdInspect { - @Override - String getName() { - return 'inspect' + static final public String NAME = 'inspect' + + interface Options { + String getPipeline() + List getArgs() + Map getParams() + + boolean getConcretize() + String getFormat() + boolean getIgnoreErrors() + String getParamsFile() + String getProfile() + String getRevision() + List getRunConfig() + + String getLauncherCli() + CliOptions getLauncherOptions() } - @Parameter(names=['-concretize'], description = "Build the container images resolved by the inspect command") - boolean concretize + @Parameters(commandDescription = "Inspect process settings in a pipeline project") + static class V1 extends CmdBase implements Options { + + @Parameter(names=['-concretize'], description = "Build the container images resolved by the inspect command") + boolean concretize + + @Parameter(names=['-c','-config'], hidden = true) + List runConfig + + @Parameter(names=['-format'], description = "Inspect output format. Can be 'json' or 'config'") + String format = 'json' + + @Parameter(names=['-i','-ignore-errors'], description = 'Ignore errors while inspecting the pipeline') + boolean ignoreErrors + + @DynamicParameter(names = '--', hidden = true) + Map params = new LinkedHashMap<>() + + @Parameter(names='-params-file', description = 'Load script parameters from a JSON/YAML file') + String paramsFile - @Parameter(names=['-c','-config'], hidden = true) - List runConfig + @Parameter(names=['-profile'], description = 'Use the given configuration profile(s)') + String profile - @Parameter(names=['-format'], description = "Inspect output format. Can be 'json' or 'config'") - String format = 'json' + @Parameter(names=['-r','-revision'], description = 'Revision of the project to inspect (either a git branch, tag or commit SHA number)') + String revision - @Parameter(names=['-i','-ignore-errors'], description = 'Ignore errors while inspecting the pipeline') - boolean ignoreErrors + @Parameter(description = 'Project name or repository url') + List args - @DynamicParameter(names = '--', hidden = true) - Map params = new LinkedHashMap<>() + @Override + String getPipeline() { + args[0] + } - @Parameter(names='-params-file', description = 'Load script parameters from a JSON/YAML file') - String paramsFile + @Override + List getArgs() { + args.size() > 1 ? args[1..-1] : [] + } - @Parameter(names=['-profile'], description = 'Use the given configuration profile(s)') - String profile + @Override + String getLauncherCli() { + launcher.cliString + } - @Parameter(names=['-r','-revision'], description = 'Revision of the project to inspect (either a git branch, tag or commit SHA number)') - String revision + @Override + CliOptions getLauncherOptions() { + launcher.options + } + + @Override + String getName() { NAME } + + @Override + void run() { + final opts = new CmdRun.V1() + opts.launcher = launcher + opts.ansiLog = false + opts.preview = true + opts.args = args + opts.params = params + opts.paramsFile = paramsFile + opts.profile = profile + opts.revision = revision + opts.runConfig = runConfig + + new CmdInspect(this).run(opts) + } + + } + + @Delegate + private Options options + + CmdInspect(Options options) { + this.options = options + } - @Parameter(description = 'Project name or repository url') - List args + /* For testing purposes only */ + CmdInspect() {} - @Override - void run() { + void run(CmdRun.Options opts) { ContainerInspectMode.activate(true) // configure quiet mode LoggerHelper.setQuiet(true) // setup the target run command - final target = new CmdRun() - target.launcher = this.launcher - target.args = args - target.profile = this.profile - target.revision = this.revision - target.runConfig = this.runConfig - target.params = this.params - target.paramsFile = this.paramsFile - target.preview = true + final target = new CmdRun(opts) target.previewAction = this.&applyInspect - target.ansiLog = false // run it target.run() } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdKubeRun.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdKubeRun.groovy index 38ebb9e695..c16f9dd21f 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdKubeRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdKubeRun.groovy @@ -16,12 +16,15 @@ package nextflow.cli +import java.util.regex.Pattern + import com.beust.jcommander.Parameter import com.beust.jcommander.Parameters import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import nextflow.exception.AbortOperationException import nextflow.k8s.K8sDriverLauncher +import nextflow.util.HistoryFile /** * Extends `run` command to support Kubernetes deployment * @@ -30,7 +33,7 @@ import nextflow.k8s.K8sDriverLauncher @Slf4j @CompileStatic @Parameters(commandDescription = "Execute a workflow in a Kubernetes cluster (experimental)") -class CmdKubeRun extends CmdRun { +class CmdKubeRun extends CmdRun.V1 { static private String POD_NAME = /[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/ @@ -68,11 +71,10 @@ class CmdKubeRun extends CmdRun { @Override String getName() { 'kuberun' } - @Override protected void checkRunName() { if( runName && !runName.matches(POD_NAME) ) throw new AbortOperationException("Not a valid K8s pod name -- It can only contain lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character") - super.checkRunName() + checkRunName0() runName = runName.replace('_','-') } @@ -99,4 +101,29 @@ class CmdKubeRun extends CmdRun { System.exit(status) } + /* copied from {@code CmdRun} */ + + protected void checkRunName0() { + if( runName == 'last' ) + throw new AbortOperationException("Not a valid run name: `last`") + if( runName && !matchRunName(runName) ) + throw new AbortOperationException("Not a valid run name: `$runName` -- It must match the pattern $RUN_NAME_PATTERN") + + if( !runName ) { + if( HistoryFile.disabled() ) + throw new AbortOperationException("Missing workflow run name") + // -- make sure the generated name does not exist already + runName = HistoryFile.DEFAULT.generateNextName() + } + + else if( !HistoryFile.disabled() && HistoryFile.DEFAULT.checkExistsByName(runName) ) + throw new AbortOperationException("Run name `$runName` has been already used -- Specify a different one") + } + + static final public Pattern RUN_NAME_PATTERN = Pattern.compile(/^[a-z](?:[a-z\d]|[-_](?=[a-z\d])){0,79}$/, Pattern.CASE_INSENSITIVE) + + static protected boolean matchRunName(String name) { + RUN_NAME_PATTERN.matcher(name).matches() + } + } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdList.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdList.groovy index 5b08be249a..b9d3a01dfa 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdList.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdList.groovy @@ -28,15 +28,32 @@ import nextflow.scm.AssetManager */ @Slf4j @CompileStatic -@Parameters(commandDescription = "List all downloaded projects") -class CmdList extends CmdBase { +class CmdList { static final public NAME = 'list' - @Override - final String getName() { NAME } + interface Options {} + + @Parameters(commandDescription = "List all downloaded projects") + static class V1 extends CmdBase implements Options { + + @Override + final String getName() { NAME } + + @Override + void run() { + new CmdList(this).run() + } + + } + + @Delegate + private Options options + + CmdList(Options options) { + this.options = options + } - @Override void run() { def all = AssetManager.list() diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdLog.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdLog.groovy index 66d88980d1..da330e4780 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdLog.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdLog.groovy @@ -41,8 +41,7 @@ import static nextflow.cli.CmdHelper.fixEqualsOp */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Print executions log and runtime info") -class CmdLog extends CmdBase implements CacheBase { +class CmdLog implements CacheBase { static private List ALL_FIELDS @@ -59,35 +58,64 @@ class CmdLog extends CmdBase implements CacheBase { static final public NAME = 'log' - @Parameter(names = ['-s'], description='Character used to separate column values') - String sep = '\\t' + interface Options { + String getAfter() + String getBefore() + String getBut() + String getFields() + String getFilterStr() + boolean getListFields() + boolean getQuiet() + String getSeparator() + String getTemplateStr() + List getArgs() + } + + @Parameters(commandDescription = "Print executions log and runtime info") + static class V1 extends CmdBase implements Options { + + @Parameter(names = ['-s'], description='Character used to separate column values') + String separator = '\\t' + + @Parameter(names=['-f','-fields'], description = 'Comma separated list of fields to include in the printed log -- Use the `-l` option to show the list of available fields') + String fields + + @Parameter(names = ['-t','-template'], description = 'Text template used to each record in the log ') + String templateStr + + @Parameter(names=['-l','-list-fields'], description = 'Show all available fields', arity = 0) + boolean listFields - @Parameter(names=['-f','-fields'], description = 'Comma separated list of fields to include in the printed log -- Use the `-l` option to show the list of available fields') - String fields + @Parameter(names=['-F','-filter'], description = "Filter log entries by a custom expression e.g. process =~ /foo.*/ && status == 'COMPLETED'") + String filterStr - @Parameter(names = ['-t','-template'], description = 'Text template used to each record in the log ') - String templateStr + @Parameter(names='-after', description = 'Show log entries for runs executed after the specified one') + String after - @Parameter(names=['-l','-list-fields'], description = 'Show all available fields', arity = 0) - boolean listFields + @Parameter(names='-before', description = 'Show log entries for runs executed before the specified one') + String before - @Parameter(names=['-F','-filter'], description = "Filter log entries by a custom expression e.g. process =~ /foo.*/ && status == 'COMPLETED'") - String filterStr + @Parameter(names='-but', description = 'Show log entries of all runs except the specified one') + String but - @Parameter(names='-after', description = 'Show log entries for runs executed after the specified one') - String after + @Parameter(names=['-q','-quiet'], description = 'Show only run names', arity = 0) + boolean quiet - @Parameter(names='-before', description = 'Show log entries for runs executed before the specified one') - String before + @Parameter(description = 'Run name or session id') + List args = [] - @Parameter(names='-but', description = 'Show log entries of all runs except the specified one') - String but + @Override + final String getName() { NAME } + + @Override + void run() { + new CmdLog(this).run() + } - @Parameter(names=['-q','-quiet'], description = 'Show only run names', arity = 0) - boolean quiet + } - @Parameter(description = 'Run name or session id') - List args + @Delegate + private Options options private Script filterScript @@ -97,9 +125,12 @@ class CmdLog extends CmdBase implements CacheBase { private Map printed = new HashMap<>() - @Override - final String getName() { NAME } + CmdLog(Options options) { + this.options = options + } + /* For testing purposes only */ + CmdLog() {} void init() { CacheBase.super.init() @@ -124,21 +155,24 @@ class CmdLog extends CmdBase implements CacheBase { // // initialize the template engine // + def templateStr0 if( !templateStr ) { - if( !fields ) fields = DEFAULT_FIELDS - templateStr = fields.tokenize(', \n').collect { '$'+it } .join(sep) + String fields0 = fields ?: DEFAULT_FIELDS + templateStr0 = fields0 + .tokenize(', \n') + .collect { '$'+it } + .join(separator) } else if( new File(templateStr).exists() ) { - templateStr = new File(templateStr).text + templateStr0 = new File(templateStr).text } - templateScript = new TaskTemplateEngine().createTemplate(templateStr) + templateScript = new TaskTemplateEngine().createTemplate(templateStr0) } /** * Implements the `log` command */ - @Override void run() { Plugins.init() init() @@ -159,9 +193,9 @@ class CmdLog extends CmdBase implements CacheBase { listIds().each { entry -> cacheFor(entry) - .openForRead() - .eachRecord(this.&printRecord) - .close() + .openForRead() + .eachRecord(this.&printRecord) + .close() } @@ -197,13 +231,13 @@ class CmdLog extends CmdBase implements CacheBase { private void printHistory() { def table = new TableBuilder(cellSeparator: '\t') - .head('TIMESTAMP') - .head('DURATION') - .head('RUN NAME') - .head('STATUS') - .head('REVISION ID') - .head('SESSION ID') - .head('COMMAND') + .head('TIMESTAMP') + .head('DURATION') + .head('RUN NAME') + .head('STATUS') + .head('REVISION ID') + .head('SESSION ID') + .head('COMMAND') history.eachRow { List row -> row[4] = row[4].size()>10 ? row[4].substring(0,10) : row[4] diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdNode.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdNode.groovy index 47666166d6..d866cf5d88 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdNode.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdNode.groovy @@ -31,29 +31,61 @@ import nextflow.util.ServiceDiscover */ @Slf4j @CompileStatic -@Parameters -class CmdNode extends CmdBase { +class CmdNode { static final public NAME = 'node' - @Override - final String getName() { NAME } + interface Options { + Map getClusterOptions() + String getProvider() - @DynamicParameter(names ='-cluster.', description='Define cluster config options') - Map clusterOptions = [:] + CliOptions getLauncherOptions() + } + + @Parameters(commandDescription = 'Launch Nextflow in daemon mode') + static class V1 extends CmdBase implements Options { + + @DynamicParameter(names ='-cluster.', description='Define cluster config options') + Map clusterOptions = [:] + + @Parameter(names = ['-bg'], arity = 0, description = 'Start the cluster node daemon in background') + void setBackground(boolean value) { + launcher.options.background = value + } + + @Parameter + List args = [] + + @Override + String getProvider() { + args.size() ? args[0] : null + } + + @Override + CliOptions getLauncherOptions() { + launcher.options + } + + @Override + String getName() { NAME } + + @Override + void run() { + new CmdNode(this).run() + } - @Parameter(names = ['-bg'], arity = 0, description = 'Start the cluster node daemon in background') - void setBackground(boolean value) { - launcher.options.background = value } - @Parameter - List provider + @Delegate + private Options options + + CmdNode(Options options) { + this.options = options + } - @Override void run() { System.setProperty('nxf.node.daemon', 'true') - launchDaemon(provider ? provider[0] : null) + launchDaemon(provider) } @@ -62,13 +94,13 @@ class CmdNode extends CmdBase { * * @param config The nextflow configuration map */ - protected launchDaemon(String name = null) { + protected launchDaemon(String name) { // create the config object def config = new ConfigBuilder() - .setOptions(launcher.options) - .setCmdNode(this) - .build() + .setOptions(launcherOptions) + .setCmdNode(this) + .build() DaemonLauncher instance if( name ) { diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdPlugin.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdPlugin.groovy index 77e36d3c71..8bd2f3ace3 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdPlugin.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdPlugin.groovy @@ -30,53 +30,65 @@ import static nextflow.cli.PluginExecAware.CMD_SEP * @author Paolo Di Tommaso */ @CompileStatic -@Parameters(commandDescription = "Execute plugin-specific commands") -class CmdPlugin extends CmdBase { +class CmdPlugin { - @Override - String getName() { - return 'plugin' - } - - @Parameter(hidden = true) - List args + @Parameters(commandDescription = "Execute plugin-specific commands") + static class V1 extends CmdBase { - @Override - void run() { - if( !args ) - throw new AbortOperationException("Missing plugin command - usage: nextflow plugin install ") - // setup plugins system - Plugins.init() - // check for the plugins install - if( args[0] == 'install' ) { - if( args.size()!=2 ) - throw new AbortOperationException("Missing plugin install target - usage: nextflow plugin install ") - Plugins.pull(args[1].tokenize(',')) + @Override + String getName() { + return 'plugin' } - // plugin run command - else if( args[0].contains(CMD_SEP) ) { - final head = args.pop() - final items = head.tokenize(CMD_SEP) - final target = items[0] - final cmd = items[1] ? items[1..-1].join(CMD_SEP) : null - // push back the command as the first item - Plugins.start(target) - final wrapper = Plugins.manager.getPlugin(target) - if( !wrapper ) - throw new AbortOperationException("Cannot find target plugin: $target") - final plugin = wrapper.getPlugin() - if( plugin instanceof PluginExecAware ) { - final ret = plugin.exec(getLauncher(), target, cmd, args) - // use explicit exit to invoke the system shutdown hooks - System.exit(ret) + @Parameter(hidden = true) + List args = [] + + @Override + void run() { + if( !args ) + throw new AbortOperationException("Missing plugin command - usage: nextflow plugin install ") + // plugin install command + if( args[0] == 'install' ) { + if( args.size()!=2 ) + throw new AbortOperationException("Missing plugin install target - usage: nextflow plugin install ") + CmdPlugin.install(args[1].tokenize(',')) + } + // plugin run command + else if( args[0].contains(CMD_SEP) ) { + CmdPlugin.exec(args.pop(), args, launcher.options) + } + + else { + throw new AbortOperationException("Invalid plugin command: ${args[0]}") } - else - throw new AbortOperationException("Invalid target plugin: $target") } - else { - throw new AbortOperationException("Invalid plugin command: ${args[0]}") + + } + + static void install(List ids) { + Plugins.init() + Plugins.pull(ids) + } + + static void exec(String head, List args, CliOptions launcherOptions) { + final items = head.tokenize(CMD_SEP) + final target = items[0] + final cmd = items[1] ? items[1..-1].join(CMD_SEP) : null + + // push back the command as the first item + Plugins.init() + Plugins.start(target) + final wrapper = Plugins.manager.getPlugin(target) + if( !wrapper ) + throw new AbortOperationException("Cannot find target plugin: $target") + final plugin = wrapper.getPlugin() + if( plugin instanceof PluginExecAware ) { + final ret = plugin.exec(launcherOptions, target, cmd, args) + // use explicit exit to invoke the system shutdown hooks + System.exit(ret) } + else + throw new AbortOperationException("Invalid target plugin: $target") } } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdPull.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdPull.groovy index 9d2fa9a01d..1f5a91dd7e 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdPull.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdPull.groovy @@ -29,36 +29,64 @@ import nextflow.scm.AssetManager */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Download or update a project") -class CmdPull extends CmdBase implements HubOptions { +class CmdPull { static final public NAME = 'pull' - @Parameter(description = 'project name or repository url to pull', arity = 1) - List args + interface Options extends HubOptions { + String getPipeline() + boolean getAll() + Integer getDepth() + String getRevision() + } + + @Parameters(commandDescription = "Download or update a project") + static class V1 extends CmdBase implements Options, HubOptions.V1 { + + @Parameter(description = 'project name or repository url to pull', arity = 1) + List args = [] + + @Parameter(names='-all', description = 'Update all downloaded projects', arity = 0) + boolean all + + @Parameter(names=['-r','-revision'], description = 'Revision of the project to run (either a git branch, tag or commit SHA number)') + String revision + + @Parameter(names=['-d','-depth','-deep'], description = 'Create a shallow clone of the specified depth') + Integer depth + + @Override + String getPipeline() { args[0] } - @Parameter(names='-all', description = 'Update all downloaded projects', arity = 0) - boolean all + @Override + final String getName() { NAME } - @Parameter(names=['-r','-revision'], description = 'Revision of the project to run (either a git branch, tag or commit SHA number)') - String revision + @Override + void run() { + new CmdPull(this).run() + } - @Parameter(names=['-d','-deep'], description = 'Create a shallow clone of the specified depth') - Integer deep + } - @Override - final String getName() { NAME } + @Delegate + private Options options /* only for testing purpose */ protected File root - @Override + CmdPull(Options options) { + this.options = options + } + + /* only for testing purpose */ + CmdPull() {} + void run() { - if( !all && !args ) - throw new AbortOperationException('Missing argument') + if( !all && !pipeline ) + throw new AbortOperationException('Project name or option `-all` is required') - def list = all ? AssetManager.list() : args.toList() + def list = all ? AssetManager.list() : [pipeline] if( !list ) { log.info "(nothing to do)" return @@ -71,12 +99,12 @@ class CmdPull extends CmdBase implements HubOptions { // init plugin system Plugins.init() - + list.each { log.info "Checking $it ..." def manager = new AssetManager(it, this) - def result = manager.download(revision,deep) + def result = manager.download(revision,depth) manager.updateModules() def scriptFile = manager.getScriptFile() diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy index 700bcad165..ea58039995 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy @@ -57,8 +57,73 @@ import org.yaml.snakeyaml.Yaml */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Execute a pipeline project") -class CmdRun extends CmdBase implements HubOptions { +class CmdRun { + + static final public String NAME = 'run' + + interface Options extends HubOptions { + String getPipeline() + List getArgs() + Map getParams() + + String getBucketDir() + Boolean getCacheable() + String getCloudCachePath() + Map getClusterOptions() + Integer getDepth() + Boolean getDisableJobsCancellation() + String getDumpChannels() + String getDumpHashes() + String getEntryName() + Map getEnv() + Map getExecutorOptions() + boolean getExportSysEnv() + boolean getLatest() + String getLibPath() + String getMainScript() + boolean getOffline() + String getParamsFile() + String getPlugins() + long getPollInterval() + Integer getPoolSize() + boolean getPreview() + Map getProcessOptions() + String getProfile() + Integer getQueueSize() + String getResume() + String getRevision() + List getRunConfig() + String getRunName() + boolean getStubRun() + String getTest() + String getWithApptainer() + String getWithCharliecloud() + String getWithConda() + Boolean getWithoutConda() + String getWithDag() + String getWithDocker() + boolean getWithoutDocker() + String getWithFusion() + boolean getWithMpi() + String getWithNotification() + String getWithPodman() + boolean getWithoutPodman() + String getWithReport() + String getWithSingularity() + String getWithSpack() + Boolean getWithoutSpack() + String getWithTimeline() + String getWithTower() + String getWithTrace() + String getWithWave() + String getWithWebLog() + String getWorkDir() + + String getLauncherCli() + CliOptions getLauncherOptions() + + void setRunName(String runName) + } static final public Pattern RUN_NAME_PATTERN = Pattern.compile(/^[a-z](?:[a-z\d]|[-_](?=[a-z\d])){0,79}$/, Pattern.CASE_INSENSITIVE) @@ -72,228 +137,267 @@ class CmdRun extends CmdBase implements HubOptions { GParsConfig.poolFactory = new CustomPoolFactory() } - static class DurationConverter implements IStringConverter { - @Override - Long convert(String value) { - if( !value ) throw new IllegalArgumentException() - if( value.isLong() ) { return value.toLong() } - return Duration.of(value).toMillis() + @Parameters(commandDescription = "Execute a pipeline project") + static class V1 extends CmdBase implements Options, HubOptions.V1 { + + static class DurationConverter implements IStringConverter { + @Override + Long convert(String value) { + if( !value ) throw new IllegalArgumentException() + if( value.isLong() ) { return value.toLong() } + return Duration.of(value).toMillis() + } } - } - static final public String NAME = 'run' + @Parameter(names=['-name'], description = 'Assign a mnemonic name to the a pipeline run') + String runName - private Map sysEnv = System.getenv() + @Parameter(names=['-lib'], description = 'Library extension path') + String libPath - @Parameter(names=['-name'], description = 'Assign a mnemonic name to the a pipeline run') - String runName + @Parameter(names=['-cache'], description = 'Enable/disable processes caching', arity = 1) + Boolean cacheable - @Parameter(names=['-lib'], description = 'Library extension path') - String libPath + @Parameter(names=['-resume'], description = 'Execute the script using the cached results, useful to continue executions that was stopped by an error') + String resume - @Parameter(names=['-cache'], description = 'Enable/disable processes caching', arity = 1) - Boolean cacheable + @Parameter(names=['-ps','-pool-size'], description = 'Number of threads in the execution pool', hidden = true) + Integer poolSize - @Parameter(names=['-resume'], description = 'Execute the script using the cached results, useful to continue executions that was stopped by an error') - String resume + @Parameter(names=['-pi','-poll-interval'], description = 'Executor poll interval (duration string ending with ms|s|m)', converter = DurationConverter, hidden = true) + long pollInterval - @Parameter(names=['-ps','-pool-size'], description = 'Number of threads in the execution pool', hidden = true) - Integer poolSize + @Parameter(names=['-qs','-queue-size'], description = 'Max number of processes that can be executed in parallel by each executor') + Integer queueSize - @Parameter(names=['-pi','-poll-interval'], description = 'Executor poll interval (duration string ending with ms|s|m)', converter = DurationConverter, hidden = true) - long pollInterval + @Parameter(names=['-test'], description = 'Test a script function with the name specified') + String test - @Parameter(names=['-qs','-queue-size'], description = 'Max number of processes that can be executed in parallel by each executor') - Integer queueSize + @Parameter(names=['-w', '-work-dir'], description = 'Directory where intermediate result files are stored') + String workDir - @Parameter(names=['-test'], description = 'Test a script function with the name specified') - String test + @Parameter(names=['-bucket-dir'], description = 'Remote bucket where intermediate result files are stored') + String bucketDir - @Parameter(names=['-w', '-work-dir'], description = 'Directory where intermediate result files are stored') - String workDir + @Parameter(names=['-with-cloudcache'], description = 'Enable the use of object storage bucket as storage for cache meta-data') + String cloudCachePath - @Parameter(names=['-bucket-dir'], description = 'Remote bucket where intermediate result files are stored') - String bucketDir + /** + * Defines the parameters to be passed to the pipeline script + */ + @DynamicParameter(names = '--', description = 'Set a parameter used by the pipeline', hidden = true) + Map params = new LinkedHashMap<>() - @Parameter(names=['-with-cloudcache'], description = 'Enable the use of object storage bucket as storage for cache meta-data') - String cloudCachePath + @Parameter(names='-params-file', description = 'Load script parameters from a JSON/YAML file') + String paramsFile - /** - * Defines the parameters to be passed to the pipeline script - */ - @DynamicParameter(names = '--', description = 'Set a parameter used by the pipeline', hidden = true) - Map params = new LinkedHashMap<>() + @DynamicParameter(names = ['-process.'], description = 'Set process options' ) + Map processOptions = [:] - @Parameter(names='-params-file', description = 'Load script parameters from a JSON/YAML file') - String paramsFile + @DynamicParameter(names = ['-e.'], description = 'Add the specified variable to execution environment') + Map env = [:] - @DynamicParameter(names = ['-process.'], description = 'Set process options' ) - Map process = [:] + @Parameter(names = ['-E'], description = 'Exports all current system environment') + boolean exportSysEnv - @DynamicParameter(names = ['-e.'], description = 'Add the specified variable to execution environment') - Map env = [:] + @DynamicParameter(names = ['-executor.'], description = 'Set executor options', hidden = true ) + Map executorOptions = [:] - @Parameter(names = ['-E'], description = 'Exports all current system environment') - boolean exportSysEnv + @Parameter(description = 'Project name or repository url') + List args = [] - @DynamicParameter(names = ['-executor.'], description = 'Set executor options', hidden = true ) - Map executorOptions = [:] + @Parameter(names=['-r','-revision'], description = 'Revision of the project to run (either a git branch, tag or commit SHA number)') + String revision - @Parameter(description = 'Project name or repository url') - List args + @Parameter(names=['-d','-depth','-deep'], description = 'Create a shallow clone of the specified depth') + Integer depth - @Parameter(names=['-r','-revision'], description = 'Revision of the project to run (either a git branch, tag or commit SHA number)') - String revision + @Parameter(names=['-latest'], description = 'Pull latest changes before run') + boolean latest - @Parameter(names=['-d','-deep'], description = 'Create a shallow clone of the specified depth') - Integer deep + @Parameter(names='-stdin', hidden = true) + boolean stdin - @Parameter(names=['-latest'], description = 'Pull latest changes before run') - boolean latest + @Parameter(names = ['-ansi'], hidden = true, arity = 0) + void setAnsi(boolean value) { + launcher.options.ansiLog = value + } - @Parameter(names='-stdin', hidden = true) - boolean stdin + @Parameter(names = ['-ansi-log'], description = 'Enable/disable ANSI console logging', arity = 1) + void setAnsiLog(boolean value) { + launcher.options.ansiLog = value + } - @Parameter(names = ['-ansi'], hidden = true, arity = 0) - void setAnsi(boolean value) { - launcher.options.ansiLog = value - } + @Parameter(names = ['-with-tower'], description = 'Monitor workflow execution with Seqera Platform (formerly Tower Cloud)') + String withTower - @Parameter(names = ['-ansi-log'], description = 'Enable/disable ANSI console logging', arity = 1) - void setAnsiLog(boolean value) { - launcher.options.ansiLog = value - } + @Parameter(names = ['-with-wave'], hidden = true) + String withWave - @Parameter(names = ['-with-tower'], description = 'Monitor workflow execution with Seqera Platform (formerly Tower Cloud)') - String withTower + @Parameter(names = ['-with-fusion'], hidden = true) + String withFusion - @Parameter(names = ['-with-wave'], hidden = true) - String withWave + @Parameter(names = ['-with-weblog'], description = 'Send workflow status messages via HTTP to target URL') + String withWebLog - @Parameter(names = ['-with-fusion'], hidden = true) - String withFusion + @Parameter(names = ['-with-trace'], description = 'Create processes execution tracing file') + String withTrace - @Parameter(names = ['-with-weblog'], description = 'Send workflow status messages via HTTP to target URL') - String withWebLog + @Parameter(names = ['-with-report'], description = 'Create processes execution html report') + String withReport - @Parameter(names = ['-with-trace'], description = 'Create processes execution tracing file') - String withTrace + @Parameter(names = ['-with-timeline'], description = 'Create processes execution timeline file') + String withTimeline - @Parameter(names = ['-with-report'], description = 'Create processes execution html report') - String withReport + @Parameter(names = '-with-charliecloud', description = 'Enable process execution in a Charliecloud container runtime') + String withCharliecloud - @Parameter(names = ['-with-timeline'], description = 'Create processes execution timeline file') - String withTimeline + @Parameter(names = '-with-singularity', description = 'Enable process execution in a Singularity container') + String withSingularity - @Parameter(names = '-with-charliecloud', description = 'Enable process execution in a Charliecloud container runtime') - def withCharliecloud + @Parameter(names = '-with-apptainer', description = 'Enable process execution in a Apptainer container') + String withApptainer - @Parameter(names = '-with-singularity', description = 'Enable process execution in a Singularity container') - def withSingularity + @Parameter(names = '-with-podman', description = 'Enable process execution in a Podman container') + String withPodman - @Parameter(names = '-with-apptainer', description = 'Enable process execution in a Apptainer container') - def withApptainer + @Parameter(names = '-without-podman', description = 'Disable process execution in a Podman container') + boolean withoutPodman - @Parameter(names = '-with-podman', description = 'Enable process execution in a Podman container') - def withPodman + @Parameter(names = '-with-docker', description = 'Enable process execution in a Docker container') + String withDocker - @Parameter(names = '-without-podman', description = 'Disable process execution in a Podman container') - def withoutPodman + @Parameter(names = '-without-docker', description = 'Disable process execution with Docker', arity = 0) + boolean withoutDocker - @Parameter(names = '-with-docker', description = 'Enable process execution in a Docker container') - def withDocker + @Parameter(names = '-with-mpi', hidden = true) + boolean withMpi - @Parameter(names = '-without-docker', description = 'Disable process execution with Docker', arity = 0) - boolean withoutDocker + @Parameter(names = '-with-dag', description = 'Create pipeline DAG file') + String withDag - @Parameter(names = '-with-mpi', hidden = true) - boolean withMpi + @Parameter(names = ['-bg','-background'], arity = 0, hidden = true) + void setBackground(boolean value) { + launcher.options.background = value + } - @Parameter(names = '-with-dag', description = 'Create pipeline DAG file') - String withDag + @Parameter(names=['-c','-config'], hidden = true ) + List runConfig - @Parameter(names = ['-bg'], arity = 0, hidden = true) - void setBackground(boolean value) { - launcher.options.background = value - } + @DynamicParameter(names = ['-cluster.'], description = 'Set cluster options', hidden = true ) + Map clusterOptions = [:] + + @Parameter(names=['-profile'], description = 'Choose a configuration profile') + String profile + + @Parameter(names=['-dump-hashes'], description = 'Dump task hash keys for debugging purpose') + String dumpHashes + + @Parameter(names=['-dump-channels'], description = 'Dump channels for debugging purpose') + String dumpChannels + + @Parameter(names=['-N','-with-notification'], description = 'Send a notification email on workflow completion to the specified recipients') + String withNotification - @Parameter(names=['-c','-config'], hidden = true ) - List runConfig + @Parameter(names=['-with-conda'], description = 'Use the specified Conda environment package or file (must end with .yml|.yaml suffix)') + String withConda - @DynamicParameter(names = ['-cluster.'], description = 'Set cluster options', hidden = true ) - Map clusterOptions = [:] + @Parameter(names=['-without-conda'], description = 'Disable the use of Conda environments') + Boolean withoutConda - @Parameter(names=['-profile'], description = 'Choose a configuration profile') - String profile + @Parameter(names=['-with-spack'], description = 'Use the specified Spack environment package or file (must end with .yaml suffix)') + String withSpack - @Parameter(names=['-dump-hashes'], description = 'Dump task hash keys for debugging purpose') - String dumpHashes + @Parameter(names=['-without-spack'], description = 'Disable the use of Spack environments') + Boolean withoutSpack - @Parameter(names=['-dump-channels'], description = 'Dump channels for debugging purpose') - String dumpChannels + @Parameter(names=['-offline'], description = 'Do not check for remote project updates') + boolean offline - @Parameter(names=['-N','-with-notification'], description = 'Send a notification email on workflow completion to the specified recipients') - String withNotification + @Parameter(names=['-entry'], description = 'Entry workflow name to be executed', arity = 1) + String entryName - @Parameter(names=['-with-conda'], description = 'Use the specified Conda environment package or file (must end with .yml|.yaml suffix)') - String withConda + @Parameter(names=['-main-script'], description = 'The script file to be executed when launching a project directory or repository' ) + String mainScript - @Parameter(names=['-without-conda'], description = 'Disable the use of Conda environments') - Boolean withoutConda + @Parameter(names=['-stub-run','-stub'], description = 'Execute the workflow replacing process scripts with command stubs') + boolean stubRun - @Parameter(names=['-with-spack'], description = 'Use the specified Spack environment package or file (must end with .yaml suffix)') - String withSpack + @Parameter(names=['-preview'], description = "Run the workflow script skipping the execution of all processes") + boolean preview - @Parameter(names=['-without-spack'], description = 'Disable the use of Spack environments') - Boolean withoutSpack + @Parameter(names=['-plugins'], description = 'Specify the plugins to be applied for this run e.g. nf-amazon,nf-tower') + String plugins - @Parameter(names=['-offline'], description = 'Do not check for remote project updates') - boolean offline = System.getenv('NXF_OFFLINE')=='true' + @Parameter(names=['-disable-jobs-cancellation'], description = 'Prevent the cancellation of child jobs on execution termination') + Boolean disableJobsCancellation - @Parameter(names=['-entry'], description = 'Entry workflow name to be executed', arity = 1) - String entryName + @Override + String getPipeline() { + stdin ? '-' : args[0] + } - @Parameter(names=['-main-script'], description = 'The script file to be executed when launching a project directory or repository' ) - String mainScript + @Override + List getArgs() { + args.size() > 1 ? args[1..-1] : [] + } - @Parameter(names=['-stub-run','-stub'], description = 'Execute the workflow replacing process scripts with command stubs') - boolean stubRun + @Override + String getLauncherCli() { + launcher.cliString + } - @Parameter(names=['-preview'], description = "Run the workflow script skipping the execution of all processes") - boolean preview + @Override + CliOptions getLauncherOptions() { + launcher.options + } - @Parameter(names=['-plugins'], description = 'Specify the plugins to be applied for this run e.g. nf-amazon,nf-tower') - String plugins + @Override + String getName() { NAME } - @Parameter(names=['-disable-jobs-cancellation'], description = 'Prevent the cancellation of child jobs on execution termination') - Boolean disableJobsCancellation + @Override + void run() { + new CmdRun(this).run() + } - Boolean getDisableJobsCancellation() { - return disableJobsCancellation!=null - ? disableJobsCancellation - : sysEnv.get('NXF_DISABLE_JOBS_CANCELLATION') as boolean } + private Map sysEnv = System.getenv() + + @Delegate + private Options options + /** * Optional closure modelling an action to be invoked when the preview mode is enabled */ Closure previewAction - @Override - String getName() { NAME } + CmdRun(Options options) { + this.options = options + } + + /* For testing purposes only */ + CmdRun() {} + + Boolean getDisableJobsCancellation() { + return options.disableJobsCancellation!=null + ? options.disableJobsCancellation + : sysEnv.get('NXF_DISABLE_JOBS_CANCELLATION') as boolean + } + + boolean getOffline() { + return options.offline || System.getenv('NXF_OFFLINE') as boolean + } String getParamsFile() { - return paramsFile ?: sysEnv.get('NXF_PARAMS_FILE') + return options.paramsFile ?: sysEnv.get('NXF_PARAMS_FILE') } boolean hasParams() { - return params || getParamsFile() + return options.params || getParamsFile() } - @Override void run() { - final scriptArgs = (args?.size()>1 ? args[1..-1] : []) as List - final pipeline = stdin ? '-' : ( args ? args[0] : null ) if( !pipeline ) throw new AbortOperationException("No project name was specified") @@ -322,7 +426,7 @@ class CmdRun extends CmdBase implements HubOptions { // create the config object final builder = new ConfigBuilder() - .setOptions(launcher.options) + .setOptions(launcherOptions) .setCmdRun(this) .setBaseDir(scriptFile.parent) final config = builder .build() @@ -348,9 +452,9 @@ class CmdRun extends CmdBase implements HubOptions { runner.setScript(scriptFile) runner.setPreview(this.preview, previewAction) runner.session.profile = profile - runner.session.commandLine = launcher.cliString - runner.session.ansiLog = launcher.options.ansiLog - runner.session.debug = launcher.options.remoteDebug + runner.session.commandLine = launcherCli + runner.session.ansiLog = launcherOptions.ansiLog + runner.session.debug = launcherOptions.remoteDebug runner.session.disableJobsCancellation = getDisableJobsCancellation() final isTowerEnabled = config.navigate('tower.enabled') as Boolean @@ -362,7 +466,7 @@ class CmdRun extends CmdBase implements HubOptions { // set the commit id (if any) runner.session.commitId = scriptFile.commitId if( this.test ) { - runner.test(this.test, scriptArgs) + runner.test(this.test, args) return } @@ -370,14 +474,14 @@ class CmdRun extends CmdBase implements HubOptions { log.debug( '\n'+info ) // -- add this run to the local history - runner.verifyAndTrackHistory(launcher.cliString, runName) + runner.verifyAndTrackHistory(launcherCli, runName) // -- run it! - runner.execute(scriptArgs, this.entryName) + runner.execute(args, this.entryName) } protected void printBanner() { - if( launcher.options.ansiLog ){ + if( launcherOptions.ansiLog ) { // Plain header for verbose log log.debug "N E X T F L O W ~ version ${BuildInfo.version}" @@ -440,7 +544,7 @@ class CmdRun extends CmdBase implements HubOptions { } protected void printLaunchInfo(String ver, String repo, String head, String revision) { - if( launcher.options.ansiLog ){ + if( launcherOptions.ansiLog ) { log.debug "${head} [$runName] DSL${ver} - revision: ${revision}" def fmt = ansi() @@ -581,7 +685,7 @@ class CmdRun extends CmdBase implements HubOptions { if( offline ) throw new AbortOperationException("Unknown project `$repo` -- NOTE: automatic download from remote repositories is disabled") log.info "Pulling $repo ..." - def result = manager.download(revision,deep) + def result = manager.download(revision,depth) if( result ) log.info " $result" checkForUpdate = false diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdSecret.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdSecret.groovy index cb4aa60acf..fa808422dd 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdSecret.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdSecret.groovy @@ -32,226 +32,258 @@ import nextflow.secret.SecretsProvider */ @Slf4j @CompileStatic -@Parameters(commandDescription = "Manage pipeline secrets") -class CmdSecret extends CmdBase implements UsageAware { - - interface SubCmd { - String getName() - void apply(List result) - void usage(List result) - } +class CmdSecret { static public final String NAME = 'secrets' - private List commands = [] - - String getName() { - return NAME + enum Command { + GET, + SET, + LIST, + DELETE } - @Parameter(hidden = true) - List args + @Parameters(commandDescription = "Manage pipeline secrets") + static class V1 extends CmdBase implements UsageAware { - private SecretsProvider provider + interface SubCmd { + String getName() + void apply(List result) + void usage(List result) + } - CmdSecret() { - commands.add( new GetCmd() ) - commands.add( new SetCmd() ) - commands.add( new ListCmd() ) - commands.add( new DeleteCmd() ) - } + private List commands = [] - /** - * Print the command usage help - */ - void usage() { - usage(args) - } + String getName() { + return NAME + } + + @Parameter(hidden = true) + List args = [] - /** - * Print the command usage help - * - * @param args The arguments as entered by the user - */ - void usage(List args) { - - List result = [] - if( !args ) { - result << this.getClass().getAnnotation(Parameters).commandDescription() - result << 'Usage: nextflow secrets [options]' - result << '' - result << 'Commands:' - commands.collect{ it.name }.sort().each { result << " $it".toString() } - result << '' + V1() { + commands.add( new GetCmd() ) + commands.add( new SetCmd() ) + commands.add( new ListCmd() ) + commands.add( new DeleteCmd() ) + } + + /** + * Print the command usage help + */ + @Override + void usage() { + usage(args) } - else { - def sub = commands.find { it.name == args[0] } - if( sub ) - sub.usage(result) + + /** + * Print the command usage help + * + * @param args The arguments as entered by the user + */ + @Override + void usage(List args) { + + List result = [] + if( !args ) { + result << this.getClass().getAnnotation(Parameters).commandDescription() + result << 'Usage: nextflow secrets [options]' + result << '' + result << 'Commands:' + commands.collect{ it.name }.sort().each { result << " $it".toString() } + result << '' + } else { - throw new AbortOperationException("Unknown secrets sub-command: ${args[0]}") + def sub = commands.find { it.name == args[0] } + if( sub ) + sub.usage(result) + else { + throw new AbortOperationException("Unknown secrets sub-command: ${args[0]}") + } } + + println result.join('\n').toString() } - println result.join('\n').toString() - } + @Override + void run() { + if( !args ) { + usage() + return + } - /** - * Main command entry point - */ - @Override - void run() { - if( !args ) { - usage() - return + getCmd(args).apply(args.drop(1)) } - // setup the plugins system and load the secrets provider - Plugins.init() - provider = SecretsLoader.instance.load() + protected SubCmd getCmd(List args) { - // run the command - try { - getCmd(args).apply(args.drop(1)) + def cmd = commands.find { it.name == args[0] } + if( cmd ) { + return cmd + } + + def matches = commands.collect{ it.name }.closest(args[0]) + def msg = "Unknown secrets sub-command: ${args[0]}" + if( matches ) + msg += " -- Did you mean one of these?\n" + matches.collect { " $it"}.join('\n') + throw new AbortOperationException(msg) } - finally { - // close the provider - provider?.close() + + private void addOption(String fieldName, List result) { + def annot = this.class.getDeclaredField(fieldName)?.getAnnotation(Parameter) + if( annot ) { + result << ' ' + annot.names().join(', ') + result << ' ' + annot.description() + } + else { + log.debug "Unknown help field: $fieldName" + } } - } - protected SubCmd getCmd(List args) { + class SetCmd implements SubCmd { - def cmd = commands.find { it.name == args[0] } - if( cmd ) { - return cmd - } + @Override + String getName() { 'set' } - def matches = commands.collect{ it.name }.closest(args[0]) - def msg = "Unknown cloud sub-command: ${args[0]}" - if( matches ) - msg += " -- Did you mean one of these?\n" + matches.collect { " $it"}.join('\n') - throw new AbortOperationException(msg) - } + @Override + void apply(List result) { + if( result.size() < 1 ) + throw new AbortOperationException("Missing secret name") + if( result.size() < 2 ) + throw new AbortOperationException("Missing secret value") - private void addOption(String fieldName, List result) { - def annot = this.class.getDeclaredField(fieldName)?.getAnnotation(Parameter) - if( annot ) { - result << ' ' + annot.names().join(', ') - result << ' ' + annot.description() - } - else { - log.debug "Unknown help field: $fieldName" + new CmdSecret().run(Command.SET, result) + } + + @Override + void usage(List result) { + result << 'Set a key-pair in the secrets store' + result << "Usage: nextflow secrets $name ".toString() + result << '' + result << '' + } } - } - class SetCmd implements SubCmd { + class GetCmd implements SubCmd { - @Override - String getName() { 'set' } + @Override + String getName() { 'get' } - @Override - void apply(List result) { - if( result.size() < 1 ) - throw new AbortOperationException("Missing secret name") - if( result.size() < 2 ) - throw new AbortOperationException("Missing secret value") - - String secretName = result.first() - String secretValue = result.last() - provider.putSecret(secretName, secretValue) - } + @Override + void apply(List result) { + if( result.size() != 1 ) + throw new AbortOperationException("Wrong number of arguments") - @Override - void usage(List result) { - result << 'Set a key-pair in the secrets store' - result << "Usage: nextflow secrets $name ".toString() - result << '' - result << '' + if( !result.first() ) + throw new AbortOperationException("Missing secret name") + + new CmdSecret().run(Command.GET, result) + } + + @Override + void usage(List result) { + result << 'Get a secret value with the name' + result << "Usage: nextflow secrets $name ".toString() + result << '' + } } - } - class GetCmd implements SubCmd { + class ListCmd implements SubCmd { + @Override + String getName() { 'list' } - @Override - String getName() { 'get' } + @Override + void apply(List result) { + if( result.size() > 0 ) + throw new AbortOperationException("Wrong number of arguments") - @Override - void apply(List result) { - if( result.size() != 1 ) - throw new AbortOperationException("Wrong number of arguments") - - String secretName = result.first() - if( !secretName ) - throw new AbortOperationException("Missing secret name") - println provider.getSecret(secretName)?.value - } + new CmdSecret().run(Command.LIST, result) + } - @Override - void usage(List result) { - result << 'Get a secret value with the name' - result << "Usage: nextflow secrets $name ".toString() - result << '' + @Override + void usage(List result) { + result << 'List all names in the secrets store' + result << "Usage: nextflow secrets $name".toString() + result << '' + } } - } - /** - * Implements the secret `list` sub-command - */ - class ListCmd implements SubCmd { - @Override - String getName() { 'list' } + class DeleteCmd implements SubCmd { + @Override + String getName() { 'delete' } - @Override - void apply(List result) { - if( result.size() ) - throw new AbortOperationException("Wrong number of arguments") - - final names = new ArrayList(provider.listSecretsNames()).sort() - if( names ) { - for( String it : names ) { - println it - } + @Override + void apply(List result) { + if( result.size() != 1 ) + throw new AbortOperationException("Wrong number of arguments") + + if( !result.first() ) + throw new AbortOperationException("Missing secret name") + + new CmdSecret().run(Command.DELETE, result) } - else { - println "no secrets available" + + @Override + void usage(List result) { + result << 'Delete an entry from the secrets store' + result << "Usage: nextflow secrets $name".toString() + result << '' + addOption('secretName', result) + result << '' } } + } - @Override - void usage(List result) { - result << 'List all names in the secrets store' - result << "Usage: nextflow secrets $name".toString() - result << '' + private SecretsProvider provider + + void run(Command command, List args) { + // setup the plugins system and load the secrets provider + Plugins.init() + provider = SecretsLoader.instance.load() + + // run the command + try { + switch( command ) { + case GET: + get(args[0]) + break + case SET: + set(args[0], args[1]) + break + case LIST: + list() + break + case DELETE: + delete(args[0]) + break + } + } + finally { + // close the provider + provider?.close() } } - /** - * Implements the secret `remove` sub-command - */ - class DeleteCmd implements SubCmd { - @Override - String getName() { 'delete' } - - @Override - void apply(List result) { - if( result.size() != 1 ) - throw new AbortOperationException("Wrong number of arguments") + void get(String name) { + println provider.getSecret(name)?.value + } - String secretName = result.first() + void set(String name, String value) { + provider.putSecret(name, value) + } - if( !secretName ) - throw new AbortOperationException("Missing secret name") - provider.removeSecret(secretName) + void list() { + final names = new ArrayList(provider.listSecretsNames()).sort() + if( names.size() == 0 ) { + println "no secrets available" } - @Override - void usage(List result) { - result << 'Delete an entry from the secrets store' - result << "Usage: nextflow secrets $name".toString() - result << '' - addOption('secretName', result) - result << '' + for( String it : names ) { + println it } } + + void delete(String name) { + provider.removeSecret(name) + } } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdView.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdView.groovy index dcdcfcae67..00f2c3a6f8 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdView.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdView.groovy @@ -31,29 +31,54 @@ import nextflow.scm.AssetManager */ @Slf4j @CompileStatic -@Parameters(commandDescription = "View project script file(s)") -class CmdView extends CmdBase { +class CmdView { static final public NAME = 'view' - @Override - String getName() { NAME } + interface Options { + String getPipeline() + boolean getQuiet() + boolean getAll() + } + + @Parameters(commandDescription = "View project script file(s)") + static class V1 extends CmdBase implements Options { + + @Override + String getName() { NAME } - @Parameter(description = 'project name', required = true) - List args = [] + @Parameter(description = 'project name', required = true) + List args = [] - @Parameter(names = '-q', description = 'Hide header line', arity = 0) - boolean quiet + @Parameter(names = '-q', description = 'Hide header line', arity = 0) + boolean quiet + + @Parameter(names = '-l', description = 'List repository content', arity = 0) + boolean all + + @Override + String getPipeline() { + args.size() > 0 ? args[0] : null + } - @Parameter(names = '-l', description = 'List repository content', arity = 0) - boolean all + @Override + void run() { + new CmdView(this).run() + } + } + + @Delegate + private Options options + + CmdView(Options options) { + this.options = options + } - @Override void run() { Plugins.init() - def manager = new AssetManager(args[0]) + def manager = new AssetManager(pipeline) if( !manager.isLocal() ) - throw new AbortOperationException("Unknown project name `${args[0]}`") + throw new AbortOperationException("Unknown project name `${pipeline}`") if( all ) { if( !quiet ) diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/HubOptions.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/HubOptions.groovy index 9a022afda1..ec21c7c915 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/HubOptions.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/HubOptions.groovy @@ -20,18 +20,28 @@ import com.beust.jcommander.Parameter import groovy.transform.CompileStatic /** * Defines the command line parameters for command that need to interact with a pipeline service hub i.e. GitHub or BitBucket - * - * @author Paolo Di Tommaso - */ + * + * @author Paolo Di Tommaso + */ @CompileStatic trait HubOptions { - @Parameter(names=['-hub'], description = "Service hub where the project is hosted") - String hubProvider + abstract String getHubProvider() + + abstract String getHubUserCli() + + abstract void setHubProvider(String hub) - @Parameter(names='-user', description = 'Private repository user name') - String hubUser + static trait V1 implements HubOptions { + + @Parameter(names=['-hub'], description = "Service hub where the project is hosted") + String hubProvider + + @Parameter(names='-user', description = 'Private repository user name') + String hubUserCli + + } /** * Return the password provided on the command line or stop allowing the user to enter it on the console @@ -40,12 +50,12 @@ trait HubOptions { */ String getHubPassword() { - if( !hubUser ) + if( !hubUserCli ) return null - def p = hubUser.indexOf(':') + def p = hubUserCli.indexOf(':') if( p != -1 ) - return hubUser.substring(p+1) + return hubUserCli.substring(p+1) def console = System.console() if( !console ) @@ -57,12 +67,12 @@ trait HubOptions { } String getHubUser() { - if(!hubUser) { - return hubUser + if( !hubUserCli ) { + return hubUserCli } - def p = hubUser.indexOf(':') - return p != -1 ? hubUser.substring(0,p) : hubUser + def p = hubUserCli.indexOf(':') + return p != -1 ? hubUserCli.substring(0,p) : hubUserCli } } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/Launcher.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/Launcher.groovy index c27ca4e933..1b57612b16 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/Launcher.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/Launcher.groovy @@ -35,10 +35,9 @@ import nextflow.exception.AbortRunException import nextflow.exception.ConfigParseException import nextflow.exception.ScriptCompilationException import nextflow.exception.ScriptRuntimeException -import nextflow.secret.SecretsLoader import nextflow.util.Escape import nextflow.util.LoggerHelper -import nextflow.util.ProxyConfig +import nextflow.util.ProxyHelper import nextflow.util.SpuriousDeps import org.eclipse.jgit.api.errors.GitAPIException /** @@ -51,15 +50,9 @@ import org.eclipse.jgit.api.errors.GitAPIException @CompileStatic class Launcher { - /** - * Create the application command line parser - * - * @return An instance of {@code CliBuilder} - */ - private JCommander jcommander - private CliOptions options + private CliOptions.V1 options private boolean fullVersion @@ -86,35 +79,33 @@ class Launcher { protected void init() { allCommands = (List)[ - new CmdClean(), - new CmdClone(), - new CmdConsole(), - new CmdFs(), - new CmdInfo(), - new CmdList(), - new CmdLog(), - new CmdPull(), - new CmdRun(), - new CmdKubeRun(), - new CmdDrop(), - new CmdConfig(), - new CmdNode(), - new CmdView(), + new CmdClean.V1(), + new CmdClone.V1(), + new CmdConfig.V1(), + new CmdConsole.V1(), + new CmdDrop.V1(), + new CmdFs.V1(), new CmdHelp(), + new CmdInfo.V1(), + new CmdInspect.V1(), + new CmdKubeRun(), + new CmdList.V1(), + new CmdLog.V1(), + new CmdNode.V1(), + new CmdPlugin.V1(), + new CmdPull.V1(), + new CmdRun.V1(), + new CmdSecret.V1(), new CmdSelfUpdate(), - new CmdPlugin(), - new CmdInspect() + new CmdView.V1() ] - if(SecretsLoader.isEnabled()) - allCommands.add(new CmdSecret()) - // legacy command final cmdCloud = SpuriousDeps.cmdCloud() if( cmdCloud ) allCommands.add(cmdCloud) - options = new CliOptions() + options = new CliOptions.V1() jcommander = new JCommander(options) allCommands.each { cmd -> cmd.launcher = this; @@ -140,7 +131,7 @@ class Launcher { fullVersion = '-version' in normalizedArgs command = allCommands.find { it.name == jcommander.getParsedCommand() } // whether is running a daemon - daemonMode = command instanceof CmdNode + daemonMode = command instanceof CmdNode.V1 // set the log file name checkLogFileName() @@ -166,7 +157,7 @@ class Launcher { if( !options.logFile ) { if( isDaemon() ) options.logFile = System.getenv('NXF_LOG_FILE') ?: '.node-nextflow.log' - else if( command instanceof CmdRun || options.debug || options.trace ) + else if( command instanceof CmdRun.V1 || options.debug || options.trace ) options.logFile = System.getenv('NXF_LOG_FILE') ?: ".nextflow.log" } } @@ -376,7 +367,7 @@ class Launcher { } println "Usage: nextflow [options] COMMAND [arg...]\n" - printOptions(CliOptions) + printOptions(CliOptions.V1) printCommands(allCommands) } @@ -430,7 +421,7 @@ class Launcher { */ try { parseMainArgs(args) - LoggerHelper.configureLogger(this) + LoggerHelper.configureLogger(options, isDaemon()) } catch( ParameterException e ) { // print command line parsing errors @@ -477,9 +468,9 @@ class Launcher { int run() { /* - * setup environment + * setup proxy environment */ - setupEnvironment() + ProxyHelper.setupEnvironment() /* * Real execution starts here @@ -571,98 +562,6 @@ class Launcher { return buffer.toString() } - /** - * set up environment and system properties. It checks the following - * environment variables: - *
  • http_proxy
  • - *
  • https_proxy
  • - *
  • ftp_proxy
  • - *
  • HTTP_PROXY
  • - *
  • HTTPS_PROXY
  • - *
  • FTP_PROXY
  • - *
  • NO_PROXY
  • - */ - private void setupEnvironment() { - - final env = System.getenv() - setProxy('HTTP',env) - setProxy('HTTPS',env) - setProxy('FTP',env) - - setProxy('http',env) - setProxy('https',env) - setProxy('ftp',env) - - setNoProxy(env) - - setHttpClientProperties(env) - } - - static void setHttpClientProperties(Map env) { - // Set the httpclient connection pool timeout to 10 seconds. - // This required because the default is 20 minutes, which cause the error - // "HTTP/1.1 header parser received no bytes" when in some circumstances - // https://github.com/nextflow-io/nextflow/issues/3983#issuecomment-1702305137 - System.setProperty("jdk.httpclient.keepalive.timeout", env.getOrDefault("NXF_JDK_HTTPCLIENT_KEEPALIVE_TIMEOUT","10")) - if( env.get("NXF_JDK_HTTPCLIENT_CONNECTIONPOOLSIZE") ) - System.setProperty("jdk.httpclient.connectionPoolSize", env.get("NXF_JDK_HTTPCLIENT_CONNECTIONPOOLSIZE")) - } - - /** - * Set no proxy property if defined in the launching env - * - * See for details - * https://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html - * - * @param env - */ - @PackageScope - static void setNoProxy(Map env) { - final noProxy = env.get('NO_PROXY') ?: env.get('no_proxy') - if(noProxy) { - System.setProperty('http.nonProxyHosts', noProxy.tokenize(',').join('|')) - } - } - - - /** - * Setup proxy system properties and optionally configure the network authenticator - * - * See: - * http://docs.oracle.com/javase/6/docs/technotes/guides/net/proxies.html - * https://github.com/nextflow-io/nextflow/issues/24 - * - * @param qualifier Either {@code http/HTTP} or {@code https/HTTPS}. - * @param env The environment variables system map - */ - @PackageScope - static void setProxy(String qualifier, Map env ) { - assert qualifier in ['http','https','ftp','HTTP','HTTPS','FTP'] - def str = null - def var = "${qualifier}_" + (qualifier.isLowerCase() ? 'proxy' : 'PROXY') - - // -- setup HTTP proxy - try { - final proxy = ProxyConfig.parse(str = env.get(var.toString())) - if( proxy ) { - // set the expected protocol - proxy.protocol = qualifier.toLowerCase() - log.debug "Setting $qualifier proxy: $proxy" - System.setProperty("${qualifier.toLowerCase()}.proxyHost", proxy.host) - if( proxy.port ) - System.setProperty("${qualifier.toLowerCase()}.proxyPort", proxy.port) - if( proxy.authenticator() ) { - log.debug "Setting $qualifier proxy authenticator" - Authenticator.setDefault(proxy.authenticator()) - } - } - } - catch ( MalformedURLException e ) { - log.warn "Not a valid $qualifier proxy: '$str' -- Check the value of variable `$var` in your environment" - } - - } - /** * Hey .. Nextflow starts here! * @@ -691,9 +590,6 @@ class Launcher { } - /* - * The application 'logo' - */ /* * The application 'logo' */ diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/PluginAbstractExec.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/PluginAbstractExec.groovy index 8cb1391b00..6e2051d749 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/PluginAbstractExec.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/PluginAbstractExec.groovy @@ -38,20 +38,16 @@ trait PluginAbstractExec implements PluginExecAware { private static Logger log = LoggerFactory.getLogger(PluginAbstractExec) private Session session - private Launcher launcher Session getSession() { session } - Launcher getLauncher() { launcher } - abstract List getCommands() @Override - final int exec(Launcher launcher1, String pluginId, String cmd, List args) { - this.launcher = launcher1 + final int exec(CliOptions options, String pluginId, String cmd, List args) { // create the config final config = new ConfigBuilder() - .setOptions(launcher1.options) + .setOptions(options) .setBaseDir(Paths.get('.')) .build() diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/PluginExecAware.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/PluginExecAware.groovy index 0357854424..920b37231e 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/PluginExecAware.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/PluginExecAware.groovy @@ -26,6 +26,6 @@ interface PluginExecAware { static final String CMD_SEP = ':' - int exec(Launcher launcher, String pluginId, String cmd, List args) + int exec(CliOptions options, String pluginId, String cmd, List args) } diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/usageAware.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/UsageAware.groovy similarity index 100% rename from modules/nextflow/src/main/groovy/nextflow/cli/usageAware.groovy rename to modules/nextflow/src/main/groovy/nextflow/cli/UsageAware.groovy diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/AbstractCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/AbstractCmd.groovy new file mode 100644 index 0000000000..845899e329 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/AbstractCmd.groovy @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.Option + +/** + * Base class for CLI v2 commands + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + headerHeading = '%n', + abbreviateSynopsis = true, + descriptionHeading = '%n', + commandListHeading = '%nCommands:%n', + requiredOptionMarker = ((char)'*'), + parameterListHeading = '%nParameters:%n', + optionListHeading = '%nOptions:%n' +) +class AbstractCmd implements Runnable { + + @CommandLine.Spec + CommandLine.Model.CommandSpec spec + + @Option(names = ['-h','--help'], description = 'Print this help', usageHelp = true) + boolean help + + @Override + void run() {} + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/CleanCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/CleanCmd.groovy new file mode 100644 index 0000000000..480588dc19 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/CleanCmd.groovy @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CliOptions +import nextflow.cli.CmdClean +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import picocli.CommandLine.ParentCommand + +/** + * CLI `clean` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'clean', + description = 'Clean up project cache and work directories' +) +class CleanCmd extends AbstractCmd implements CmdClean.Options { + + @ParentCommand + private Launcher launcher + + @Option(names = ['--after'], paramLabel = '|', description = 'Clean up runs executed after the specified one') + String after + + @Option(names = ['--before'], paramLabel = '|', description = 'Clean up runs executed before the specified one') + String before + + @Option(names = ['--but'], paramLabel = '|', description = 'Clean up all runs except the specified one') + String but + + @Option(names = ['-n', '--dry-run'], arity = '0', description = 'Print names of file to be removed without deleting them') + boolean dryRun + + @Option(names = ['-f', '--force'], arity = '0', description = 'Force clean command') + boolean force + + @Option(names = ['-k', '--keep-logs'], description = 'Removes only temporary files but retains execution log entries and metadata') + boolean keepLogs + + @Option(names = ['-q', '--quiet'], arity = '0', description = 'Do not print names of files removed') + boolean quiet + + @Parameters(description = 'Session IDs or run names') + List args = [] + + @Override + CliOptions getLauncherOptions() { + launcher.options + } + + @Override + void run() { + new CmdClean(this).run() + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/CliOptionsV2.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/CliOptionsV2.groovy new file mode 100644 index 0000000000..9e30195e9a --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/CliOptionsV2.groovy @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CliOptions +import picocli.CommandLine.Option + +/** + * Top-level CLI (v2) options + * + * @author Ben Sherman + */ +@CompileStatic +class CliOptionsV2 extends CliOptions { + + Boolean ansiLogCli + + void setAnsiLog(boolean value) { ansiLogCli = value } + + @Option(names = ['--bg','--background'], arity = '0', description = 'Execute nextflow in background') + boolean background + + @Option(names = ['-C'], split = ',', description = 'Use the specified configuration file(s), overriding any defaults') + List config + + @Option(names = ['-c','--config'], split = ',', paramLabel = '', description = 'Add the specified file to configuration set') + List userConfig + + @Option(names = ['--config-ignore-includes'], description = 'Disable the parsing of config includes') + boolean ignoreConfigIncludes + + @Option(names = ['-D'], paramLabel = '=', description = 'Set JVM properties') + Map jvmOpts = [:] + + @Option(names = ['--debug'], split = ',', paramLabel = '', description = 'Enable DEBUG level logging for the specified package name', hidden = true) + List debug + + @Option(names = ['--log'], paramLabel = '', description = 'Set the log file path') + String logFile + + @Option(names = ['-q','--quiet'], description = 'Do not print information messages') + boolean quiet + + @Option(names = ['--remote-debug'], description = "Enable JVM interactive remote debugging (experimental)") + boolean remoteDebug + + @Option(names = ['--self-update'], arity = '0', description = 'Update Nextflow to the latest version', hidden = true) + boolean selfUpdate + + @Option(names = ['--syslog'], arity = '0..1', fallbackValue = 'localhost', paramLabel = '', description = 'Send logs to syslog server (e.g. localhost:514)') + String syslog + + @Option(names = ['--trace'], split = ',', paramLabel = '', description = 'Enable TRACE level logging for the specified package name') + List trace + + @Option(names = ['-v'], description = 'Print the version number and exit') + boolean version + + @Option(names = ['-V','--version'], description = 'Print the full version info and exit') + boolean fullVersion + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/CloneCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/CloneCmd.groovy new file mode 100644 index 0000000000..91f7749929 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/CloneCmd.groovy @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CmdClone +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters + +/** + * CLI `clone` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'clone', + description = 'Clone a project into a folder' +) +class CloneCmd extends AbstractCmd implements CmdClone.Options, HubOptionsV2 { + + @Parameters(index = '0', description = 'name of the project to clone') + String pipeline + + @Parameters(arity = '0..1', paramLabel = '', description = 'target directory') + String targetName + + @Option(names = ['-d','--depth'], description = 'Create a shallow clone of the specified depth') + Integer depth + + @Option(names = ['-r','--revision'], description = 'Revision to clone - It can be a git branch, tag or revision number') + String revision + + @Override + void run() { + new CmdClone(this).run() + } +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/ConfigCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/ConfigCmd.groovy new file mode 100644 index 0000000000..0eb244fa35 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/ConfigCmd.groovy @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CliOptions +import nextflow.cli.CmdConfig +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import picocli.CommandLine.ParentCommand + +/** + * CLI `config` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'config', + description = 'Print a project configuration' +) +class ConfigCmd extends AbstractCmd implements CmdConfig.Options { + + @ParentCommand + private Launcher launcher + + @Parameters(arity = '0..1', description = 'project name') + String pipeline + + @Option(names = ['-a','--show-profiles'], description = 'Show all configuration profiles') + boolean showAllProfiles + + @Option(names = ['--profile'], description = 'Choose a configuration profile') + String profile + + @Option(names = ['--properties'], description = 'Prints config using Java properties notation') + boolean printProperties + + @Option(names = ['--flat'], description = 'Print config using flat notation') + boolean printFlatten + + @Option(names = ['--sort'], description = 'Sort config attributes') + boolean sort + + @Option(names = ['--value'], description = 'Print the value of a config option, or fail if the option is not defined') + String printValue + + @Override + CliOptions getLauncherOptions() { + launcher.options + } + + @Override + void run() { + new CmdConfig(this).run() + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/ConsoleCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/ConsoleCmd.groovy new file mode 100644 index 0000000000..7498951e8f --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/ConsoleCmd.groovy @@ -0,0 +1,43 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CmdConsole +import picocli.CommandLine.Command +import picocli.CommandLine.Parameters + +/** + * CLI `console` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'console', + description = 'Launch Nextflow interactive console' +) +class ConsoleCmd extends AbstractCmd implements CmdConsole.Options { + + @Parameters(arity = '0..1', description = 'script filename') + String script + + @Override + void run() { + new CmdConsole(this).run() + } +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/DropCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/DropCmd.groovy new file mode 100644 index 0000000000..ff7034f978 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/DropCmd.groovy @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CmdDrop +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters + +/** + * CLI `drop` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'drop', + description = 'Delete the local copy of a project' +) +class DropCmd extends AbstractCmd implements CmdDrop.Options { + + @Parameters(description = 'name of the project to drop') + String pipeline + + @Option(names = ['-f','--force'], description = 'Delete the repository without taking care of local changes') + boolean force + + @Override + void run() { + new CmdDrop(this).run() + } +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/FsCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/FsCmd.groovy new file mode 100644 index 0000000000..ce8effc8c0 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/FsCmd.groovy @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CliOptions +import nextflow.cli.CmdFs +import picocli.CommandLine.Command +import picocli.CommandLine.Parameters +import picocli.CommandLine.ParentCommand + +/** + * CLI `fs` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'fs', + description = 'Perform basic filesystem operations' +) +class FsCmd extends AbstractCmd implements CmdFs.Options { + + @ParentCommand + private Launcher launcher + + @Override + CliOptions getLauncherOptions() { + launcher.options + } + + @Command(description = 'Copy a file') + void copy( + @Parameters(paramLabel = '') String source, + @Parameters(paramLabel = '') String target) { + new CmdFs(this).run(CmdFs.Command.COPY, [ source, target ]) + } + + @Command(description = 'Move a file') + void move( + @Parameters(paramLabel = '') String source, + @Parameters(paramLabel = '') String target) { + new CmdFs(this).run(CmdFs.Command.MOVE, [ source, target ]) + } + + @Command(description = 'List the contents of a folder') + void list( + @Parameters(paramLabel = '') String source) { + new CmdFs(this).run(CmdFs.Command.LIST, [ source ]) + } + + @Command(description = 'Print a file to stdout') + void cat( + @Parameters(paramLabel = '') String source) { + new CmdFs(this).run(CmdFs.Command.CAT, [ source ]) + } + + @Command(description = 'Remove a file') + void remove( + @Parameters(paramLabel = '') String source) { + new CmdFs(this).run(CmdFs.Command.REMOVE, [ source ]) + } + + @Command(description = 'Print file metadata') + void stat( + @Parameters(paramLabel = '') String source) { + new CmdFs(this).run(CmdFs.Command.STAT, [ source ]) + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/HubOptionsV2.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/HubOptionsV2.groovy new file mode 100644 index 0000000000..da196e95d4 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/HubOptionsV2.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.HubOptions +import picocli.CommandLine.Option + +/** + * CLI v2 implementation of command line options related to interacting with + * a git registry (GitHub, BitBucket, etc) + * + * @author Ben Sherman + */ +@CompileStatic +trait HubOptionsV2 implements HubOptions { + + @Option(names = ['--hub'], paramLabel = '', description = 'Service hub where the project is hosted') + String hubProvider + + @Option(names = ['--user'], paramLabel = '', description = 'Private repository user name') + String hubUserCli + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/InfoCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/InfoCmd.groovy new file mode 100644 index 0000000000..14797c0903 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/InfoCmd.groovy @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CmdInfo +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters + +/** + * CLI `info` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'info', + description = 'Print project and system runtime information' +) +class InfoCmd extends AbstractCmd implements CmdInfo.Options { + + @Parameters(arity = '0..1', description = 'project name') + String pipeline + + @Option(names = ['-d'], arity = '0', description = 'Show detailed information') + boolean detailed + + @Option(names = ['-dd'], arity = '0', hidden = true) + boolean moreDetailed + + @Option(names = ['-o','--output-format'], description = 'Output format, either: text (default), json, yaml') + String format + + @Option(names = ['-u','--check-updates'], description = 'Check for remote updates') + boolean checkForUpdates + + @Override + void run() { + new CmdInfo(this).run() + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/InspectCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/InspectCmd.groovy new file mode 100644 index 0000000000..fe4a56ee19 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/InspectCmd.groovy @@ -0,0 +1,105 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.cli.CliOptions +import nextflow.cli.CmdInspect +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import picocli.CommandLine.ParentCommand +import picocli.CommandLine.Unmatched + +/** + * CLI `inspect` sub-command (v2) + * + * @author Ben Sherman + */ +@Slf4j +@CompileStatic +@Command( + name = 'inspect', + description = 'Inspect process settings in a pipeline project' +) +class InspectCmd extends AbstractCmd implements CmdInspect.Options { + + @ParentCommand + private Launcher launcher + + @Parameters(description = 'Project name or repository url') + String pipeline + + @Unmatched + List unmatched = [] + + @Option(names = ['-concretize'], description = "Build the container images resolved by the inspect command") + boolean concretize + + @Option(names = ['-c','-config'], hidden = true) + List runConfig + + @Option(names = ['-format'], description = "Inspect output format. Can be 'json' or 'config'") + String format = 'json' + + @Option(names = ['-i','-ignore-errors'], description = 'Ignore errors while inspecting the pipeline') + boolean ignoreErrors + + @Option(names = '-params-file', description = 'Load script parameters from a JSON/YAML file') + String paramsFile + + @Option(names = ['-profile'], description = 'Use the given configuration profile(s)') + String profile + + @Option(names = ['-r','-revision'], description = 'Revision of the project to inspect (either a git branch, tag or commit SHA number)') + String revision + + Map params + + @Override + List getArgs() { [] } + + @Override + String getLauncherCli() { + launcher.cliString + } + + @Override + CliOptions getLauncherOptions() { + launcher.options + } + + @Override + void run() { + params = ParamsHelper.parseParams(unmatched) + + final opts = new RunCmd() + opts.launcher = launcher + opts.ansiLog = false + opts.preview = true + opts.pipeline = pipeline + opts.params = params + opts.paramsFile = paramsFile + opts.profile = profile + opts.revision = revision + opts.runConfig = runConfig + + new CmdInspect(this).run(opts) + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/Launcher.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/Launcher.groovy new file mode 100644 index 0000000000..04071c9446 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/Launcher.groovy @@ -0,0 +1,293 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.BuildInfo +import nextflow.Const +import nextflow.cli.CliOptions +import nextflow.exception.AbortOperationException +import nextflow.exception.AbortRunException +import nextflow.exception.ConfigParseException +import nextflow.exception.ScriptCompilationException +import nextflow.exception.ScriptRuntimeException +import nextflow.util.Escape +import nextflow.util.LoggerHelper +import nextflow.util.ProxyHelper +import org.eclipse.jgit.api.errors.GitAPIException +import picocli.CommandLine +import picocli.CommandLine.ArgGroup +import picocli.CommandLine.Command +import picocli.CommandLine.HelpCommand +import picocli.CommandLine.IExecutionExceptionHandler +import picocli.CommandLine.Option +import picocli.CommandLine.ParseResult + +/** + * Main application entry point. It parses the command line and + * launches the pipeline execution. + * + * @author Ben Sherman + */ +@Slf4j +@CompileStatic +@Command( + name = 'nf', + description = 'Nextflow CLI v2', + subcommands = [ + CleanCmd.class, + CloneCmd.class, + ConfigCmd.class, + ConsoleCmd.class, + DropCmd.class, + FsCmd.class, + HelpCommand.class, + InfoCmd.class, + InspectCmd.class, + ListCmd.class, + LogCmd.class, + NodeCmd.class, + PluginCmd.class, + PullCmd.class, + RunCmd.class, + SecretsCmd.class, + SelfUpdateCmd.class, + ViewCmd.class + ] +) +class Launcher extends AbstractCmd { + + @ArgGroup(validate = false) + private CliOptionsV2 options + + private String cliString + + private boolean daemonMode + + Launcher() { + this.options = new CliOptionsV2() + } + + protected int executionStrategy(ParseResult parseResult) { + if( parseResult.subcommand() ) + parseResult = parseResult.subcommand() + + def command = parseResult.commandSpec().commandLine().getCommand() + def args = parseResult.originalArgs() as String[] + + // make command line string + this.cliString = makeCli(System.getenv('NXF_CLI'), args) + + // whether is running a daemon + this.daemonMode = command instanceof NodeCmd + + // set the log file name + if( !options.logFile ) { + if( isDaemon() ) + options.logFile = System.getenv('NXF_LOG_FILE') ?: '.node-nextflow.log' + else if( command instanceof RunCmd || options.debug || options.trace ) + options.logFile = System.getenv('NXF_LOG_FILE') ?: '.nextflow.log' + } + + LoggerHelper.configureLogger(options, isDaemon()) + + // setup proxy environment + ProxyHelper.setupEnvironment() + + // launch the command + log.debug '$> ' + cliString + + int exitCode = new CommandLine.RunLast().execute(parseResult) + + if( log.isTraceEnabled() ) + log.trace "Exit\n" + dumpThreads() + + return exitCode + } + + protected String makeCli(String cli, String... args) { + if( !cli ) + cli = 'nf' + if( !args ) + return cli + def cmd = ' ' + args[0] + int p = cli.indexOf(cmd) + if( p!=-1 ) + cli = cli.substring(0,p) + if( cli.endsWith('nf') ) + cli = 'nf' + cli += ' ' + Escape.cli(args) + return cli + } + + CliOptionsV2 getOptions() { options } + + String getCliString() { cliString } + + boolean isDaemon() { daemonMode } + + @Override + void run() { + // -- print out the version number, then exit + if( options.version ) { + println getVersion(false) + return + } + + if( options.fullVersion ) { + println getVersion(true) + return + } + + // -- print out the program help, then exit + spec.commandLine().usage(System.err) + } + + /** + * Dump the stack trace of current running threads + */ + private String dumpThreads() { + + def buffer = new StringBuffer() + Map m = Thread.getAllStackTraces() + for(Map.Entry e : m.entrySet()) { + buffer.append('\n').append(e.getKey().toString()).append('\n') + for (StackTraceElement s : e.getValue()) { + buffer.append(" " + s).append('\n') + } + } + + return buffer.toString() + } + + static class ExecutionExceptionHandler implements IExecutionExceptionHandler { + int handleExecutionException(Exception ex, CommandLine cmd, ParseResult parseResult) { + // bold red error message + cmd.getErr().println(cmd.getColorScheme().errorText(ex.getMessage() ?: '')) + + return cmd.getExitCodeExceptionMapper() != null + ? cmd.getExitCodeExceptionMapper().getExitCode(ex) + : cmd.getCommandSpec().exitCodeOnExecutionException() + } + } + + /** + * Hey .. Nextflow starts here! + * + * @param args The program options as specified by the user on the CLI + */ + static void main(String[] args) { + try { + // create launcher + def launcher = new Launcher() + def cmd = new CommandLine(launcher) + .setExecutionStrategy(launcher::executionStrategy) + .setExecutionExceptionHandler(new ExecutionExceptionHandler()) + .setAllowSubcommandsAsOptionParameters(true) + .setPosixClusteredShortOptionsAllowed(false) + .setUnmatchedOptionsArePositionalParams(true) + + // when the first argument is a file, it's supposed to be a script to be executed + if( args.length > 0 && !cmd.getCommandSpec().subcommands().containsKey(args[0]) && new File(args[0]).isFile() ) { + def argsList = args as List + argsList.add(0, 'run') + args = argsList as String[] + } + + // execute command + System.exit(cmd.execute(args)) + } + + catch( AbortRunException e ) { + System.exit(1) + } + + catch( AbortOperationException e ) { + def message = e.getMessage() + if( message ) System.err.println(message) + log.debug ("Operation aborted", e.cause ?: e) + System.exit(1) + } + + catch( GitAPIException e ) { + System.err.println (e.getMessage() ?: e.toString()) + log.debug ("Operation aborted", e.cause ?: e) + System.exit(1) + } + + catch( ConfigParseException e ) { + def message = e.message + if( e.cause?.message ) { + message += "\n\n${e.cause.message.toString().indent(' ')}" + } + log.error(message, e.cause ?: e) + System.exit(1) + } + + catch( ScriptCompilationException e ) { + log.error(e.message, e) + System.exit(1) + } + + catch( ScriptRuntimeException | IllegalArgumentException e) { + log.error(e.message, e) + System.exit(1) + } + + catch( IOException e ) { + log.error(e.message, e) + System.exit(1) + } + + catch( Throwable fail ) { + log.error("@unknown", fail) + System.exit(1) + } + } + + + /** + * Print the version number. + * + * @param full When {@code true} prints full version number including build timestamp + */ + static String getVersion(boolean full = false) { + + if ( full ) { + SPLASH + } + else { + "${Const.APP_NAME} version ${BuildInfo.version}.${BuildInfo.buildNum}" + } + + } + + /* + * The application 'logo' + */ + static public final String SPLASH = + +""" + N E X T F L O W + version ${BuildInfo.version} build ${BuildInfo.buildNum} + created ${BuildInfo.timestampUTC} ${BuildInfo.timestampDelta} + cite doi:10.1038/nbt.3820 + http://nextflow.io +""" + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/ListCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/ListCmd.groovy new file mode 100644 index 0000000000..4d98554739 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/ListCmd.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CmdList +import picocli.CommandLine.Command + +/** + * CLI `list` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'list', + description = 'List all downloaded projects' +) +class ListCmd extends AbstractCmd implements CmdList.Options { + + @Override + void run() { + new CmdList(this).run() + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/v2/LogCmd.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/v2/LogCmd.groovy new file mode 100644 index 0000000000..2df7c693ad --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/cli/v2/LogCmd.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.cli.v2 + +import groovy.transform.CompileStatic +import nextflow.cli.CmdLog +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters + +/** + * CLI `log` sub-command (v2) + * + * @author Ben Sherman + */ +@CompileStatic +@Command( + name = 'log', + description = 'Print executions log and runtime info' +) +class LogCmd extends AbstractCmd implements CmdLog.Options { + + @Option(names = ['--after'], paramLabel = '|', description = 'Show log entries for runs executed after the specified one') + String after + + @Option(names = ['--before'], paramLabel = '|', description = 'Show log entries for runs executed before the specified one') + String before + + @Option(names = ['--but'], paramLabel = '|', description = 'Show log entries of all runs except the specified one') + String but + + @Option(names = ['-f','--fields'], description = 'Comma separated list of fields to include in the printed log -- Use the `-l` option to show the list of available fields') + String fields + + @Option(names = ['-F','--filter'], paramLabel = '', description = "Filter log entries by a custom expression e.g. process =~ /foo.*/ && status == 'COMPLETED'") + String filterStr + + @Option(names = ['-l','--list-fields'], arity = '0', description = 'Show all available fields') + boolean listFields + + @Option(names = ['-q','--quiet'], arity = '0', description = 'Show only run names') + boolean quiet + + @Option(names = ['-s','--separator'], description = 'Character used to separate column values') + String separator = '\\t' + + @Option(names = ['-t','--template'], paramLabel = '