0

I have a CLI with multiple sub-commands, some of the sub-commands have an optional flag -f with which an input file can be specified, e.g.

@CommandLine.Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {

    @Option(names = ["-f", "--file"], description = ["Input file"])
    var filename: String? = null
    
    override fun run() {
       var content = read_file(filename)
    }
}


@CommandLine.Command(name = "query", description = ["Query something"])
class QueryUserCommand: Runnable {

    @Option(names = ["-f", "--file"], description = ["Input file"])
    var filename: String? = null
    
    override fun run() {
       var content = read_file(filename)
    }
}


The input file format can be different from command to command. Ideally, I'd like to parse the file automatically if it was specified as an argument. Also the file content can be different on each command (but will be a specific format, CSV or JSON).

For example I'd like to have something like this

data class First(val col1, val col2)

data class Second(val col1, val col2, val col3)

class CustomOption(// regular @Option parameters, targetClass=...) {
  // do generic file parsing
}


@CommandLine.Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {

    @CustomOption(names = ["-f", "--file"], description = ["Input file"], targetClass=First))
    var content: List<First> = emptyList()
    
    override fun run() {
       // content now contains the parse file
    }
}


@CommandLine.Command(name = "query", description = ["Query something"])
class QueryUserCommand: Runnable {

    @CustomOption(names = ["-f", "--file"], description = ["Input file"], targetClass=Second))
    var content: List<Second> = emptyList()
    
    override fun run() {
       // content now contains the parse file
    }
}

Would anyone have an idea if this is possible or how to do it?

wasp256
  • 5,943
  • 12
  • 72
  • 119

1 Answers1

1

To rephrase the question: how to do additional processing of input parameters during the parsing process rather than during the command execution?

(Note that the OP did not specify why this is desirable. I assume the goal is either to leverage picocli's error reporting, or to encapsulate the parsing logic somewhere for easier testing and reuse. The OP may want to expand on the underlying goal if the solution below is not satisfactory.)

One idea is to use picocli's custom parameter processing.

It is possible to specify a IParameterConsumer for an option that will process the parameter for that option.

So, for example when the user specifies get -f somefile, the custom parameter consumer will be responsible for processing the somefile argument. An implementation can look something like this:

// java implementation, sorry I am not that fluent in Kotlin...
class FirstConsumer implements IParameterConsumer {
    public void consumeParameters(Stack<String> args,
                                  ArgSpec argSpec,
                                  CommandSpec commandSpec) {
        if (args.isEmpty()) {
            throw new ParameterException(commandSpec.commandLine(),
                    "Missing required filename for option " +
                    ((OptionSpec) argSpec).longestName());
        }
        String arg = args.pop();
        First first = parseFile(new File(arg), commandSpec);
        List<String> list = argSpec.getValue();
        list.add(first);
    }

    private First parseFile(File file,
                            ArgSpec argSpec,
                            CommandSpec commandSpec) {
        if (!file.isReadable()) {
            throw new ParameterException(commandSpec.commandLine(),
                    "Cannot find or read file " + file + " for option " +
                    ((OptionSpec) argSpec).longestName());
        }
        // other validation...
        // parse file contents...
        // finally, return the result...
        return new First(...);
    }
}

Once the parameter consumer classes are defined, you can use them as follows:

@Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {

    @Option(names = ["-f", "--file"], description = ["Input file"],
                  parameterConsumer = FirstConsumer::class))
    var content: List<First> = emptyList()
    
    override fun run() {
       // content now contains the parsed file
    }
}
Remko Popma
  • 35,130
  • 11
  • 92
  • 114