Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/cmd/limactl/list.go
2611 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package main
5
6
import (
7
"bufio"
8
"bytes"
9
"errors"
10
"fmt"
11
"reflect"
12
"slices"
13
"strings"
14
15
"github.com/cheggaaa/pb/v3/termutil"
16
"github.com/mikefarah/yq/v4/pkg/yqlib"
17
"github.com/sirupsen/logrus"
18
"github.com/spf13/cobra"
19
20
"github.com/lima-vm/lima/v2/pkg/limatype"
21
"github.com/lima-vm/lima/v2/pkg/store"
22
"github.com/lima-vm/lima/v2/pkg/uiutil"
23
"github.com/lima-vm/lima/v2/pkg/yqutil"
24
)
25
26
func fieldNames() []string {
27
names := []string{}
28
t := reflect.TypeFor[store.FormatData]()
29
for i := range t.NumField() {
30
f := t.Field(i)
31
if f.Anonymous {
32
for j := range f.Type.NumField() {
33
if tag := f.Tag.Get("lima"); tag != "deprecated" {
34
names = append(names, f.Type.Field(j).Name)
35
}
36
}
37
} else {
38
if tag := f.Tag.Get("lima"); tag != "deprecated" {
39
names = append(names, t.Field(i).Name)
40
}
41
}
42
}
43
return names
44
}
45
46
func newListCommand() *cobra.Command {
47
listCommand := &cobra.Command{
48
Use: "list [flags] [INSTANCE]...",
49
Aliases: []string{"ls"},
50
Short: "List instances of Lima",
51
Long: `List instances of Lima.
52
53
The output can be presented in one of several formats, using the --format <format> flag.
54
55
--format json - Output in JSON format
56
--format yaml - Output in YAML format
57
--format table - Output in table format
58
--format '{{ <go template> }}' - If the format begins and ends with '{{ }}', then it is used as a go template.
59
` + store.FormatHelp,
60
Args: WrapArgsError(cobra.ArbitraryArgs),
61
RunE: listAction,
62
ValidArgsFunction: listBashComplete,
63
GroupID: basicCommand,
64
}
65
66
listCommand.Flags().StringP("format", "f", "table", "Output format, one of: json, yaml, table, go-template")
67
listCommand.Flags().Bool("list-fields", false, "List fields available for format")
68
listCommand.Flags().Bool("json", false, "Same as --format=json")
69
listCommand.Flags().BoolP("quiet", "q", false, "Only show names")
70
listCommand.Flags().Bool("all-fields", false, "Show all fields")
71
listCommand.Flags().StringArray("yq", nil, "Apply yq expression to each instance")
72
73
return listCommand
74
}
75
76
func instanceMatches(arg string, instances []string) []string {
77
matches := []string{}
78
for _, instance := range instances {
79
if instance == arg {
80
matches = append(matches, instance)
81
}
82
}
83
return matches
84
}
85
86
// unmatchedInstancesError is created when unmatched instance names found.
87
type unmatchedInstancesError struct{}
88
89
// Error implements error.
90
func (unmatchedInstancesError) Error() string {
91
return "unmatched instances"
92
}
93
94
// ExitCode implements ExitCoder.
95
func (unmatchedInstancesError) ExitCode() int {
96
return 1
97
}
98
99
func listAction(cmd *cobra.Command, args []string) error {
100
ctx := cmd.Context()
101
quiet, err := cmd.Flags().GetBool("quiet")
102
if err != nil {
103
return err
104
}
105
format, err := cmd.Flags().GetString("format")
106
if err != nil {
107
return err
108
}
109
listFields, err := cmd.Flags().GetBool("list-fields")
110
if err != nil {
111
return err
112
}
113
jsonFormat, err := cmd.Flags().GetBool("json")
114
if err != nil {
115
return err
116
}
117
yq, err := cmd.Flags().GetStringArray("yq")
118
if err != nil {
119
return err
120
}
121
122
if jsonFormat {
123
format = "json"
124
}
125
126
// conflicts
127
if jsonFormat && cmd.Flags().Changed("format") {
128
return errors.New("option --json conflicts with option --format")
129
}
130
if listFields && cmd.Flags().Changed("format") {
131
return errors.New("option --list-fields conflicts with option --format")
132
}
133
if len(yq) != 0 {
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
}
141
142
if quiet && format != "table" {
143
return errors.New("option --quiet can only be used with '--format table'")
144
}
145
146
if listFields {
147
names := fieldNames()
148
slices.Sort(names)
149
fmt.Fprintln(cmd.OutOrStdout(), strings.Join(names, "\n"))
150
return nil
151
}
152
153
if err := store.Validate(); err != nil {
154
logrus.Warnf("The directory %q does not look like a valid Lima directory: %v", store.Directory(), err)
155
}
156
157
allInstances, err := store.Instances()
158
if err != nil {
159
return err
160
}
161
if len(args) == 0 && len(allInstances) == 0 {
162
logrus.Warn("No instance found. Run `limactl create` to create an instance.")
163
return nil
164
}
165
166
instanceNames := []string{}
167
unmatchedInstances := false
168
if len(args) > 0 {
169
for _, arg := range args {
170
matches := instanceMatches(arg, allInstances)
171
if len(matches) > 0 {
172
instanceNames = append(instanceNames, matches...)
173
} else {
174
logrus.Warnf("No instance matching %v found.", arg)
175
unmatchedInstances = true
176
}
177
}
178
} else {
179
instanceNames = allInstances
180
}
181
182
if quiet && len(yq) == 0 {
183
for _, instName := range instanceNames {
184
fmt.Fprintln(cmd.OutOrStdout(), instName)
185
}
186
if unmatchedInstances {
187
return unmatchedInstancesError{}
188
}
189
return nil
190
}
191
192
// get the state and config for all the requested instances
193
var instances []*limatype.Instance
194
for _, instanceName := range instanceNames {
195
instance, err := store.Inspect(ctx, instanceName)
196
if err != nil {
197
return fmt.Errorf("unable to load instance %s: %w", instanceName, err)
198
}
199
instances = append(instances, instance)
200
}
201
202
for _, instance := range instances {
203
if len(instance.Errors) > 0 {
204
logrus.WithField("errors", instance.Errors).Warnf("instance %q has errors", instance.Name)
205
}
206
}
207
208
allFields, err := cmd.Flags().GetBool("all-fields")
209
if err != nil {
210
return err
211
}
212
213
options := store.PrintOptions{AllFields: allFields}
214
isTTY := uiutil.OutputIsTTY(cmd.OutOrStdout())
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 len(yq) != 0 && !cmd.Flags().Changed("format") {
222
format = "json"
223
}
224
// Always pipe JSON and YAML through yq to colorize it if isTTY
225
if len(yq) == 0 && (format == "json" || format == "yaml") {
226
yq = append(yq, ".")
227
}
228
229
if len(yq) == 0 {
230
err = store.PrintInstances(cmd.OutOrStdout(), instances, format, &options)
231
if err == nil && unmatchedInstances {
232
return unmatchedInstancesError{}
233
}
234
return err
235
}
236
237
if quiet {
238
yq = append(yq, ".name")
239
}
240
yqExpr := strings.Join(yq, " | ")
241
242
buf := new(bytes.Buffer)
243
err = store.PrintInstances(buf, instances, format, &options)
244
if err != nil {
245
return err
246
}
247
248
if format == "json" {
249
// The JSON encoder will create empty objects (YAML maps), even when they have the ",omitempty" tag.
250
deleteEmptyObjects := `del(.. | select(tag == "!!map" and length == 0))`
251
yqExpr += " | " + deleteEmptyObjects
252
253
encoderPrefs := yqlib.ConfiguredJSONPreferences.Copy()
254
encoderPrefs.ColorsEnabled = false
255
encoderPrefs.Indent = 0
256
plainEncoder := yqlib.NewJSONEncoder(encoderPrefs)
257
// Using non-0 indent means the instance will be printed over multiple lines,
258
// so is no longer in JSON Lines format. This is a compromise for readability.
259
encoderPrefs.Indent = 4
260
encoderPrefs.ColorsEnabled = true
261
colorEncoder := yqlib.NewJSONEncoder(encoderPrefs)
262
263
// Each line contains the JSON object for one Lima instance.
264
scanner := bufio.NewScanner(buf)
265
for scanner.Scan() {
266
var str string
267
if str, err = yqutil.EvaluateExpressionWithEncoder(yqExpr, scanner.Text(), plainEncoder); err != nil {
268
return err
269
}
270
// Repeatedly delete empty objects until there are none left.
271
for {
272
length := len(str)
273
if str, err = yqutil.EvaluateExpressionWithEncoder(deleteEmptyObjects, str, plainEncoder); err != nil {
274
return err
275
}
276
if len(str) >= length {
277
break
278
}
279
}
280
if isTTY {
281
// pretty-print and colorize the output
282
if str, err = yqutil.EvaluateExpressionWithEncoder(".", str, colorEncoder); err != nil {
283
return err
284
}
285
}
286
if _, err = fmt.Fprint(cmd.OutOrStdout(), str); err != nil {
287
return err
288
}
289
}
290
err = scanner.Err()
291
if err == nil && unmatchedInstances {
292
return unmatchedInstancesError{}
293
}
294
return err
295
}
296
297
var str string
298
if isTTY {
299
// This branch is trading the better formatting from yamlfmt for colorizing from yqlib.
300
if str, err = yqutil.EvaluateExpressionPlain(yqExpr, buf.String(), true); err != nil {
301
return err
302
}
303
} else {
304
var res []byte
305
if res, err = yqutil.EvaluateExpression(yqExpr, buf.Bytes()); err != nil {
306
return err
307
}
308
str = string(res)
309
}
310
_, err = fmt.Fprint(cmd.OutOrStdout(), str)
311
if err == nil && unmatchedInstances {
312
return unmatchedInstancesError{}
313
}
314
return err
315
}
316
317
func listBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
318
return bashCompleteInstanceNames(cmd)
319
}
320
321