Closed HoffiMuc closed 1 year ago
Groups can't be repeated, but subcommands can. Use a subcommand instead of a group and you can have something like this:
$ ./tool --unrelatedOption person --name hoffi --age=30 person --name both --age=77 --opt=78 person --name jesus --opt=42
My goal is NOT to run()
a command consecutively, as I am transforming very big files,
I want to have some opts that are "globally" for "how" (e.g. case insensitive) to transform all the files,
PLUS multiple groups of options which tell "what" to do with different(!) parts/regions in each file.
So: for a region identified by A do X, for a region identified by B do Y, ...
As the files are very big, I don't want to run the subcommand x times for achieving all the x transformations, but give a set of transformations (plus the global options once) so I can do a "transform once"-run on each file, doing ALL the transformations to it which in turn are defined by the combined information in each group of options.
any way on how to achieve something like that?
addendum: how could I pass trailing arguments (the files to operate on) to ALL subcommand's execution??
Because if my subcommand has arguments val args: Set<String> by argument().multiple().unique()
It will "eat up" anything after the last option of the first subcommand as arguments (and not calling the subcommand a second time, but passing the 2nd 'person' as first arg to the subcommand)
example of what I have in mind:
$ kscript replaceInFiles \
--verbose --ignore-nonexisting --backup \
\
--region-start '^# START REPLACE Region 1' \
--region-end '^# END REPLACE Region 1' \
--replace '\d' "X" --replace 'ri' 'ir' \
\
--region-start '^# START REPLACE Region 2' \
--region-end '^# END REPLACE Region 2' \
--replace '^(\w+) ([A-Z]+)(.*)$' 'CHANGED $2' --replace 'regex' 'replaced by $2' \
\
~/tmp/original.txt ~/tmp/nonex.txt
I know, a bit more "invasive" ... but also would be way cool! :)
You don't have to do any processing in your run
; you can collect the info into your context object and process everything at the end.
But if you're fixed on not using subcommands, you'll have to collect the repeated options as arguments and group them yourself. Global options can be declared as usual.
You can't have arguments after a subcommand with multiple
arguments. There would be no way to know whether a token should belong to the parent or subcommand.
just to confirm if I correctly understood: (and if so, for others who come acoss this as reference)
for re-cooccuring OptionGroup
s you'd need
CliktCommand
that has a single OptionGroup
in it
and puts them in parents currentContext (or its own context to be fetched by subcommand name)CliktCommand(allowMultipleSubcommands = true)
CliktCommand
that actually uses the multiple gatherd OptionGroup
s
(which each of aboves CliktCommand
puts in the context)
by fetching them from the Clikt context (List or Map)so e.g.:
the gathering one OptionGroup
CliktCommand:
class UserOptGroup() : CliktCommand(name = "user") {
class UserOptions : OptionGroup() {
override fun toString(): String = "user($login, ${name.singleQuote()}, $age, $opt)"
val login by option().required()
val name by option()
val age by option().int()
val opt by option().int()
}
data class User(val login: String, val name: String?, val age: Int?, val opt: Int?) {
override fun toString(): String = "User('$login', ${name.singleQuote()}, $age, $opt)"
}
val userOptions by UserOptions().cooccurring()
val unrelatedOption by option("--unrelatedOption").flag()
override fun run() {
val parentCtxUsers: MutableMap<String, User> = currentContext.parent!!.findOrSetObject { mutableMapOf() }
userOptions?.let { parentCtxUsers[it.login] = User(it.login, it.name, it.age, it.opt) }
// debug output
echo("UserOptGroup run():")
echo(" unrelatedOption: '${unrelatedOption}'")
userOptions?.let { echo(" $it") } ?: echo(" no userOptions")
}
}
the actual CliktCommand using all of above's OptionGroup
s
class DoWithUsers() : CliktCommand(name = "doWithUsers") {
val users: MutableMap<String, UserOptGroup.User> = mutableMapOf() // set/filled in run() from parent CliktCommand context
val args: Set<String> by argument().multiple().unique().help("files to do replacement(s) in")
override fun run() {
val parentUsers = currentContext.parent!!.findObject<MutableMap<String, UserOptGroup.User>>()
parentUsers?.let { users.putAll(it) }
// debug output
if (users.isEmpty()) echo("no users!")
echo("DoWithUsers: ${users.size} users: ${users.entries.joinToStringSingleQuoted()}")
echo("DoWithUsers: args: ${args.joinToStringSingleQuoted()}")
}
}
btw: doing it this way the actual CliktCommand cannot be unit tested without both CliktCommands
or if connected via a parent CliktComand all three of them,
as fun CliktCommand.test(...)
only tests a CliktCommand in isolation of all other CliktCommands
(at least not without some dirty hardcoded inject the List/Map of OptionGroup's into it.)
or do you have a fancy idea for testing things like above's class DoWithUsers() : CliktCommand(name = "doWithUsers")
? :)
That's one way to do it, although you don't necessarily need the group. You can test each command individually if you use findOrSetObject
instead.
data class User(val login: String, val name: String?)
class UserCommand : CliktCommand() {
val login by option().required()
val name by option()
val users by findOrSetObject { mutableMapOf<String, User>() }
override fun run() {
users[login] = User(login, name)
}
}
class ReplaceCommand : CliktCommand() {
val users by findOrSetObject { mutableMapOf<String, User>() }
val args: Set<String> by argument().multiple().unique().help("files to do replacement(s) in")
override fun run() {
echo("users: $users")
echo("args: $args")
}
}
class Tool : CliktCommand(allowMultipleSubcommands = true) {
override fun run() {
// set the context object for subcommands
currentContext.obj = mutableMapOf<String, User>()
}
}
test
works with subcommands, you just have to register them like you normally would:
val command = Tool().subcommands(UserCommand(), ReplaceCommand())
val result = command.test(
"user --login a --name b user --login c replace file1 file2"
)
assertEquals(
result.output,
"""
users: {a=User(login=a, name=b), c=User(login=c, name=null)}
args: [file1, file2]
""".trimIndent()
)
docs for co-occurring-option-groups
Hi, would it be possible to have multiple cooccurring option groups?
val userOptions: List<UserOptions> by UserOptions().cooccurring().multiple()
on calling:
required()
option or anything that IS "part of" thatOptionGroup
would "start" a List entryrequired()
option starts a new List entry and options of thatOptionGroup
following it belong to that new oneOptionGroup
"seals" that List entryOptionGroup
also "seals" itand then be able to:
with result:
Would be cool (I hope it would be possible without any problems, but I'm not sure)