diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy index 0eee1f3583..add812a5f2 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy @@ -516,8 +516,10 @@ class ProcessConfig implements Map, Cloneable { /// input parameters - InParam _in_val( obj ) { - new ValueInParam(this).bind(obj) + InParam _in_val( Map opts=null, Object obj ) { + new ValueInParam(this) + .setOptions(opts) + .bind(obj) } InParam _in_file( obj ) { diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ProcessDef.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ProcessDef.groovy index 6774db05e2..4eb2e7ffc7 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ProcessDef.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ProcessDef.groovy @@ -158,26 +158,46 @@ class ProcessDef extends BindableDef implements IterableDef, ChainableDef { String getType() { 'process' } - private String missMatchErrMessage(String name, int expected, int actual) { - final ch = expected > 1 ? "channels" : "channel" - return "Process `$name` declares ${expected} input ${ch} but ${actual} were specified" - } - @Override Object run(Object[] args) { // initialise process config initialize() - // get params - final params = ChannelOut.spread(args) - // sanity check - if( params.size() != declaredInputs.size() ) - throw new ScriptRuntimeException(missMatchErrMessage(processName, declaredInputs.size(), params.size())) + // separate named args and positional args + def namedArgs = [:] + def indexArgs = ChannelOut.spread(args) + if( !indexArgs.isEmpty() && indexArgs[0] instanceof Map ) + namedArgs = indexArgs.remove(0) as Map + + log.debug("named args: ${namedArgs}, positional args: ${indexArgs}") + + // set named args + def names = namedArgs.keySet().collect() + def remainingInputs = [] + + for( def input : declaredInputs ) { + final inParam = (BaseInParam)input + final name = inParam.channelTakeName ?: inParam.name + + if( name && namedArgs.containsKey(name) ) { + inParam.setFrom(namedArgs[name]) + inParam.init() + names.remove(name) + } + else + remainingInputs << inParam + } + + if( !names.isEmpty() ) + throw new ScriptRuntimeException("Process `$name` was invoked with invalid named arguments: ${names.join(', ')}") + + // set positional args + if( indexArgs.size() != remainingInputs.size() ) + throw new ScriptRuntimeException("Process `$name` was invoked with ${indexArgs.size()} positional argument(s) but ${remainingInputs.size()} were expected") - // set input channels - for( int i=0; i() + + for( String name : declaredInputs ) { + if( name && namedArgs.containsKey(name) ) { + context.setProperty( name, namedArgs[name] ) + names.remove(name) + } + else + remainingInputs << name + } + + if( !names.isEmpty() ) { + final prefix = name ? "Workflow `$name`" : "Main workflow" + throw new IllegalArgumentException("$prefix was invoked with invalid named arguments: ${names.join(', ')}") + } + + // set positional args + if( indexArgs.size() != remainingInputs.size() ) { final prefix = name ? "Workflow `$name`" : "Main workflow" - throw new IllegalArgumentException("$prefix declares ${declaredInputs.size()} input channels but ${params.size()} were given") + throw new IllegalArgumentException("$prefix was invoked with ${indexArgs.size()} positional argument(s) but ${remainingInputs.size()} were expected") } - // attach declared inputs with the invocation arguments - for( int i=0; i< declaredInputs.size(); i++ ) { - final name = declaredInputs[i] - context.setProperty( name, params[i] ) + for( int i = 0; i < indexArgs.size(); i++ ) { + final name = remainingInputs[i] + context.setProperty( name, indexArgs[i] ) } } diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/BaseInParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/BaseInParam.groovy index d127a89c09..10816cc40c 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/params/BaseInParam.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/params/BaseInParam.groovy @@ -27,6 +27,7 @@ import nextflow.exception.ScriptRuntimeException import nextflow.extension.CH import nextflow.script.ProcessConfig import nextflow.script.TokenVar +import nextflow.util.ConfigHelper /** * Model a process generic input parameter * @@ -47,6 +48,8 @@ abstract class BaseInParam extends BaseParam implements InParam { */ private inChannel + String channelTakeName + /** * @return The input channel instance used by this parameter to receive the process inputs */ @@ -236,4 +239,19 @@ abstract class BaseInParam extends BaseParam implements InParam { return value } + BaseInParam setTake( value ) { + if( isNestedParam() ) + throw new IllegalArgumentException("Input `take` option is not allowed in tuple components") + if( !value ) + throw new IllegalArgumentException("Missing input `take` name") + if( !ConfigHelper.isValidIdentifier(value) ) { + final msg = "Input take '$value' is not a valid name -- Make sure it starts with an alphabetic or underscore character and it does not contain any blank, dot or other special characters" + if( NF.strictMode ) + throw new IllegalArgumentException(msg) + log.warn(msg) + } + this.channelTakeName = value + return this + } + } diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/InParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/InParam.groovy index c806e23d7b..f3e8896f0d 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/params/InParam.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/params/InParam.groovy @@ -42,4 +42,6 @@ interface InParam extends Cloneable { def decodeInputs( List values ) + String getChannelTakeName() + } diff --git a/modules/nextflow/src/test/groovy/nextflow/script/ScriptDslTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/ScriptDslTest.groovy index 7f8a69ee5a..c93bf5d8ff 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/ScriptDslTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/ScriptDslTest.groovy @@ -359,7 +359,7 @@ class ScriptDslTest extends Dsl2Spec { then: def err = thrown(ScriptRuntimeException) - err.message == 'Process `bar` declares 1 input channel but 0 were specified' + err.message == 'Process `bar` was invoked with 0 positional argument(s) but 1 were expected' } def 'should report error accessing undefined out/a' () { @@ -451,7 +451,7 @@ class ScriptDslTest extends Dsl2Spec { then: def err = thrown(ScriptRuntimeException) - err.message == "Process `bar` declares 1 input channel but 0 were specified" + err.message == "Process `bar` was invoked with 0 positional argument(s) but 1 were expected" } def 'should report error accessing undefined out/e' () { diff --git a/tests/process-named-inputs.nf b/tests/process-named-inputs.nf new file mode 100644 index 0000000000..28e89a0feb --- /dev/null +++ b/tests/process-named-inputs.nf @@ -0,0 +1,32 @@ +#!/usr/bin/env nextflow + +process foo { + input: + val bar + val baz + output: + stdout + + script: + """ + echo $bar + echo $baz + """ +} + + +workflow foo_wrapper { + take: + bar + baz + main: + foo(bar: bar, baz: baz) + emit: + foo.out +} + + +workflow { + foo_wrapper(bar: 'bar', baz: 'baz') + | view +} \ No newline at end of file