Repository: lima-vm/lima
Pull request #2593 (2024-09-05 14:24:49)

Improve `limactl edit`
- yqutil: use yamlfmt to fix indentation of sequence
- `limactl edit`: support editing `lima.yaml` file  
  This allows `limactl edit --set "..." <limayaml>` to be used instead of `yq -i eval "..." <limayaml>` in scripts like `hack/inject-cmdline-to-template.sh`.

The changes made to `examples/ubuntu.yaml` using `limactl edit --set` in `hack/inject-cmdline-to-template.sh` are as follows:
```patch
diff --git forkSrcPrefix/examples/ubuntu.yaml forkDstPrefix/examples/ubuntu.yaml
index a7fb0b3246e71bd9f98df2e8849185ed1339c76b..9f19540104091b13e08cf255bd074831560af654 100644
--- forkSrcPrefix/examples/ubuntu.yaml
+++ forkDstPrefix/examples/ubuntu.yaml
@@ -7,13 +7,19 @@ images:
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release-20240821/ubuntu-24.04-server-cloudimg-arm64.img"
   arch: "aarch64"
   digest: "sha256:5ecac6447be66a164626744a87a27fd4e6c6606dc683e0a233870af63df4276a"
+  kernel:
+    location: https://cloud-images.ubuntu.com/releases/24.04/release-20240821/unpacked/ubuntu-24.04-server-cloudimg-arm64-vmlinuz-generic
+    digest: sha256:69e66230d6acc503082f098fd072a672a804774007209110b353d25bb7007669
+    cmdline: root=LABEL=cloudimg-rootfs ro console=tty1 console=ttyAMA0 console=hvc0
+  initrd:
+    location: https://cloud-images.ubuntu.com/releases/24.04/release-20240821/unpacked/ubuntu-24.04-server-cloudimg-arm64-initrd-generic
+    digest: sha256:92acf9f2976b7a8b3b5e9e3f22211d17a6fdb3f2aed981467f91cd6dbfa16ae5
 # Fallback to the latest release image.
 # Hint: run `limactl prune` to invalidate the cache
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
   arch: "x86_64"
 - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64.img"
   arch: "aarch64"
-
 mounts:
 - location: "~"
 - location: "/tmp/lima"
```

The issue with line breaks disappearing cannot be fixed with yamlfmt, so it still remains. However, the sequence is no longer indented, and I personally think it has become usable.

I'm considering making a separate PR to change `hack/inject-cmdline-to-template.sh` from `yq -i eval` to `limactl edit --set` after #2562, which is likely to have overlapping changes, is resolved.

jandubois: Tests are failing on Windows because it is adding `\r\n` line endings to the YAML file instead of sticking to plain newlines.
jandubois: Please squash commits!

