Skip to content

Commit bdef056

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

File tree

2 files changed

+92
-1
lines changed

2 files changed

+92
-1
lines changed

cmd/limactl/list.go

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ package main
66
import (
77
"bufio"
88
"bytes"
9+
"encoding/json"
910
"errors"
1011
"fmt"
1112
"reflect"
13+
"regexp"
1214
"sort"
1315
"strings"
1416

@@ -57,6 +59,14 @@ The output can be presented in one of several formats, using the --format <forma
5759
--format yaml - Output in YAML format
5860
--format table - Output in table format
5961
--format '{{ <go template> }}' - If the format begins and ends with '{{ }}', then it is used as a go template.
62+
63+
Filtering instances:
64+
--filter EXPR - Filter instances using yq expression (this is equivalent to --yq 'select(EXPR)')
65+
Can be specified multiple times and it works with all output formats.
66+
Examples:
67+
--filter '.status == "Running"'
68+
--filter '.vmType == "vz"'
69+
--filter '.status == "Running"' --filter '.vmType == "vz"'
6070
` + store.FormatHelp + `
6171
The following legacy flags continue to function:
6272
--json - equal to '--format json'`,
@@ -72,6 +82,7 @@ The following legacy flags continue to function:
7282
listCommand.Flags().BoolP("quiet", "q", false, "Only show names")
7383
listCommand.Flags().Bool("all-fields", false, "Show all fields")
7484
listCommand.Flags().StringArray("yq", nil, "Apply yq expression to each instance")
85+
listCommand.Flags().StringArrayP("filter", "l", nil, "Filter instances using yq expression (equivalent to --yq 'select(EXPR)')")
7586

7687
return listCommand
7788
}
@@ -121,6 +132,10 @@ func listAction(cmd *cobra.Command, args []string) error {
121132
if err != nil {
122133
return err
123134
}
135+
filter, err := cmd.Flags().GetStringArray("filter")
136+
if err != nil {
137+
return err
138+
}
124139

125140
if jsonFormat {
126141
format = "json"
@@ -141,6 +156,11 @@ func listAction(cmd *cobra.Command, args []string) error {
141156
return errors.New("option --list-fields conflicts with option --yq")
142157
}
143158
}
159+
if len(filter) != 0 {
160+
if listFields {
161+
return errors.New("option --list-fields conflicts with option --filter")
162+
}
163+
}
144164

145165
if quiet && format != "table" {
146166
return errors.New("option --quiet can only be used with '--format table'")
@@ -220,15 +240,35 @@ func listAction(cmd *cobra.Command, args []string) error {
220240
options.TerminalWidth = w
221241
}
222242
}
223-
// --yq implies --format json unless --format yaml has been explicitly specified
243+
244+
// --yq implies --format json unless --format has been explicitly specified
224245
if len(yq) != 0 && !cmd.Flags().Changed("format") {
225246
format = "json"
226247
}
248+
227249
// Always pipe JSON and YAML through yq to colorize it if isTTY
228250
if len(yq) == 0 && (format == "json" || format == "yaml") {
229251
yq = append(yq, ".")
230252
}
231253

254+
for _, f := range filter {
255+
// only allow fields, ==, !=, and literals.
256+
valid := regexp.MustCompile(`^[a-zA-Z0-9_.\s"'-=><!]+$`)
257+
if !valid.MatchString(f) {
258+
return fmt.Errorf("unsafe characters in filter expression: %q", f)
259+
}
260+
261+
yq = append(yq, "select("+f+")")
262+
}
263+
264+
if len(filter) != 0 && (format != "json" && format != "yaml") {
265+
instances, err = filterInstances(instances, yq)
266+
if err != nil {
267+
return err
268+
}
269+
yq = nil
270+
}
271+
232272
if len(yq) == 0 {
233273
err = store.PrintInstances(cmd.OutOrStdout(), instances, format, &options)
234274
if err == nil && unmatchedInstances {
@@ -320,3 +360,31 @@ func listAction(cmd *cobra.Command, args []string) error {
320360
func listBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
321361
return bashCompleteInstanceNames(cmd)
322362
}
363+
364+
// filterInstances applies yq expressions to instances and returns the filtered results.
365+
func filterInstances(instances []*limatype.Instance, yqExprs []string) ([]*limatype.Instance, error) {
366+
if len(yqExprs) == 0 {
367+
return instances, nil
368+
}
369+
370+
yqExpr := strings.Join(yqExprs, " | ")
371+
372+
var filteredInstances []*limatype.Instance
373+
for _, instance := range instances {
374+
jsonBytes, err := json.Marshal(instance)
375+
if err != nil {
376+
return nil, fmt.Errorf("failed to marshal instance %q: %w", instance.Name, err)
377+
}
378+
379+
result, err := yqutil.EvaluateExpression(yqExpr, jsonBytes)
380+
if err != nil {
381+
return nil, fmt.Errorf("failed to apply filter %q: %w", yqExpr, err)
382+
}
383+
384+
if len(result) > 0 {
385+
filteredInstances = append(filteredInstances, instance)
386+
}
387+
}
388+
389+
return filteredInstances, nil
390+
}

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)