diff --git a/docs/cli.md b/docs/cli.md index e05b5b84d9..867b12cf66 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -297,8 +297,8 @@ Parameters are applied in the following order (from lowest to highest priority): 1. Parameters defined in pipeline scripts (e.g. `main.nf`) 2. Parameters defined in {ref}`config files ` -6. Parameters specified in a params file (`-params-file`) -7. Parameters specified on the command line (`--something value`) +3. Parameters specified in a params file (`-params-file`) +4. Parameters specified on the command line (`--something value`) ## Managing projects diff --git a/docs/reference/syntax.md b/docs/reference/syntax.md index 155179b70b..9a691536c3 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 + save_intermeds = 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 80df4e89e1..48ba8b630f 100644 --- a/docs/vscode.md +++ b/docs/vscode.md @@ -26,6 +26,19 @@ 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, function, type definition, 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 +/** + * Say hello to someone. + * + * @param name + */ +def sayHello(name) { + println "Hello, ${name}!" +} +``` + ### Code navigation The **Outline** section in the Explorer panel lists top-level definitions when you view a script. Include declarations in scripts and config files act as links, and ctrl-clicking them opens the corresponding script or config file. diff --git a/docs/workflow.md b/docs/workflow.md index 2c26f8e0c0..f11ada6d09 100644 --- a/docs/workflow.md +++ b/docs/workflow.md @@ -22,18 +22,67 @@ workflow { } ``` -### Parameters +## Parameters -Parameters can be defined in the script with a default value that can be overridden from the CLI, params file, or config file. Parameters should only be used by the entry workflow: +Parameters can be declared in a Nextflow script with the `params` block or with *legacy* parameter declarations. + +### Params block + +:::{versionadded} 25.05.0-edge +::: + +:::{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 + + /** + * Whether to save intermediate files. + */ + save_intermeds = false +} +``` + +Parameters can be used in the entry workflow: + +```nextflow +workflow { + if( params.input ) + analyze(params.input, params.save_intermeds) + else + analyze(fake_input(), params.save_intermeds) +} +``` + +:::{note} +While params can be used outside the entry workflow, Nextflow will not be able to validate them at compile-time. Only params used in the entry workflow are validated against the params definition. Params can be passed to workflows and processes as explicit inputs to enable compile-time validation. +::: + +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`. + +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. + +### Legacy parameters + +Parameters can be declared by assigning a `params` property to a default value: ```nextflow -params.data = '/some/data/file' +params.input = '/some/data/file' +params.save_intermeds = false workflow { - if( params.data ) - bar(params.data) + if( params.input ) + analyze(params.input, params.save_intermeds) else - bar(foo()) + analyze(fake_input(), params.save_intermeds) } ``` diff --git a/modules/nextflow/src/main/groovy/nextflow/Session.groovy b/modules/nextflow/src/main/groovy/nextflow/Session.groovy index d45f26c528..3b76d42883 100644 --- a/modules/nextflow/src/main/groovy/nextflow/Session.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/Session.groovy @@ -113,6 +113,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 */ @@ -418,7 +428,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)}]" @@ -445,6 +455,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..0c64cba5aa 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() { diff --git a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy index 0341f73523..906f65a9f2 100644 --- a/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/config/ConfigBuilder.groovy @@ -61,6 +61,8 @@ class ConfigBuilder { Path currentDir + Map cliParams + boolean showAllProfiles String profile = DEFAULT_PROFILE @@ -81,9 +83,11 @@ class ConfigBuilder { Map env = new HashMap<>(System.getenv()) - List warnings = new ArrayList<>(10); + List warnings = new ArrayList<>(10) + + Map declaredParams = [:] - { + ConfigBuilder() { setHomeDir(Const.APP_HOME_DIR) setCurrentDir(Paths.get('.')) } @@ -114,6 +118,11 @@ class ConfigBuilder { return this } + ConfigBuilder setCliParams( Map cliParams ) { + this.cliParams = cliParams + return this + } + ConfigBuilder setBaseDir( Path path ) { this.baseDir = path.complete() return this @@ -162,6 +171,10 @@ class ConfigBuilder { return this } + Map getConfigParams() { + return declaredParams + } + static private wrapValue( value ) { if( !value ) return '' @@ -327,11 +340,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) @@ -351,8 +364,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) } @@ -383,7 +396,7 @@ class ConfigBuilder { } if( validateProfile ) { - checkValidProfile(parser.getProfiles()) + checkValidProfile(parser.getDeclaredProfiles()) } } @@ -414,9 +427,10 @@ class ConfigBuilder { parser.setProfiles(profile.tokenize(',')) } - final config = parse0(parser, entry) + def config = parse0(parser, entry) if( NF.getSyntaxParserVersion() == 'v1' ) validate(config, entry) + declaredParams.putAll(parser.getDeclaredParams()) result.merge(config) } @@ -734,8 +748,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..4db207c600 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) @@ -162,6 +177,9 @@ abstract class BaseScript extends Script implements ExecutionContext { } // if an `entryName` was specified via the command line, override the `entryFlow` to be executed + if( binding.entryName ) + log.warn "The `-entry` command line option is deprecated -- use a pipeline parameter instead" + if( binding.entryName && !(entryFlow=meta.getWorkflow(binding.entryName) ) ) { def msg = "Unknown workflow entry name: ${binding.entryName}" final allNames = meta.getWorkflowNames() 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..e9b98984cd --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/script/ParamsDsl.groovy @@ -0,0 +1,74 @@ +/* + * 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 groovy.transform.CompileStatic +import groovy.util.logging.Slf4j +import nextflow.Session +import nextflow.exception.ScriptRuntimeException +/** + * Implements the DSL for defining workflow params + * + * @author Ben Sherman + */ +@Slf4j +@CompileStatic +class ParamsDsl { + + private Map> declarations = [:] + + void declare(String name) { + declare0(name, Optional.empty()) + } + + void declare(String name, Object defaultValue) { + declare0(name, Optional.of(defaultValue)) + } + + private void declare0(String name, Optional defaultValue) { + if( declarations.containsKey(name) ) + throw new ScriptRuntimeException("Parameter '${name}' is declared more than once in the workflow params definition") + + declarations[name] = 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 defaultValue = declarations[name] + if( cliParams.containsKey(name) ) + params[name] = cliParams[name] + else if( configParams.containsKey(name) ) + params[name] = configParams[name] + else if( defaultValue.isPresent() ) + params[name] = defaultValue.get() + else + throw new ScriptRuntimeException("Parameter `$name` is required but was not specified on the command line, params file, or config") + } + + session.binding.setParams(params) + } + +} diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptBinding.groovy index 9ef4999905..6f7fe03080 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") @@ -136,6 +136,7 @@ class ScriptBinding extends WorkflowBinding { ScriptBinding setParams(Map values ) { if( values ) 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 ead6b64d59..98dbb8fdd9 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 @@ -44,6 +44,24 @@ class ConfigBuilderTest extends Specification { TraceHelper.testTimestampFmt = '20221001' } + 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: @@ -140,7 +158,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" @@ -158,8 +176,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" } @@ -182,9 +200,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 @@ -215,9 +231,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 @@ -267,9 +281,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 @@ -313,9 +325,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'] @@ -330,7 +340,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' @@ -398,44 +407,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' @@ -450,10 +449,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" @@ -477,9 +476,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 @@ -492,7 +489,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' () { @@ -519,9 +516,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 @@ -1630,56 +1625,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'] } @@ -2256,9 +2251,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' @@ -2341,10 +2336,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" @@ -2374,7 +2366,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" @@ -2402,7 +2394,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 43fbf761cd..e2aad77e61 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 e4843d6205..943203c0ea 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..d1d1d019e3 --- /dev/null +++ b/modules/nextflow/src/test/groovy/nextflow/script/ParamsDslTest.groovy @@ -0,0 +1,62 @@ +package nextflow.script + +import nextflow.Session +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'] + def configParams = [outdir: 'results'] + def session = new Session() + session.init(null, null, cliParams, configParams) + + when: + def dsl = new ParamsDsl() + dsl.declare('input') + dsl.declare('save_intermeds', false) + dsl.apply(session) + then: + session.binding.getParams() == [input: './data', save_intermeds: false] + } + + 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') + dsl.declare('save_intermeds', 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') + dsl.declare('save_intermeds', 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' + } + +} 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 f35740d7b9..843125c443 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 (ASSIGN expression)? + | statement + ; + +// -- legacy parameter declaration +paramDeclarationV1 : PARAMS (DOT identifier)+ nls ASSIGN nls expression ; @@ -243,7 +260,12 @@ workflowBody ; workflowTakes - : identifier (sep identifier)* + : workflowTake (sep workflowTake)* + ; + +workflowTake + : identifier + | statement ; workflowMain 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 e5bd0767e5..444a05129a 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 @@ -31,7 +31,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<>(); @@ -53,7 +54,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 ) @@ -79,10 +82,14 @@ public List getIncludes() { return includes; } - public List getParams() { + public ParamBlockNode getParams() { return params; } + public List getParamsV1() { + return paramsV1; + } + public WorkflowNode getEntry() { return entry; } @@ -115,8 +122,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..3b61edadc3 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 @@ -26,7 +26,9 @@ public interface ScriptVisitor extends GroovyCodeVisitor { void visitInclude(IncludeNode node); - void visitParam(ParamNode node); + void visitParams(ParamBlockNode 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 bbb47dd6b8..95c5e378c8 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 @@ -30,8 +30,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() ) @@ -56,13 +58,16 @@ public void visitInclude(IncludeNode node) { } @Override - public void visitParam(ParamNode node) { + public void visitParams(ParamBlockNode node) { + } + + @Override + public void visitParamV1(ParamNodeV1 node) { visit(node.value); } @Override public void visitWorkflow(WorkflowNode node) { - visit(node.takes); visit(node.main); visit(node.emits); visit(node.publishers); diff --git a/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java b/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java index 9e3453488d..225d09ddd5 100644 --- a/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java +++ b/modules/nf-lang/src/main/java/nextflow/script/ast/WorkflowNode.java @@ -39,14 +39,12 @@ * @author Ben Sherman */ public class WorkflowNode extends MethodNode { - public final Statement takes; public final Statement main; public final Statement emits; public final Statement publishers; - public WorkflowNode(String name, Statement takes, Statement main, Statement emits, Statement publishers) { - super(name, 0, dummyReturnType(emits), dummyParams(takes), ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE); - this.takes = takes; + public WorkflowNode(String name, Parameter[] takes, Statement main, Statement emits, Statement publishers) { + super(name, 0, dummyReturnType(emits), takes, ClassNode.EMPTY_ARRAY, EmptyStatement.INSTANCE); this.main = main; this.emits = emits; this.publishers = publishers; @@ -60,13 +58,6 @@ public boolean isCodeSnippet() { return getLineNumber() == -1; } - private static Parameter[] dummyParams(Statement takes) { - return asBlockStatements(takes) - .stream() - .map((stmt) -> new Parameter(ClassHelper.dynamicType(), "")) - .toArray(Parameter[]::new); - } - private static ClassNode dummyReturnType(Statement emits) { var cn = new ClassNode(NamedTuple.class); asBlockStatements(emits).stream() 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 67f6813db7..137a74f2cb 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,7 +20,8 @@ import nextflow.script.ast.FunctionNode; import nextflow.script.ast.OutputNode; -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; @@ -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 visitParams(ParamBlockNode node) { + for( var param : node.declarations ) + param.setInitialExpression(resolver.transform(param.getInitialExpression())); + } + + @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 6a1b97cb6d..a45cabc234 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.List; import java.util.Set; import java.util.stream.Collectors; @@ -24,13 +25,15 @@ 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; import nextflow.script.ast.WorkflowNode; import org.codehaus.groovy.ast.ASTNode; import org.codehaus.groovy.ast.CodeVisitorSupport; +import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.VariableScope; import org.codehaus.groovy.ast.expr.ArgumentListExpression; import org.codehaus.groovy.ast.expr.BinaryExpression; @@ -117,14 +120,29 @@ 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 arguments = param.hasInitialExpression() + ? args(name, param.getInitialExpression()) + : args(name); + 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); } @Override public void visitWorkflow(WorkflowNode node) { - visitWorkflowTakes(node.takes); visit(node.main); visitWorkflowEmits(node.emits, node.main); visitWorkflowPublishers(node.publishers, node.main); @@ -138,7 +156,7 @@ public void visitWorkflow(WorkflowNode node) { ) )); var closure = closureX(block(new VariableScope(), List.of( - node.takes, + workflowTakes(node.getParameters()), node.emits, bodyDef ))); @@ -149,12 +167,13 @@ public void visitWorkflow(WorkflowNode node) { moduleNode.addStatement(result); } - private void visitWorkflowTakes(Statement takes) { - for( var stmt : asBlockStatements(takes) ) { - var es = (ExpressionStatement)stmt; - var take = (VariableExpression)es.getExpression(); - es.setExpression(callThisX("_take_", args(constX(take.getName())))); - } + private Statement workflowTakes(Parameter[] takes) { + var statements = Arrays.stream(takes) + .map((take) -> + stmt(callThisX("_take_", args(constX(take.getName())))) + ) + .toList(); + return block(null, statements); } private void visitWorkflowEmits(Statement emits, Statement main) { 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 a4bf593a31..aa07aeb8b6 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 @@ -24,6 +24,7 @@ import nextflow.script.ast.FunctionNode; 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; @@ -167,6 +168,14 @@ public void visitFeatureFlag(FeatureFlagNode node) { } } + @Override + public void visitParams(ParamBlockNode node) { + for( var parameter : node.declarations ) { + if( parameter.hasInitialExpression() ) + visit(parameter.getInitialExpression()); + } + } + private boolean inWorkflowEmit; @Override @@ -175,7 +184,8 @@ public void visitWorkflow(WorkflowNode node) { currentDefinition = node; node.setVariableScope(currentScope()); - declareWorkflowInputs(node.takes); + for( var take : node.getParameters() ) + vsc.declare(take, take); visit(node.main); if( node.main instanceof BlockStatement block ) @@ -188,15 +198,6 @@ public void visitWorkflow(WorkflowNode node) { vsc.popScope(); } - private void declareWorkflowInputs(Statement takes) { - for( var stmt : asBlockStatements(takes) ) { - var ve = asVarX(stmt); - if( ve == null ) - continue; - vsc.declare(ve); - } - } - private void copyVariableScope(VariableScope source) { for( var it = source.getDeclaredVariablesIterator(); it.hasNext(); ) { var variable = it.next(); @@ -344,7 +345,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/ScriptFormattingVisitor.java b/modules/nf-lang/src/main/java/nextflow/script/formatter/ScriptFormattingVisitor.java index 5dc31c9216..7c45ab4e78 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,7 +104,7 @@ public void visit() { .map(this::getIncludeWidth) .max(Integer::compare).orElse(0); - maxParamWidth = scriptNode.getParams().stream() + maxParamWidth = scriptNode.getParamsV1().stream() .map(this::getParamWidth) .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,7 +193,40 @@ protected int getIncludeWidth(IncludeModuleNode module) { } @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( param.hasInitialExpression() ) { + if( alignmentWidth > 0 ) { + var padding = alignmentWidth - param.getName().length(); + fmt.append(" ".repeat(padding)); + } + fmt.append(" = "); + fmt.visit(param.getInitialExpression()); + } + fmt.appendNewLine(); + } + fmt.decIndent(); + fmt.append("}\n"); + } + + private 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); @@ -201,7 +239,7 @@ public void visitParam(ParamNode node) { fmt.appendNewLine(); } - protected int getParamWidth(ParamNode node) { + private int getParamWidth(ParamNodeV1 node) { var target = (PropertyExpression) node.target; var name = target.getPropertyAsString(); return name != null ? name.length() : 0; @@ -217,14 +255,15 @@ public void visitWorkflow(WorkflowNode node) { } fmt.append(" {\n"); fmt.incIndent(); - if( node.takes instanceof BlockStatement ) { + var takes = node.getParameters(); + if( takes.length > 0 ) { fmt.appendIndent(); fmt.append("take:\n"); - visitWorkflowTakes(asBlockStatements(node.takes)); + visitWorkflowTakes(takes); fmt.appendNewLine(); } if( node.main instanceof BlockStatement ) { - if( node.takes instanceof BlockStatement || node.emits instanceof BlockStatement || node.publishers instanceof BlockStatement ) { + if( takes.length > 0 || node.emits instanceof BlockStatement || node.publishers instanceof BlockStatement ) { fmt.appendIndent(); fmt.append("main:\n"); } @@ -246,29 +285,28 @@ public void visitWorkflow(WorkflowNode node) { fmt.append("}\n"); } - protected void visitWorkflowTakes(List takes) { + private void visitWorkflowTakes(Parameter[] takes) { var alignmentWidth = options.harshilAlignment() - ? getMaxParameterWidth(takes) + ? maxParameterWidth(takes) : 0; - for( var stmt : takes ) { - var ve = asVarX(stmt); + for( var take : takes ) { fmt.appendIndent(); - fmt.visit(ve); - if( fmt.hasTrailingComment(stmt) ) { + fmt.append(take.getName()); + if( fmt.hasTrailingComment(take) ) { if( alignmentWidth > 0 ) { - var padding = alignmentWidth - ve.getName().length(); + var padding = alignmentWidth - take.getName().length(); fmt.append(" ".repeat(padding)); } - fmt.appendTrailingComment(stmt); + fmt.appendTrailingComment(take); } fmt.appendNewLine(); } } - protected void visitWorkflowEmits(List emits) { + private void visitWorkflowEmits(List emits) { var alignmentWidth = options.harshilAlignment() - ? getMaxParameterWidth(emits) + ? maxParameterWidth(emits) : 0; for( var stmt : emits ) { @@ -305,27 +343,24 @@ else if( emit instanceof VariableExpression ve ) { } } - protected int getMaxParameterWidth(List statements) { + private 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 7080277a46..f6b25bf584 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 = ClassHelper.dynamicType(); + 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"; @@ -507,7 +549,7 @@ private WorkflowNode workflowDef(WorkflowDefContext ctx) { var name = ctx.name != null ? ctx.name.getText() : null; if( ctx.body == null ) { - var result = ast( new WorkflowNode(name, null, null, null, null), ctx ); + var result = ast( new WorkflowNode(name, Parameter.EMPTY_ARRAY, null, null, null), ctx ); groovydocManager.handle(result, ctx); return result; } @@ -522,10 +564,10 @@ private WorkflowNode workflowDef(WorkflowDefContext ctx) { ); if( name == null ) { - if( takes instanceof BlockStatement ) - collectSyntaxError(new SyntaxException("Entry workflow cannot have a take section", takes)); - if( emits instanceof BlockStatement ) - collectSyntaxError(new SyntaxException("Entry workflow cannot have an emit section", emits)); + if( ctx.body.TAKE() != null ) + collectSyntaxError(new SyntaxException("Entry workflow cannot have a take section", ast( new EmptyStatement(), ctx.body.TAKE() ))); + if( ctx.body.EMIT() != null ) + collectSyntaxError(new SyntaxException("Entry workflow cannot have an emit section", ast( new EmptyStatement(), ctx.body.EMIT() ))); } if( name != null ) { if( publishers instanceof BlockStatement ) @@ -538,24 +580,31 @@ private WorkflowNode workflowDef(WorkflowDefContext ctx) { } private WorkflowNode workflowDef(BlockStatement main) { - var takes = EmptyStatement.INSTANCE; + var takes = Parameter.EMPTY_ARRAY; var emits = EmptyStatement.INSTANCE; var publishers = EmptyStatement.INSTANCE; return new WorkflowNode(null, takes, main, emits, publishers); } - private Statement workflowTakes(WorkflowTakesContext ctx) { + private Parameter[] workflowTakes(WorkflowTakesContext ctx) { if( ctx == null ) - return EmptyStatement.INSTANCE; + return Parameter.EMPTY_ARRAY; - var statements = ctx.identifier().stream() + return ctx.workflowTake().stream() .map(this::workflowTake) - .toList(); - return ast( block(null, statements), ctx ); + .filter(take -> take != null) + .toArray(Parameter[]::new); } - private Statement workflowTake(IdentifierContext ctx) { - var result = ast( stmt(variableName(ctx)), ctx ); + private Parameter workflowTake(WorkflowTakeContext ctx) { + if( ctx.statement() != null ) { + collectSyntaxError(new SyntaxException("Invalid workflow take", ast( new EmptyStatement(), ctx.statement() ))); + return null; + } + var type = ClassHelper.dynamicType(); + var name = identifier(ctx.identifier()); + var result = ast( param(type, name), ctx ); + checkInvalidVarName(name, result); saveTrailingComment(result, ctx); return result; } diff --git a/tests/checks/.IGNORE-PARSER-V2 b/tests/checks/.IGNORE-PARSER-V2 index 24e47da582..4b18b57ae4 100644 --- a/tests/checks/.IGNORE-PARSER-V2 +++ b/tests/checks/.IGNORE-PARSER-V2 @@ -1 +1,2 @@ # TESTS THAT SHOULD ONLY BE RUN BY THE V2 PARSER +params-dsl.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/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..7438097ce6 --- /dev/null +++ b/tests/params-dsl.nf @@ -0,0 +1,34 @@ +#!/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 + + /** + * Whether to save intermediate outputs. + */ + save_intermeds = false +} + +workflow { + main: + println "params.input = ${params.input.tokenize(',')}" + println "params.save_intermeds = ${params.save_intermeds}" +}