Patch:
diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go
index 32305f8b..6cafe0c8 100644
--- a/cmd/limactl/edit.go
+++ b/cmd/limactl/edit.go
@@ -8,6 +8,7 @@ import (
 	"path/filepath"
 
 	"github.com/lima-vm/lima/cmd/limactl/editflags"
+	"github.com/lima-vm/lima/cmd/limactl/guessarg"
 	"github.com/lima-vm/lima/pkg/editutil"
 	"github.com/lima-vm/lima/pkg/instance"
 	"github.com/lima-vm/lima/pkg/limayaml"
@@ -22,8 +23,8 @@ import (
 
 func newEditCommand() *cobra.Command {
 	editCommand := &cobra.Command{
-		Use:               "edit INSTANCE",
-		Short:             "Edit an instance of Lima",
+		Use:               "edit INSTANCE|FILE.yaml",
+		Short:             "Edit an instance of Lima or a template",
 		Args:              WrapArgsError(cobra.MaximumNArgs(1)),
 		RunE:              editAction,
 		ValidArgsFunction: editBashComplete,
@@ -34,24 +35,43 @@ func newEditCommand() *cobra.Command {
 }
 
 func editAction(cmd *cobra.Command, args []string) error {
-	instName := DefaultInstanceName
+	var arg string
 	if len(args) > 0 {
-		instName = args[0]
+		arg = args[0]
 	}
 
-	inst, err := store.Inspect(instName)
-	if err != nil {
-		if errors.Is(err, os.ErrNotExist) {
-			return fmt.Errorf("instance %q not found", instName)
+	var filePath string
+	var err error
+	var inst *store.Instance
+	switch {
+	case guessarg.SeemsYAMLPath(arg):
+		// absolute path is required for `limayaml.Validate`
+		filePath, err = filepath.Abs(arg)
+		if err != nil {
+			return err
+		}
+	default:
+		var instName string
+		if arg != "" {
+			instName = arg
+		} else {
+			instName = DefaultInstanceName
 		}
-		return err
-	}
 
-	if inst.Status == store.StatusRunning {
-		return errors.New("cannot edit a running instance")
+		inst, err = store.Inspect(instName)
+		if err != nil {
+			if errors.Is(err, os.ErrNotExist) {
+				return fmt.Errorf("instance %q not found", instName)
+			}
+			return err
+		}
+
+		if inst.Status == store.StatusRunning {
+			return errors.New("cannot edit a running instance")
+		}
+		filePath = filepath.Join(inst.Dir, filenames.LimaYAML)
 	}
 
-	filePath := filepath.Join(inst.Dir, filenames.LimaYAML)
 	yContent, err := os.ReadFile(filePath)
 	if err != nil {
 		return err
@@ -73,7 +93,12 @@ func editAction(cmd *cobra.Command, args []string) error {
 			return err
 		}
 	} else if tty {
-		hdr := fmt.Sprintf("# Please edit the following configuration for Lima instance %q\n", instName)
+		var hdr string
+		if inst != nil {
+			hdr = fmt.Sprintf("# Please edit the following configuration for Lima instance %q\n", inst.Name)
+		} else {
+			hdr = fmt.Sprintf("# Please edit the following configuration %q\n", filePath)
+		}
 		hdr += "# and an empty file will abort the edit.\n"
 		hdr += "\n"
 		hdr += editutil.GenerateEditorWarningHeader()
@@ -105,12 +130,18 @@ func editAction(cmd *cobra.Command, args []string) error {
 	if err := os.WriteFile(filePath, yBytes, 0o644); err != nil {
 		return err
 	}
-	logrus.Infof("Instance %q configuration edited", instName)
+	if inst != nil {
+		logrus.Infof("Instance %q configuration edited", inst.Name)
+	}
 
 	if !tty {
 		// use "start" to start it
 		return nil
 	}
+	if inst == nil {
+		// edited a limayaml file directly
+		return nil
+	}
 	startNow, err := askWhetherToStart()
 	if err != nil {
 		return err
diff --git a/go.mod b/go.mod
index 8f84ea13..6096dc5a 100644
--- a/go.mod
+++ b/go.mod
@@ -23,6 +23,7 @@ require (
 	github.com/foxcpp/go-mockdns v1.1.0
 	github.com/goccy/go-yaml v1.12.0
 	github.com/google/go-cmp v0.6.0
+	github.com/google/yamlfmt v0.13.0
 	github.com/lima-vm/go-qcow2reader v0.1.2
 	github.com/lima-vm/sshocker v0.3.4
 	github.com/mattn/go-isatty v0.0.20
@@ -58,6 +59,8 @@ require (
 	github.com/VividCortex/ewma v1.2.0 // indirect
 	github.com/a8m/envsubst v1.4.2 // indirect
 	github.com/alecthomas/participle/v2 v2.1.1 // indirect
+	github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
+	github.com/braydonk/yaml v0.7.0 // indirect
 	github.com/containerd/errdefs v0.1.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/digitalocean/go-libvirt v0.0.0-20220804181439-8648fbde413e // indirect
@@ -95,6 +98,7 @@ require (
 	github.com/mattn/go-runewidth v0.0.15 // indirect
 	github.com/mdlayher/socket v0.4.1 // indirect
 	github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
+	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -104,6 +108,7 @@ require (
 	github.com/pkg/sftp v1.13.6 // indirect
 	github.com/rivo/uniseg v0.2.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
 	github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
 	github.com/yuin/gopher-lua v1.1.1 // indirect
diff --git a/go.sum b/go.sum
index f8481ab3..02472d8b 100644
--- a/go.sum
+++ b/go.sum
@@ -25,6 +25,10 @@ github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/Y
 github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
 github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e h1:IdMhFPEfTZQU971tIHx3UhY4l+yCeynprnINrDTSrOc=
 github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e/go.mod h1:aXGMJsd3XrnUFTuyf/pTGg5jG6CY8JMZ5juywvShjgQ=
+github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
+github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
+github.com/braydonk/yaml v0.7.0 h1:ySkqO7r0MGoCNhiRJqE0Xe9yhINMyvOAB3nFjgyJn2k=
+github.com/braydonk/yaml v0.7.0/go.mod h1:hcm3h581tudlirk8XEUPDBAimBPbmnL0Y45hCRl47N4=
 github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk=
 github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI=
 github.com/containerd/containerd v1.7.22 h1:nZuNnNRA6T6jB975rx2RRNqqH2k6ELYKDZfqTHqwyy0=
@@ -123,6 +127,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/yamlfmt v0.13.0 h1:OS8cjQNhRhEP437e1axMgZiwrYu60mWLtvb4/jtQCtE=
+github.com/google/yamlfmt v0.13.0/go.mod h1:y8JNH/2TqTaCSUjk/zhn0lYlibMvS0R+pbVUDo8oz9o=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
@@ -192,6 +198,8 @@ github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
 github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
 github.com/mikefarah/yq/v4 v4.44.3 h1:3zxHntH67maSHr6ynCjM44htw7LZNINmTzYn3tM2t+I=
 github.com/mikefarah/yq/v4 v4.44.3/go.mod h1:1pm9sJoyZLDql3OqgklvRCkD0XIIHMZV38jKZgAuxwY=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -234,6 +242,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
+github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
 github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
 github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
 github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
diff --git a/pkg/yqutil/yqutil.go b/pkg/yqutil/yqutil.go
index e8628adc..139d9fcb 100644
--- a/pkg/yqutil/yqutil.go
+++ b/pkg/yqutil/yqutil.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"strings"
 
+	"github.com/google/yamlfmt/formatters/basic"
 	"github.com/mikefarah/yq/v4/pkg/yqlib"
 	"github.com/sirupsen/logrus"
 	logging "gopkg.in/op/go-logging.v1"
@@ -69,7 +70,7 @@ func EvaluateExpression(expression string, content []byte) ([]byte, error) {
 		return nil, err
 	}
 
-	return out.Bytes(), nil
+	return yamlfmt(out.Bytes())
 }
 
 func Join(yqExprs []string) string {
@@ -78,3 +79,18 @@ func Join(yqExprs []string) string {
 	}
 	return strings.Join(yqExprs, " | ")
 }
+
+func yamlfmt(content []byte) ([]byte, error) {
+	factory := basic.BasicFormatterFactory{}
+	config := map[string]interface{}{
+		"indentless_arrays":  true,
+		"line_ending":        "lf", // prefer LF even on Windows
+		"pad_line_comments":  2,
+		"retain_line_breaks": true, // does not affect to the output because yq removes empty lines before formatting
+	}
+	formatter, err := factory.NewFormatter(config)
+	if err != nil {
+		return nil, err
+	}
+	return formatter.Format(content)
+}


Test patch:
diff --git a/pkg/yqutil/yqutil_test.go b/pkg/yqutil/yqutil_test.go
index 297a2e2c..fcf1260e 100644
--- a/pkg/yqutil/yqutil_test.go
+++ b/pkg/yqutil/yqutil_test.go
@@ -41,17 +41,18 @@ mounts:
 `
 	// Note: yq will use canonical yaml, with indented sequences
 	// Note: yq will not explicitly quote strings, when not needed
+	// Note: yamlfmt will fix indentation of sequences
 	expected := `
 # Expose host directories to the guest, the mount point might be accessible from all UIDs in the guest
 # 🟢 Builtin default: null (Mount nothing)
 # 🔵 This file: Mount the home as read-only, /tmp/lima as writable
 mounts:
-  - location: "~"
-    # Configure the mountPoint inside the guest.
-    # 🟢 Builtin default: value of location
-    mountPoint: null
-  - location: foo
-    mountPoint: bar
+- location: "~"
+  # Configure the mountPoint inside the guest.
+  # 🟢 Builtin default: value of location
+  mountPoint: null
+- location: foo
+  mountPoint: bar
 `
 	out, err := EvaluateExpression(expression, []byte(content))
 	assert.NilError(t, err)