Skip to content

Commit b5eb83b

Browse files
committed
FIx issues with multiple positional args
Signed-off-by: Ben Sherman <[email protected]>
1 parent e20788b commit b5eb83b

File tree

4 files changed

+243
-45
lines changed

4 files changed

+243
-45
lines changed

modules/nextflow/src/main/groovy/nextflow/cli/v2/PluginCmd.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ class PluginCmd extends AbstractCmd {
4141
@ParentCommand
4242
private Launcher launcher
4343

44-
@Parameters
44+
@Parameters(index = '0')
4545
String command
4646

47-
@Parameters
47+
@Parameters(index = '1..*')
4848
List<String> args
4949

5050
@Override

modules/nextflow/src/main/groovy/nextflow/cli/v2/RunCmd.groovy

Lines changed: 75 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ class RunCmd extends AbstractCmd implements RunImpl.Options, HubOptions {
5252
@ParentCommand
5353
private Launcher launcher
5454

55-
@Parameters(description = 'Project name or repository url')
55+
@Parameters(index = '0', description = 'Project name or repository url')
5656
String pipeline
5757

58-
@Parameters(description = 'Pipeline script args')
58+
@Parameters(index = '1..*', description = 'Pipeline script args')
5959
List<String> args
6060

6161
@Option(names = ['--ansi-log'], arity = '1', description = 'Use ANSI logging')
@@ -224,54 +224,88 @@ class RunCmd extends AbstractCmd implements RunImpl.Options, HubOptions {
224224
@Option(names = ['--with-weblog'], arity = '0..1', fallbackValue = '-', description = 'Send workflow status messages via HTTP to target URL')
225225
String withWebLog
226226

227-
@Parameters(description = 'Pipeline parameters')
228-
List<String> params
227+
private List<String> pipelineArgs = null
229228

230-
private Map<String,String> paramsMap = null
229+
private Map<String,String> pipelineParams = null
231230

232231
/**
233-
* Get the pipeline params as a map.
232+
* Parse the pipeline args and params from the positional
233+
* args parsed by picocli. This method assumes that the first
234+
* positional arg that starts with '--' is the first param,
235+
* and parses the remaining args as params.
234236
*
235-
* The double-dash ('--') notation is normally used to separate
236-
* positional parameters from options. As a result, params will also
237-
* contain the positional parameters of the `run` command (i.e. args),
238-
* so they must be skipped when constructing the params map.
239-
*
240-
* This method assumes that params are specified as option-value pairs
241-
* separated by a space. The equals-sign ('=') separator is not supported.
237+
* NOTE: While the double-dash ('--') notation can be used to
238+
* distinguish pipeline params from CLI options, it cannot be
239+
* used to distinguish pipeline params from pipeline args.
242240
*/
243-
@Override
244-
Map<String,String> getParams() {
245-
if( paramsMap == null ) {
246-
paramsMap = [:]
247-
248-
int i = args.size()
249-
while( i < params.size() ) {
250-
String current = params[i++]
251-
252-
String key
253-
String value
254-
if( current.contains('=') ) {
255-
int split = current.indexOf('=')
256-
key = current.substring(0, split)
257-
value = current.substring(split+1)
258-
}
259-
else if( i < params.size() && !params[i].startsWith('--') ) {
260-
key = current
261-
value = params[i++]
262-
}
263-
else {
264-
key = current
265-
value = 'true'
266-
}
267-
268-
paramsMap.put(key, value)
241+
private void parseArgs() {
242+
// parse pipeline args
243+
int i = args.findIndexOf { it.startsWith('--') }
244+
pipelineArgs = args[0..<i]
245+
246+
// parse pipeline params
247+
pipelineParams = [:]
248+
249+
if( i == -1 )
250+
return
251+
252+
while( i < args.size() ) {
253+
String current = args[i++]
254+
if( !current.startsWith('--') ) {
255+
throw new IllegalArgumentException("Invalid argument '${current}' -- unable to parse it as a pipeline arg, pipeline param, or CLI option")
256+
}
257+
258+
String key
259+
String value
260+
261+
// parse '--param=value'
262+
if( current.contains('=') ) {
263+
int split = current.indexOf('=')
264+
key = current.substring(2, split)
265+
value = current.substring(split+1)
266+
}
267+
268+
// parse '--param value'
269+
else if( i < args.size() && !args[i].startsWith('--') ) {
270+
key = current.substring(2)
271+
value = args[i++]
269272
}
270273

271-
log.trace "Parsing params from CLI: $paramsMap"
274+
// parse '--param1 --param2 ...' as '--param1 true --param2 ...'
275+
else {
276+
key = current.substring(2)
277+
value = 'true'
278+
}
279+
280+
pipelineParams.put(key, value)
281+
}
282+
283+
log.trace "Parsing pipeline args from CLI: $pipelineArgs"
284+
log.trace "Parsing pipeline params from CLI: $pipelineParams"
285+
}
286+
287+
/**
288+
* Get the list of pipeline args.
289+
*/
290+
@Override
291+
List<String> getArgs() {
292+
if( pipelineArgs == null ) {
293+
parseArgs()
294+
}
295+
296+
return pipelineArgs
297+
}
298+
299+
/**
300+
* Get the map of pipeline params.
301+
*/
302+
@Override
303+
Map<String,String> getParams() {
304+
if( pipelineParams == null ) {
305+
parseArgs()
272306
}
273307

274-
return paramsMap
308+
return pipelineParams
275309
}
276310

277311
@Override

modules/nextflow/src/test/groovy/nextflow/cli/v1/LauncherTest.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ class LauncherTest extends Specification {
4545
then:
4646
assert launcher.options.fullVersion
4747

48-
4948
}
5049

5150
def 'should return `help` command' () {
@@ -135,9 +134,11 @@ class LauncherTest extends Specification {
135134
launcher.command.hubProvider == 'github'
136135

137136
when:
138-
launcher = new Launcher().parseMainArgs('run', 'script.nf', '--alpha', '0', '--omega', '9')
137+
launcher = new Launcher().parseMainArgs('run', 'script.nf', 'arg1', 'arg2', '--alpha', '0', '--omega', '9')
139138
then:
140139
launcher.command instanceof RunCmd
140+
launcher.command.pipeline == 'script.nf'
141+
launcher.command.args == ['arg1', 'arg2']
141142
launcher.command.params.'alpha' == '0'
142143
launcher.command.params.'omega' == '9'
143144

@@ -357,4 +358,5 @@ class LauncherTest extends Specification {
357358
String opt4
358359

359360
}
361+
360362
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright 2023, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.cli.v2
18+
19+
import java.nio.file.Files
20+
21+
import picocli.CommandLine
22+
import spock.lang.Specification
23+
24+
/**
25+
*
26+
* @author Ben Sherman <[email protected]>
27+
*/
28+
class LauncherTest extends Specification {
29+
30+
def parseArgs(String... args) {
31+
def launcher = new Launcher()
32+
def cmd = new CommandLine(launcher)
33+
cmd.parseArgs(args)
34+
35+
return launcher
36+
}
37+
38+
def getCommand(Launcher launcher) {
39+
launcher.spec.commandLine().getParseResult().subcommand().commandSpec().commandLine().getCommand()
40+
}
41+
42+
def getArgs(Launcher launcher) {
43+
launcher.spec.commandLine().getParseResult().originalArgs()
44+
}
45+
46+
47+
def 'should return `version` option' () {
48+
49+
when:
50+
def launcher = parseArgs('-v')
51+
then:
52+
assert launcher.options.version
53+
54+
when:
55+
launcher = parseArgs('--version')
56+
then:
57+
assert launcher.options.fullVersion
58+
59+
}
60+
61+
def 'should return `info` command'() {
62+
63+
when:
64+
def launcher = parseArgs('info')
65+
def command = getCommand(launcher)
66+
then:
67+
command instanceof InfoCmd
68+
command.pipeline == null
69+
70+
when:
71+
launcher = parseArgs('info', 'xxx')
72+
command = getCommand(launcher)
73+
then:
74+
command instanceof InfoCmd
75+
command.pipeline == 'xxx'
76+
77+
}
78+
79+
def 'should return `pull` command'() {
80+
81+
when:
82+
def launcher = parseArgs('pull', 'alpha')
83+
def command = getCommand(launcher)
84+
then:
85+
command instanceof PullCmd
86+
command.pipeline == 'alpha'
87+
88+
when:
89+
launcher = parseArgs('pull', 'xxx', '--hub', 'bitbucket', '--user', 'xx:11')
90+
command = getCommand(launcher)
91+
then:
92+
command instanceof PullCmd
93+
command.pipeline == 'xxx'
94+
command.hubProvider == 'bitbucket'
95+
command.hubUser == 'xx'
96+
command.hubPassword == '11'
97+
98+
}
99+
100+
def 'should return `clone` command'() {
101+
when:
102+
def launcher = parseArgs('clone', 'xxx', 'yyy')
103+
def command = getCommand(launcher)
104+
then:
105+
command instanceof CloneCmd
106+
command.pipeline == 'xxx'
107+
command.targetName == 'yyy'
108+
109+
when:
110+
launcher = parseArgs('clone', 'xxx', '--hub', 'bitbucket', '--user', 'xx:yy')
111+
command = getCommand(launcher)
112+
then:
113+
command instanceof CloneCmd
114+
command.pipeline == 'xxx'
115+
command.targetName == null
116+
command.hubProvider == 'bitbucket'
117+
command.hubUser == 'xx'
118+
command.hubPassword == 'yy'
119+
}
120+
121+
def 'should return `run` command'() {
122+
when:
123+
def launcher = parseArgs('run', 'xxx', '--hub', 'bitbucket', '--user', 'xx:yy')
124+
def command = getCommand(launcher)
125+
then:
126+
command instanceof RunCmd
127+
command.pipeline == 'xxx'
128+
command.hubProvider == 'bitbucket'
129+
command.hubUser == 'xx'
130+
command.hubPassword == 'yy'
131+
132+
when:
133+
launcher = parseArgs('run', 'alpha', '--hub', 'github')
134+
command = getCommand(launcher)
135+
then:
136+
command instanceof RunCmd
137+
command.pipeline == 'alpha'
138+
command.hubProvider == 'github'
139+
140+
when:
141+
launcher = parseArgs('run', 'script.nf', 'arg1', 'arg2', '--', '--alpha', '0', '--omega', '9')
142+
command = getCommand(launcher)
143+
then:
144+
command instanceof RunCmd
145+
command.pipeline == 'script.nf'
146+
command.args == ['arg1', 'arg2']
147+
command.params.'alpha' == '0'
148+
command.params.'omega' == '9'
149+
150+
}
151+
152+
def 'should make cli' () {
153+
given:
154+
def launcher = new Launcher()
155+
expect:
156+
launcher.makeCli('nextflow', 'run', 'foo.nf') == 'nextflow run foo.nf'
157+
launcher.makeCli('nextflow', 'run', 'foo.nf', '*.txt') == "nextflow run foo.nf '*.txt'"
158+
launcher.makeCli('/this/that/nextflow run foo.nf', 'run', 'foo.nf', 'a{1,2}.z') == "nextflow run foo.nf 'a{1,2}.z'"
159+
launcher.makeCli('/this/that/launch run bar.nf', 'run', 'bar.nf') == '/this/that/launch run bar.nf'
160+
}
161+
162+
}

0 commit comments

Comments
 (0)