From bdef0563ddf3e94f486cc77c5c9975daa7fa0114 Mon Sep 17 00:00:00 2001 From: olalekan odukoya Date: Sun, 12 Oct 2025 20:57:01 +0100 Subject: [PATCH] add filter option to list command Signed-off-by: olalekan odukoya --- cmd/limactl/list.go | 70 ++++++++++++++++++++++++++++++++++++++- hack/bats/tests/list.bats | 23 +++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/cmd/limactl/list.go b/cmd/limactl/list.go index ab77ea16d67..ef9f210a774 100644 --- a/cmd/limactl/list.go +++ b/cmd/limactl/list.go @@ -6,9 +6,11 @@ package main import ( "bufio" "bytes" + "encoding/json" "errors" "fmt" "reflect" + "regexp" "sort" "strings" @@ -57,6 +59,14 @@ The output can be presented in one of several formats, using the --format }}' - 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'`, @@ -72,6 +82,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 } @@ -121,6 +132,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" @@ -141,6 +156,11 @@ 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 quiet && format != "table" { return errors.New("option --quiet can only be used with '--format table'") @@ -220,15 +240,35 @@ 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 { + // only allow fields, ==, !=, and literals. + valid := regexp.MustCompile(`^[a-zA-Z0-9_.\s"'-=> 0 { + filteredInstances = append(filteredInstances, instance) + } + } + + return filteredInstances, nil +} diff --git a/hack/bats/tests/list.bats b/hack/bats/tests/list.bats index 95ff1aafaf1..9709859f6fe 100644 --- a/hack/bats/tests/list.bats +++ b/hack/bats/tests/list.bats @@ -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" +}