Skip to content

Make it possible to pass boolean flags dynamically #7260

@Veetaha

Description

@Veetaha

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]
 1def build [--push, --release] {
 2build-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: truebuild --push
Doing a build with push: true and release: truebuild --push --release
Doing a build with push: true and release: truebuild --release
Doing a build with push: true and release: true

Workarounds

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 --flag syntax: #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 $args

This 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    category:enhancementNew feature or requestsemanticsPlaces where we should define/clarify nushell's semantics

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions