Skip to content

Commit 38a1f4a

Browse files
committed
Add --yq option to limactl list and limactl info
The --yq option is only compatible with JSON or YAML output and will apply the yq expression to the result before writing it to the output. The following 2 commands are functionally identical (but may result in different formatting, see below): limactl ls default --yq .dir limactl ls default --format json | limactl yq .dir The output of `limactl list --json` will be pretty-printed when it goes to a terminal. This means it is no longer one line per JSON object, but has much improved legibility. Output to a file or pipe maintains JSON Lines rules. Also colorize the output of `limactl list`, `limactl info`, `limactl tmpl copy`, and `limactl tmpl yq` if the output goes to the terminal. Unfortunately the output of `yamlfmt` cannot be colorized, so the terminal output may look slightly different from the output to a file or pipe (mostly the extra indentation of list elements), but this seems like a reasonable compromise. Signed-off-by: Jan Dubois <jan.dubois@suse.com>
1 parent b9b9aa6 commit 38a1f4a

File tree

5 files changed

+137
-15
lines changed

5 files changed

+137
-15
lines changed

cmd/limactl/info.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ package main
66
import (
77
"encoding/json"
88
"fmt"
9+
"os"
910

11+
"github.com/lima-vm/lima/v2/pkg/uiutil"
12+
"github.com/lima-vm/lima/v2/pkg/yqutil"
1013
"github.com/spf13/cobra"
1114

1215
"github.com/lima-vm/lima/v2/pkg/limainfo"
@@ -20,11 +23,19 @@ func newInfoCommand() *cobra.Command {
2023
RunE: infoAction,
2124
GroupID: advancedCommand,
2225
}
26+
infoCommand.Flags().String("yq", ".", "Apply yq expression to output")
27+
2328
return infoCommand
2429
}
2530

