-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Make it possible to pass boolean flags dynamically #7260
Description
Related problem
Suppose we have a command that wraps another command where we want to forward --push and --release boolean flags to the inner command called build-imp.
def build-imp [--push, --release] {
# This isn't a real example. Here we would actually do smth useful like building a docker image
# optionally in release mode and optionally pushing it to the remote registry
echo $"Doing a build with push: ($push) and release: ($release)"
}At the time of this writing, it isn't possible to do this naturally. The following doesn't work, because nu complains that there are excessive positional parameters $push and $release:
def build [--push, --release] {
build-imp --push $push --release $release
}Error message
Error: nu::parser::extra_positional (link)
× Extra positional argument.
╭─[entry #6:1:1]
1 │ def build [--push, --release] {
2 │ build-imp --push $push --release $release
· ──┬──
· ╰── extra positional argument
3 │ }
╰────
help: Usage: build-imp {flags}This also doesn't work, although it compiles. Any invocation of build (even with all flags unset) results in $push and $release arguments inside build-imp set to true.
def build [--push, --release] {
build-imp --push=$push --release=$release
}Demo of the variant higher
❯ build
Doing a build with push: true and release: true
❯ build --push
Doing a build with push: true and release: true
❯ build --push --release
Doing a build with push: true and release: true
❯ build --release
Doing a build with push: true and release: trueWorkarounds
Combinatorial explosion
Handle all possible combinations of all flags with an if-else chain. This solution preserves the desired command signature but requires a huge amount of boilerplate. This solution suffers from copy-paste exponentially (pow(2, number_of_flags)) with the total number of flags to forward. The situation is worsened if there are other arguments to pass other than the flags that will also need to be copy-pasted
def build [--push, --release] {
if $push and $release {
build-imp --push --release
} else if $push and not $release {
build-imp --push
} else if not $push and $release {
build-imp --release
} else {
build-imp
}
}A variation of the solution that uses a bit nicer grouping of combinations with bitflags
def build [--push, --release] {
let invocations = [
{ build-imp }
{ build-imp --push }
{ build-imp --release }
{ build-imp --release --push }
]
do ($invocations | get (($release | into int) bit-shl 1 bit-or ($push | into int)))
}Use non-boolean type for flags
UPD: there was found a better version of this workaround that allows you to use booleans, but doesn't enable a bare
--flagsyntax: #7260 (comment)
The downside of this approach is that we make the API uglier, although the boilerplate doesn't grow exponentially here. We can define the underlying command with int arguments, but also make a wrapper that provides the same nice boolean flag API and just converts its arguments to integers.
def build-imp-int [--push: int, --release: int] {
let $push = $push == 1
let $release = $release == 1
# ...
}
# If this function needs to be reused in a bunch of places where boolean flags API suffices,
# then we may define a simple wrapper around it that takes boolean flags and forwards
# them to the inner function with `int` flags, but that's also boilerplate, so not ideal:
def build-imp [--push, --release] {
build-imp-int --push ($push | into int) --release ($release | into int)
}Describe the solution you'd like
The easiest solution that may probably be implemented quickly is to allow using --flag=boolean_expression syntax for boolean flags. The equals sign (=) between the argument name and the boolean expression disambiguates that the argument following the boolean flag isn't a positional parameter.
Right now we can pass literally any expression using --flag=expression, but that results in the boolean $flag variable to be set to true unconditionally. Maybe this was the intended solution of this problem at all, but it doesn't work today. Maybe it's a bug then?
Describe alternatives you've considered
A more general solution to the problem may also encompass solving the problem of passing optional flags to external commands.
For example, today we can't do this:
let push = if condition { "--push" } else { null }
docker buildx build $push ...args...Because nutshell will complain that it can't convert null to string. If we were to use an empty string instead of null nushell would forward it to the external binary as a separate empty string positional parameter. So instead we have to do this today:
let args = [...args...]
let args = if condition { $args | prepend "--push" } else { $args }
docker buildx build $argsThis works because an array is spread to positional parameters when invoking an external binary from nushell, but it also looks like some kind of inconvenient dark magic.
A more general problem is omitting named parameters to commands. I think it deserves a special syntax like this:
docker buildx build (--push $push)? (--named-arg-with-value $value)?With the question mark operator appended nushell would omit the parameter if the value is false or null for example. This is quite a naive first-comes-to-mind solution. I bet there could be some limitations with this, so feel free to share better proposals.