Skip to content

Commit c7631d4

Browse files
committed
add filter option to list command
Signed-off-by: olalekan odukoya <odukoyaonline@gmail.com>
1 parent 67edfc2 commit c7631d4

File tree

2 files changed

+107
-1
lines changed

2 files changed

+107
-1
lines changed

cmd/limactl/list.go

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package main
66
import (
77
"bufio"
88
"bytes"
9+
"encoding/json"
910
"errors"
1011
"fmt"
1112
"reflect"
@@ -57,6 +58,14 @@ The output can be presented in one of several formats, using the --format <forma
5758
--format yaml - Output in YAML format
5859
--format table - Output in table format
5960
--format '{{ <go template> }}' - If the format begins and ends with '{{ }}', then it is used as a go template.
61+
62+
Filtering instances:
63+
--filter EXPR - Filter instances using yq expression (this is equivalent to --yq 'select(EXPR)')
64+
Can be specified multiple times and it works with all output formats.
65+
Examples:
66+
--filter '.status == "Running"'
67+
--filter '.vmType == "vz"'
68+
--filter '.status == "Running"' --filter '.vmType == "vz"'
6069
` + store.FormatHelp + `
6170
The following legacy flags continue to function:
6271
--json - equal to '--format json'`,
@@ -72,6 +81,7 @@ The following legacy flags continue to function:
7281
listCommand.Flags().BoolP("quiet", "q", false, "Only show names")
7382
listCommand.Flags().Bool("all-fields", false, "Show all fields")
7483
listCommand.Flags().StringArray("yq", nil, "Apply yq expression to each instance")
84+
listCommand.Flags().StringArray("filter", nil, "Filter instances using yq expression (equivalent to --yq 'select(EXPR)')")
7585

7686
return listCommand
7787
}
@@ -121,6 +131,10 @@ func listAction(cmd *cobra.Command, args []string) error {
121131
if err != nil {
122132
return err
123133
}
134+
filter, err := cmd.Flags().GetStringArray("filter")
135+
if err != nil {
136+
return err
137+
}
124138

125139
if jsonFormat {
126140
format = "json"
@@ -141,6 +155,14 @@ func listAction(cmd *cobra.Command, args []string) error {
141155
return errors.New("option --list-fields conflicts with option --yq")
142156
}
143157
}
158+
if len(filter) != 0 {
159+
if listFields {
160+
return errors.New("option --list-fields conflicts with option --filter")
161+
}
162+
if len(yq) != 0 {
163+
return errors.New("option --filter conflicts with option --yq")
164+
}
165+
}
144166

145167
if quiet && format != "table" {
146168
return errors.New("option --quiet can only be used with '--format table'")
@@ -220,15 +242,36 @@ func listAction(cmd *cobra.Command, args []string) error {
220242
options.TerminalWidth = w
221243
}
222244
}
245+
if len(filter) != 0 {
246+
for _, f := range filter {
247+
yq = append(yq, "select("+f+")")
248+
}
249+
}
250+
223251
// --yq implies --format json unless --format yaml has been explicitly specified
224-
if len(yq) != 0 && !cmd.Flags().Changed("format") {
252+
// But --filter should preserve the original format
253+
if len(yq) != 0 && !cmd.Flags().Changed("format") && len(filter) == 0 {
225254
format = "json"
226255
}
227256
// Always pipe JSON and YAML through yq to colorize it if isTTY
228257
if len(yq) == 0 && (format == "json" || format == "yaml") {
229258
yq = append(yq, ".")
230259
}
231260

261+
// handle --filter with table and go-template formats
262+
tmpl := strings.HasPrefix(format, "{{") && strings.HasSuffix(format, "}}")
263+
if len(filter) != 0 && (format == "table" || tmpl) {
264+
filteredInstances, err := filterInstances(instances, filter)
265+
if err != nil {
266+
return err
267+
}
268+
err = store.PrintInstances(cmd.OutOrStdout(), filteredInstances, format, &options)
269+
if err == nil && unmatchedInstances {
270+
return unmatchedInstancesError{}
271+
}
272+
return err
273+
}
274+
232275
if len(yq) == 0 {
233276
err = store.PrintInstances(cmd.OutOrStdout(), instances, format, &options)
234277
if err == nil && unmatchedInstances {
@@ -320,3 +363,43 @@ func listAction(cmd *cobra.Command, args []string) error {
320363
func listBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
321364
return bashCompleteInstanceNames(cmd)
322365
}
366+
367+
// filterInstances applies yq filter expressions to instances and returns the filtered results.
368+
func filterInstances(instances []*limatype.Instance, filters []string) ([]*limatype.Instance, error) {
369+
if len(filters) == 0 {
370+
return instances, nil
371+
}
372+
373+
var jsonLines []string
374+
for _, instance := range instances {
375+
jsonBytes, err := json.Marshal(instance)
376+
if err != nil {
377+
return nil, fmt.Errorf("failed to marshal instance %s: %w", instance.Name, err)
378+
}
379+
jsonLines = append(jsonLines, string(jsonBytes))
380+
}
381+
382+
var yqExprs []string
383+
for _, filter := range filters {
384+
yqExprs = append(yqExprs, "select("+filter+")")
385+
}
386+
yqExpr := strings.Join(yqExprs, " | ")
387+
388+
var filteredInstances []*limatype.Instance
389+
for _, jsonLine := range jsonLines {
390+
result, err := yqutil.EvaluateExpression(yqExpr, []byte(jsonLine))
391+
if err != nil {
392+
return nil, fmt.Errorf("failed to apply filter %s: %w", yqExpr, err)
393+
}
394+
395+
if len(result) > 0 && string(result) != "null" {
396+
var instance limatype.Instance
397+
if err := json.Unmarshal(result, &instance); err != nil {
398+
return nil, fmt.Errorf("failed to unmarshal filtered instance: %w", err)
399+
}
400+
filteredInstances = append(filteredInstances, &instance)
401+
}
402+
}
403+
404+
return filteredInstances, nil
405+
}

hack/bats/tests/list.bats

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,26 @@ local_setup() {
264264
run -0 limactl ls --quiet --yq 'select(.name == "foo")'
265265
assert_output "foo"
266266
}
267+
268+
@test '--filter option filters instances' {
269+
run -0 limactl ls --filter '.name == "foo"'
270+
assert_line --index 0 --regexp '^NAME'
271+
assert_line --index 1 --regexp '^foo'
272+
assert_output_lines_count 2
273+
}
274+
275+
@test '--filter option works with all output formats' {
276+
run -0 limactl ls --filter '.name == "foo"'
277+
assert_line --index 1 --regexp '^foo'
278+
279+
run -0 limactl ls --filter '.name == "foo"' --format json
280+
assert_line --index 0 --regexp '^\{"name":"foo",'
281+
282+
run -0 limactl ls --filter '.name == "foo"' --format '{{.Name}}'
283+
assert_output "foo"
284+
}
285+
286+
@test '--filter option is incompatible with --yq' {
287+
run_e -1 limactl ls --filter '.name == "foo"' --yq '.name'
288+
assert_fatal "option --filter conflicts with option --yq"
289+
}

0 commit comments

Comments
 (0)