From c5531a8994f60b62a260a1fe5698a568a14eca89 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Wed, 8 Nov 2023 04:34:57 +0100 Subject: [PATCH 01/11] Add cmd output type Signed-off-by: Paolo Di Tommaso --- .../nextflow/ast/NextflowDSLImpl.groovy | 2 +- .../executor/BashWrapperBuilder.groovy | 25 +++++--- .../groovy/nextflow/processor/TaskBean.groovy | 3 + .../nextflow/processor/TaskProcessor.groovy | 8 ++- .../groovy/nextflow/processor/TaskRun.groovy | 10 ++++ .../nextflow/script/ProcessConfig.groovy | 32 +++++++++- .../nextflow/script/params/CmdOutParam.groovy | 54 +++++++++++++++++ .../script/params/CmdOutParamTest.groovy | 59 +++++++++++++++++++ 8 files changed, 181 insertions(+), 12 deletions(-) create mode 100644 modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy create mode 100644 modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy diff --git a/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy b/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy index a278fdd8f6..14371e5303 100644 --- a/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy @@ -951,7 +951,7 @@ class NextflowDSLImpl implements ASTTransformation { def nested = methodCall.objectExpression instanceof MethodCallExpression log.trace "convert > output method: $methodName" - if( methodName in ['val','env','file','set','stdout','path','tuple'] && !nested ) { + if( methodName in ['val','env','cmd','file','set','stdout','path','tuple'] && !nested ) { // prefix the method name with the string '_out_' methodCall.setMethod( new ConstantExpression('_out_' + methodName) ) fixMethodCall(methodCall) diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy index d53ad6aea1..c1d6cf1ecb 100644 --- a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy @@ -177,16 +177,24 @@ class BashWrapperBuilder { } } - protected String getOutputEnvCaptureSnippet(List names) { + protected String getOutputEnvCaptureSnippet(List outEnvs, List outCmds) { def result = new StringBuilder() result.append('\n') result.append('# capture process environment\n') result.append('set +u\n') result.append('cd "$NXF_TASK_WORKDIR"\n') - for( int i=0; iof()) ) { result.append "echo $key=\${$key[@]} " - result.append( i==0 ? '> ' : '>> ' ) + result.append( count++==0 ? '> ' : '>> ' ) + result.append(TaskRun.CMD_ENV) + result.append('\n') + } + // out cmd + for( String cmd : (outCmds ?: List.of()) ) { + result.append "echo $cmd " + result.append( count++==0 ? '> ' : '>> ' ) result.append(TaskRun.CMD_ENV) result.append('\n') } @@ -239,9 +247,12 @@ class BashWrapperBuilder { */ final interpreter = TaskProcessor.fetchInterpreter(script) - if( outputEnvNames ) { - if( !isBash(interpreter) ) throw new IllegalArgumentException("Process output of type env is only allowed with Bash process command -- Current interpreter: $interpreter") - script += getOutputEnvCaptureSnippet(outputEnvNames) + if( outputEnvNames || outputCommands ) { + if( !isBash(interpreter) && outputEnvNames ) + throw new IllegalArgumentException("Process output of type env is only allowed with Bash process command -- Current interpreter: $interpreter") + if( !isBash(interpreter) && outputCommands ) + throw new IllegalArgumentException("Process output of type env is only allowed with Bash process command -- Current interpreter: $interpreter") + script += getOutputEnvCaptureSnippet(outputEnvNames, outputCommands) } final binding = new HashMap(20) diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy index 441c19795e..c3003ceee6 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy @@ -73,6 +73,8 @@ class TaskBean implements Serializable, Cloneable { List outputEnvNames + List outputCommands + String beforeScript String afterScript @@ -144,6 +146,7 @@ class TaskBean implements Serializable, Cloneable { // stats this.outputEnvNames = task.getOutputEnvNames() + this.outputCommands = task.getOutputCommands() this.statsEnabled = task.getProcessor().getSession().statsEnabled this.inputFiles = task.getInputFilesMap() diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy index 69b95e689a..a2e2e53120 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy @@ -84,6 +84,8 @@ import nextflow.script.ScriptMeta import nextflow.script.ScriptType import nextflow.script.TaskClosure import nextflow.script.bundle.ResourcesBundle +import nextflow.script.params.BaseOutParam +import nextflow.script.params.CmdOutParam import nextflow.script.params.DefaultOutParam import nextflow.script.params.EachInParam import nextflow.script.params.EnvInParam @@ -1500,6 +1502,10 @@ class TaskProcessor { collectOutEnvParam(task, (EnvOutParam)param, workDir) break + case CmdOutParam: + collectOutEnvParam(task, (CmdOutParam)param, workDir) + break + case DefaultOutParam: task.setOutput(param, DefaultOutParam.Completion.DONE) break @@ -1514,7 +1520,7 @@ class TaskProcessor { task.canBind = true } - protected void collectOutEnvParam(TaskRun task, EnvOutParam param, Path workDir) { + protected void collectOutEnvParam(TaskRun task, BaseOutParam param, Path workDir) { // fetch the output value final val = collectOutEnvMap(workDir).get(param.name) diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy index 205951016e..43df27c6c7 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy @@ -38,6 +38,7 @@ import nextflow.script.BodyDef import nextflow.script.ScriptType import nextflow.script.TaskClosure import nextflow.script.bundle.ResourcesBundle +import nextflow.script.params.CmdOutParam import nextflow.script.params.EnvInParam import nextflow.script.params.EnvOutParam import nextflow.script.params.FileInParam @@ -590,6 +591,15 @@ class TaskRun implements Cloneable { return items ? new ArrayList(items.keySet()*.name) : Collections.emptyList() } + List getOutputCommands() { + final items = getOutputsByType(CmdOutParam) + final result = new ArrayList(items.size()) + for( CmdOutParam param : items.keySet() ) { + result.add( "${param.name}=\$($param.target)" ) + } + return result + } + Path getCondaEnv() { cache0.computeIfAbsent('condaEnv', (it)-> getCondaEnv0()) } diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy index a9c2a86cf9..1bcc3c96ab 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy @@ -16,12 +16,13 @@ package nextflow.script +import static nextflow.util.CacheHelper.* + import java.util.regex.Pattern import groovy.transform.PackageScope import groovy.util.logging.Slf4j import nextflow.Const -import nextflow.NF import nextflow.ast.NextflowDSLImpl import nextflow.exception.ConfigParseException import nextflow.exception.IllegalConfigException @@ -30,8 +31,24 @@ import nextflow.executor.BashWrapperBuilder import nextflow.processor.ConfigList import nextflow.processor.ErrorStrategy import nextflow.processor.TaskConfig -import static nextflow.util.CacheHelper.HashMode -import nextflow.script.params.* +import nextflow.script.params.CmdOutParam +import nextflow.script.params.DefaultInParam +import nextflow.script.params.DefaultOutParam +import nextflow.script.params.EachInParam +import nextflow.script.params.EnvInParam +import nextflow.script.params.EnvOutParam +import nextflow.script.params.FileInParam +import nextflow.script.params.FileOutParam +import nextflow.script.params.InParam +import nextflow.script.params.InputsList +import nextflow.script.params.OutParam +import nextflow.script.params.OutputsList +import nextflow.script.params.StdInParam +import nextflow.script.params.StdOutParam +import nextflow.script.params.TupleInParam +import nextflow.script.params.TupleOutParam +import nextflow.script.params.ValueInParam +import nextflow.script.params.ValueOutParam /** * Holds the process configuration properties @@ -572,6 +589,15 @@ class ProcessConfig implements Map, Cloneable { .bind(obj) } + OutParam _out_cmd( Object obj ) { + new CmdOutParam(this).bind(obj) + } + + OutParam _out_cmd( Map opts, Object obj ) { + new CmdOutParam(this) + .setOptions(opts) + .bind(obj) + } OutParam _out_file( Object obj ) { // note: check that is a String type to avoid to force diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy new file mode 100644 index 0000000000..116580ed37 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package nextflow.script.params + +import java.util.concurrent.atomic.AtomicInteger + +import groovy.transform.InheritConstructors +/** + * Model process `output: cmd PARAM` definition + * + * @author Paolo Di Tommaso + */ +@InheritConstructors +class CmdOutParam extends BaseOutParam implements OptionalParam { + + private static AtomicInteger counter = new AtomicInteger() + + private target + + private int count + + { + count = counter.incrementAndGet() + } + + String getName() { + return "nxf_out_cmd_${count}" + } + + BaseOutParam bind( def obj ) { + // the target value object + target = obj + return this + } + + String getTarget() { + return target + } +} diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy new file mode 100644 index 0000000000..3925464d1e --- /dev/null +++ b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy @@ -0,0 +1,59 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package nextflow.script.params + +import static test.TestParser.* + +import test.Dsl2Spec +/** + * + * @author Paolo Di Tommaso + */ +class CmdOutParamTest extends Dsl2Spec { + + def 'should define env outputs' () { + setup: + def text = ''' + process hola { + output: + cmd 'foo --version' + cmd 'bar --help' + + /echo command/ + } + + workflow { hola() } + ''' + + def binding = [:] + def process = parseAndReturnProcess(text, binding) + + when: + def outs = process.config.getOutputs() as List + + then: + outs.size() == 2 + and: + outs[0].name == 'nxf_out_cmd_1' + outs[0].target == 'foo --version' + and: + outs[1].name == 'nxf_out_cmd_2' + outs[1].target == 'bar --help' + + } + +} From 3dbe230c63a68ce89ab54d44185b0640170bc6c9 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 5 Dec 2023 10:46:11 +0000 Subject: [PATCH 02/11] Add support for multiline env and cmd Signed-off-by: Paolo Di Tommaso --- .../executor/BashWrapperBuilder.groovy | 18 +++---- .../groovy/nextflow/processor/TaskBean.groovy | 2 +- .../nextflow/processor/TaskProcessor.groovy | 17 ++++-- .../groovy/nextflow/processor/TaskRun.groovy | 11 ++-- .../nextflow/script/params/EnvOutParam.groovy | 6 +++ .../executor/BashWrapperBuilderTest.groovy | 26 ++++++++- .../processor/TaskProcessorTest.groovy | 9 +++- .../script/params/EnvOutParamTest.groovy | 53 +++++++++++++++++++ 8 files changed, 122 insertions(+), 20 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy index 61ee533f3c..87e0871838 100644 --- a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy @@ -177,7 +177,7 @@ class BashWrapperBuilder { } } - protected String getOutputEnvCaptureSnippet(List outEnvs, List outCmds) { + protected String getOutputEnvCaptureSnippet(List outEnvs, Map outCmds=Map.of()) { def result = new StringBuilder() result.append('\n') result.append('# capture process environment\n') @@ -186,17 +186,15 @@ class BashWrapperBuilder { int count=0 // out env for( String key : (outEnvs ?: List.of()) ) { - result.append "echo $key=\${$key[@]} " - result.append( count++==0 ? '> ' : '>> ' ) - result.append(TaskRun.CMD_ENV) - result.append('\n') + result.append "echo $key=\"\${$key[@]}\" " + result.append( count++==0 ? '> ' : '>> ' ) .append(TaskRun.CMD_ENV) .append('\n') + result.append("echo END_$key >> ") .append(TaskRun.CMD_ENV) .append('\n') } // out cmd - for( String cmd : (outCmds ?: List.of()) ) { - result.append "echo $cmd " - result.append( count++==0 ? '> ' : '>> ' ) - result.append(TaskRun.CMD_ENV) - result.append('\n') + for( Map.Entry cmd : outCmds ) { + result.append "echo $cmd.key=\"\$($cmd.value)\" " + result.append( count++==0 ? '> ' : '>> ' ) .append(TaskRun.CMD_ENV) .append('\n') + result.append("echo END_$cmd.key >> ") .append(TaskRun.CMD_ENV) .append('\n') } result.toString() } diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy index c3003ceee6..745ac76e50 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy @@ -73,7 +73,7 @@ class TaskBean implements Serializable, Cloneable { List outputEnvNames - List outputCommands + Map outputCommands String beforeScript diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy index 6f7875a1bd..19a0643d6f 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy @@ -1537,10 +1537,21 @@ class TaskProcessor { protected Map collectOutEnvMap(Path workDir) { final env = workDir.resolve(TaskRun.CMD_ENV).text final result = new HashMap(50) + String current=null for(String line : env.readLines() ) { - def (k,v) = tokenize0(line) - if (!k) continue - result.put(k,v) + if( !current ) { + def (k,v) = tokenize0(line) + if (!k) continue + result.put(k,v) + current = k + } + else if( line=="END_$current" ) { + current = null + } + else { + result[current] += '\n' + line + } + } return result } diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy index aead34be19..ef49a74c93 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy @@ -591,11 +591,16 @@ class TaskRun implements Cloneable { return items ? new ArrayList(items.keySet()*.name) : Collections.emptyList() } - List getOutputCommands() { + /** + * @return A {@link Map} instance holding a collection of key-pairs + * where the key represents a environment variable name holding the command + * output and the value the command the executed. + */ + Map getOutputCommands() { final items = getOutputsByType(CmdOutParam) - final result = new ArrayList(items.size()) + final result = new LinkedHashMap(items.size()) for( CmdOutParam param : items.keySet() ) { - result.add( "${param.name}=\$($param.target)" ) + result.put(param.name, param.target) } return result } diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/EnvOutParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/EnvOutParam.groovy index 812c739b90..bb563e5bbd 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/params/EnvOutParam.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/params/EnvOutParam.groovy @@ -41,6 +41,12 @@ class EnvOutParam extends BaseOutParam implements OptionalParam { if( obj instanceof TokenVar ) { this.nameObj = obj.name } + else if( obj instanceof CharSequence ) { + this.nameObj = obj.toString() + } + else { + throw new IllegalArgumentException("Unexpected environment output definition - it should be either a string or a variable identifier - offending value: ${obj?.getClass()?.getName()}") + } return this } diff --git a/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy index 34617cdc78..0bd628966b 100644 --- a/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy @@ -1129,11 +1129,33 @@ class BashWrapperBuilderTest extends Specification { # capture process environment set +u cd "$NXF_TASK_WORKDIR" - echo FOO=${FOO[@]} > .command.env - echo BAR=${BAR[@]} >> .command.env + echo FOO="${FOO[@]}" > .command.env + echo END_FOO >> .command.env + echo BAR="${BAR[@]}" >> .command.env + echo END_BAR >> .command.env ''' .stripIndent() + } + + def 'should return env & cmd capture snippet' () { + given: + def builder = new BashWrapperBuilder() + when: + def str = builder.getOutputEnvCaptureSnippet(['FOO'], [THIS: 'this --cmd', THAT: 'other --cmd']) + then: + str == ''' + # capture process environment + set +u + cd "$NXF_TASK_WORKDIR" + echo FOO="${FOO[@]}" > .command.env + echo END_FOO >> .command.env + echo THIS="$(this --cmd)" >> .command.env + echo END_THIS >> .command.env + echo THAT="$(other --cmd)" >> .command.env + echo END_THAT >> .command.env + ''' + .stripIndent() } def 'should validate bash interpreter' () { diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy index b616669c2e..ed757a79b8 100644 --- a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy @@ -944,8 +944,15 @@ class TaskProcessorTest extends Specification { def envFile = workDir.resolve(TaskRun.CMD_ENV) envFile.text = ''' ALPHA=one + END_ALPHA DELTA=x=y + END_DELTA OMEGA= + END_OMEGA + LONG=one + two + three + END_LONG '''.stripIndent() and: def processor = Spy(TaskProcessor) @@ -953,7 +960,7 @@ class TaskProcessorTest extends Specification { when: def result = processor.collectOutEnvMap(workDir) then: - result == [ALPHA:'one', DELTA: "x=y", OMEGA: ''] + result == [ALPHA:'one', DELTA: "x=y", OMEGA: '', LONG: 'one\ntwo\nthree'] } def 'should create a task preview' () { diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/EnvOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/EnvOutParamTest.groovy index a2b5118053..53b7b2dfb1 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/params/EnvOutParamTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/params/EnvOutParamTest.groovy @@ -54,6 +54,35 @@ class EnvOutParamTest extends Dsl2Spec { } + def 'should define env outputs with quotes' () { + setup: + def text = ''' + process hola { + output: + env 'FOO' + env 'BAR' + + /echo command/ + } + + workflow { hola() } + ''' + + def binding = [:] + def process = parseAndReturnProcess(text, binding) + + when: + def outs = process.config.getOutputs() as List + + then: + outs.size() == 2 + and: + outs[0].name == 'FOO' + and: + outs[1].name == 'BAR' + + } + def 'should define optional env outputs' () { setup: def text = ''' @@ -85,4 +114,28 @@ class EnvOutParamTest extends Dsl2Spec { out1.getOptional() == true } + + def 'should handle invalid env definition' () { + given: + def text = ''' + process hola { + output: + env { 0 } + + /echo command/ + } + + workflow { hola() } + ''' + + when: + def binding = [:] + parseAndReturnProcess(text, binding) + + then: + def e = thrown(IllegalArgumentException) + and: + e.message.startsWith('Unexpected environment output definition') + + } } From 9b92706e74eafd0accd4c87262dd1690dc79cea3 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 5 Dec 2023 10:46:33 +0000 Subject: [PATCH 03/11] Fix smoke tests Signed-off-by: Paolo Di Tommaso --- .../nextflow/src/test/groovy/nextflow/cli/CmdCloneTest.groovy | 2 ++ .../test/groovy/nextflow/scm/GiteaRepositoryProviderTest.groovy | 2 ++ 2 files changed, 4 insertions(+) diff --git a/modules/nextflow/src/test/groovy/nextflow/cli/CmdCloneTest.groovy b/modules/nextflow/src/test/groovy/nextflow/cli/CmdCloneTest.groovy index 5ecbcda749..a65e1cb1e6 100644 --- a/modules/nextflow/src/test/groovy/nextflow/cli/CmdCloneTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/cli/CmdCloneTest.groovy @@ -18,6 +18,7 @@ package nextflow.cli import java.nio.file.Files import nextflow.plugin.Plugins +import spock.lang.IgnoreIf import spock.lang.Requires import spock.lang.Specification /** @@ -26,6 +27,7 @@ import spock.lang.Specification */ class CmdCloneTest extends Specification { + @IgnoreIf({System.getenv('NXF_SMOKE')}) @Requires({System.getenv('NXF_GITHUB_ACCESS_TOKEN')}) def testClone() { diff --git a/modules/nextflow/src/test/groovy/nextflow/scm/GiteaRepositoryProviderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/scm/GiteaRepositoryProviderTest.groovy index f933b469d2..bdfea1d0f8 100644 --- a/modules/nextflow/src/test/groovy/nextflow/scm/GiteaRepositoryProviderTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/scm/GiteaRepositoryProviderTest.groovy @@ -16,6 +16,7 @@ package nextflow.scm +import spock.lang.IgnoreIf import spock.lang.Requires import spock.lang.Specification @@ -76,6 +77,7 @@ class GiteaRepositoryProviderTest extends Specification { } + @IgnoreIf({System.getenv('NXF_SMOKE')}) @Requires({System.getenv('NXF_GITEA_ACCESS_TOKEN')}) def 'should read file content'() { From b49a38208efd2e8e90bcba00a51804f130c41ffb Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Tue, 5 Dec 2023 10:53:42 +0000 Subject: [PATCH 04/11] Improve get env names Signed-off-by: Paolo Di Tommaso --- .../main/groovy/nextflow/processor/TaskRun.groovy | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy index ef49a74c93..72597d5668 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy @@ -588,7 +588,14 @@ class TaskRun implements Cloneable { List getOutputEnvNames() { final items = getOutputsByType(EnvOutParam) - return items ? new ArrayList(items.keySet()*.name) : Collections.emptyList() + if( !items ) + return List.of() + final result = new ArrayList(items.size()) + for( EnvOutParam it : items.keySet() ) { + if( !it.name ) throw new IllegalStateException("Missing output environment name - offending parameter: $it") + result.add(it.name) + } + return result } /** @@ -599,8 +606,9 @@ class TaskRun implements Cloneable { Map getOutputCommands() { final items = getOutputsByType(CmdOutParam) final result = new LinkedHashMap(items.size()) - for( CmdOutParam param : items.keySet() ) { - result.put(param.name, param.target) + for( CmdOutParam it : items.keySet() ) { + if( !it.name ) throw new IllegalStateException("Missing output command name - offending parameter: $it") + result.put(it.name, it.target) } return result } From be6084b7eb8537765d1e6f2a7c4e4d733d1282d0 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sun, 10 Dec 2023 18:21:39 +0100 Subject: [PATCH 05/11] Add support for err handling Signed-off-by: Paolo Di Tommaso --- .../exception/ProcessCommandException.groovy | 40 +++++++++ .../executor/BashWrapperBuilder.groovy | 46 ++++++---- .../nextflow/processor/TaskProcessor.groovy | 86 +++++++++++++++---- .../nextflow/executor/command-env.txt | 34 ++++++++ .../executor/BashWrapperBuilderTest.groovy | 61 ++++++++++--- .../processor/TaskProcessorTest.groovy | 36 ++++++-- tests/checks/cmd-out.nf/.checks | 17 ++++ tests/cmd-out.nf | 30 +++++++ 8 files changed, 302 insertions(+), 48 deletions(-) create mode 100644 modules/nextflow/src/main/groovy/nextflow/exception/ProcessCommandException.groovy create mode 100644 modules/nextflow/src/main/resources/nextflow/executor/command-env.txt create mode 100644 tests/checks/cmd-out.nf/.checks create mode 100644 tests/cmd-out.nf diff --git a/modules/nextflow/src/main/groovy/nextflow/exception/ProcessCommandException.groovy b/modules/nextflow/src/main/groovy/nextflow/exception/ProcessCommandException.groovy new file mode 100644 index 0000000000..333a720b87 --- /dev/null +++ b/modules/nextflow/src/main/groovy/nextflow/exception/ProcessCommandException.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package nextflow.exception + +import groovy.transform.CompileStatic + +/** + * Exception thrown when a command output returns a non-zero exit status + * + * @author Paolo Di Tommaso + */ +@CompileStatic +class ProcessCommandException extends RuntimeException implements ShowOnlyExceptionMessage { + + String command + String output + int status + + ProcessCommandException(String message, String command, String output, int status) { + super(message) + this.command = command + this.output = output + this.status = status + } +} diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy index 87e0871838..f1b15b2925 100644 --- a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy @@ -16,6 +16,8 @@ package nextflow.executor +import static java.nio.file.StandardOpenOption.* + import java.nio.file.FileSystemException import java.nio.file.FileSystems import java.nio.file.Files @@ -35,11 +37,7 @@ import nextflow.processor.TaskProcessor import nextflow.processor.TaskRun import nextflow.secret.SecretsLoader import nextflow.util.Escape - -import static java.nio.file.StandardOpenOption.* - import nextflow.util.MemoryUnit - /** * Builder to create the Bash script which is used to * wrap and launch the user task @@ -177,24 +175,36 @@ class BashWrapperBuilder { } } - protected String getOutputEnvCaptureSnippet(List outEnvs, Map outCmds=Map.of()) { - def result = new StringBuilder() - result.append('\n') - result.append('# capture process environment\n') - result.append('set +u\n') - result.append('cd "$NXF_TASK_WORKDIR"\n') - int count=0 + protected String getOutputEnvCaptureSnippet(List outEnvs, Map outCmds) { + // load the env template + def template = BashWrapperBuilder.class + .getResourceAsStream('command-env.txt') + .newReader() + def binding = Map.of('env_file', TaskRun.CMD_ENV) + def result = engine.render(template, binding) + // avoid nulls + if( outEnvs==null ) + outEnvs = List.of() + if( outCmds==null ) + outCmds = Map.of() // out env - for( String key : (outEnvs ?: List.of()) ) { - result.append "echo $key=\"\${$key[@]}\" " - result.append( count++==0 ? '> ' : '>> ' ) .append(TaskRun.CMD_ENV) .append('\n') - result.append("echo END_$key >> ") .append(TaskRun.CMD_ENV) .append('\n') + for( String key : outEnvs ) { + result += "#\n" + result += "echo $key=\"\${$key[@]}\" >> ${TaskRun.CMD_ENV}\n" + result += "echo /$key/ >> ${TaskRun.CMD_ENV}\n" } // out cmd for( Map.Entry cmd : outCmds ) { - result.append "echo $cmd.key=\"\$($cmd.value)\" " - result.append( count++==0 ? '> ' : '>> ' ) .append(TaskRun.CMD_ENV) .append('\n') - result.append("echo END_$cmd.key >> ") .append(TaskRun.CMD_ENV) .append('\n') + result += "#\n" + result += "nxf_catch STDOUT STDERR ${cmd.value}\n" + result += 'status=$?\n' + result += 'if [ $status -eq 0 ]; then\n' + result += " echo $cmd.key=\"\$STDOUT\" >> ${TaskRun.CMD_ENV}\n" + result += " echo /$cmd.key/=exit:0 >> ${TaskRun.CMD_ENV}\n" + result += 'else\n' + result += " echo $cmd.key=\"\$STDERR\" >> ${TaskRun.CMD_ENV}\n" + result += " echo /$cmd.key/=exit:\$status >> ${TaskRun.CMD_ENV}\n" + result += 'fi\n' } result.toString() } diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy index 19a0643d6f..f5981182f0 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy @@ -15,6 +15,7 @@ */ package nextflow.processor + import static nextflow.processor.ErrorStrategy.* import java.lang.reflect.InvocationTargetException @@ -61,6 +62,7 @@ import nextflow.exception.FailedGuardException import nextflow.exception.IllegalArityException import nextflow.exception.MissingFileException import nextflow.exception.MissingValueException +import nextflow.exception.ProcessCommandException import nextflow.exception.ProcessException import nextflow.exception.ProcessFailedException import nextflow.exception.ProcessRetryableException @@ -1081,6 +1083,10 @@ class TaskProcessor { formatTaskError( message, error, task ) break + case ProcessCommandException: + formatCommandError( message, error, task ) + break + case FailedGuardException: formatGuardError( message, error as FailedGuardException, task ) break; @@ -1152,6 +1158,35 @@ class TaskProcessor { return action } + final protected List formatCommandError( List message, ProcessCommandException error, TaskRun task) { + // compose a readable error message + message << formatErrorCause(error) + + // - print the executed command + message << "Command executed:\n" + error.command.stripIndent(true)?.trim()?.eachLine { + message << " ${it}" + } + + // - the exit status + message << "\nCommand exit status:\n ${error.status}" + + // - the tail of the process stdout + message << "\nCommand output:" + def lines = error.output.readLines() + if( lines.size() == 0 ) { + message << " (empty)" + } + for( String it : lines ) { + message << " ${stripWorkDir(it, task.workDir)}" + } + + if( task?.workDir ) + message << "\nWork dir:\n ${task.workDirStr}" + + return message + } + final protected List formatGuardError( List message, FailedGuardException error, TaskRun task ) { // compose a readable error message message << formatErrorCause(error) @@ -1523,7 +1558,8 @@ class TaskProcessor { protected void collectOutEnvParam(TaskRun task, BaseOutParam param, Path workDir) { // fetch the output value - final val = collectOutEnvMap(workDir).get(param.name) + final outCmds = param instanceof CmdOutParam ? task.getOutputCommands() : null + final val = collectOutEnvMap(workDir,outCmds).get(param.name) if( val == null && !param.optional ) throw new MissingValueException("Missing environment variable: $param.name") // set into the output set @@ -1533,36 +1569,56 @@ class TaskProcessor { } + /** + * Parse the `.command.env` file which holds the value for `env` and `cmd` + * output types + * + * @param workDir + * The task work directory that contains the `.command.env` file + * @param outCommands + * A {@link Map} instance containing key-value pairs + * @return + */ + @CompileStatic @Memoized(maxCacheSize = 10_000) - protected Map collectOutEnvMap(Path workDir) { + protected Map collectOutEnvMap(Path workDir, Map outCommands) { final env = workDir.resolve(TaskRun.CMD_ENV).text - final result = new HashMap(50) + final result = new HashMap(50) + Matcher matcher + // `current` represent the current capturing env variable name String current=null for(String line : env.readLines() ) { - if( !current ) { - def (k,v) = tokenize0(line) + // Opening condition: + // line should match a KEY=VALUE syntax + if( !current && (matcher = (line=~/([a-zA-Z_][a-zA-Z0-9_]*)=(.*)/)) ) { + final k = matcher.group(1) + final v = matcher.group(2) if (!k) continue result.put(k,v) current = k } - else if( line=="END_$current" ) { + // Closing condition: + // line should match /KEY/ or /KEY/=exit_status + else if( current && (matcher = (line=~/\/${current}\/(?:=exit:(\d+))?/)) ) { + final status = matcher.group(1) as Integer ?: 0 + // when exit status is defined and it is a non-zero, it should be interpreted + // as a failure of the execution of the output command; in this case the variable + // holds the std error message + if( outCommands!=null && status ) { + final cmd = outCommands.get(current) + final out = result[current] + throw new ProcessCommandException("Unable to evaluate command output", cmd, out, status) + } + // reset current key current = null } - else { + else if( current && line!=null) { result[current] += '\n' + line } - } return result } - private List tokenize0(String line) { - int p=line.indexOf('=') - return p==-1 - ? List.of(line,'') - : List.of(line.substring(0,p), line.substring(p+1)) - } - /** * Collects the process 'std output' * diff --git a/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt b/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt new file mode 100644 index 0000000000..31371e2431 --- /dev/null +++ b/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt @@ -0,0 +1,34 @@ +## +## Copyright 2013-2023, Seqera Labs +## +## Licensed under the Apache License, Version 2.0 (the "License"); +## you may not use this file except in compliance with the License. +## You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software +## distributed under the License is distributed on an "AS IS" BASIS, +## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +## See the License for the specific language governing permissions and +## limitations under the License. + +# capture process environment +set +u +set +e +cd "$NXF_TASK_WORKDIR" + +## SYNTAX: +## catch STDOUT_VARIABLE STDERR_VARIABLE COMMAND [ARG1[ ARG2[ ...[ ARGN]]]] +## +## See solution 7 at https://stackoverflow.com/a/59592881/395921 +## +nxf_catch() { + { + IFS=$'\n' read -r -d '' "${1}"; + IFS=$'\n' read -r -d '' "${2}"; + (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_}); + } < <((printf '\0%s\0%d\0' "$(((({ shift 2; "${@}"; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1) +} + +echo '' > {{env_file}} diff --git a/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy index 0bd628966b..aefdf1bafa 100644 --- a/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy @@ -1123,16 +1123,29 @@ class BashWrapperBuilderTest extends Specification { def builder = new BashWrapperBuilder() when: - def str = builder.getOutputEnvCaptureSnippet(['FOO','BAR']) + def str = builder.getOutputEnvCaptureSnippet(['FOO','BAR'], Map.of()) then: str == ''' # capture process environment set +u + set +e cd "$NXF_TASK_WORKDIR" - echo FOO="${FOO[@]}" > .command.env - echo END_FOO >> .command.env + + nxf_catch() { + { + IFS=$'\\n' read -r -d '' "${1}"; + IFS=$'\\n' read -r -d '' "${2}"; + (IFS=$'\\n' read -r -d '' _ERRNO_; return ${_ERRNO_}); + } < <((printf '\\0%s\\0%d\\0' "$(((({ shift 2; "${@}"; echo "${?}" 1>&3-; } | tr -d '\\0' 1>&4-) 4>&2- 2>&1- | tr -d '\\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1) + } + + echo '' > .command.env + # + echo FOO="${FOO[@]}" >> .command.env + echo /FOO/ >> .command.env + # echo BAR="${BAR[@]}" >> .command.env - echo END_BAR >> .command.env + echo /BAR/ >> .command.env ''' .stripIndent() } @@ -1147,13 +1160,41 @@ class BashWrapperBuilderTest extends Specification { str == ''' # capture process environment set +u + set +e cd "$NXF_TASK_WORKDIR" - echo FOO="${FOO[@]}" > .command.env - echo END_FOO >> .command.env - echo THIS="$(this --cmd)" >> .command.env - echo END_THIS >> .command.env - echo THAT="$(other --cmd)" >> .command.env - echo END_THAT >> .command.env + + nxf_catch() { + { + IFS=$'\\n' read -r -d '' "${1}"; + IFS=$'\\n' read -r -d '' "${2}"; + (IFS=$'\\n' read -r -d '' _ERRNO_; return ${_ERRNO_}); + } < <((printf '\\0%s\\0%d\\0' "$(((({ shift 2; "${@}"; echo "${?}" 1>&3-; } | tr -d '\\0' 1>&4-) 4>&2- 2>&1- | tr -d '\\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1) + } + + echo '' > .command.env + # + echo FOO="${FOO[@]}" >> .command.env + echo /FOO/ >> .command.env + # + nxf_catch STDOUT STDERR this --cmd + status=$? + if [ $status -eq 0 ]; then + echo THIS="$STDOUT" >> .command.env + echo /THIS/=exit:0 >> .command.env + else + echo THIS="$STDERR" >> .command.env + echo /THIS/=exit:$status >> .command.env + fi + # + nxf_catch STDOUT STDERR other --cmd + status=$? + if [ $status -eq 0 ]; then + echo THAT="$STDOUT" >> .command.env + echo /THAT/=exit:0 >> .command.env + else + echo THAT="$STDERR" >> .command.env + echo /THAT/=exit:$status >> .command.env + fi ''' .stripIndent() } diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy index ed757a79b8..ebfe36f5da 100644 --- a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy @@ -28,6 +28,7 @@ import nextflow.ISession import nextflow.Session import nextflow.exception.IllegalArityException import nextflow.exception.MissingFileException +import nextflow.exception.ProcessCommandException import nextflow.exception.ProcessException import nextflow.exception.ProcessUnrecoverableException import nextflow.executor.Executor @@ -944,25 +945,50 @@ class TaskProcessorTest extends Specification { def envFile = workDir.resolve(TaskRun.CMD_ENV) envFile.text = ''' ALPHA=one - END_ALPHA + /ALPHA/ DELTA=x=y - END_DELTA + /DELTA/ OMEGA= - END_OMEGA + /OMEGA/ LONG=one two three - END_LONG + /LONG/=exit:0 '''.stripIndent() and: def processor = Spy(TaskProcessor) when: - def result = processor.collectOutEnvMap(workDir) + def result = processor.collectOutEnvMap(workDir, Map.of()) then: result == [ALPHA:'one', DELTA: "x=y", OMEGA: '', LONG: 'one\ntwo\nthree'] } + def 'should parse env map with command error' () { + given: + def workDir = TestHelper.createInMemTempDir() + def envFile = workDir.resolve(TaskRun.CMD_ENV) + envFile.text = ''' + ALPHA=one + /ALPHA/ + cmd_out_1=Hola + /cmd_out_1/=exit:0 + cmd_out_2=This is an error message + for unknown reason + /cmd_out_2/=exit:100 + '''.stripIndent() + and: + def processor = Spy(TaskProcessor) + + when: + processor.collectOutEnvMap(workDir, [cmd_out_1: 'foo --this', cmd_out_2: 'bar --that']) + then: + def e = thrown(ProcessCommandException) + e.message == 'Unable to evaluate command output' + e.command == 'bar --that' + e.output == 'This is an error message\nfor unknown reason' + e.status == 100 + } def 'should create a task preview' () { given: def config = new ProcessConfig([cpus: 10, memory: '100 GB']) diff --git a/tests/checks/cmd-out.nf/.checks b/tests/checks/cmd-out.nf/.checks new file mode 100644 index 0000000000..b8a38fb379 --- /dev/null +++ b/tests/checks/cmd-out.nf/.checks @@ -0,0 +1,17 @@ +# +# run normal mode +# +$NXF_RUN | tee .stdout + +[[ `grep INFO .nextflow.log | grep -c 'Submitted process'` == 1 ]] || false +[[ `< .stdout grep 'GNU bash'` ]] || false + + +# +# run resume mode +# +$NXF_RUN -resume | tee .stdout + +[[ `grep INFO .nextflow.log | grep -c 'Cached process'` == 1 ]] || false +[[ `< .stdout grep 'GNU bash'` ]] || false + diff --git a/tests/cmd-out.nf b/tests/cmd-out.nf new file mode 100644 index 0000000000..6b0db54d50 --- /dev/null +++ b/tests/cmd-out.nf @@ -0,0 +1,30 @@ +#!/usr/bin/env nextflow +/* + * Copyright 2013-2023, Seqera Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +process foo { + output: + cmd 'bash --version', emit: bash_version + ''' + echo Hello + ''' +} + + +workflow { + foo() + foo.out.bash_version.view{ it.readLines()[0] } +} From 624d864483ef8c5fb98b18f4cd129f2f225cccee Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Sun, 10 Dec 2023 19:02:27 +0100 Subject: [PATCH 06/11] Fix support for variables Signed-off-by: Paolo Di Tommaso --- .../groovy/nextflow/processor/TaskRun.groovy | 2 +- .../nextflow/script/params/CmdOutParam.groovy | 13 ++++++++++--- .../script/params/CmdOutParamTest.groovy | 19 +++++++++++-------- tests/cmd-out.nf | 8 +++++--- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy index 72597d5668..942d6b5622 100644 --- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy @@ -608,7 +608,7 @@ class TaskRun implements Cloneable { final result = new LinkedHashMap(items.size()) for( CmdOutParam it : items.keySet() ) { if( !it.name ) throw new IllegalStateException("Missing output command name - offending parameter: $it") - result.put(it.name, it.target) + result.put(it.name, it.getTarget(context)) } return result } diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy index 116580ed37..0ee822affd 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy @@ -20,6 +20,8 @@ package nextflow.script.params import java.util.concurrent.atomic.AtomicInteger import groovy.transform.InheritConstructors +import groovy.transform.Memoized + /** * Model process `output: cmd PARAM` definition * @@ -30,7 +32,7 @@ class CmdOutParam extends BaseOutParam implements OptionalParam { private static AtomicInteger counter = new AtomicInteger() - private target + private Object target private int count @@ -43,12 +45,17 @@ class CmdOutParam extends BaseOutParam implements OptionalParam { } BaseOutParam bind( def obj ) { + if( obj !instanceof CharSequence ) + throw new IllegalArgumentException("Invalid argument for command output: $this") // the target value object target = obj return this } - String getTarget() { - return target + @Memoized + String getTarget(Map context) { + return target instanceof GString + ? target.cloneAsLazy(context).toString() + : target.toString() } } diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy index 3925464d1e..023dd54273 100644 --- a/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy +++ b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy @@ -31,7 +31,8 @@ class CmdOutParamTest extends Dsl2Spec { process hola { output: cmd 'foo --version' - cmd 'bar --help' + cmd "$params.cmd --help" + cmd "$tool --test" /echo command/ } @@ -39,21 +40,23 @@ class CmdOutParamTest extends Dsl2Spec { workflow { hola() } ''' - def binding = [:] + def binding = [params:[cmd:'bar'], tool: 'other'] def process = parseAndReturnProcess(text, binding) when: def outs = process.config.getOutputs() as List then: - outs.size() == 2 + outs.size() == 3 and: - outs[0].name == 'nxf_out_cmd_1' - outs[0].target == 'foo --version' + outs[0].getName() == 'nxf_out_cmd_1' + outs[0].getTarget(binding) == 'foo --version' and: - outs[1].name == 'nxf_out_cmd_2' - outs[1].target == 'bar --help' - + outs[1].getName() == 'nxf_out_cmd_2' + outs[1].getTarget(binding) == 'bar --help' + and: + outs[2].getName() == 'nxf_out_cmd_3' + outs[2].getTarget(binding) == 'other --test' } } diff --git a/tests/cmd-out.nf b/tests/cmd-out.nf index 6b0db54d50..b97ed3226e 100644 --- a/tests/cmd-out.nf +++ b/tests/cmd-out.nf @@ -16,8 +16,10 @@ */ process foo { + input: + val shell output: - cmd 'bash --version', emit: bash_version + cmd "$shell --version", emit: shell_version ''' echo Hello ''' @@ -25,6 +27,6 @@ process foo { workflow { - foo() - foo.out.bash_version.view{ it.readLines()[0] } + foo('bash') + foo.out.shell_version.view{ it.readLines()[0] } } From 56677755d6b896b40c6ac5cad36d6355eb720979 Mon Sep 17 00:00:00 2001 From: Paolo Di Tommaso Date: Mon, 11 Dec 2023 11:45:57 +0100 Subject: [PATCH 07/11] Add support for tuple Signed-off-by: Paolo Di Tommaso --- .../nextflow/ast/NextflowDSLImpl.groovy | 6 +++ .../nextflow/script/ScriptTokens.groovy | 13 +++++++ .../script/params/TupleOutParam.groovy | 5 ++- .../script/params/TupleOutParamTest.groovy | 38 +++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy b/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy index 2d264e81d9..04b56f7ace 100644 --- a/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy @@ -27,6 +27,7 @@ import nextflow.script.BaseScript import nextflow.script.BodyDef import nextflow.script.IncludeDef import nextflow.script.TaskClosure +import nextflow.script.TokenCmdCall import nextflow.script.TokenEnvCall import nextflow.script.TokenFileCall import nextflow.script.TokenPathCall @@ -1123,6 +1124,11 @@ class NextflowDSLImpl implements ASTTransformation { return createX( TokenEnvCall, args ) } + if( methodCall.methodAsString == 'cmd' && withinTupleMethod ) { + def args = (TupleExpression) varToStrX(methodCall.arguments) + return createX( TokenCmdCall, args ) + } + /* * input: * tuple val(x), .. from q diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy index c7d38a1b7c..f5c33aee22 100644 --- a/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy +++ b/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy @@ -109,6 +109,19 @@ class TokenEnvCall { Object val } +/** + * Token used by the DSL to identify a command output declaration, like this + *
+ *     input:
+ *     tuple( cmd(X), ... )
+ *     
+ */
+@ToString
+@EqualsAndHashCode
+@TupleConstructor
+class TokenCmdCall {
+    Object val
+}
 
 /**
  * This class is used to identify a 'val' when used like in this example:
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy
index e0de9e24ed..95c70ed3fc 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy
@@ -17,7 +17,7 @@
 package nextflow.script.params
 
 import groovy.transform.InheritConstructors
-import nextflow.NF
+import nextflow.script.TokenCmdCall
 import nextflow.script.TokenEnvCall
 import nextflow.script.TokenFileCall
 import nextflow.script.TokenPathCall
@@ -59,6 +59,9 @@ class TupleOutParam extends BaseOutParam implements OptionalParam {
             else if( item instanceof TokenEnvCall ) {
                 create(EnvOutParam).bind(item.val)
             }
+            else if( item instanceof TokenCmdCall ) {
+                create(CmdOutParam).bind(item.val)
+            }
             else if( item instanceof GString ) {
                 throw new IllegalArgumentException("Unqualified output path declaration is not allowed - replace `tuple \"$item\",..` with `tuple path(\"$item\"),..`")
             }
diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
index 498224ec97..effb26d25e 100644
--- a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
@@ -178,4 +178,42 @@ class TupleOutParamTest extends Dsl2Spec {
         outs[0].inner[1] instanceof EnvOutParam
         outs[0].inner[1].getName() == 'BAR'
     }
+
+    def 'should create tuple of cmd' () {
+        setup:
+        def text = '''
+            process hola {
+              output:
+                tuple cmd('this --one'), cmd("$other --two")
+              
+              /echo command/ 
+            }
+            
+            workflow {
+              hola()
+            }
+            '''
+
+        def binding = [other:'tool']
+        def process = parseAndReturnProcess(text, binding)
+
+        when:
+        def outs = process.config.getOutputs() as List
+        then:
+        println outs.outChannel
+        outs.size() == 1
+        and:
+        outs[0].outChannel instanceof DataflowVariable
+        and:
+        outs[0].inner.size() == 2
+        and:
+        outs[0].inner[0] instanceof CmdOutParam
+        outs[0].inner[0].getName() == 'nxf_out_cmd_1'
+        (outs[0].inner[0] as CmdOutParam).getTarget(binding) == 'this --one'
+        and:
+        outs[0].inner[1] instanceof CmdOutParam
+        outs[0].inner[1].getName() == 'nxf_out_cmd_2'
+        (outs[0].inner[1] as CmdOutParam).getTarget(binding) == 'tool --two'
+
+    }
 }

From dddca032142f8d7fedad9df2e24f8d16a0109e42 Mon Sep 17 00:00:00 2001
From: Paolo Di Tommaso 
Date: Mon, 11 Dec 2023 12:03:08 +0100
Subject: [PATCH 08/11] Fix failing tests

Signed-off-by: Paolo Di Tommaso 
---
 .../groovy/nextflow/script/params/CmdOutParamTest.groovy    | 6 +++---
 .../groovy/nextflow/script/params/TupleOutParamTest.groovy  | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy
index 023dd54273..99bc3c048d 100644
--- a/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy
@@ -49,13 +49,13 @@ class CmdOutParamTest extends Dsl2Spec {
         then:
         outs.size() == 3
         and:
-        outs[0].getName() == 'nxf_out_cmd_1'
+        outs[0].getName() =~ /nxf_out_cmd_\d+/
         outs[0].getTarget(binding) == 'foo --version'
         and:
-        outs[1].getName() == 'nxf_out_cmd_2'
+        outs[1].getName() =~ /nxf_out_cmd_\d+/
         outs[1].getTarget(binding) == 'bar --help'
         and:
-        outs[2].getName() == 'nxf_out_cmd_3'
+        outs[2].getName() =~ /nxf_out_cmd_\d+/
         outs[2].getTarget(binding) == 'other --test'
     }
 
diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
index effb26d25e..152b38f4d1 100644
--- a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
@@ -208,11 +208,11 @@ class TupleOutParamTest extends Dsl2Spec {
         outs[0].inner.size() == 2
         and:
         outs[0].inner[0] instanceof CmdOutParam
-        outs[0].inner[0].getName() == 'nxf_out_cmd_1'
+        outs[0].inner[0].getName() =~ /nxf_out_cmd_\d+/
         (outs[0].inner[0] as CmdOutParam).getTarget(binding) == 'this --one'
         and:
         outs[0].inner[1] instanceof CmdOutParam
-        outs[0].inner[1].getName() == 'nxf_out_cmd_2'
+        outs[0].inner[1].getName() =~ /nxf_out_cmd_\d+/
         (outs[0].inner[1] as CmdOutParam).getTarget(binding) == 'tool --two'
 
     }

From 8d0c73793b2262a79663a531a77cc69484911e2b Mon Sep 17 00:00:00 2001
From: Ben Sherman 
Date: Sun, 10 Dec 2023 04:37:11 -0600
Subject: [PATCH 09/11] Restore lost changes

Signed-off-by: Ben Sherman 
---
 docs/process.md                               | 48 +++++++++----------
 docs/snippets/process-out-cmd.nf              | 12 +++++
 docs/snippets/process-out-cmd.out             |  6 +++
 docs/snippets/process-out-env.nf              | 13 +++++
 docs/snippets/process-out-env.out             |  8 ++++
 docs/snippets/process-stdout.nf               | 12 +++++
 docs/snippets/process-stdout.out              |  2 +
 .../executor/BashWrapperBuilder.groovy        |  4 +-
 .../script/params/TupleInParam.groovy         |  4 ++
 9 files changed, 82 insertions(+), 27 deletions(-)
 create mode 100644 docs/snippets/process-out-cmd.nf
 create mode 100644 docs/snippets/process-out-cmd.out
 create mode 100644 docs/snippets/process-out-env.nf
 create mode 100644 docs/snippets/process-out-env.out
 create mode 100644 docs/snippets/process-stdout.nf
 create mode 100644 docs/snippets/process-stdout.out

diff --git a/docs/process.md b/docs/process.md
index 57062d19f1..6b255c8264 100644
--- a/docs/process.md
+++ b/docs/process.md
@@ -1057,43 +1057,41 @@ To sum up, the use of output files with static names over dynamic ones is prefer
 
 The `env` qualifier allows you to output a variable defined in the process execution environment:
 
-```groovy
-process myTask {
-    output:
-    env FOO
-
-    script:
-    '''
-    FOO=$(ls -la)
-    '''
-}
-
-workflow {
-    myTask | view { "directory contents: $it" }
-}
+```{literalinclude} snippets/process-out-env.nf
+:language: groovy
 ```
 
+:::{versionchanged} 23.12.0-edge
+Prior to this version, if the environment variable contained multiple lines of output, the output would be compressed to a single line by converting newlines to spaces.
+:::
+
 (process-stdout)=
 
 ### Output type `stdout`
 
 The `stdout` qualifier allows you to output the `stdout` of the executed process:
 
-```groovy
-process sayHello {
-    output:
-    stdout
+```{literalinclude} snippets/process-stdout.nf
+:language: groovy
+```
 
-    """
-    echo Hello world!
-    """
-}
+(process-out-cmd)=
 
-workflow {
-    sayHello | view { "I say... $it" }
-}
+### Output type `cmd`
+
+:::{versionadded} 23.12.0-edge
+:::
+
+The `cmd` qualifier allows you to capture the standard output of an arbitrary shell command:
+
+```{literalinclude} snippets/process-out-cmd.nf
+:language: groovy
 ```
 
+Only one-line Bash commands are supported. You can use a semi-colon `;` to specify multiple Bash commands on a single line, and many interpreters can execute arbitrary code on the command line, e.g. `python -c 'print("Hello world!")'`.
+
+If the command fails, the task will also fail. In Bash, you can append `|| true` to a command to suppress any command failure.
+
 (process-set)=
 
 ### Output type `set`
diff --git a/docs/snippets/process-out-cmd.nf b/docs/snippets/process-out-cmd.nf
new file mode 100644
index 0000000000..7c3591e42d
--- /dev/null
+++ b/docs/snippets/process-out-cmd.nf
@@ -0,0 +1,12 @@
+process sayHello {
+    output:
+    cmd('bash --version')
+
+    """
+    echo Hello world!
+    """
+}
+
+workflow {
+    sayHello | view
+}
\ No newline at end of file
diff --git a/docs/snippets/process-out-cmd.out b/docs/snippets/process-out-cmd.out
new file mode 100644
index 0000000000..9c08b97a7a
--- /dev/null
+++ b/docs/snippets/process-out-cmd.out
@@ -0,0 +1,6 @@
+GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)
+Copyright (C) 2020 Free Software Foundation, Inc.
+License GPLv3+: GNU GPL version 3 or later 
+
+This is free software; you are free to change and redistribute it.
+There is NO WARRANTY, to the extent permitted by law.
\ No newline at end of file
diff --git a/docs/snippets/process-out-env.nf b/docs/snippets/process-out-env.nf
new file mode 100644
index 0000000000..9a80b7382c
--- /dev/null
+++ b/docs/snippets/process-out-env.nf
@@ -0,0 +1,13 @@
+process myTask {
+    output:
+    env FOO
+
+    script:
+    '''
+    FOO=$(ls -a)
+    '''
+}
+
+workflow {
+    myTask | view
+}
\ No newline at end of file
diff --git a/docs/snippets/process-out-env.out b/docs/snippets/process-out-env.out
new file mode 100644
index 0000000000..390f5c6e56
--- /dev/null
+++ b/docs/snippets/process-out-env.out
@@ -0,0 +1,8 @@
+.
+..
+.command.begin
+.command.err
+.command.log
+.command.out
+.command.run
+.command.sh
\ No newline at end of file
diff --git a/docs/snippets/process-stdout.nf b/docs/snippets/process-stdout.nf
new file mode 100644
index 0000000000..9e2e719896
--- /dev/null
+++ b/docs/snippets/process-stdout.nf
@@ -0,0 +1,12 @@
+process sayHello {
+    output:
+    stdout
+
+    """
+    echo Hello world!
+    """
+}
+
+workflow {
+    sayHello | view { "I say... $it" }
+}
\ No newline at end of file
diff --git a/docs/snippets/process-stdout.out b/docs/snippets/process-stdout.out
new file mode 100644
index 0000000000..6df702c6e8
--- /dev/null
+++ b/docs/snippets/process-stdout.out
@@ -0,0 +1,2 @@
+I say... Hello world!
+
diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy
index f1b15b2925..21b69ccd18 100644
--- a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy
@@ -257,9 +257,9 @@ class BashWrapperBuilder {
 
         if( outputEnvNames || outputCommands ) {
             if( !isBash(interpreter) && outputEnvNames )
-                throw new IllegalArgumentException("Process output of type env is only allowed with Bash process command -- Current interpreter: $interpreter")
+                throw new IllegalArgumentException("Process output of type env is only allowed with Bash process scripts -- Current interpreter: $interpreter")
             if( !isBash(interpreter) && outputCommands )
-                throw new IllegalArgumentException("Process output of type env is only allowed with Bash process command -- Current interpreter: $interpreter")
+                throw new IllegalArgumentException("Process output of type cmd is only allowed with Bash process scripts -- Current interpreter: $interpreter")
             script += getOutputEnvCaptureSnippet(outputEnvNames, outputCommands)
         }
 
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy
index 6255954eec..041fa7a190 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy
@@ -18,6 +18,7 @@ package nextflow.script.params
 
 import groovy.transform.InheritConstructors
 import nextflow.NF
+import nextflow.script.TokenCmdCall
 import nextflow.script.TokenEnvCall
 import nextflow.script.TokenFileCall
 import nextflow.script.TokenPathCall
@@ -76,6 +77,9 @@ class TupleInParam extends BaseInParam {
             else if( item instanceof TokenEnvCall ) {
                 newItem(EnvInParam).bind(item.val)
             }
+            else if( item instanceof TokenCmdCall ) {
+                throw new IllegalArgumentException('Command input declaration is not supported')
+            }
             else if( item instanceof TokenStdinCall ) {
                 newItem(StdInParam)
             }

From 1d97d49335046693b6f46e0c7fbd36e7fec74fea Mon Sep 17 00:00:00 2001
From: Paolo Di Tommaso 
Date: Sat, 10 Feb 2024 20:52:04 +0100
Subject: [PATCH 10/11] Improvement and cleanup

Signed-off-by: Paolo Di Tommaso 
---
 docs/process.md                               |  10 +-
 ...process-out-cmd.nf => process-out-eval.nf} |   4 +-
 ...ocess-out-cmd.out => process-out-eval.out} |   0
 .../nextflow/ast/NextflowDSLImpl.groovy       |   8 +-
 ...ion.groovy => ProcessEvalException.groovy} |   4 +-
 .../executor/BashWrapperBuilder.groovy        | 102 ++++++++++++------
 .../groovy/nextflow/processor/TaskBean.groovy |   4 +-
 .../nextflow/processor/TaskProcessor.groovy   |  25 +++--
 .../groovy/nextflow/processor/TaskRun.groovy  |  10 +-
 .../nextflow/script/ProcessConfig.groovy      |  10 +-
 .../nextflow/script/ScriptTokens.groovy       |   4 +-
 ...CmdOutParam.groovy => CmdEvalParam.groovy} |   6 +-
 .../script/params/TupleInParam.groovy         |   5 +-
 .../script/params/TupleOutParam.groovy        |   6 +-
 .../nextflow/executor/command-env.txt         |   4 +-
 .../processor/TaskProcessorTest.groovy        |   4 +-
 ...ramTest.groovy => CmdEvalParamTest.groovy} |  18 ++--
 .../script/params/TupleOutParamTest.groovy    |   8 +-
 .../{cmd-out.nf => eval-out.nf}/.checks       |   0
 tests/{cmd-out.nf => eval-out.nf}             |   2 +-
 20 files changed, 137 insertions(+), 97 deletions(-)
 rename docs/snippets/{process-out-cmd.nf => process-out-eval.nf} (78%)
 rename docs/snippets/{process-out-cmd.out => process-out-eval.out} (100%)
 rename modules/nextflow/src/main/groovy/nextflow/exception/{ProcessCommandException.groovy => ProcessEvalException.groovy} (84%)
 rename modules/nextflow/src/main/groovy/nextflow/script/params/{CmdOutParam.groovy => CmdEvalParam.groovy} (90%)
 rename modules/nextflow/src/test/groovy/nextflow/script/params/{CmdOutParamTest.groovy => CmdEvalParamTest.groovy} (76%)
 rename tests/checks/{cmd-out.nf => eval-out.nf}/.checks (100%)
 rename tests/{cmd-out.nf => eval-out.nf} (94%)

diff --git a/docs/process.md b/docs/process.md
index dd168e7b2f..25772c5585 100644
--- a/docs/process.md
+++ b/docs/process.md
@@ -1067,16 +1067,16 @@ The `stdout` qualifier allows you to output the `stdout` of the executed process
 :language: groovy
 ```
 
-(process-out-cmd)=
+(process-out-eval)=
 
-### Output type `cmd`
+### Output type `eval`
 
-:::{versionadded} 23.12.0-edge
+:::{versionadded} 24.02.0-edge
 :::
 
-The `cmd` qualifier allows you to capture the standard output of an arbitrary shell command:
+The `eval` qualifier allows you to capture the standard output of an arbitrary command evaluated the task shell interpreter context:
 
-```{literalinclude} snippets/process-out-cmd.nf
+```{literalinclude} snippets/process-out-eval.nf
 :language: groovy
 ```
 
diff --git a/docs/snippets/process-out-cmd.nf b/docs/snippets/process-out-eval.nf
similarity index 78%
rename from docs/snippets/process-out-cmd.nf
rename to docs/snippets/process-out-eval.nf
index 7c3591e42d..58081aafc7 100644
--- a/docs/snippets/process-out-cmd.nf
+++ b/docs/snippets/process-out-eval.nf
@@ -1,6 +1,6 @@
 process sayHello {
     output:
-    cmd('bash --version')
+    eval('bash --version')
 
     """
     echo Hello world!
@@ -9,4 +9,4 @@ process sayHello {
 
 workflow {
     sayHello | view
-}
\ No newline at end of file
+}
diff --git a/docs/snippets/process-out-cmd.out b/docs/snippets/process-out-eval.out
similarity index 100%
rename from docs/snippets/process-out-cmd.out
rename to docs/snippets/process-out-eval.out
diff --git a/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy b/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy
index b39c9ff6be..7396a5ab16 100644
--- a/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/ast/NextflowDSLImpl.groovy
@@ -27,7 +27,7 @@ import nextflow.script.BaseScript
 import nextflow.script.BodyDef
 import nextflow.script.IncludeDef
 import nextflow.script.TaskClosure
-import nextflow.script.TokenCmdCall
+import nextflow.script.TokenEvalCall
 import nextflow.script.TokenEnvCall
 import nextflow.script.TokenFileCall
 import nextflow.script.TokenPathCall
@@ -956,7 +956,7 @@ class NextflowDSLImpl implements ASTTransformation {
             def nested = methodCall.objectExpression instanceof MethodCallExpression
             log.trace "convert > output method: $methodName"
 
-            if( methodName in ['val','env','cmd','file','set','stdout','path','tuple'] && !nested ) {
+            if( methodName in ['val','env','eval','file','set','stdout','path','tuple'] && !nested ) {
                 // prefix the method name with the string '_out_'
                 methodCall.setMethod( new ConstantExpression('_out_' + methodName) )
                 fixMethodCall(methodCall)
@@ -1124,9 +1124,9 @@ class NextflowDSLImpl implements ASTTransformation {
                     return createX( TokenEnvCall, args )
                 }
 
-                if( methodCall.methodAsString == 'cmd' && withinTupleMethod ) {
+                if( methodCall.methodAsString == 'eval' && withinTupleMethod ) {
                     def args = (TupleExpression) varToStrX(methodCall.arguments)
-                    return createX( TokenCmdCall, args )
+                    return createX( TokenEvalCall, args )
                 }
 
                 /*
diff --git a/modules/nextflow/src/main/groovy/nextflow/exception/ProcessCommandException.groovy b/modules/nextflow/src/main/groovy/nextflow/exception/ProcessEvalException.groovy
similarity index 84%
rename from modules/nextflow/src/main/groovy/nextflow/exception/ProcessCommandException.groovy
rename to modules/nextflow/src/main/groovy/nextflow/exception/ProcessEvalException.groovy
index 333a720b87..a2babd2b2e 100644
--- a/modules/nextflow/src/main/groovy/nextflow/exception/ProcessCommandException.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/exception/ProcessEvalException.groovy
@@ -25,13 +25,13 @@ import groovy.transform.CompileStatic
  * @author Paolo Di Tommaso 
  */
 @CompileStatic
-class ProcessCommandException extends RuntimeException implements ShowOnlyExceptionMessage {
+class ProcessEvalException extends RuntimeException implements ShowOnlyExceptionMessage {
 
     String command
     String output
     int status
 
-    ProcessCommandException(String message, String command, String output, int status) {
+    ProcessEvalException(String message, String command, String output, int status) {
         super(message)
         this.command = command
         this.output = output
diff --git a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy
index 5e308263d3..dd296c1e0b 100644
--- a/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/executor/BashWrapperBuilder.groovy
@@ -175,38 +175,76 @@ class BashWrapperBuilder {
         }
     }
 
-    protected String getOutputEnvCaptureSnippet(List outEnvs, Map outCmds) {
+    /**
+     * Generate a Bash script to be appended to the task command script
+     * that takes care of capturing the process output environment variables
+     * and evaluation commands
+     *
+     * @param outEnvs
+     *      The list of environment variables names whose value need to be captured
+     * @param outEvals
+     *      The set of commands to be evaluated to determine the output value to be captured
+     * @return
+     *      The Bash script to capture the output environment and eval commands
+     */
+    protected String getOutputEnvCaptureSnippet(List outEnvs, Map outEvals) {
         // load the env template
-        def template = BashWrapperBuilder.class
+        final template = BashWrapperBuilder.class
             .getResourceAsStream('command-env.txt')
             .newReader()
-        def binding = Map.of('env_file', TaskRun.CMD_ENV)
-        def result = engine.render(template, binding)
-        // avoid nulls
+        final binding = Map.of('env_file', TaskRun.CMD_ENV)
+        final result = new StringBuilder()
+        result.append( engine.render(template, binding) )
+        appendOutEnv(result, outEnvs)
+        appendOutEval(result, outEvals)
+        return result.toString()
+    }
+
+    /**
+     * Render a Bash script to capture the one or more env variables
+     *
+     * @param result A {@link StringBuilder} instance to which append the result Bash script
+     * @param outEnvs The environment variables to be captured
+     */
+    protected void appendOutEnv(StringBuilder result, List outEnvs) {
         if( outEnvs==null )
             outEnvs = List.of()
-        if( outCmds==null )
-            outCmds = Map.of()
         // out env
         for( String key : outEnvs ) {
-            result += "#\n"
-            result += "echo $key=\"\${$key[@]}\" >> ${TaskRun.CMD_ENV}\n"
-            result += "echo /$key/ >> ${TaskRun.CMD_ENV}\n"
-        }
-        // out cmd
-        for( Map.Entry cmd : outCmds ) {
-            result += "#\n"
-            result += "nxf_catch STDOUT STDERR ${cmd.value}\n"
-            result += 'status=$?\n'
-            result += 'if [ $status -eq 0 ]; then\n'
-            result += "  echo $cmd.key=\"\$STDOUT\" >> ${TaskRun.CMD_ENV}\n"
-            result += "  echo /$cmd.key/=exit:0 >> ${TaskRun.CMD_ENV}\n"
-            result += 'else\n'
-            result += "  echo $cmd.key=\"\$STDERR\" >> ${TaskRun.CMD_ENV}\n"
-            result += "  echo /$cmd.key/=exit:\$status >> ${TaskRun.CMD_ENV}\n"
-            result += 'fi\n'
-        }
-        result.toString()
+            result << "#\n"
+            result << "echo $key=\"\${$key[@]}\" >> ${TaskRun.CMD_ENV}\n"
+            result << "echo /$key/ >> ${TaskRun.CMD_ENV}\n"
+        }
+    }
+
+    /**
+     * Render a Bash script to capture the result of one or more commands
+     * evaluated in the task script context
+     *
+     * @param result
+     *      A {@link StringBuilder} instance to which append the result Bash script
+     * @param outEvals
+     *      A {@link Map} of key-value pairs modeling the commands to be evaluated;
+     *      where the key represents the environment variable (name) holding the
+     *      resulting output, and the pair value represent the Bash command to be
+     *      evaluated.
+     */
+    protected void appendOutEval(StringBuilder result, Map outEvals) {
+        if( outEvals==null )
+            outEvals = Map.of()
+        // out eval
+        for( Map.Entry eval : outEvals ) {
+            result << "#\n"
+            result <<"nxf_eval_cmd STDOUT STDERR ${eval.value}\n"
+            result << 'status=$?\n'
+            result << 'if [ $status -eq 0 ]; then\n'
+            result << "  echo $eval.key=\"\$STDOUT\" >> ${TaskRun.CMD_ENV}\n"
+            result << "  echo /$eval.key/=exit:0 >> ${TaskRun.CMD_ENV}\n"
+            result << 'else\n'
+            result << "  echo $eval.key=\"\$STDERR\" >> ${TaskRun.CMD_ENV}\n"
+            result << "  echo /$eval.key/=exit:\$status >> ${TaskRun.CMD_ENV}\n"
+            result << 'fi\n'
+        }
     }
 
     protected String stageCommand(String stagingScript) {
@@ -255,12 +293,16 @@ class BashWrapperBuilder {
          */
         final interpreter = TaskProcessor.fetchInterpreter(script)
 
-        if( outputEnvNames || outputCommands ) {
+        /*
+         * append to the command script a prolog to capture the declared
+         * output environment (variable) and evaluation commands
+         */
+        if( outputEnvNames || outputEvals ) {
             if( !isBash(interpreter) && outputEnvNames )
-                throw new IllegalArgumentException("Process output of type env is only allowed with Bash process scripts -- Current interpreter: $interpreter")
-            if( !isBash(interpreter) && outputCommands )
-                throw new IllegalArgumentException("Process output of type cmd is only allowed with Bash process scripts -- Current interpreter: $interpreter")
-            script += getOutputEnvCaptureSnippet(outputEnvNames, outputCommands)
+                throw new IllegalArgumentException("Process output of type 'env' is only allowed with Bash process scripts -- Current interpreter: $interpreter")
+            if( !isBash(interpreter) && outputEvals )
+                throw new IllegalArgumentException("Process output of type 'eval' is only allowed with Bash process scripts -- Current interpreter: $interpreter")
+            script += getOutputEnvCaptureSnippet(outputEnvNames, outputEvals)
         }
 
         final binding = new HashMap(20)
diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy
index 6b54bb41b7..c1ef240388 100644
--- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskBean.groovy
@@ -73,7 +73,7 @@ class TaskBean implements Serializable, Cloneable {
 
     List outputEnvNames
 
-    Map outputCommands
+    Map outputEvals
 
     String beforeScript
 
@@ -146,7 +146,7 @@ class TaskBean implements Serializable, Cloneable {
 
         // stats
         this.outputEnvNames = task.getOutputEnvNames()
-        this.outputCommands = task.getOutputCommands()
+        this.outputEvals = task.getOutputEvals()
         this.statsEnabled = task.getProcessor().getSession().statsEnabled
 
         this.inputFiles = task.getInputFilesMap()
diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy
index 27c44447bb..3e332569ab 100644
--- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskProcessor.groovy
@@ -15,7 +15,6 @@
  */
 package nextflow.processor
 
-
 import static nextflow.processor.ErrorStrategy.*
 
 import java.lang.reflect.InvocationTargetException
@@ -62,7 +61,7 @@ import nextflow.exception.FailedGuardException
 import nextflow.exception.IllegalArityException
 import nextflow.exception.MissingFileException
 import nextflow.exception.MissingValueException
-import nextflow.exception.ProcessCommandException
+import nextflow.exception.ProcessEvalException
 import nextflow.exception.ProcessException
 import nextflow.exception.ProcessFailedException
 import nextflow.exception.ProcessRetryableException
@@ -87,7 +86,7 @@ import nextflow.script.ScriptType
 import nextflow.script.TaskClosure
 import nextflow.script.bundle.ResourcesBundle
 import nextflow.script.params.BaseOutParam
-import nextflow.script.params.CmdOutParam
+import nextflow.script.params.CmdEvalParam
 import nextflow.script.params.DefaultOutParam
 import nextflow.script.params.EachInParam
 import nextflow.script.params.EnvInParam
@@ -1083,7 +1082,7 @@ class TaskProcessor {
                     formatTaskError( message, error, task )
                     break
 
-                case ProcessCommandException:
+                case ProcessEvalException:
                     formatCommandError( message, error, task )
                     break
 
@@ -1158,7 +1157,7 @@ class TaskProcessor {
         return action
     }
 
-    final protected List formatCommandError( List message, ProcessCommandException error, TaskRun task) {
+    final protected List formatCommandError(List message, ProcessEvalException error, TaskRun task) {
         // compose a readable error message
         message << formatErrorCause(error)
 
@@ -1537,8 +1536,8 @@ class TaskProcessor {
                     collectOutEnvParam(task, (EnvOutParam)param, workDir)
                     break
 
-                case CmdOutParam:
-                    collectOutEnvParam(task, (CmdOutParam)param, workDir)
+                case CmdEvalParam:
+                    collectOutEnvParam(task, (CmdEvalParam)param, workDir)
                     break
 
                 case DefaultOutParam:
@@ -1558,7 +1557,7 @@ class TaskProcessor {
     protected void collectOutEnvParam(TaskRun task, BaseOutParam param, Path workDir) {
 
         // fetch the output value
-        final outCmds =  param instanceof CmdOutParam ? task.getOutputCommands() : null
+        final outCmds =  param instanceof CmdEvalParam ? task.getOutputEvals() : null
         final val = collectOutEnvMap(workDir,outCmds).get(param.name)
         if( val == null && !param.optional )
             throw new MissingValueException("Missing environment variable: $param.name")
@@ -1575,13 +1574,13 @@ class TaskProcessor {
      *
      * @param workDir
      *      The task work directory that contains the `.command.env` file
-     * @param outCommands
+     * @param outEvals
      *      A {@link Map} instance containing key-value pairs
      * @return
      */
     @CompileStatic
     @Memoized(maxCacheSize = 10_000)
-    protected Map collectOutEnvMap(Path workDir, Map outCommands) {
+    protected Map collectOutEnvMap(Path workDir, Map outEvals) {
         final env = workDir.resolve(TaskRun.CMD_ENV).text
         final result = new HashMap(50)
         Matcher matcher
@@ -1604,10 +1603,10 @@ class TaskProcessor {
                 // when exit status is defined and it is a non-zero, it should be interpreted
                 // as a failure of the execution of the output command; in this case the variable
                 // holds the std error message
-                if( outCommands!=null && status ) {
-                    final cmd = outCommands.get(current)
+                if( outEvals!=null && status ) {
+                    final cmd = outEvals.get(current)
                     final out = result[current]
-                    throw new ProcessCommandException("Unable to evaluate command output", cmd, out, status)
+                    throw new ProcessEvalException("Unable to evaluate output", cmd, out, status)
                 }
                 // reset current key
                 current = null
diff --git a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy
index 039f67f865..69e217291e 100644
--- a/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/processor/TaskRun.groovy
@@ -38,7 +38,7 @@ import nextflow.script.BodyDef
 import nextflow.script.ScriptType
 import nextflow.script.TaskClosure
 import nextflow.script.bundle.ResourcesBundle
-import nextflow.script.params.CmdOutParam
+import nextflow.script.params.CmdEvalParam
 import nextflow.script.params.EnvInParam
 import nextflow.script.params.EnvOutParam
 import nextflow.script.params.FileInParam
@@ -603,11 +603,11 @@ class TaskRun implements Cloneable {
      * where the key represents a environment variable name holding the command
      * output and the value the command the executed.
      */
-    Map getOutputCommands() {
-        final items = getOutputsByType(CmdOutParam)
+    Map getOutputEvals() {
+        final items = getOutputsByType(CmdEvalParam)
         final result = new LinkedHashMap(items.size())
-        for( CmdOutParam it : items.keySet() ) {
-            if( !it.name ) throw new IllegalStateException("Missing output command name - offending parameter: $it")
+        for( CmdEvalParam it : items.keySet() ) {
+            if( !it.name ) throw new IllegalStateException("Missing output eval name - offending parameter: $it")
             result.put(it.name, it.getTarget(context))
         }
         return result
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy
index 4a9120c060..67bac675a7 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/ProcessConfig.groovy
@@ -31,7 +31,7 @@ import nextflow.executor.BashWrapperBuilder
 import nextflow.processor.ConfigList
 import nextflow.processor.ErrorStrategy
 import nextflow.processor.TaskConfig
-import nextflow.script.params.CmdOutParam
+import nextflow.script.params.CmdEvalParam
 import nextflow.script.params.DefaultInParam
 import nextflow.script.params.DefaultOutParam
 import nextflow.script.params.EachInParam
@@ -589,12 +589,12 @@ class ProcessConfig implements Map, Cloneable {
                 .bind(obj)
     }
 
-    OutParam _out_cmd( Object obj ) {
-        new CmdOutParam(this).bind(obj)
+    OutParam _out_eval(Object obj ) {
+        new CmdEvalParam(this).bind(obj)
     }
 
-    OutParam _out_cmd( Map opts, Object obj ) {
-        new CmdOutParam(this)
+    OutParam _out_eval(Map opts, Object obj ) {
+        new CmdEvalParam(this)
             .setOptions(opts)
             .bind(obj)
     }
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy b/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy
index 265c2eca27..72498b2452 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/ScriptTokens.groovy
@@ -113,13 +113,13 @@ class TokenEnvCall {
  * Token used by the DSL to identify a command output declaration, like this
  *     
  *     input:
- *     tuple( cmd(X), ... )
+ *     tuple( eval(X), ... )
  *     
  */
 @ToString
 @EqualsAndHashCode
 @TupleConstructor
-class TokenCmdCall {
+class TokenEvalCall {
     Object val
 }
 
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/CmdEvalParam.groovy
similarity index 90%
rename from modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy
rename to modules/nextflow/src/main/groovy/nextflow/script/params/CmdEvalParam.groovy
index 0ee822affd..786f3fbaea 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/params/CmdOutParam.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/params/CmdEvalParam.groovy
@@ -23,12 +23,12 @@ import groovy.transform.InheritConstructors
 import groovy.transform.Memoized
 
 /**
- * Model process `output: cmd PARAM` definition
+ * Model process `output: eval PARAM` definition
  *
  * @author Paolo Di Tommaso 
  */
 @InheritConstructors
-class CmdOutParam extends BaseOutParam implements OptionalParam {
+class CmdEvalParam extends BaseOutParam implements OptionalParam {
 
     private static AtomicInteger counter = new AtomicInteger()
 
@@ -41,7 +41,7 @@ class CmdOutParam extends BaseOutParam implements OptionalParam {
     }
 
     String getName() {
-        return "nxf_out_cmd_${count}"
+        return "nxf_out_eval_${count}"
     }
 
     BaseOutParam bind( def obj ) {
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy
index 17aa11b0ed..d58a97f925 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleInParam.groovy
@@ -17,8 +17,7 @@
 package nextflow.script.params
 
 import groovy.transform.InheritConstructors
-import nextflow.NF
-import nextflow.script.TokenCmdCall
+import nextflow.script.TokenEvalCall
 import nextflow.script.TokenEnvCall
 import nextflow.script.TokenFileCall
 import nextflow.script.TokenPathCall
@@ -77,7 +76,7 @@ class TupleInParam extends BaseInParam {
             else if( item instanceof TokenEnvCall ) {
                 newItem(EnvInParam).bind(item.val)
             }
-            else if( item instanceof TokenCmdCall ) {
+            else if( item instanceof TokenEvalCall ) {
                 throw new IllegalArgumentException('Command input declaration is not supported')
             }
             else if( item instanceof TokenStdinCall ) {
diff --git a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy
index c33d1d44f5..91df753b8c 100644
--- a/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy
+++ b/modules/nextflow/src/main/groovy/nextflow/script/params/TupleOutParam.groovy
@@ -17,7 +17,7 @@
 package nextflow.script.params
 
 import groovy.transform.InheritConstructors
-import nextflow.script.TokenCmdCall
+import nextflow.script.TokenEvalCall
 import nextflow.script.TokenEnvCall
 import nextflow.script.TokenFileCall
 import nextflow.script.TokenPathCall
@@ -59,8 +59,8 @@ class TupleOutParam extends BaseOutParam implements OptionalParam {
             else if( item instanceof TokenEnvCall ) {
                 create(EnvOutParam).bind(item.val)
             }
-            else if( item instanceof TokenCmdCall ) {
-                create(CmdOutParam).bind(item.val)
+            else if( item instanceof TokenEvalCall ) {
+                create(CmdEvalParam).bind(item.val)
             }
             else if( item instanceof GString ) {
                 throw new IllegalArgumentException("Unqualified output path declaration is not allowed - replace `tuple \"$item\",..` with `tuple path(\"$item\"),..`")
diff --git a/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt b/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt
index 31371e2431..22906619df 100644
--- a/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt
+++ b/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt
@@ -23,12 +23,12 @@ cd "$NXF_TASK_WORKDIR"
 ##
 ## See solution 7 at https://stackoverflow.com/a/59592881/395921
 ##
-nxf_catch() {
+nxf_eval_cmd() {
     {
         IFS=$'\n' read -r -d '' "${1}";
         IFS=$'\n' read -r -d '' "${2}";
         (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
     } < <((printf '\0%s\0%d\0' "$(((({ shift 2; "${@}"; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
 }
-
+## reset/create command env file
 echo '' > {{env_file}}
diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy
index 7a635b4f22..9e925abb6d 100644
--- a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy
@@ -28,7 +28,7 @@ import nextflow.ISession
 import nextflow.Session
 import nextflow.exception.IllegalArityException
 import nextflow.exception.MissingFileException
-import nextflow.exception.ProcessCommandException
+import nextflow.exception.ProcessEvalException
 import nextflow.exception.ProcessException
 import nextflow.exception.ProcessUnrecoverableException
 import nextflow.executor.Executor
@@ -983,7 +983,7 @@ class TaskProcessorTest extends Specification {
         when:
         processor.collectOutEnvMap(workDir, [cmd_out_1: 'foo --this', cmd_out_2: 'bar --that'])
         then:
-        def e = thrown(ProcessCommandException)
+        def e = thrown(ProcessEvalException)
         e.message == 'Unable to evaluate command output'
         e.command == 'bar --that'
         e.output == 'This is an error message\nfor unknown reason'
diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdEvalParamTest.groovy
similarity index 76%
rename from modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy
rename to modules/nextflow/src/test/groovy/nextflow/script/params/CmdEvalParamTest.groovy
index 99bc3c048d..39cbc26322 100644
--- a/modules/nextflow/src/test/groovy/nextflow/script/params/CmdOutParamTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/script/params/CmdEvalParamTest.groovy
@@ -23,16 +23,16 @@ import test.Dsl2Spec
  *
  * @author Paolo Di Tommaso 
  */
-class CmdOutParamTest extends Dsl2Spec {
+class CmdEvalParamTest extends Dsl2Spec {
 
-    def 'should define env outputs' () {
+    def 'should define eval outputs' () {
         setup:
         def text = '''
             process hola {
               output:
-              cmd 'foo --version' 
-              cmd "$params.cmd --help"
-              cmd "$tool --test"  
+              eval 'foo --version' 
+              eval "$params.cmd --help"
+              eval "$tool --test"  
               
               /echo command/ 
             }
@@ -44,18 +44,18 @@ class CmdOutParamTest extends Dsl2Spec {
         def process = parseAndReturnProcess(text, binding)
 
         when:
-        def outs = process.config.getOutputs() as List
+        def outs = process.config.getOutputs() as List
 
         then:
         outs.size() == 3
         and:
-        outs[0].getName() =~ /nxf_out_cmd_\d+/
+        outs[0].getName() =~ /nxf_out_eval_\d+/
         outs[0].getTarget(binding) == 'foo --version'
         and:
-        outs[1].getName() =~ /nxf_out_cmd_\d+/
+        outs[1].getName() =~ /nxf_out_eval_\d+/
         outs[1].getTarget(binding) == 'bar --help'
         and:
-        outs[2].getName() =~ /nxf_out_cmd_\d+/
+        outs[2].getName() =~ /nxf_out_eval_\d+/
         outs[2].getTarget(binding) == 'other --test'
     }
 
diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
index e7e987c315..99a26caf72 100644
--- a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
@@ -207,13 +207,13 @@ class TupleOutParamTest extends Dsl2Spec {
         and:
         outs[0].inner.size() == 2
         and:
-        outs[0].inner[0] instanceof CmdOutParam
+        outs[0].inner[0] instanceof CmdEvalParam
         outs[0].inner[0].getName() =~ /nxf_out_cmd_\d+/
-        (outs[0].inner[0] as CmdOutParam).getTarget(binding) == 'this --one'
+        (outs[0].inner[0] as CmdEvalParam).getTarget(binding) == 'this --one'
         and:
-        outs[0].inner[1] instanceof CmdOutParam
+        outs[0].inner[1] instanceof CmdEvalParam
         outs[0].inner[1].getName() =~ /nxf_out_cmd_\d+/
-        (outs[0].inner[1] as CmdOutParam).getTarget(binding) == 'tool --two'
+        (outs[0].inner[1] as CmdEvalParam).getTarget(binding) == 'tool --two'
 
     }
 }
diff --git a/tests/checks/cmd-out.nf/.checks b/tests/checks/eval-out.nf/.checks
similarity index 100%
rename from tests/checks/cmd-out.nf/.checks
rename to tests/checks/eval-out.nf/.checks
diff --git a/tests/cmd-out.nf b/tests/eval-out.nf
similarity index 94%
rename from tests/cmd-out.nf
rename to tests/eval-out.nf
index b97ed3226e..91fb8ac5b7 100644
--- a/tests/cmd-out.nf
+++ b/tests/eval-out.nf
@@ -19,7 +19,7 @@ process foo {
     input:
     val shell
     output:
-    cmd "$shell --version", emit: shell_version
+    eval "$shell --version", emit: shell_version
     '''
     echo Hello
     '''

From c4c61826c4eee41cc384526d9526e13ba5dc0ce9 Mon Sep 17 00:00:00 2001
From: Paolo Di Tommaso 
Date: Sat, 10 Feb 2024 21:25:29 +0100
Subject: [PATCH 11/11] Fix failing tests

Signed-off-by: Paolo Di Tommaso 
---
 .../src/main/resources/nextflow/executor/command-env.txt  | 1 +
 .../nextflow/executor/BashWrapperBuilderTest.groovy       | 8 ++++----
 .../groovy/nextflow/processor/TaskProcessorTest.groovy    | 2 +-
 .../nextflow/script/params/TupleOutParamTest.groovy       | 8 ++++----
 4 files changed, 10 insertions(+), 9 deletions(-)

diff --git a/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt b/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt
index 22906619df..8a476122ed 100644
--- a/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt
+++ b/modules/nextflow/src/main/resources/nextflow/executor/command-env.txt
@@ -30,5 +30,6 @@ nxf_eval_cmd() {
         (IFS=$'\n' read -r -d '' _ERRNO_; return ${_ERRNO_});
     } < <((printf '\0%s\0%d\0' "$(((({ shift 2; "${@}"; echo "${?}" 1>&3-; } | tr -d '\0' 1>&4-) 4>&2- 2>&1- | tr -d '\0' 1>&4-) 3>&1- | exit "$(cat)") 4>&1-)" "${?}" 1>&2) 2>&1)
 }
+
 ## reset/create command env file
 echo '' > {{env_file}}
diff --git a/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy b/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy
index 29580acf18..1a084086fc 100644
--- a/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/executor/BashWrapperBuilderTest.groovy
@@ -1131,7 +1131,7 @@ class BashWrapperBuilderTest extends Specification {
             set +e
             cd "$NXF_TASK_WORKDIR"
             
-            nxf_catch() {
+            nxf_eval_cmd() {
                 {
                     IFS=$'\\n' read -r -d '' "${1}";
                     IFS=$'\\n' read -r -d '' "${2}";
@@ -1163,7 +1163,7 @@ class BashWrapperBuilderTest extends Specification {
             set +e
             cd "$NXF_TASK_WORKDIR"
             
-            nxf_catch() {
+            nxf_eval_cmd() {
                 {
                     IFS=$'\\n' read -r -d '' "${1}";
                     IFS=$'\\n' read -r -d '' "${2}";
@@ -1176,7 +1176,7 @@ class BashWrapperBuilderTest extends Specification {
             echo FOO="${FOO[@]}" >> .command.env
             echo /FOO/ >> .command.env
             #
-            nxf_catch STDOUT STDERR this --cmd
+            nxf_eval_cmd STDOUT STDERR this --cmd
             status=$?
             if [ $status -eq 0 ]; then
               echo THIS="$STDOUT" >> .command.env
@@ -1186,7 +1186,7 @@ class BashWrapperBuilderTest extends Specification {
               echo /THIS/=exit:$status >> .command.env
             fi
             #
-            nxf_catch STDOUT STDERR other --cmd
+            nxf_eval_cmd STDOUT STDERR other --cmd
             status=$?
             if [ $status -eq 0 ]; then
               echo THAT="$STDOUT" >> .command.env
diff --git a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy
index 9e925abb6d..751feeb03f 100644
--- a/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/processor/TaskProcessorTest.groovy
@@ -984,7 +984,7 @@ class TaskProcessorTest extends Specification {
         processor.collectOutEnvMap(workDir, [cmd_out_1: 'foo --this', cmd_out_2: 'bar --that'])
         then:
         def e = thrown(ProcessEvalException)
-        e.message == 'Unable to evaluate command output'
+        e.message == 'Unable to evaluate output'
         e.command == 'bar --that'
         e.output == 'This is an error message\nfor unknown reason'
         e.status == 100
diff --git a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
index 99a26caf72..65c9981a37 100644
--- a/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
+++ b/modules/nextflow/src/test/groovy/nextflow/script/params/TupleOutParamTest.groovy
@@ -179,12 +179,12 @@ class TupleOutParamTest extends Dsl2Spec {
         outs[0].inner[1].getName() == 'BAR'
     }
 
-    def 'should create tuple of cmd' () {
+    def 'should create tuple of eval' () {
         setup:
         def text = '''
             process hola {
               output:
-                tuple cmd('this --one'), cmd("$other --two")
+                tuple eval('this --one'), eval("$other --two")
               
               /echo command/ 
             }
@@ -208,11 +208,11 @@ class TupleOutParamTest extends Dsl2Spec {
         outs[0].inner.size() == 2
         and:
         outs[0].inner[0] instanceof CmdEvalParam
-        outs[0].inner[0].getName() =~ /nxf_out_cmd_\d+/
+        outs[0].inner[0].getName() =~ /nxf_out_eval_\d+/
         (outs[0].inner[0] as CmdEvalParam).getTarget(binding) == 'this --one'
         and:
         outs[0].inner[1] instanceof CmdEvalParam
-        outs[0].inner[1].getName() =~ /nxf_out_cmd_\d+/
+        outs[0].inner[1].getName() =~ /nxf_out_eval_\d+/
         (outs[0].inner[1] as CmdEvalParam).getTarget(binding) == 'tool --two'
 
     }