Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion cmd/limactl/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"reflect"
Expand Down Expand Up @@ -57,6 +58,14 @@ The output can be presented in one of several formats, using the --format <forma
--format yaml - Output in YAML format
--format table - Output in table format
--format '{{ <go template> }}' - If the format begins and ends with '{{ }}', then it is used as a go template.

Filtering instances:
--filter EXPR - Filter instances using yq expression (this is equivalent to --yq 'select(EXPR)')
Can be specified multiple times and it works with all output formats.
Examples:
--filter '.status == "Running"'
--filter '.vmType == "vz"'
--filter '.status == "Running"' --filter '.vmType == "vz"'
` + store.FormatHelp + `
The following legacy flags continue to function:
--json - equal to '--format json'`,
Expand All @@ -72,6 +81,7 @@ The following legacy flags continue to function:
listCommand.Flags().BoolP("quiet", "q", false, "Only show names")
listCommand.Flags().Bool("all-fields", false, "Show all fields")
listCommand.Flags().StringArray("yq", nil, "Apply yq expression to each instance")
listCommand.Flags().StringArrayP("filter", "l", nil, "Filter instances using yq expression (equivalent to --yq 'select(EXPR)')")

return listCommand
}
Expand Down Expand Up @@ -121,6 +131,10 @@ func listAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
filter, err := cmd.Flags().GetStringArray("filter")
if err != nil {
return err
}

if jsonFormat {
format = "json"
Expand All @@ -141,6 +155,14 @@ func listAction(cmd *cobra.Command, args []string) error {
return errors.New("option --list-fields conflicts with option --yq")
}
}
if len(filter) != 0 {
if listFields {
return errors.New("option --list-fields conflicts with option --filter")
}
if len(yq) != 0 {
return errors.New("option --filter conflicts with option --yq")
}
Comment on lines +162 to +164
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should be able to combine --filter and --yq as long as the output format is json or yaml.

So you could just drop the check here because yq is already incompatible with the other output formats.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't too clear to me but is there anything wrong with the way it currently is?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes:

l ls -l '.status == "Running"'
NAME          STATUS     SSH                VMTYPE    ARCH       CPUS    MEMORY    DISK      DIR
alpine-iso    Running    127.0.0.1:50496    vz        aarch64    4       4GiB      100GiB    ~/.lima/alpine-isol ls -l '.status == "Running"' --yq .name
FATA[0000] option --filter conflicts with option --yql ls --yq 'select(.status == "Running")' --yq .name
alpine-iso

I expect the last 2 commands to work exactly the same. There is no reason for the len(yq) check to exist.

}

if quiet && format != "table" {
return errors.New("option --quiet can only be used with '--format table'")
Expand Down Expand Up @@ -220,15 +242,31 @@ func listAction(cmd *cobra.Command, args []string) error {
options.TerminalWidth = w
}
}
// --yq implies --format json unless --format yaml has been explicitly specified

// --yq implies --format json unless --format has been explicitly specified
if len(yq) != 0 && !cmd.Flags().Changed("format") {
format = "json"
}

// Always pipe JSON and YAML through yq to colorize it if isTTY
if len(yq) == 0 && (format == "json" || format == "yaml") {
yq = append(yq, ".")
}

for _, f := range filter {
yq = append(yq, "select("+f+")")
}

// handle --filter with table and go-template formats
isGoTemplate := strings.HasPrefix(format, "{{") && strings.HasSuffix(format, "}}")
if len(filter) != 0 && (format == "table" || isGoTemplate) {
Comment on lines +261 to +262
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
isGoTemplate := strings.HasPrefix(format, "{{") && strings.HasSuffix(format, "}}")
if len(filter) != 0 && (format == "table" || isGoTemplate) {
if len(filter) != 0 && format != "json" && format != "yaml") {

Despite what the --help output says, every string except table, json, and yaml is treated as a Go template, so the check for the double braces prefix/suffix is not correct:

l ls -f 'Name is: {{.Name}}'
Name is: alpine-iso
Name is: alpine-lima-3.22.2
Name is: foo

instances, err = filterInstances(instances, yq)
if err != nil {
return err
}
yq = nil
}

if len(yq) == 0 {
err = store.PrintInstances(cmd.OutOrStdout(), instances, format, &options)
if err == nil && unmatchedInstances {
Expand Down Expand Up @@ -320,3 +358,31 @@ func listAction(cmd *cobra.Command, args []string) error {
func listBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}

// filterInstances applies yq expressions to instances and returns the filtered results.
func filterInstances(instances []*limatype.Instance, yqExprs []string) ([]*limatype.Instance, error) {
if len(yqExprs) == 0 {
return instances, nil
}

yqExpr := strings.Join(yqExprs, " | ")

var filteredInstances []*limatype.Instance
for _, instance := range instances {
jsonBytes, err := json.Marshal(instance)
if err != nil {
return nil, fmt.Errorf("failed to marshal instance %s: %w", instance.Name, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil, fmt.Errorf("failed to marshal instance %s: %w", instance.Name, err)
return nil, fmt.Errorf("failed to marshal instance %q: %w", instance.Name, err)

Always use %q when printing use input.

Also applies to the next error message.

}

result, err := yqutil.EvaluateExpression(yqExpr, jsonBytes)
if err != nil {
return nil, fmt.Errorf("failed to apply filter %s: %w", yqExpr, err)
}

if len(result) > 0 {
filteredInstances = append(filteredInstances, instance)
}
}

return filteredInstances, nil
}
23 changes: 23 additions & 0 deletions hack/bats/tests/list.bats
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,26 @@ local_setup() {
run -0 limactl ls --quiet --yq 'select(.name == "foo")'
assert_output "foo"
}

@test '--filter option filters instances' {
run -0 limactl ls --filter '.name == "foo"'
assert_line --index 0 --regexp '^NAME'
assert_line --index 1 --regexp '^foo'
assert_output_lines_count 2
}

@test '--filter option works with all output formats' {
run -0 limactl ls --filter '.name == "foo"'
assert_line --index 1 --regexp '^foo'

run -0 limactl ls --filter '.name == "foo"' --format json
assert_line --index 0 --regexp '^\{"name":"foo",'

run -0 limactl ls --filter '.name == "foo"' --format '{{.Name}}'
assert_output "foo"
}

@test '--filter option is incompatible with --yq' {
run_e -1 limactl ls --filter '.name == "foo"' --yq '.name'
assert_fatal "option --filter conflicts with option --yq"
}
Loading