github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/workcmd/edit.go (about) 1 // Copyright 2021 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // go work edit 6 7 package workcmd 8 9 import ( 10 "context" 11 "encoding/json" 12 "fmt" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "github.com/go-asm/go/cmd/go/base" 18 "github.com/go-asm/go/cmd/go/gover" 19 "github.com/go-asm/go/cmd/go/modload" 20 21 "golang.org/x/mod/module" 22 23 "golang.org/x/mod/modfile" 24 ) 25 26 var cmdEdit = &base.Command{ 27 UsageLine: "go work edit [editing flags] [go.work]", 28 Short: "edit go.work from tools or scripts", 29 Long: `Edit provides a command-line interface for editing go.work, 30 for use primarily by tools or scripts. It only reads go.work; 31 it does not look up information about the modules involved. 32 If no file is specified, Edit looks for a go.work file in the current 33 directory and its parent directories 34 35 The editing flags specify a sequence of editing operations. 36 37 The -fmt flag reformats the go.work file without making other changes. 38 This reformatting is also implied by any other modifications that use or 39 rewrite the go.mod file. The only time this flag is needed is if no other 40 flags are specified, as in 'go work edit -fmt'. 41 42 The -use=path and -dropuse=path flags 43 add and drop a use directive from the go.work file's set of module directories. 44 45 The -replace=old[@v]=new[@v] flag adds a replacement of the given 46 module path and version pair. If the @v in old@v is omitted, a 47 replacement without a version on the left side is added, which applies 48 to all versions of the old module path. If the @v in new@v is omitted, 49 the new path should be a local module root directory, not a module 50 path. Note that -replace overrides any redundant replacements for old[@v], 51 so omitting @v will drop existing replacements for specific versions. 52 53 The -dropreplace=old[@v] flag drops a replacement of the given 54 module path and version pair. If the @v is omitted, a replacement without 55 a version on the left side is dropped. 56 57 The -use, -dropuse, -replace, and -dropreplace, 58 editing flags may be repeated, and the changes are applied in the order given. 59 60 The -go=version flag sets the expected Go language version. 61 62 The -toolchain=name flag sets the Go toolchain to use. 63 64 The -print flag prints the final go.work in its text format instead of 65 writing it back to go.mod. 66 67 The -json flag prints the final go.work file in JSON format instead of 68 writing it back to go.mod. The JSON output corresponds to these Go types: 69 70 type GoWork struct { 71 Go string 72 Toolchain string 73 Use []Use 74 Replace []Replace 75 } 76 77 type Use struct { 78 DiskPath string 79 ModulePath string 80 } 81 82 type Replace struct { 83 Old Module 84 New Module 85 } 86 87 type Module struct { 88 Path string 89 Version string 90 } 91 92 See the workspaces reference at https://go.dev/ref/mod#workspaces 93 for more information. 94 `, 95 } 96 97 var ( 98 editFmt = cmdEdit.Flag.Bool("fmt", false, "") 99 editGo = cmdEdit.Flag.String("go", "", "") 100 editToolchain = cmdEdit.Flag.String("toolchain", "", "") 101 editJSON = cmdEdit.Flag.Bool("json", false, "") 102 editPrint = cmdEdit.Flag.Bool("print", false, "") 103 workedits []func(file *modfile.WorkFile) // edits specified in flags 104 ) 105 106 type flagFunc func(string) 107 108 func (f flagFunc) String() string { return "" } 109 func (f flagFunc) Set(s string) error { f(s); return nil } 110 111 func init() { 112 cmdEdit.Run = runEditwork // break init cycle 113 114 cmdEdit.Flag.Var(flagFunc(flagEditworkUse), "use", "") 115 cmdEdit.Flag.Var(flagFunc(flagEditworkDropUse), "dropuse", "") 116 cmdEdit.Flag.Var(flagFunc(flagEditworkReplace), "replace", "") 117 cmdEdit.Flag.Var(flagFunc(flagEditworkDropReplace), "dropreplace", "") 118 base.AddChdirFlag(&cmdEdit.Flag) 119 } 120 121 func runEditwork(ctx context.Context, cmd *base.Command, args []string) { 122 if *editJSON && *editPrint { 123 base.Fatalf("go: cannot use both -json and -print") 124 } 125 126 if len(args) > 1 { 127 base.Fatalf("go: 'go help work edit' accepts at most one argument") 128 } 129 var gowork string 130 if len(args) == 1 { 131 gowork = args[0] 132 } else { 133 modload.InitWorkfile() 134 gowork = modload.WorkFilePath() 135 } 136 if gowork == "" { 137 base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)") 138 } 139 140 if *editGo != "" && *editGo != "none" { 141 if !modfile.GoVersionRE.MatchString(*editGo) { 142 base.Fatalf(`go work: invalid -go option; expecting something like "-go %s"`, gover.Local()) 143 } 144 } 145 if *editToolchain != "" && *editToolchain != "none" { 146 if !modfile.ToolchainRE.MatchString(*editToolchain) { 147 base.Fatalf(`go work: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local()) 148 } 149 } 150 151 anyFlags := *editGo != "" || 152 *editToolchain != "" || 153 *editJSON || 154 *editPrint || 155 *editFmt || 156 len(workedits) > 0 157 158 if !anyFlags { 159 base.Fatalf("go: no flags specified (see 'go help work edit').") 160 } 161 162 workFile, err := modload.ReadWorkFile(gowork) 163 if err != nil { 164 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gowork), err) 165 } 166 167 if *editGo == "none" { 168 workFile.DropGoStmt() 169 } else if *editGo != "" { 170 if err := workFile.AddGoStmt(*editGo); err != nil { 171 base.Fatalf("go: internal error: %v", err) 172 } 173 } 174 if *editToolchain == "none" { 175 workFile.DropToolchainStmt() 176 } else if *editToolchain != "" { 177 if err := workFile.AddToolchainStmt(*editToolchain); err != nil { 178 base.Fatalf("go: internal error: %v", err) 179 } 180 } 181 182 if len(workedits) > 0 { 183 for _, edit := range workedits { 184 edit(workFile) 185 } 186 } 187 188 workFile.SortBlocks() 189 workFile.Cleanup() // clean file after edits 190 191 // Note: No call to modload.UpdateWorkFile here. 192 // Edit's job is only to make the edits on the command line, 193 // not to apply the kinds of semantic changes that 194 // UpdateWorkFile does (or would eventually do, if we 195 // decide to add the module comments in go.work). 196 197 if *editJSON { 198 editPrintJSON(workFile) 199 return 200 } 201 202 if *editPrint { 203 os.Stdout.Write(modfile.Format(workFile.Syntax)) 204 return 205 } 206 207 modload.WriteWorkFile(gowork, workFile) 208 } 209 210 // flagEditworkUse implements the -use flag. 211 func flagEditworkUse(arg string) { 212 workedits = append(workedits, func(f *modfile.WorkFile) { 213 _, mf, err := modload.ReadModFile(filepath.Join(arg, "go.mod"), nil) 214 modulePath := "" 215 if err == nil { 216 modulePath = mf.Module.Mod.Path 217 } 218 f.AddUse(modload.ToDirectoryPath(arg), modulePath) 219 if err := f.AddUse(modload.ToDirectoryPath(arg), ""); err != nil { 220 base.Fatalf("go: -use=%s: %v", arg, err) 221 } 222 }) 223 } 224 225 // flagEditworkDropUse implements the -dropuse flag. 226 func flagEditworkDropUse(arg string) { 227 workedits = append(workedits, func(f *modfile.WorkFile) { 228 if err := f.DropUse(modload.ToDirectoryPath(arg)); err != nil { 229 base.Fatalf("go: -dropdirectory=%s: %v", arg, err) 230 } 231 }) 232 } 233 234 // allowedVersionArg returns whether a token may be used as a version in go.mod. 235 // We don't call modfile.CheckPathVersion, because that insists on versions 236 // being in semver form, but here we want to allow versions like "master" or 237 // "1234abcdef", which the go command will resolve the next time it runs (or 238 // during -fix). Even so, we need to make sure the version is a valid token. 239 func allowedVersionArg(arg string) bool { 240 return !modfile.MustQuote(arg) 241 } 242 243 // parsePathVersionOptional parses path[@version], using adj to 244 // describe any errors. 245 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) { 246 before, after, found := strings.Cut(arg, "@") 247 if !found { 248 path = arg 249 } else { 250 path, version = strings.TrimSpace(before), strings.TrimSpace(after) 251 } 252 if err := module.CheckImportPath(path); err != nil { 253 if !allowDirPath || !modfile.IsDirectoryPath(path) { 254 return path, version, fmt.Errorf("invalid %s path: %v", adj, err) 255 } 256 } 257 if path != arg && !allowedVersionArg(version) { 258 return path, version, fmt.Errorf("invalid %s version: %q", adj, version) 259 } 260 return path, version, nil 261 } 262 263 // flagEditworkReplace implements the -replace flag. 264 func flagEditworkReplace(arg string) { 265 before, after, found := strings.Cut(arg, "=") 266 if !found { 267 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg) 268 } 269 old, new := strings.TrimSpace(before), strings.TrimSpace(after) 270 if strings.HasPrefix(new, ">") { 271 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg) 272 } 273 oldPath, oldVersion, err := parsePathVersionOptional("old", old, false) 274 if err != nil { 275 base.Fatalf("go: -replace=%s: %v", arg, err) 276 } 277 newPath, newVersion, err := parsePathVersionOptional("new", new, true) 278 if err != nil { 279 base.Fatalf("go: -replace=%s: %v", arg, err) 280 } 281 if newPath == new && !modfile.IsDirectoryPath(new) { 282 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg) 283 } 284 285 workedits = append(workedits, func(f *modfile.WorkFile) { 286 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil { 287 base.Fatalf("go: -replace=%s: %v", arg, err) 288 } 289 }) 290 } 291 292 // flagEditworkDropReplace implements the -dropreplace flag. 293 func flagEditworkDropReplace(arg string) { 294 path, version, err := parsePathVersionOptional("old", arg, true) 295 if err != nil { 296 base.Fatalf("go: -dropreplace=%s: %v", arg, err) 297 } 298 workedits = append(workedits, func(f *modfile.WorkFile) { 299 if err := f.DropReplace(path, version); err != nil { 300 base.Fatalf("go: -dropreplace=%s: %v", arg, err) 301 } 302 }) 303 } 304 305 type replaceJSON struct { 306 Old module.Version 307 New module.Version 308 } 309 310 // editPrintJSON prints the -json output. 311 func editPrintJSON(workFile *modfile.WorkFile) { 312 var f workfileJSON 313 if workFile.Go != nil { 314 f.Go = workFile.Go.Version 315 } 316 for _, d := range workFile.Use { 317 f.Use = append(f.Use, useJSON{DiskPath: d.Path, ModPath: d.ModulePath}) 318 } 319 320 for _, r := range workFile.Replace { 321 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New}) 322 } 323 data, err := json.MarshalIndent(&f, "", "\t") 324 if err != nil { 325 base.Fatalf("go: internal error: %v", err) 326 } 327 data = append(data, '\n') 328 os.Stdout.Write(data) 329 } 330 331 // workfileJSON is the -json output data structure. 332 type workfileJSON struct { 333 Go string `json:",omitempty"` 334 Use []useJSON 335 Replace []replaceJSON 336 } 337 338 type useJSON struct { 339 DiskPath string 340 ModPath string `json:",omitempty"` 341 }