cue-lang / cue

The home of the CUE language! Validate and define text-based and dynamic configuration
https://cuelang.org
Apache License 2.0
5.06k stars 288 forks source link

Tooling: cycle error when splitting task output #1531

Open mvienne-orange opened 2 years ago

mvienne-orange commented 2 years ago

What version of CUE are you using (cue version)?

$ cue version
cue version v0.4.2 linux/amd64

Does this issue reproduce with the latest release?

Yes (v0.4.2)

What did you do?

cd $(mktemp -d)
(
cat <<EOF > some_tool.cue
package some_package

import (
    "encoding/yaml"
    "tool/cli"
    "tool/exec"
    "strings"
)

command: some_command1: {
    task: getent_hosts: {
        for host in ["google.com", "github.com", "stackoverflow.com"] {
            "\(host)": exec.Run & {
                cmd: ["getent", "hosts", host]
                stdout: string
            }
        }
    }
    output: [
        for host in ["google.com", "github.com", "stackoverflow.com"]
        let entries_lst = strings.Split(strings.TrimSuffix(task.getent_hosts[host].stdout, "\n"), "\n")
        for entry_ in entries_lst {
            entry: entry_
        },
    ]
    print: cli.Print & {
        text: yaml.Marshal(output)
    }
}

// Workaround I found : same as some_command_1, but with force_eval_some_task
command: some_command2: {
    task: getent_hosts: {
        for host in ["google.com", "github.com", "stackoverflow.com"] {
            "\(host)": exec.Run & {
                cmd: ["getent", "hosts", host]
                stdout: string
            }
        }
    }
    force_eval_some_task: {
        for k, v in task.getent_hosts {
            "\(k)": v.stdout
        }
    }
    output: [
        for host in ["google.com", "github.com", "stackoverflow.com"]
        let entries_lst = strings.Split(strings.TrimSuffix(force_eval_some_task[host], "\n"), "\n")
        for entry_ in entries_lst {
            entry: entry_
        },
    ]
    print: cli.Print & {
        text: yaml.Marshal(output)
    }
}
EOF

echo some_command1:
cue cmd some_command1
echo
echo some_command2:
cue cmd some_command2
)

What did you expect to see?

I expected some_command1 to be working the same as the workaround some_command2.

Expected output :

some_command1:
- entry: 2a00:1450:4007:813::200e google.com
- entry: 140.82.121.3    github.com
- entry: 151.101.129.69  stackoverflow.com
- entry: 151.101.1.69    stackoverflow.com
- entry: 151.101.193.69  stackoverflow.com
- entry: 151.101.65.69   stackoverflow.com

some_command2:
- entry: 2a00:1450:4007:813::200e google.com
- entry: 140.82.121.3    github.com
- entry: 151.101.129.69  stackoverflow.com
- entry: 151.101.1.69    stackoverflow.com
- entry: 151.101.193.69  stackoverflow.com
- entry: 151.101.65.69   stackoverflow.com

What did you see instead?

some_command1:
command.some_command1.print.text: invalid string argument: cycle error:
    ./some_tool.cue:26:2
    ./some_tool.cue:22:16
    ./some_tool.cue:27:3
    tool/cli:4:3

some_command2:
- entry: 2a00:1450:4007:813::200e google.com
- entry: 140.82.121.3    github.com
- entry: 151.101.129.69  stackoverflow.com
- entry: 151.101.1.69    stackoverflow.com
- entry: 151.101.193.69  stackoverflow.com
- entry: 151.101.65.69   stackoverflow.com
mvienne-orange commented 2 years ago

Interestingly, it seems that string.Split is causing the cycle error. Replacing this line :

let entries_lst = strings.Split(strings.TrimSuffix(task.getent_hosts[host].stdout, "\n"), "\n")

by this :

let entries_lst = [strings.TrimSuffix(task.getent_hosts[host].stdout, "\n")]

does not end up in a cycle error (but the output is not the same, obvisously).

Output :

some_command1:
- entry: 2a00:1450:4007:813::200e google.com
- entry: 140.82.121.3    github.com
- entry: |-
    151.101.65.69   stackoverflow.com
    151.101.129.69  stackoverflow.com
    151.101.193.69  stackoverflow.com
    151.101.1.69    stackoverflow.com

some_command2:
- entry: 2a00:1450:4007:813::200e google.com
- entry: 140.82.121.3    github.com
- entry: 151.101.65.69   stackoverflow.com
- entry: 151.101.129.69  stackoverflow.com
- entry: 151.101.193.69  stackoverflow.com
- entry: 151.101.1.69    stackoverflow.com
mvienne-orange commented 2 years ago

The problem also does not happen if there is no loop to create the tasks :

command: some_command3: {
    task: getent_hosts_stackoverflow: exec.Run & {
        cmd: ["getent", "hosts", "stackoverflow.com"]
        stdout: string
    }
    output: [
        for host in ["stackoverflow.com"]
        let entries_lst = strings.Split(strings.TrimSuffix(task.getent_hosts_stackoverflow.stdout, "\n"), "\n")
        for entry_ in entries_lst {
            entry: entry_
        },
    ]
    print: cli.Print & {
        text: yaml.Marshal(output)
    }
}
cue some_command3
- entry: 151.101.1.69    stackoverflow.com
- entry: 151.101.129.69  stackoverflow.com
- entry: 151.101.193.69  stackoverflow.com
- entry: 151.101.65.69   stackoverflow.com
rogpeppe commented 2 years ago

Here's a slightly simpler reproducer in testscript form:

exec cue cmd some_command1

-- in_tool.cue --

package some_package

import (
    "encoding/yaml"
    "tool/cli"
    "tool/exec"
    "strings"
)

command: some_command1: {
    task: getent_hosts: {
        for host in ["google.com"] {
            "\(host)": exec.Run & {
                cmd: ["echo"]
                stdout: string
            }
        }
    }
    output: [
        for host in ["google.com"]
        for entry_ in strings.TrimSuffix(task.getent_hosts[host].stdout, "\n") {
            entry: entry_
        },
    ]
    print: cli.Print & {
        text: yaml.Marshal(output)
    }
}
rogpeppe commented 2 years ago

Note that I don't see a cycle error, but an incomplete error instead:

command.some_command1.print.text: invalid string argument: error in call to strings.TrimSuffix: non-concrete value string:
    ./in_tool.cue:26:2
    ./in_tool.cue:16:13
    ./in_tool.cue:22:17
    ./in_tool.cue:27:3
    tool/cli:4:3