diff --git a/docs/migrations/25-10.md b/docs/migrations/25-10.md index dcb07a64be..67b43914ed 100644 --- a/docs/migrations/25-10.md +++ b/docs/migrations/25-10.md @@ -8,6 +8,31 @@ This page summarizes the upcoming changes in Nextflow 25.10, which will be relea This page is a work in progress and will be updated as features are finalized. It should not be considered complete until the 25.10 release. ::: +## New features + +

Workflow params

+ +The `params` block is a new way to declare pipeline parameters in a Nextflow script: + +```nextflow +params { + // Path to input data. + input: Path + + // Whether to save intermediate files. + save_intermeds: Boolean = false +} + +workflow { + println "params.input = ${params.input}" + println "params.save_intermeds = ${params.save_intermeds}" +} +``` + +This syntax allows you to declare all parameters in one place with explicit type annotations, and it allows Nextflow to validate parameters at runtime. + +See {ref}`workflow-params-def` for details. + ## Enhancements

New syntax for workflow handlers

@@ -74,4 +99,6 @@ This feature addresses previous inconsistencies in timestamp representations. ## Deprecations +- The legacy type detection of CLI parameters is disabled when using the strict syntax (`NXF_SYNTAX_PARSER=v2`). {ref}`Legacy parameters ` in the strict syntax should not rely on legacy type detection. Alternatively, use the new `params` block to convert CLI parameters based on their type annotations. Legacy type detection can be disabled globally by setting the environment variable `NXF_DISABLE_PARAMS_TYPE_DETECTION=true`. + - The use of workflow handlers in the configuration file has been deprecated. You should define workflow handlers in the pipeline script or a plugin instead. See {ref}`config-workflow-handlers` for details. diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md index 696ef6b42a..5d794b1d62 100644 --- a/docs/reference/syntax.md +++ b/docs/reference/syntax.md @@ -28,7 +28,8 @@ A Nextflow script may contain the following top-level declarations: - Shebang - Feature flags - Include declarations -- Parameter declarations +- Params block +- Parameter declarations (legacy) - Workflow definitions - Process definitions - Function definitions @@ -107,9 +108,22 @@ The following definitions can be included: - Processes - Named workflows -### Parameter +### Params block -A parameter declaration is an assignment. The target should be a pipeline parameter and the source should be an expression: +The params block consists of one or more *parameter declarations*. A parameter declaration consists of a name and an optional default value: + +```nextflow +params { + input: Path + save_intermeds: Boolean = false +} +``` + +Only one params block may be defined in a script. + +### Parameter (legacy) + +A legacy parameter declaration is an assignment. The target should be a pipeline parameter and the source should be an expression: ```nextflow params.message = 'Hello world!' diff --git a/docs/vscode.md b/docs/vscode.md index 76f1367594..ac3a716edf 100644 --- a/docs/vscode.md +++ b/docs/vscode.md @@ -26,7 +26,7 @@ The language server parses scripts and config files according to the {ref}`Nextf When you hover over certain source code elements, such as variable names and function calls, the extension provides a tooltip with related information, such as the definition and/or documentation for the element. -If a [Javadoc](https://en.wikipedia.org/wiki/Javadoc) comment is defined above a workflow, process, or function, the extension will include the contents of the comment in hover hints. The following is an example Javadoc comment: +If a [Javadoc](https://en.wikipedia.org/wiki/Javadoc) comment is defined above a workflow, process, function, or parameter in a `params` block, the extension will include the contents of the comment in hover hints. The following is an example Javadoc comment: ```nextflow /** diff --git a/docs/workflow.md b/docs/workflow.md index d44f92289a..6cfc61b11c 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -22,7 +22,60 @@ workflow { } ``` -### Parameters +(workflow-params-def)= + +## Parameters + +Parameters can be declared in a Nextflow script with the `params` block or with *legacy* parameter declarations. + +### Params block + +:::{versionadded} 25.10.0 +::: + +:::{note} +This feature requires the {ref}`strict syntax ` to be enabled (`NXF_SYNTAX_PARSER=v2`). +::: + +A script can declare parameters using the `params` block: + +```nextflow +params { + // Path to input data. + input: Path + + // Whether to save intermediate files. + save_intermeds: Boolean = false +} +``` + +The following types can be used for parameters: + +- {ref}`stdlib-types-boolean` +- {ref}`stdlib-types-float` +- {ref}`stdlib-types-integer` +- {ref}`stdlib-types-path` +- {ref}`stdlib-types-string` + +Parameters can be used in the entry workflow: + +```nextflow +workflow { + analyze(params.input, params.save_intermeds) +} +``` + +:::{note} +As a best practice, parameters should only be used directly in the entry workflow and passed to workflows and processes as explicit inputs. +::: + +The default value can be overridden by the command line, params file, or config file. Parameters from multiple sources are resolved in the order described in {ref}`cli-params`. Parameters specified on the command line are converted to the appropriate type based on the corresponding type annotation. + +A parameter that doesn't specify a default value is a *required* param. If a required param is not given a value at runtime, the run will fail. + +(workflow-params-legacy)= + +### Legacy parameters Parameters can be declared by assigning a `params` property to a default value: @@ -38,10 +91,6 @@ workflow { } ``` -:::{note} -As a best practice, `params` should be used only in the entry workflow and passed to workflows and processes as explicit inputs. -::: - The default value can be overridden by the command line, params file, or config file. Parameters from multiple sources are resolved in the order described in {ref}`cli-params`. ## Named workflows diff --git a/modules/nextflow/src/main/groovy/nextflow/Session.groovy b/modules/nextflow/src/main/groovy/nextflow/Session.groovy index 0e8ae74108..2a07576f09 100644 --- a/modules/nextflow/src/main/groovy/nextflow/Session.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/Session.groovy @@ -120,6 +120,16 @@ class Session implements ISession { */ ScriptBinding binding + /** + * Params that were specified on the command line. + */ + Map cliParams + + /** + * Params that were specified in the configuration. + */ + Map configParams + /** * Holds the configuration object */ @@ -425,7 +435,7 @@ class Session implements ISession { /** * Initialize the session workDir, libDir, baseDir and scriptName variables */ - Session init( ScriptFile scriptFile, List args=null ) { + Session init( ScriptFile scriptFile, List args=null, Map cliParams=null, Map configParams=null ) { if(!workDir.mkdirs()) throw new AbortOperationException("Cannot create work-dir: $workDir -- Make sure you have write permissions or specify a different directory by using the `-w` command line option") log.debug "Work-dir: ${workDir.toUriString()} [${FileHelper.getPathFsType(workDir)}]" @@ -452,6 +462,8 @@ class Session implements ISession { this.workflowMetadata = new WorkflowMetadata(this, scriptFile) // configure script params + this.cliParams = cliParams + this.configParams = configParams binding.setParams( (Map)config.params ) binding.setArgs( new ScriptRunner.ArgsList(args) ) diff --git a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy index d096e678e6..838bd173fe 100644 --- a/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/cli/CmdRun.groovy @@ -325,12 +325,18 @@ class CmdRun extends CmdBase implements HubOptions { // -- specify the arguments final scriptFile = getScriptFile(pipeline) + // -- load command line params + final baseDir = scriptFile.parent + final cliParams = parsedParams(ConfigBuilder.getConfigVars(baseDir)) + // create the config object final builder = new ConfigBuilder() .setOptions(launcher.options) .setCmdRun(this) - .setBaseDir(scriptFile.parent) - final config = builder .build() + .setBaseDir(baseDir) + .setCliParams(cliParams) + final config = builder.build() + final configParams = builder.getConfigParams() // check DSL syntax in the config launchInfo(config, scriptFile) @@ -376,7 +382,7 @@ class CmdRun extends CmdBase implements HubOptions { } // -- run it! - runner.execute(scriptArgs, this.entryName) + runner.execute(scriptArgs, cliParams, configParams, this.entryName) } protected void printBanner() { @@ -698,7 +704,7 @@ class CmdRun extends CmdBase implements HubOptions { } static protected parseParamValue(String str) { - if ( SysEnv.get('NXF_DISABLE_PARAMS_TYPE_DETECTION') ) + if ( SysEnv.get('NXF_DISABLE_PARAMS_TYPE_DETECTION') || NF.isSyntaxParserV2() ) return str if ( str == null ) return null diff --git a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy index c37e40591c..a3bfc4f27d 100644 --- a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy @@ -58,6 +58,8 @@ class ConfigBuilder { Path currentDir + Map cliParams + boolean showAllProfiles String profile = DEFAULT_PROFILE @@ -78,9 +80,11 @@ class ConfigBuilder { Map env = new HashMap<>(SysEnv.get()) - List warnings = new ArrayList<>(10); + List warnings = new ArrayList<>(10) + + Map declaredParams = [:] - { + ConfigBuilder() { setHomeDir(Const.APP_HOME_DIR) setCurrentDir(Paths.get('.')) } @@ -111,6 +115,11 @@ class ConfigBuilder { return this } + ConfigBuilder setCliParams( Map cliParams ) { + this.cliParams = cliParams + return this + } + ConfigBuilder setBaseDir( Path path ) { this.baseDir = path.complete() return this @@ -159,6 +168,10 @@ class ConfigBuilder { return this } + Map getConfigParams() { + return declaredParams + } + static private wrapValue( value ) { if( !value ) return '' @@ -324,11 +337,11 @@ class ConfigBuilder { // this is needed to make sure to reuse the same // instance of the config vars across different instances of the ConfigBuilder // and prevent multiple parsing of the same params file (which can even be remote resource) - return cacheableConfigVars(baseDir) + return getConfigVars(baseDir) } @Memoized - static private Map cacheableConfigVars(Path base) { + static Map getConfigVars(Path base) { final binding = new HashMap(10) binding.put('baseDir', base) binding.put('projectDir', base) @@ -348,8 +361,8 @@ class ConfigBuilder { .setIgnoreIncludes(ignoreIncludes) ConfigObject result = new ConfigObject() - if( cmdRun && (cmdRun.hasParams()) ) - parser.setParams(cmdRun.parsedParams(configVars())) + if( cliParams ) + parser.setParams(cliParams) // add the user specified environment to the session env env.sort().each { name, value -> result.env.put(name,value) } @@ -380,7 +393,7 @@ class ConfigBuilder { } if( validateProfile ) { - checkValidProfile(parser.getProfiles()) + checkValidProfile(parser.getDeclaredProfiles()) } } @@ -414,6 +427,7 @@ class ConfigBuilder { final config = parse0(parser, entry) if( NF.getSyntaxParserVersion() == 'v1' ) validate(config, entry) + declaredParams.putAll(parser.getDeclaredParams()) result.merge(config) } @@ -723,8 +737,8 @@ class ConfigBuilder { } // -- add the command line parameters to the 'taskConfig' object - if( cmdRun.hasParams() ) - config.params = mergeMaps( (Map)config.params, cmdRun.parsedParams(configVars()), NF.strictMode ) + if( cliParams ) + config.params = mergeMaps( (Map)config.params, cliParams, NF.strictMode ) if( cmdRun.withoutDocker && config.docker instanceof Map ) { // disable docker execution diff --git a/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy b/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy index 9a51a02f81..78d86e01f8 100644 --- a/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/config/ConfigParser.groovy @@ -67,6 +67,11 @@ interface ConfigParser { */ ConfigParser setParams(Map vars) + /** + * Set the profiles that should be applied. + */ + ConfigParser setProfiles(List profiles) + /** * Parse a config object from the given source. */ @@ -75,13 +80,13 @@ interface ConfigParser { ConfigObject parse(Path path) /** - * Set the profiles that should be applied. + * Get the set of declared profiles. */ - ConfigParser setProfiles(List profiles) + Set getDeclaredProfiles() /** - * Get the set of available profiles. + * Get the map of declared params. */ - Set getProfiles() + Map getDeclaredParams() } diff --git a/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy b/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy index a259185a96..22ba8159e9 100644 --- a/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/config/parser/v1/ConfigParserV1.groovy @@ -123,10 +123,15 @@ class ConfigParserV1 implements ConfigParser { } @Override - Set getProfiles() { + Set getDeclaredProfiles() { Collections.unmodifiableSet(conditionalNames) } + @Override + Map getDeclaredParams() { + [:] + } + private Grengine getGrengine() { if( grengine ) { return grengine diff --git a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy index cf7e0ab835..a450ea7604 100644 --- a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigDsl.groovy @@ -48,7 +48,9 @@ class ConfigDsl extends Script { private Map target = [:] - private Set parsedProfiles = [] + private Set declaredProfiles = [] + + private Map declaredParams = [:] void setIgnoreIncludes(boolean value) { this.ignoreIncludes = value @@ -74,12 +76,20 @@ class ConfigDsl extends Script { this.profiles = profiles } - void addParsedProfile(String profile) { - parsedProfiles.add(profile) + void declareProfile(String profile) { + declaredProfiles.add(profile) + } + + Set getDeclaredProfiles() { + return declaredProfiles + } + + void declareParam(String name, Object value) { + declaredParams.put(name, value) } - Set getParsedProfiles() { - return parsedProfiles + Map getDeclaredParams() { + return declaredParams } Map getTarget() { @@ -104,8 +114,10 @@ class ConfigDsl extends Script { } } - void assign(List names, Object right) { - navigate(names.init()).put(names.last(), right) + void assign(List names, Object value) { + if( names.size() == 2 && names.first() == 'params' ) + declareParam(names.last(), value) + navigate(names.init()).put(names.last(), value) } private Map navigate(List names) { @@ -177,7 +189,8 @@ class ConfigDsl extends Script { .setParams(target.params as Map) .setProfiles(profiles) final config = parser.parse(configText, includePath) - parsedProfiles.addAll(parser.getProfiles()) + declaredProfiles.addAll(parser.getDeclaredProfiles()) + declaredParams.putAll(parser.getDeclaredParams()) final ctx = navigate(names) ctx.putAll(Bolts.deepMerge(ctx, config)) @@ -280,7 +293,7 @@ class ConfigDsl extends Script { @Override void block(String name, Closure closure) { blocks[name] = closure - dsl.addParsedProfile(name) + dsl.declareProfile(name) } @Override diff --git a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy index d6a7bc3e7d..7ff23c4b3a 100644 --- a/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/config/parser/v2/ConfigParserV2.groovy @@ -48,7 +48,9 @@ class ConfigParserV2 implements ConfigParser { private List appliedProfiles - private Set parsedProfiles + private Set declaredProfiles + + private Map declaredParams private GroovyShell groovyShell @@ -58,11 +60,6 @@ class ConfigParserV2 implements ConfigParser { return this } - @Override - Set getProfiles() { - return parsedProfiles - } - @Override ConfigParserV2 setIgnoreIncludes(boolean value) { this.ignoreIncludes = value @@ -101,6 +98,16 @@ class ConfigParserV2 implements ConfigParser { return this } + @Override + Set getDeclaredProfiles() { + return declaredProfiles + } + + @Override + Map getDeclaredParams() { + return declaredParams + } + /** * Parse the given script as a string and return the configuration object * @@ -126,7 +133,8 @@ class ConfigParserV2 implements ConfigParser { script.run() final target = script.getTarget() - parsedProfiles = script.getParsedProfiles() + declaredProfiles = script.getDeclaredProfiles() + declaredParams = script.getDeclaredParams() return Bolts.toConfigObject(target) } catch( CompilationFailedException e ) { diff --git a/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy b/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy index 67075cb1a3..f916cfd8bf 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/BaseScript.groovy @@ -98,6 +98,21 @@ abstract class BaseScript extends Script implements ExecutionContext { binding.setVariable( 'secrets', SecretsLoader.secretContext() ) } + protected void params(Closure body) { + if( entryFlow ) + throw new IllegalStateException("Workflow params definition must be defined before the entry workflow") + if( ExecutionStack.withinWorkflow() ) + throw new IllegalStateException("Workflow params definition is not allowed within a workflow") + + final dsl = new ParamsDsl() + final cl = (Closure)body.clone() + cl.setDelegate(dsl) + cl.setResolveStrategy(Closure.DELEGATE_FIRST) + cl.call() + + dsl.apply(session) + } + protected process( String name, Closure body ) { final process = new ProcessDef(this,body,name) meta.addDefinition(process) diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy new file mode 100644 index 0000000000..d2c665938c --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy @@ -0,0 +1,124 @@ +/* + * Copyright 2013-2024, 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.script + +import java.nio.file.Path + +import groovy.transform.Canonical +import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.file.FileHelper +import nextflow.exception.ScriptRuntimeException +import nextflow.script.types.Types +/** + * Implements the DSL for defining workflow params + * + * @author Ben Sherman + */ +@Slf4j +@CompileStatic +class ParamsDsl { + + private Map declarations = [:] + + void declare(String name, Class type) { + declarations[name] = new Param(name, type, Optional.empty()) + } + + void declare(String name, Class type, Object defaultValue) { + declarations[name] = new Param(name, type, Optional.of(defaultValue)) + } + + void apply(Session session) { + final cliParams = session.cliParams ?: [:] + final configParams = session.configParams ?: [:] + + for( final name : cliParams.keySet() ) { + if( !declarations.containsKey(name) && !configParams.containsKey(name) ) + throw new ScriptRuntimeException("Parameter `$name` was specified on the command line or params file but is not declared in the script or config") + } + + final params = new HashMap() + for( final name : declarations.keySet() ) { + final decl = declarations[name] + if( cliParams.containsKey(name) ) + params[name] = resolveFromCli(decl, cliParams[name]) + else if( configParams.containsKey(name) ) + params[name] = resolveFromCode(decl, configParams[name]) + else if( decl.defaultValue.isPresent() ) + params[name] = resolveFromCode(decl, decl.defaultValue.get()) + else + throw new ScriptRuntimeException("Parameter `$name` is required but was not specified on the command line, params file, or config") + + final actualType = params[name].getClass() + if( !decl.type.isAssignableFrom(actualType) ) + throw new ScriptRuntimeException("Parameter `$name` with type ${Types.getName(decl.type)} cannot be assigned to ${params[name]} [${Types.getName(actualType)}]") + } + + session.binding.setParams(params, true) + } + + private Object resolveFromCli(Param decl, Object value) { + if( value == null ) + return null + + if( value !instanceof CharSequence ) + return value + + final str = value.toString() + + if( decl.type == Boolean ) { + if( str.toLowerCase() == 'true' ) return Boolean.TRUE + if( str.toLowerCase() == 'false' ) return Boolean.FALSE + } + + if( decl.type == Integer || decl.type == Float ) { + if( str.isInteger() ) return str.toInteger() + if( str.isLong() ) return str.toLong() + } + + if( decl.type == Float ) { + if( str.isFloat() ) return str.toFloat() + if( str.isDouble() ) return str.toDouble() + } + + if( decl.type == Path ) { + return FileHelper.asPath(str) + } + + return value + } + + private Object resolveFromCode(Param decl, Object value) { + if( value == null ) + return null + + if( decl.type == Path && value instanceof CharSequence ) + return FileHelper.asPath(value.toString()) + + return value + } + + @Canonical + private static class Param { + String name + Class type + Optional defaultValue + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy index 9ef4999905..0ea8b267c9 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy @@ -75,7 +75,7 @@ class ScriptBinding extends WorkflowBinding { } vars.put('args', args) - // create and populate args + // create and populate params params = new ParamsMap() if( vars.params ) { if( !(vars.params instanceof Map) ) throw new IllegalArgumentException("ScriptBinding 'params' must be a Map value") @@ -132,10 +132,17 @@ class ScriptBinding extends WorkflowBinding { * The map of the CLI named parameters * * @param values + * @param override */ - ScriptBinding setParams(Map values ) { - if( values ) + ScriptBinding setParams(Map values, boolean override=false) { + if( values ) { + if( override ) { + for( final key : values.keySet() ) + params.remove(key) + } params.putAll(values) + super.setVariable0('params', params) + } return this } diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy index 54bd1e1dac..4449ea27c5 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ScriptRunner.groovy @@ -113,21 +113,21 @@ class ScriptRunner { /** - * Execute a Nextflow script, it does the following: - *
  • parse the script - *
  • launch script execution - *
  • await for all tasks completion + * Execute a Nextflow script: + * 1. compile and load the script + * 2. execute the script + * 3. await for all tasks to complete * - * @param scriptFile The file containing the script to be executed - * @param args The arguments to be passed to the script - * @return The result as returned by the {@code #run} method + * @param args command-line positional arguments + * @param cliParams parameters specified on the command-line + * @param configParams parameters specified in the config + * @param entryName named workflow entrypoint */ - - def execute( List args = null, String entryName=null ) { + def execute( List args=null, Map cliParams=null, Map configParams=null, String entryName=null ) { assert scriptFile // init session - session.init(scriptFile, args) + session.init(scriptFile, args, cliParams, configParams) // start session session.start() diff --git a/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy b/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy index 8b03bee573..bddaa0ed35 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/WorkflowBinding.groovy @@ -135,6 +135,10 @@ class WorkflowBinding extends Binding { @Override void setVariable(String name, Object value) { lookupTable.put(value, name) + setVariable0(name, value) + } + + protected void setVariable0(String name, Object value) { super.setVariable(name, value) } diff --git a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy index fff17bbe92..65b616188b 100644 --- a/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/config/ConfigBuilderTest.groovy @@ -17,7 +17,7 @@ package nextflow.config import java.nio.file.Files -import java.nio.file.Paths +import java.nio.file.Path import nextflow.SysEnv import nextflow.cli.CliOptions @@ -47,6 +47,24 @@ class ConfigBuilderTest extends Specification { SysEnv.pop() } + ConfigObject configWithParams(Path file, Map runOpts, Path baseDir=null) { + def run = new CmdRun(runOpts) + return new ConfigBuilder() + .setOptions(new CliOptions()) + .setCmdRun(run) + .setCliParams(run.parsedParams(ConfigBuilder.getConfigVars(baseDir))) + .buildGivenFiles(file) + } + + ConfigObject configWithParams(Map config, Map runOpts, Map cliOpts=[:]) { + def run = new CmdRun(runOpts) + return new ConfigBuilder(config) + .setOptions(new CliOptions(cliOpts)) + .setCmdRun(run) + .setCliParams(run.parsedParams(ConfigBuilder.getConfigVars(null))) + .build() + } + def 'build config object' () { setup: @@ -143,7 +161,7 @@ class ConfigBuilderTest extends Specification { setup: def builder = [:] as ConfigBuilder - builder.baseDir = Paths.get('/base/path') + builder.baseDir = Path.of('/base/path') def text = ''' params.p = "$baseDir/1" @@ -161,8 +179,8 @@ class ConfigBuilderTest extends Specification { cfg.params.p == '/base/path/1' cfg.params.q == '/base/path/2' cfg.params.x == '/base/path/3' - cfg.params.y == "${Paths.get('.').toRealPath()}/4" - cfg.params.z == "${Paths.get('results').complete()}/5" + cfg.params.y == "${Path.of('.').toRealPath()}/4" + cfg.params.z == "${Path.of('results').complete()}/5" } @@ -185,9 +203,7 @@ class ConfigBuilderTest extends Specification { } ''' when: - def opt = new CliOptions() - def run = new CmdRun(params: [alpha: 'Hello', beta: 'World', omega: 'Last']) - def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file) + def result = configWithParams(file, [params: [alpha: 'Hello', beta: 'World', omega: 'Last']]) then: result.params.alpha == 'Hello' // <-- params defined as CLI options override the ones in the config file @@ -218,9 +234,7 @@ class ConfigBuilderTest extends Specification { } ''' when: - def opt = new CliOptions() - def run = new CmdRun(params: [alpha: 'Hello', beta: 'World', omega: 'Last']) - def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file) + def result = configWithParams(file, [params: [alpha: 'Hello', beta: 'World', omega: 'Last']]) then: result.params.alpha == 'Hello' // <-- params defined as CLI options override the ones in the config file @@ -270,9 +284,7 @@ class ConfigBuilderTest extends Specification { ''' when: - def opt = new CliOptions() - def run = new CmdRun(params: [one: '1', two: 'dos', three: 'tres']) - def config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(configMain.toPath()) + def config = configWithParams(configMain.toPath(), [params: [one: '1', two: 'dos', three: 'tres']]) then: config.params.one == 1 @@ -316,9 +328,7 @@ class ConfigBuilderTest extends Specification { ''' when: - def opt = new CliOptions() - def run = new CmdRun(params: [igenomes_base: 'test']) - def config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(configMain.toPath()) + def config = configWithParams(configMain.toPath(), [params: [igenomes_base: 'test']]) then: config.params.genomes.GRCh37 == [fasta:'test/genome.fa', bwa:'test/BWAIndex/genome.fa'] @@ -333,7 +343,6 @@ class ConfigBuilderTest extends Specification { def folder = File.createTempDir() def configMain = new File(folder,'my.config').absoluteFile - configMain.text = """ process.name = 'alpha' params.one = 'a' @@ -401,44 +410,34 @@ class ConfigBuilderTest extends Specification { publishDir = [path: params.alpha] } } - } - ''' - when: - def opt = new CliOptions() - def run = new CmdRun(params: [alpha: 'AAA', beta: 'BBB']) - def config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file) + def config = configWithParams(file, [params: [alpha: 'AAA', beta: 'BBB']]) then: config.params.alpha == 'AAA' config.params.beta == 'BBB' config.params.delta == 'Foo' config.params.gamma == 'AAA' - config.params.genomes.GRCh37.bed12 == '/data/genes.bed' - config.params.genomes.GRCh37.bismark == '/data/BismarkIndex' - config.params.genomes.GRCh37.bowtie == '/data/genome' + config.params.genomes.'GRCh37'.bed12 == '/data/genes.bed' + config.params.genomes.'GRCh37'.bismark == '/data/BismarkIndex' + config.params.genomes.'GRCh37'.bowtie == '/data/genome' when: - opt = new CliOptions() - run = new CmdRun(params: [alpha: 'AAA', beta: 'BBB'], profile: 'first') - config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file) + config = configWithParams(file, [params: [alpha: 'AAA', beta: 'BBB'], profile: 'first']) then: config.params.alpha == 'AAA' config.params.beta == 'BBB' config.params.delta == 'Foo' config.params.gamma == 'First' config.process.name == 'Bar' - config.params.genomes.GRCh37.bed12 == '/data/genes.bed' - config.params.genomes.GRCh37.bismark == '/data/BismarkIndex' - config.params.genomes.GRCh37.bowtie == '/data/genome' - + config.params.genomes.'GRCh37'.bed12 == '/data/genes.bed' + config.params.genomes.'GRCh37'.bismark == '/data/BismarkIndex' + config.params.genomes.'GRCh37'.bowtie == '/data/genome' when: - opt = new CliOptions() - run = new CmdRun(params: [alpha: 'AAA', beta: 'BBB', genomes: 'xxx'], profile: 'second') - config = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file) + config = configWithParams(file, [params: [alpha: 'AAA', beta: 'BBB', genomes: 'xxx'], profile: 'second']) then: config.params.alpha == 'AAA' config.params.beta == 'BBB' @@ -453,10 +452,10 @@ class ConfigBuilderTest extends Specification { def 'params-file should override params in the config file' () { setup: - def baseDir = Paths.get('/my/base/dir') + def baseDir = Path.of('/my/base/dir') and: - def params = Files.createTempFile('test', '.yml') - params.text = ''' + def paramsFile = Files.createTempFile('test', '.yml') + paramsFile.text = ''' alpha: "Hello" beta: "World" omega: "Last" @@ -480,9 +479,7 @@ class ConfigBuilderTest extends Specification { } ''' when: - def opt = new CliOptions() - def run = new CmdRun(paramsFile: params) - def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).setBaseDir(baseDir).buildGivenFiles(file) + def result = configWithParams(file, [paramsFile: paramsFile], baseDir) then: result.params.alpha == 'Hello' // <-- params defined in the params-file overrides the ones in the config file @@ -495,7 +492,7 @@ class ConfigBuilderTest extends Specification { cleanup: file?.delete() - params?.delete() + paramsFile?.delete() } def 'params should override params-file and override params in the config file' () { @@ -522,9 +519,7 @@ class ConfigBuilderTest extends Specification { } ''' when: - def opt = new CliOptions() - def run = new CmdRun(paramsFile: params, params: [alpha: 'Hola', beta: 'Mundo']) - def result = new ConfigBuilder().setOptions(opt).setCmdRun(run).buildGivenFiles(file) + def result = configWithParams(file, [paramsFile: params, params: [alpha: 'Hola', beta: 'Mundo']]) then: result.params.alpha == 'Hola' // <-- this comes from the CLI @@ -1629,56 +1624,56 @@ class ConfigBuilderTest extends Specification { def config when: - config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun()).build() + config = configWithParams([:], [:], [config: EMPTY]) then: config.params == [:] // get params for the CLI when: - config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(params: [foo:'one', bar:'two'])).build() + config = configWithParams([:], [params: [foo:'one', bar:'two']], [config: EMPTY]) then: config.params == [foo:'one', bar:'two'] // get params from config file when: - config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun()).build() + config = configWithParams([:], [:], [config: [configFile]]) then: config.params == [foo:1, bar:2, data: '/some/path'] // get params form JSON file when: - config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(paramsFile: jsonFile)).build() + config = configWithParams([:], [paramsFile: jsonFile], [config: EMPTY]) then: config.params == [foo:10, bar:20] // get params from YAML file when: - config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(paramsFile: yamlFile)).build() + config = configWithParams([:], [paramsFile: yamlFile], [config: EMPTY]) then: config.params == [foo:100, bar:200] // cli override config when: - config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun(params:[foo:'hello', baz:'world'])).build() + config = configWithParams([:], [params: [foo:'hello', baz:'world']], [config: [configFile]]) then: config.params == [foo:'hello', bar:2, baz: 'world', data: '/some/path'] // CLI override JSON when: - config = new ConfigBuilder().setOptions(new CliOptions(config: EMPTY)).setCmdRun(new CmdRun(params:[foo:'hello', baz:'world'], paramsFile: jsonFile)).build() + config = configWithParams([:], [params: [foo:'hello', baz:'world'], paramsFile: jsonFile], [config: EMPTY]) then: config.params == [foo:'hello', bar:20, baz: 'world'] // JSON override config when: - config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun(paramsFile: jsonFile)).build() + config = configWithParams([:], [paramsFile: jsonFile], [config: [configFile]]) then: config.params == [foo:10, bar:20, data: '/some/path'] // CLI override JSON that override config when: - config = new ConfigBuilder().setOptions(new CliOptions(config: [configFile])).setCmdRun(new CmdRun(paramsFile: jsonFile, params: [foo:'Ciao'])).build() + config = configWithParams([:], [paramsFile: jsonFile, params: [foo:'Ciao']], [config: [configFile]]) then: config.params == [foo:'Ciao', bar:20, data: '/some/path'] } @@ -2257,9 +2252,9 @@ class ConfigBuilderTest extends Specification { """ when: - def opt = new CliOptions() - def run = new CmdRun(params: [bar: "world", 'baz.y': "mondo", 'baz.z.beta': "Welt"]) - def config = new ConfigBuilder(env: [NXF_CONFIG_FILE: configMain.toString()]).setOptions(opt).setCmdRun(run).build() + def config = configWithParams( + [env: [NXF_CONFIG_FILE: configMain.toString()]], + [params: [bar: "world", 'baz.y': "mondo", 'baz.z.beta': "Welt"]] ) then: config.params.foo == 'Hello' @@ -2342,10 +2337,7 @@ class ConfigBuilderTest extends Specification { when: - def cfg2 = new ConfigBuilder() - .setOptions( new CliOptions(userConfig: [config.toString()])) - .setCmdRun( new CmdRun(params: ['test.foo': 'CLI_FOO'] )) - .build() + def cfg2 = configWithParams([:], [params: ['test.foo': 'CLI_FOO']], [userConfig: [config.toString()]]) then: cfg2.params.test.foo == "CLI_FOO" cfg2.params.test.bar == "bar_def" @@ -2375,7 +2367,7 @@ class ConfigBuilderTest extends Specification { '''.stripIndent() when: - def cfg1 = new ConfigBuilder().setCmdRun(new CmdRun(paramsFile: config.toString())).build() + def cfg1 = configWithParams([:], [paramsFile: config.toString()]) then: cfg1.params.title == "something" @@ -2403,7 +2395,7 @@ class ConfigBuilderTest extends Specification { '''.stripIndent() when: - def cfg1 = new ConfigBuilder().setCmdRun(new CmdRun(paramsFile: config.toString())).build() + def cfg1 = configWithParams([:], [paramsFile: config.toString()]) then: cfg1.params.title == "something" diff --git a/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy b/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy index 85d6f38a76..36f6cc51f9 100644 --- a/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/config/parser/v1/ConfigParserV1Test.groovy @@ -399,7 +399,7 @@ class ConfigParserV1Test extends Specification { } - def 'should return the set of visited block names' () { + def 'should return the set of declared profiles' () { given: def text = ''' @@ -417,13 +417,13 @@ class ConfigParserV1Test extends Specification { def slurper = new ConfigParserV1().setProfiles(['alpha']) slurper.parse(text) then: - slurper.getProfiles() == ['alpha','beta'] as Set + slurper.getDeclaredProfiles() == ['alpha','beta'] as Set when: slurper = new ConfigParserV1().setProfiles(['omega']) slurper.parse(text) then: - slurper.getProfiles() == ['alpha','beta'] as Set + slurper.getDeclaredProfiles() == ['alpha','beta'] as Set } def 'should disable includeConfig parsing' () { diff --git a/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy b/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy index 0943603030..98913ebf73 100644 --- a/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/config/parser/v2/ConfigParserV2Test.groovy @@ -348,7 +348,7 @@ class ConfigParserV2Test extends Specification { } - def 'should return the set of parsed profiles' () { + def 'should return the set of declared profiles' () { given: def text = ''' @@ -366,13 +366,42 @@ class ConfigParserV2Test extends Specification { def slurper = new ConfigParserV2().setProfiles(['alpha']) slurper.parse(text) then: - slurper.getProfiles() == ['alpha','beta'] as Set + slurper.getDeclaredProfiles() == ['alpha','beta'] as Set when: slurper = new ConfigParserV2().setProfiles(['omega']) slurper.parse(text) then: - slurper.getProfiles() == ['alpha','beta'] as Set + slurper.getDeclaredProfiles() == ['alpha','beta'] as Set + } + + def 'should return the map of declared params' () { + + given: + def text = ''' + params { + a = 1 + b = 2 + } + + profiles { + alpha { + params.a = 3 + } + } + ''' + + when: + def slurper = new ConfigParserV2().setParams([c: 4]) + slurper.parse(text) + then: + slurper.getDeclaredParams() == [a: 1, b: 2] + + when: + slurper = new ConfigParserV2().setParams([c: 4]).setProfiles(['alpha']) + slurper.parse(text) + then: + slurper.getDeclaredParams() == [a: 3, b: 2] } def 'should ignore config includes when specified' () { diff --git a/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy index cdd8dc9727..6db6f66aed 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/BaseScriptTest.groovy @@ -84,7 +84,6 @@ class BaseScriptTest extends Dsl2Spec { def script = Files.createTempFile('test',null) and: def session = Mock(Session) { - getPublishTargets() >> [:] getConfig() >> [:] } def binding = new ScriptBinding([:]) @@ -119,7 +118,6 @@ class BaseScriptTest extends Dsl2Spec { def script = folder.resolve('main.nf') and: def session = Mock(Session) { - getPublishTargets() >> [:] getConfig() >> [:] } def binding = new ScriptBinding([:]) diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ParamsDslTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ParamsDslTest.groovy new file mode 100644 index 0000000000..51d75d39b7 --- /dev/null +++ b/modules/nextflow/src/test/groovy/nextflow/script/ParamsDslTest.groovy @@ -0,0 +1,83 @@ +package nextflow.script + +import java.nio.file.Path + +import nextflow.Session +import nextflow.file.FileHelper +import nextflow.exception.ScriptRuntimeException +import spock.lang.Specification +/** + * + * @author Ben Sherman + */ +class ParamsDslTest extends Specification { + + def 'should declare workflow params with CLI overrides'() { + given: + def cliParams = [input: './data', chunk_size: '3'] + def configParams = [outdir: 'results'] + def session = new Session([params: configParams + cliParams]) + session.init(null, null, cliParams, configParams) + + when: + def dsl = new ParamsDsl() + dsl.declare('input', Path) + dsl.declare('chunk_size', Integer, 1) + dsl.declare('save_intermeds', Boolean, false) + dsl.apply(session) + then: + session.binding.getParams() == [input: FileHelper.asPath('./data'), chunk_size: 3, save_intermeds: false, outdir: 'results'] + } + + def 'should report error for missing required param'() { + given: + def cliParams = [:] + def configParams = [outdir: 'results'] + def session = new Session() + session.init(null, null, cliParams, configParams) + + when: + def dsl = new ParamsDsl() + dsl.declare('input', Path) + dsl.declare('save_intermeds', Boolean, false) + dsl.apply(session) + then: + def e = thrown(ScriptRuntimeException) + e.message == 'Parameter `input` is required but was not specified on the command line, params file, or config' + } + + def 'should report error for invalid param'() { + given: + def cliParams = [inputs: './data'] + def configParams = [outdir: 'results'] + def session = new Session() + session.init(null, null, cliParams, configParams) + + when: + def dsl = new ParamsDsl() + dsl.declare('input', Path) + dsl.declare('save_intermeds', Boolean, false) + dsl.apply(session) + then: + def e = thrown(ScriptRuntimeException) + e.message == 'Parameter `inputs` was specified on the command line or params file but is not declared in the script or config' + } + + def 'should report error for invalid type'() { + given: + def cliParams = [input: './data', save_intermeds: 42] + def configParams = [:] + def session = new Session() + session.init(null, null, cliParams, configParams) + + when: + def dsl = new ParamsDsl() + dsl.declare('input', Path) + dsl.declare('save_intermeds', Boolean, false) + dsl.apply(session) + then: + def e = thrown(ScriptRuntimeException) + e.message == 'Parameter `save_intermeds` with type Boolean cannot be assigned to 42 [Integer]' + } + +} diff --git a/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy b/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy index a30c57f916..63ef37765b 100644 --- a/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy +++ b/modules/nextflow/src/testFixtures/groovy/test/Dsl2Spec.groovy @@ -53,6 +53,6 @@ class Dsl2Spec extends BaseSpec { def dsl_eval(String entry, String str) { new MockScriptRunner() - .setScript(str).execute(null, entry) + .setScript(str).execute(null, null, null, entry) } } diff --git a/modules/nf-lang/src/main/antlr/ScriptParser.g4 b/modules/nf-lang/src/main/antlr/ScriptParser.g4 index 5bffbafc1a..ba5ed64d9a 100644 --- a/modules/nf-lang/src/main/antlr/ScriptParser.g4 +++ b/modules/nf-lang/src/main/antlr/ScriptParser.g4 @@ -110,7 +110,8 @@ scriptDeclaration : featureFlagDeclaration #featureFlagDeclAlt | includeDeclaration #includeDeclAlt | importDeclaration #importDeclAlt - | paramDeclaration #paramDeclAlt + | paramsDef #paramsDefAlt + | paramDeclarationV1 #paramDeclV1Alt | enumDef #enumDefAlt | processDef #processDefAlt | workflowDef #workflowDefAlt @@ -147,8 +148,24 @@ importDeclaration : IMPORT qualifiedClassName ; -// -- param declaration +// -- params definition +paramsDef + : PARAMS nls LBRACE + paramsBody? + sep? RBRACE + ; + +paramsBody + : sep? paramDeclaration (sep paramDeclaration)* + ; + paramDeclaration + : identifier (COLON type)? (ASSIGN expression)? + | statement + ; + +// -- legacy parameter declaration +paramDeclarationV1 : PARAMS (DOT identifier)+ nls ASSIGN nls expression ; diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ParamBlockNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamBlockNode.java new file mode 100644 index 0000000000..feabebd7f5 --- /dev/null +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamBlockNode.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024-2025, 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.script.ast; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.Parameter; + +/** + * A workflow params definition. + * + * @author Ben Sherman + */ +public class ParamBlockNode extends ASTNode { + public final Parameter[] declarations; + + public ParamBlockNode(Parameter[] declarations) { + this.declarations = declarations; + } +} diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNodeV1.java similarity index 86% rename from modules/nf-lang/src/main/java/nextflow/script/ast/ParamNode.java rename to modules/nf-lang/src/main/java/nextflow/script/ast/ParamNodeV1.java index 13a57e7a07..d331ddf16c 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNode.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ParamNodeV1.java @@ -19,15 +19,15 @@ import org.codehaus.groovy.ast.expr.Expression; /** - * A parameter declaration. + * A legacy parameter declaration. * * @author Ben Sherman */ -public class ParamNode extends ASTNode { +public class ParamNodeV1 extends ASTNode { public final Expression target; public Expression value; - public ParamNode(Expression target, Expression value) { + public ParamNodeV1(Expression target, Expression value) { this.target = target; this.value = value; } diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java index 6abb50a6a3..230b51feef 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptNode.java @@ -32,7 +32,8 @@ public class ScriptNode extends ModuleNode { private String shebang; private List featureFlags = new ArrayList<>(); private List includes = new ArrayList<>(); - private List params = new ArrayList<>(); + private ParamBlockNode params; + private List paramsV1 = new ArrayList<>(); private WorkflowNode entry; private OutputBlockNode outputs; private List workflows = new ArrayList<>(); @@ -54,7 +55,9 @@ public List getDeclarations() { var declarations = new ArrayList(); declarations.addAll(featureFlags); declarations.addAll(includes); - declarations.addAll(params); + if( params != null ) + declarations.add(params); + declarations.addAll(paramsV1); if( entry != null ) declarations.add(entry); if( outputs != null ) @@ -77,10 +80,14 @@ public List getIncludes() { return includes; } - public List getParams() { + public ParamBlockNode getParams() { return params; } + public List getParamsV1() { + return paramsV1; + } + public WorkflowNode getEntry() { return entry; } @@ -119,8 +126,12 @@ public void addInclude(IncludeNode includeNode) { includes.add(includeNode); } - public void addParam(ParamNode paramNode) { - params.add(paramNode); + public void setParams(ParamBlockNode params) { + this.params = params; + } + + public void addParamV1(ParamNodeV1 paramNode) { + paramsV1.add(paramNode); } public void setEntry(WorkflowNode entry) { diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java index 080c1dbed0..cc1ea845e4 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitor.java @@ -16,6 +16,7 @@ package nextflow.script.ast; import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.GroovyCodeVisitor; public interface ScriptVisitor extends GroovyCodeVisitor { @@ -26,7 +27,11 @@ public interface ScriptVisitor extends GroovyCodeVisitor { void visitInclude(IncludeNode node); - void visitParam(ParamNode node); + void visitParams(ParamBlockNode node); + + void visitParam(Parameter node); + + void visitParamV1(ParamNodeV1 node); void visitWorkflow(WorkflowNode node); diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java index 4b5293850e..f1af301fb0 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/ScriptVisitorSupport.java @@ -17,6 +17,7 @@ import org.codehaus.groovy.ast.ClassCodeVisitorSupport; import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.expr.ElvisOperatorExpression; import org.codehaus.groovy.ast.expr.MethodCallExpression; @@ -31,8 +32,10 @@ public void visit(ScriptNode script) { visitFeatureFlag(featureFlag); for( var includeNode : script.getIncludes() ) visitInclude(includeNode); - for( var paramNode : script.getParams() ) - visitParam(paramNode); + if( script.getParams() != null ) + visitParams(script.getParams()); + for( var paramNode : script.getParamsV1() ) + visitParamV1(paramNode); for( var workflowNode : script.getWorkflows() ) visitWorkflow(workflowNode); for( var processNode : script.getProcesses() ) @@ -57,7 +60,17 @@ public void visitInclude(IncludeNode node) { } @Override - public void visitParam(ParamNode node) { + public void visitParams(ParamBlockNode node) { + for( var param : node.declarations ) + visitParam(param); + } + + @Override + public void visitParam(Parameter node) { + } + + @Override + public void visitParamV1(ParamNodeV1 node) { visit(node.value); } diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java index 090303f97b..6736cf2ace 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptResolveVisitor.java @@ -20,13 +20,14 @@ import nextflow.script.ast.FunctionNode; import nextflow.script.ast.OutputNode; -import nextflow.script.ast.ParamNode; +import nextflow.script.ast.ParamNodeV1; import nextflow.script.ast.ProcessNode; import nextflow.script.ast.ScriptNode; import nextflow.script.ast.ScriptVisitorSupport; import nextflow.script.ast.WorkflowNode; import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.DynamicVariable; +import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.expr.VariableExpression; import org.codehaus.groovy.control.CompilationUnit; import org.codehaus.groovy.control.SourceUnit; @@ -62,8 +63,10 @@ public void visit() { variableScopeVisitor.visit(); // resolve type names - for( var paramNode : sn.getParams() ) - visitParam(paramNode); + if( sn.getParams() != null ) + visitParams(sn.getParams()); + for( var paramNode : sn.getParamsV1() ) + visitParamV1(paramNode); for( var workflowNode : sn.getWorkflows() ) visitWorkflow(workflowNode); for( var processNode : sn.getProcesses() ) @@ -79,7 +82,13 @@ public void visit() { } @Override - public void visitParam(ParamNode node) { + public void visitParam(Parameter node) { + node.setInitialExpression(resolver.transform(node.getInitialExpression())); + resolver.resolveOrFail(node.getType(), node); + } + + @Override + public void visitParamV1(ParamNodeV1 node) { node.value = resolver.transform(node.value); } diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java index 39e8d06138..5ee56e92e9 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/ScriptToGroovyVisitor.java @@ -15,6 +15,7 @@ */ package nextflow.script.control; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -25,7 +26,8 @@ import nextflow.script.ast.FunctionNode; import nextflow.script.ast.IncludeNode; import nextflow.script.ast.OutputBlockNode; -import nextflow.script.ast.ParamNode; +import nextflow.script.ast.ParamBlockNode; +import nextflow.script.ast.ParamNodeV1; import nextflow.script.ast.ProcessNode; import nextflow.script.ast.ScriptNode; import nextflow.script.ast.ScriptVisitorSupport; @@ -119,7 +121,24 @@ public void visitInclude(IncludeNode node) { } @Override - public void visitParam(ParamNode node) { + public void visitParams(ParamBlockNode node) { + var statements = Arrays.stream(node.declarations) + .map((param) -> { + var name = constX(param.getName()); + var type = classX(param.getType()); + var arguments = param.hasInitialExpression() + ? args(name, type, param.getInitialExpression()) + : args(name, type); + return stmt(callThisX("declare", arguments)); + }) + .toList(); + var closure = closureX(block(new VariableScope(), statements)); + var result = stmt(callThisX("params", args(closure))); + moduleNode.addStatement(result); + } + + @Override + public void visitParamV1(ParamNodeV1 node) { var result = stmt(assignX(node.target, node.value)); moduleNode.addStatement(result); } diff --git a/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java index 679166e458..68f2dee2bc 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/control/VariableScopeVisitor.java @@ -25,6 +25,7 @@ import nextflow.script.ast.ImplicitClosureParameter; import nextflow.script.ast.IncludeNode; import nextflow.script.ast.OutputNode; +import nextflow.script.ast.ParamBlockNode; import nextflow.script.ast.ProcessNode; import nextflow.script.ast.ScriptNode; import nextflow.script.ast.ScriptVisitorSupport; @@ -168,6 +169,22 @@ public void visitFeatureFlag(FeatureFlagNode node) { } } + @Override + public void visitParams(ParamBlockNode node) { + var declaredParams = new HashMap(); + for( var param : node.declarations ) { + var name = param.getName(); + var other = declaredParams.get(name); + if( other != null ) + vsc.addError("Parameter " + name + "` is already declared", param, "First declared here", other); + else + declaredParams.put(name, param); + + if( param.hasInitialExpression() ) + visit(param.getInitialExpression()); + } + } + private boolean inWorkflowEmit; @Override @@ -348,7 +365,7 @@ public void visitFunction(FunctionNode node) { for( var parameter : node.getParameters() ) { if( parameter.hasInitialExpression() ) visit(parameter.getInitialExpression()); - vsc.declare(parameter, node); + vsc.declare(parameter, parameter); } visit(node.getCode()); diff --git a/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java index c907c2bb72..4df64b9aa8 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java +++ b/modules/nf-lang/src/main/java/nextflow/script/formatter/Formatter.java @@ -24,6 +24,7 @@ import org.codehaus.groovy.ast.ClassNode; import org.codehaus.groovy.ast.CodeVisitorSupport; import org.codehaus.groovy.ast.Parameter; +import org.codehaus.groovy.ast.Variable; import org.codehaus.groovy.ast.expr.BinaryExpression; import org.codehaus.groovy.ast.expr.BitwiseNegationExpression; import org.codehaus.groovy.ast.expr.CastExpression; @@ -713,6 +714,10 @@ private static boolean hasTrailingComma(Expression node) { return node.getNodeMetaData(ASTNodeMarker.TRAILING_COMMA) != null; } + public static boolean hasType(Variable variable) { + return !variable.isDynamicTyped() || isLegacyType(variable.getType()); + } + public static boolean isLegacyType(ClassNode cn) { return cn.getNodeMetaData(ASTNodeMarker.LEGACY_TYPE) != null; } diff --git a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java index 72d988b5fe..06ee88118c 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java +++ b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java @@ -15,6 +15,7 @@ */ package nextflow.script.formatter; +import java.util.Arrays; import java.util.Comparator; import java.util.List; @@ -25,12 +26,14 @@ import nextflow.script.ast.IncludeNode; import nextflow.script.ast.OutputBlockNode; import nextflow.script.ast.OutputNode; -import nextflow.script.ast.ParamNode; +import nextflow.script.ast.ParamNodeV1; +import nextflow.script.ast.ParamBlockNode; import nextflow.script.ast.ProcessNode; import nextflow.script.ast.ScriptNode; import nextflow.script.ast.ScriptVisitorSupport; import nextflow.script.ast.WorkflowNode; import org.codehaus.groovy.ast.ClassNode; +import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.expr.EmptyExpression; import org.codehaus.groovy.ast.expr.PropertyExpression; import org.codehaus.groovy.ast.expr.VariableExpression; @@ -101,8 +104,8 @@ public void visit() { .map(this::getIncludeWidth) .max(Integer::compare).orElse(0); - maxParamWidth = scriptNode.getParams().stream() - .map(this::getParamWidth) + maxParamWidth = scriptNode.getParamsV1().stream() + .map(ScriptFormattingVisitor::parameterWidth) .max(Integer::compare).orElse(0); } @@ -117,8 +120,10 @@ else if( decl instanceof IncludeNode in ) visitInclude(in); else if( decl instanceof OutputBlockNode obn ) visitOutputs(obn); - else if( decl instanceof ParamNode pn ) - visitParam(pn); + else if( decl instanceof ParamBlockNode pbn ) + visitParams(pbn); + else if( decl instanceof ParamNodeV1 pn ) + visitParamV1(pn); else if( decl instanceof ProcessNode pn ) visitProcess(pn); else if( decl instanceof WorkflowNode wn ) @@ -188,12 +193,49 @@ protected int getIncludeWidth(IncludeEntryNode entry) { } @Override - public void visitParam(ParamNode node) { + public void visitParams(ParamBlockNode node) { + var alignmentWidth = options.harshilAlignment() + ? maxParameterWidth(node.declarations) + : 0; + + fmt.appendLeadingComments(node); + fmt.append("params {\n"); + fmt.incIndent(); + for( var param : node.declarations ) { + fmt.appendLeadingComments(param); + fmt.appendIndent(); + fmt.append(param.getName()); + if( fmt.hasType(param) ) { + if( alignmentWidth > 0 ) { + var padding = alignmentWidth - param.getName().length() + 1; + fmt.append(" ".repeat(padding)); + } + fmt.append(": "); + fmt.visitTypeAnnotation(param.getType()); + } + if( param.hasInitialExpression() ) { + fmt.append(" = "); + fmt.visit(param.getInitialExpression()); + } + fmt.appendNewLine(); + } + fmt.decIndent(); + fmt.append("}\n"); + } + + private static int maxParameterWidth(Parameter[] parameters) { + return Arrays.stream(parameters) + .map(param -> param.getName().length()) + .max(Integer::compare).orElse(0); + } + + @Override + public void visitParamV1(ParamNodeV1 node) { fmt.appendLeadingComments(node); fmt.appendIndent(); fmt.visit(node.target); if( maxParamWidth > 0 ) { - var padding = maxParamWidth - getParamWidth(node); + var padding = maxParamWidth - parameterWidth(node); fmt.append(" ".repeat(padding)); } fmt.append(" = "); @@ -201,7 +243,7 @@ public void visitParam(ParamNode node) { fmt.appendNewLine(); } - protected int getParamWidth(ParamNode node) { + private static int parameterWidth(ParamNodeV1 node) { var target = (PropertyExpression) node.target; var name = target.getPropertyAsString(); return name != null ? name.length() : 0; @@ -262,7 +304,7 @@ public void visitWorkflow(WorkflowNode node) { protected void visitWorkflowTakes(List takes) { var alignmentWidth = options.harshilAlignment() - ? getMaxParameterWidth(takes) + ? maxParameterWidth(takes) : 0; for( var stmt : takes ) { @@ -282,7 +324,7 @@ protected void visitWorkflowTakes(List takes) { protected void visitWorkflowEmits(List emits) { var alignmentWidth = options.harshilAlignment() - ? getMaxParameterWidth(emits) + ? maxParameterWidth(emits) : 0; for( var stmt : emits ) { @@ -319,27 +361,24 @@ else if( emit instanceof VariableExpression ve ) { } } - protected int getMaxParameterWidth(List statements) { + private static int maxParameterWidth(List statements) { if( statements.size() == 1 ) return 0; - int maxWidth = 0; - for( var stmt : statements ) { - var stmtX = (ExpressionStatement)stmt; - var emit = stmtX.getExpression(); - int width = 0; - if( emit instanceof VariableExpression ve ) { - width = ve.getName().length(); - } - else if( emit instanceof AssignmentExpression assign ) { - var target = (VariableExpression)assign.getLeftExpression(); - width = target.getName().length(); - } - - if( maxWidth < width ) - maxWidth = width; - } - return maxWidth; + return statements.stream() + .map((stmt) -> { + var stmtX = (ExpressionStatement)stmt; + var emit = stmtX.getExpression(); + if( emit instanceof VariableExpression ve ) { + return ve.getName().length(); + } + if( emit instanceof AssignmentExpression assign ) { + var target = (VariableExpression)assign.getLeftExpression(); + return target.getName().length(); + } + return 0; + }) + .max(Integer::compare).orElse(0); } @Override diff --git a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java index 9a5f766108..7475a23181 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java +++ b/modules/nf-lang/src/main/java/nextflow/script/parser/ScriptAstBuilder.java @@ -36,7 +36,8 @@ import nextflow.script.ast.InvalidDeclaration; import nextflow.script.ast.OutputBlockNode; import nextflow.script.ast.OutputNode; -import nextflow.script.ast.ParamNode; +import nextflow.script.ast.ParamNodeV1; +import nextflow.script.ast.ParamBlockNode; import nextflow.script.ast.ProcessNode; import nextflow.script.ast.ScriptNode; import nextflow.script.ast.WorkflowNode; @@ -286,10 +287,22 @@ else if( ctx instanceof OutputDefAltContext odac ) { moduleNode.setOutputs(node); } - else if( ctx instanceof ParamDeclAltContext pac ) { - var node = paramDeclaration(pac.paramDeclaration()); + else if( ctx instanceof ParamsDefAltContext pac ) { + var node = paramsDef(pac.paramsDef()); saveLeadingComments(node, ctx); - moduleNode.addParam(node); + if( moduleNode.getParams() != null ) + collectSyntaxError(new SyntaxException("Params block defined more than once", node)); + if( !moduleNode.getParamsV1().isEmpty() ) + collectSyntaxError(new SyntaxException("Params block cannot be mixed with legacy parameter declarations", node)); + moduleNode.setParams(node); + } + + else if( ctx instanceof ParamDeclV1AltContext pac ) { + var node = paramDeclarationV1(pac.paramDeclarationV1()); + saveLeadingComments(node, ctx); + if( moduleNode.getParams() != null ) + collectSyntaxError(new SyntaxException("Legacy parameter declarations cannot be mixed with the params block", node)); + moduleNode.addParamV1(node); } else if( ctx instanceof ProcessDefAltContext pdac ) { @@ -329,14 +342,43 @@ private FeatureFlagNode featureFlagDeclaration(FeatureFlagDeclarationContext ctx return result; } - private ParamNode paramDeclaration(ParamDeclarationContext ctx) { + private ParamBlockNode paramsDef(ParamsDefContext ctx) { + var declarations = paramsBody(ctx.paramsBody()); + return ast( new ParamBlockNode(declarations), ctx ); + } + + private Parameter[] paramsBody(ParamsBodyContext ctx) { + if( ctx == null ) + return Parameter.EMPTY_ARRAY; + return ctx.paramDeclaration().stream() + .map(this::paramDeclaration) + .filter(param -> param != null) + .toArray(Parameter[]::new); + } + + private Parameter paramDeclaration(ParamDeclarationContext ctx) { + if( ctx.statement() != null ) { + collectSyntaxError(new SyntaxException("Invalid parameter declaration", ast( new EmptyStatement(), ctx.statement() ))); + return null; + } + var type = type(ctx.type()); + var name = identifier(ctx.identifier()); + var defaultValue = ctx.expression() != null ? expression(ctx.expression()) : null; + var result = ast( param(type, name, defaultValue), ctx ); + checkInvalidVarName(name, result); + groovydocManager.handle(result, ctx); + saveLeadingComments(result, ctx); + return result; + } + + private ParamNodeV1 paramDeclarationV1(ParamDeclarationV1Context ctx) { Expression target = ast( varX("params"), ctx.PARAMS() ); for( var ident : ctx.identifier() ) { var name = ast( constX(identifier(ident)), ident ); target = ast( propX(target, name), target, name ); } var value = expression(ctx.expression()); - return ast( new ParamNode(target, value), ctx ); + return ast( new ParamNodeV1(target, value), ctx ); } private IncludeNode includeDeclaration(IncludeDeclarationContext ctx) { @@ -491,7 +533,7 @@ private String processType(ProcessExecContext ctx) { return "exec"; } if( ctx.SHELL() != null ) { - collectWarning("The `shell` block is deprecated, use `script` instead", ctx.SHELL().getText(), ast( new EmptyExpression(), ctx.SHELL() )); + collectWarning("The `shell` block is deprecated, use `script` instead", ctx.SHELL().getText(), ast( new EmptyStatement(), ctx.SHELL() )); return "shell"; } return "script"; diff --git a/tests-v1/checks/.PARSER-V1 b/tests-v1/checks/.PARSER-V1 index f48c838fa5..1e6b46ac74 100644 --- a/tests-v1/checks/.PARSER-V1 +++ b/tests-v1/checks/.PARSER-V1 @@ -1,4 +1,5 @@ # ADDITIONAL TESTS TO VERIFY THE V1 PARSER +chunk.nf complex-names.nf env-out.nf env2.nf diff --git a/tests/checks/.IGNORE-PARSER-V2 b/tests/checks/.IGNORE-PARSER-V2 index c97215480e..157a392cbe 100644 --- a/tests/checks/.IGNORE-PARSER-V2 +++ b/tests/checks/.IGNORE-PARSER-V2 @@ -1,2 +1,4 @@ # TESTS THAT SHOULD ONLY BE RUN BY THE V2 PARSER +chunk.nf +params-dsl.nf workflow-oncomplete-v2.nf \ No newline at end of file diff --git a/tests/checks/params-dsl.nf/.checks b/tests/checks/params-dsl.nf/.checks new file mode 100644 index 0000000000..76af690189 --- /dev/null +++ b/tests/checks/params-dsl.nf/.checks @@ -0,0 +1,37 @@ + +echo "Test successful run" +echo +$NXF_RUN --input ./data > stdout + +< stdout grep -F 'params.input = [./data]' +< stdout grep -F 'params.save_intermeds = false' + +echo +echo "Test missing required param" +echo +set +e +$NXF_RUN &> stdout ; ret=$? +set -e + +[[ $ret != 0 ]] || false + +< stdout grep -F 'Parameter `input` is required' + +echo +echo "Test overwrite script param from config profile" +echo +$NXF_RUN -c ../../params-dsl.config -profile test > stdout + +< stdout grep -F 'params.input = [alpha, beta, delta]' +< stdout grep -F 'params.save_intermeds = true' + +echo +echo "Test invalid param" +echo +set +e +$NXF_RUN --inputs ./data &> stdout ; ret=$? +set -e + +[[ $ret != 0 ]] || false + +< stdout grep -F 'Parameter `inputs` was specified' diff --git a/tests/chunk.nf b/tests/chunk.nf index eabc02d6f7..4cc0ed9237 100644 --- a/tests/chunk.nf +++ b/tests/chunk.nf @@ -1,7 +1,9 @@ #!/usr/bin/env nextflow -params.input = null -params.chunkSize = 1 +params { + input: Path + chunkSize: Integer = 1 +} process foo { debug true @@ -14,7 +16,7 @@ process foo { } workflow { - channel.fromPath(params.input) + channel.of(params.input) | splitFasta(by: params.chunkSize) | foo } diff --git a/tests/params-dsl.config b/tests/params-dsl.config new file mode 100644 index 0000000000..18ba6c6c01 --- /dev/null +++ b/tests/params-dsl.config @@ -0,0 +1,9 @@ + +params.outdir = 'results' + +profiles { + test { + params.input = 'alpha,beta,delta' + params.save_intermeds = true + } +} diff --git a/tests/params-dsl.nf b/tests/params-dsl.nf new file mode 100644 index 0000000000..f14088ebdb --- /dev/null +++ b/tests/params-dsl.nf @@ -0,0 +1,30 @@ +#!/usr/bin/env nextflow +/* + * Copyright 2013-2024, 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. + */ + +params { + // List of IDs. + input: String + + // Whether to save intermediate outputs. + save_intermeds: Boolean = false +} + +workflow { + main: + println "params.input = ${params.input.tokenize(',')}" + println "params.save_intermeds = ${params.save_intermeds}" +}