2631
func infoAction(cmd *cobra.Command, _ []string) error {
2732
ctx := cmd.Context()
33+
34+
yq, err := cmd.Flags().GetString("yq")
35+
if err != nil {
36+
return err
37+
}
38+
2839
info, err := limainfo.New(ctx)
2940
if err != nil {
3041
return err
@@ -33,6 +44,10 @@ func infoAction(cmd *cobra.Command, _ []string) error {
3344
if err != nil {
3445
return err
3546
}
36-
_, err = fmt.Fprintln(cmd.OutOrStdout(), string(j))
47+
colorsEnabled := uiutil.FileIsTTY(cmd.OutOrStdout(), os.Stdout)
48+
str, err := yqutil.EvaluateExpressionPlain(yq, string(j), colorsEnabled)
49+
if err == nil {
50+
_, err = fmt.Fprint(cmd.OutOrStdout(), str)
51+
}
3752
return err
3853
}

cmd/limactl/list.go

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package main
55

66
import (
7+
"bufio"
8+
"bytes"
79
"errors"
810
"fmt"
911
"os"
@@ -12,12 +14,14 @@ import (
1214
"strings"
1315

1416
"github.com/cheggaaa/pb/v3/termutil"
15-
"github.com/mattn/go-isatty"
17+
"github.com/mikefarah/yq/v4/pkg/yqlib"
1618
"github.com/sirupsen/logrus"
1719
"github.com/spf13/cobra"
1820

1921
"github.com/lima-vm/lima/v2/pkg/limatype"
2022
"github.com/lima-vm/lima/v2/pkg/store"
23+
"github.com/lima-vm/lima/v2/pkg/uiutil"
24+
"github.com/lima-vm/lima/v2/pkg/yqutil"
2125
)
2226

2327
func fieldNames() []string {
@@ -64,6 +68,7 @@ The following legacy flags continue to function:
6468
listCommand.Flags().Bool("json", false, "JSONify output")
6569
listCommand.Flags().BoolP("quiet", "q", false, "Only show names")
6670
listCommand.Flags().Bool("all-fields", false, "Show all fields")
71+
listCommand.Flags().String("yq", "", "Apply yq expression to each instance")
6772

6873
return listCommand
6974
}
@@ -109,6 +114,10 @@ func listAction(cmd *cobra.Command, args []string) error {
109114
if err != nil {
110115
return err
111116
}
117+
yq, err := cmd.Flags().GetString("yq")
118+
if err != nil {
119+
return err
120+
}
112121

113122
if jsonFormat {
114123
format = "json"
@@ -121,6 +130,14 @@ func listAction(cmd *cobra.Command, args []string) error {
121130
if listFields && cmd.Flags().Changed("format") {
122131
return errors.New("option --list-fields conflicts with option --format")
123132
}
133+
if yq != "" {
134+
if cmd.Flags().Changed("format") && format != "json" && format != "yaml" {
135+
return errors.New("option --yq only works with --format json or yaml")
136+
}
137+
if listFields {
138+
return errors.New("option --list-fields conflicts with option --yq")
139+
}
140+
}
124141

125142
if quiet && format != "table" {
126143
return errors.New("option --quiet can only be used with '--format table'")
@@ -194,16 +211,80 @@ func listAction(cmd *cobra.Command, args []string) error {
194211
}
195212

196213
options := store.PrintOptions{AllFields: allFields}
197-
out := cmd.OutOrStdout()
198-
if out == os.Stdout {
199-
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
200-
if w, err := termutil.TerminalWidth(); err == nil {
201-
options.TerminalWidth = w
214+
isTTY := uiutil.FileIsTTY(cmd.OutOrStdout(), os.Stdout)
215+
if isTTY {
216+
if w, err := termutil.TerminalWidth(); err == nil {
217+
options.TerminalWidth = w
218+
}
219+
}
220+
// --yq implies --format json unless --format yaml has been explicitly specified
221+
if yq != "" && !cmd.Flags().Changed("format") {
222+
format = "json"
223+
}
224+
// Always pipe JSON and YAML through yq to colorize it if isTTY
225+
if yq == "" && (format == "json" || format == "yaml") {
226+
yq = "."
227+
}
228+
229+
if yq == "" {
230+
err = store.PrintInstances(cmd.OutOrStdout(), instances, format, &options)
231+
if err == nil && unmatchedInstances {
232+
return unmatchedInstancesError{}
233+
}
234+
return err
235+
}
236+
237+
buf := new(bytes.Buffer)
238+
err = store.PrintInstances(buf, instances, format, &options)
239+
if err != nil {
240+
return err
241+
}
242+
243+
if format == "json" {
244+
encoderPrefs := yqlib.ConfiguredJSONPreferences.Copy()
245+
if isTTY {
246+
// Using non-0 indent means the instance will be printed over multiple lines,
247+
// so is no longer in JSON Lines format. This is a compromise for readability.
248+
encoderPrefs.Indent = 4
249+
encoderPrefs.ColorsEnabled = true
250+
} else {
251+
encoderPrefs.Indent = 0
252+
encoderPrefs.ColorsEnabled = false
253+
}
254+
encoder := yqlib.NewJSONEncoder(encoderPrefs)
255+
256+
// Each line contains the JSON object for one Lima instance.
257+
scanner := bufio.NewScanner(buf)
258+
for scanner.Scan() {
259+
var str string
260+
if str, err = yqutil.EvaluateExpressionWithEncoder(yq, scanner.Text(), encoder); err != nil {
261+
return err
262+
}
263+
if _, err = fmt.Fprint(cmd.OutOrStdout(), str); err != nil {
264+
return err
202265
}
203266
}
267+
err = scanner.Err()
268+
if err == nil && unmatchedInstances {
269+
return unmatchedInstancesError{}
270+
}
271+
return err
204272
}
205273

206-
err = store.PrintInstances(out, instances, format, &options)
274+
var str string
275+
if isTTY {
276+
// This branch is trading the better formatting from yamlfmt for colorizing from yqlib.
277+
if str, err = yqutil.EvaluateExpressionPlain(yq, buf.String(), true); err != nil {
278+
return err
279+
}
280+
} else {
281+
if res, err := yqutil.EvaluateExpression(yq, buf.Bytes()); err != nil {
282+
return err
283+
} else {
284+
str = string(res)
285+
}
286+
}
287+
_, err = fmt.Fprint(cmd.OutOrStdout(), str)
207288
if err == nil && unmatchedInstances {
208289
return unmatchedInstancesError{}
209290
}

cmd/limactl/template.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/lima-vm/lima/v2/pkg/limatmpl"
1818
"github.com/lima-vm/lima/v2/pkg/limatype/dirnames"
1919
"github.com/lima-vm/lima/v2/pkg/limayaml"
20+
"github.com/lima-vm/lima/v2/pkg/uiutil"
2021
"github.com/lima-vm/lima/v2/pkg/yqutil"
2122
)
2223

@@ -148,6 +149,15 @@ func templateCopyAction(cmd *cobra.Command, args []string) error {
148149
return err
149150
}
150151
}
152+
if target == "-" && uiutil.FileIsTTY(cmd.OutOrStdout(), os.Stdout) {
153+
// run the output through YQ to colorize it
154+
out, err := yqutil.EvaluateExpressionPlain(".", string(tmpl.Bytes), true)
155+
if err == nil {
156+
_, err = fmt.Fprint(cmd.OutOrStdout(), out)
157+
}
158+
return err
159+
}
160+
151161
writer := cmd.OutOrStdout()
152162
if target != "-" {
153163
file, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
@@ -200,7 +210,8 @@ func templateYQAction(cmd *cobra.Command, args []string) error {
200210
if err := fillDefaults(ctx, tmpl); err != nil {
201211
return err
202212
}
203-
out, err := yqutil.EvaluateExpressionPlain(expr, string(tmpl.Bytes))
213+
colorsEnabled := uiutil.FileIsTTY(cmd.OutOrStdout(), os.Stdout)
214+
out, err := yqutil.EvaluateExpressionPlain(expr, string(tmpl.Bytes), colorsEnabled)
204215
if err == nil {
205216
_, err = fmt.Fprint(cmd.OutOrStdout(), out)
206217
}

pkg/uiutil/uiutil.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
package uiutil
55

66
import (
7+
"io"
8+
"os"
9+
710
"github.com/AlecAivazis/survey/v2"
811
"github.com/AlecAivazis/survey/v2/terminal"
12+
"github.com/mattn/go-isatty"
913
)
1014

1115
var InterruptErr = terminal.InterruptErr
@@ -36,3 +40,9 @@ func Select(message string, options []string) (int, error) {
3640
}
3741
return ans, nil
3842
}
43+
44+
// FileIsTTY returns true if out is going to file, and file is a terminal device,
45+
// not a regular file, stream, or pipe etc.
46+
func FileIsTTY(out io.Writer, file *os.File) bool {
47+
return out == file && isatty.IsTerminal(file.Fd()) || isatty.IsCygwinTerminal(file.Fd())
48+
}

pkg/yqutil/yqutil.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func ValidateContent(content []byte) error {
4040
}
4141

4242
// EvaluateExpressionPlain evaluates the yq expression and returns the yq result.
43-
func EvaluateExpressionPlain(expression, content string) (string, error) {
43+
func EvaluateExpressionWithEncoder(expression, content string, encoder yqlib.Encoder) (string, error) {
4444
if expression == "" {
4545
return content, nil
4646
}
@@ -50,10 +50,6 @@ func EvaluateExpressionPlain(expression, content string) (string, error) {
5050
logging.SetBackend(backend)
5151
yqlib.InitExpressionParser()
5252

53-
encoderPrefs := yqlib.ConfiguredYamlPreferences.Copy()
54-
encoderPrefs.Indent = 2
55-
encoderPrefs.ColorsEnabled = false
56-
encoder := yqlib.NewYamlEncoder(encoderPrefs)
5753
decoder := yqlib.NewYamlDecoder(yqlib.ConfiguredYamlPreferences)
5854
out, err := yqlib.NewStringEvaluator().EvaluateAll(expression, content, encoder, decoder)
5955
if err != nil {
@@ -82,6 +78,15 @@ func EvaluateExpressionPlain(expression, content string) (string, error) {
8278
return out, nil
8379
}
8480

81+
// EvaluateExpressionPlain evaluates the yq expression and returns the yq result.
82+
func EvaluateExpressionPlain(expression, content string, colorsEnabled bool) (string, error) {
83+
encoderPrefs := yqlib.ConfiguredYamlPreferences.Copy()
84+
encoderPrefs.Indent = 2
85+
encoderPrefs.ColorsEnabled = colorsEnabled
86+
encoder := yqlib.NewYamlEncoder(encoderPrefs)
87+
return EvaluateExpressionWithEncoder(expression, content, encoder)
88+
}
89+
8590
// EvaluateExpression evaluates the yq expression and returns the output formatted with yamlfmt.
8691
func EvaluateExpression(expression string, content []byte) ([]byte, error) {
8792
if expression == "" {
@@ -101,7 +106,7 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
101106
return nil, err
102107
}
103108

104-
out, err := EvaluateExpressionPlain(expression, string(contentModified))
109+
out, err := EvaluateExpressionPlain(expression, string(contentModified), false)
105110
if err != nil {
106111
return nil, err
107112
}

0 commit comments

Comments
 (0)