| // Copyright 2019 The CUE Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package cmd |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "html/template" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "github.com/spf13/cobra" |
| |
| "cuelang.org/go/cue" |
| "cuelang.org/go/cue/build" |
| "cuelang.org/go/cue/format" |
| "cuelang.org/go/cue/load" |
| "cuelang.org/go/cue/parser" |
| "cuelang.org/go/internal" |
| ) |
| |
| func newAddCmd(c *Command) *cobra.Command { |
| cmd := &cobra.Command{ |
| // TODO: this command is still experimental, don't show it in |
| // the documentation just yet. |
| Hidden: true, |
| |
| Use: "add <glob> [--list]", |
| Short: "bulk append to CUE files", |
| Long: `Append a common snippet of CUE to many files and commit atomically. |
| `, |
| RunE: mkRunE(c, runAdd), |
| } |
| |
| f := cmd.Flags() |
| f.Bool(string(flagList), false, |
| "text executed as Go template with instance info") |
| f.BoolP(string(flagDryrun), "n", false, |
| "only run simulation") |
| f.StringP(string(flagPackage), "p", "", "package to append to") |
| |
| return cmd |
| } |
| |
| func runAdd(cmd *Command, args []string) (err error) { |
| return doAdd(cmd, args) |
| } |
| |
| func doAdd(cmd *Command, args []string) (err error) { |
| // Offsets at which to restore original files, if any, if any of the |
| // appends fail. |
| // Ideally this is placed below where it is used, but we want to make |
| // absolutely sure that the error variable used in defer is the named |
| // returned value and not some shadowed value. |
| |
| originals := []originalFile{} |
| defer func() { |
| if err != nil { |
| restoreOriginals(cmd, originals) |
| } |
| }() |
| |
| // build instance cache |
| builds := map[string]*build.Instance{} |
| |
| getBuild := func(path string) *build.Instance { |
| if b, ok := builds[path]; ok { |
| return b |
| } |
| b := load.Instances([]string{path}, nil)[0] |
| builds[path] = b |
| return b |
| } |
| |
| // determine file set. |
| |
| todo := []*fileInfo{} |
| |
| done := map[string]bool{} |
| |
| for _, arg := range args { |
| dir, base := filepath.Split(arg) |
| dir = filepath.Clean(dir) |
| matches, err := filepath.Glob(dir) |
| if err != nil { |
| return err |
| } |
| for _, m := range matches { |
| if fi, err := os.Stat(m); err != nil || !fi.IsDir() { |
| continue |
| } |
| file := filepath.Join(m, base) |
| if done[file] { |
| continue |
| } |
| if s := filepath.ToSlash(file); strings.HasPrefix(s, "pkg/") || strings.Contains(s, "/pkg/") { |
| continue |
| } |
| done[file] = true |
| fi, err := initFile(cmd, file, getBuild) |
| if err != nil { |
| return err |
| } |
| todo = append(todo, fi) |
| b := fi.build |
| if flagList.Bool(cmd) && (b == nil) { |
| return fmt.Errorf("instance info not available for %s", fi.filename) |
| } |
| } |
| } |
| |
| // Read text to be appended. |
| text, err := ioutil.ReadAll(cmd.InOrStdin()) |
| if err != nil { |
| return err |
| } |
| |
| var tmpl *template.Template |
| if flagList.Bool(cmd) { |
| tmpl, err = template.New("append").Parse(string(text)) |
| if err != nil { |
| return err |
| } |
| } |
| |
| for _, fi := range todo { |
| if tmpl == nil { |
| fi.contents.Write(text) |
| continue |
| } |
| if err := tmpl.Execute(fi.contents, fi.build); err != nil { |
| return err |
| } |
| } |
| |
| if flagDryrun.Bool(cmd) { |
| stdout := cmd.OutOrStdout() |
| for _, fi := range todo { |
| fmt.Fprintln(stdout, "---", fi.filename) |
| _, _ = io.Copy(stdout, fi.contents) |
| } |
| return nil |
| } |
| |
| // All verified. Execute the todo plan |
| for _, fi := range todo { |
| fo, err := fi.appendAndCheck() |
| if err != nil { |
| return err |
| } |
| originals = append(originals, fo) |
| } |
| |
| // Verify resulting builds |
| for _, fi := range todo { |
| builds = map[string]*build.Instance{} |
| |
| b := getBuild(fi.buildArg) |
| if b.Err != nil { |
| return b.Err |
| } |
| i := cue.Build([]*build.Instance{b})[0] |
| if i.Err != nil { |
| return i.Err |
| } |
| if err := i.Value().Validate(); err != nil { |
| return i.Err |
| } |
| } |
| |
| return nil |
| } |
| |
| type originalFile struct { |
| filename string |
| contents []byte |
| } |
| |
| func restoreOriginals(cmd *Command, originals []originalFile) { |
| for _, fo := range originals { |
| if err := fo.restore(); err != nil { |
| fmt.Fprintln(cmd.Stderr(), "Error restoring file: ", err) |
| } |
| } |
| } |
| |
| func (fo *originalFile) restore() error { |
| if len(fo.contents) == 0 { |
| return os.Remove(fo.filename) |
| } |
| return ioutil.WriteFile(fo.filename, fo.contents, 0644) |
| } |
| |
| type fileInfo struct { |
| filename string |
| buildArg string |
| contents *bytes.Buffer |
| build *build.Instance |
| } |
| |
| func initFile(cmd *Command, file string, getBuild func(path string) *build.Instance) (todo *fileInfo, err error) { |
| defer func() { |
| if err != nil { |
| err = fmt.Errorf("init file: %v", err) |
| } |
| }() |
| dir := filepath.Dir(file) |
| todo = &fileInfo{file, dir, &bytes.Buffer{}, nil} |
| |
| if fi, err := os.Stat(file); err != nil { |
| if !os.IsNotExist(err) { |
| return nil, err |
| } |
| // File does not exist |
| b := getBuild(dir) |
| todo.build = b |
| pkg := flagPackage.String(cmd) |
| if pkg != "" { |
| // TODO: do something more intelligent once the package name is |
| // computed on a module basis, even for empty directories. |
| b.PkgName = pkg |
| b.Err = nil |
| } else { |
| pkg = b.PkgName |
| } |
| if pkg == "" { |
| return nil, errors.New("must specify package using -p for new files") |
| } |
| todo.buildArg = file |
| fmt.Fprintf(todo.contents, "package %s\n\n", pkg) |
| } else { |
| if fi.IsDir() { |
| return nil, fmt.Errorf("cannot append to directory %s", file) |
| } |
| |
| f, err := parser.ParseFile(file, nil) |
| if err != nil { |
| return nil, err |
| } |
| if _, pkgName, _ := internal.PackageInfo(f); pkgName != "" { |
| if pkg := flagPackage.String(cmd); pkg != "" && pkgName != pkg { |
| return nil, fmt.Errorf("package mismatch (%s vs %s) for file %s", pkgName, pkg, file) |
| } |
| todo.build = getBuild(dir) |
| } else { |
| if pkg := flagPackage.String(cmd); pkg != "" { |
| return nil, fmt.Errorf("file %s has no package clause but package %s requested", file, pkg) |
| } |
| todo.build = getBuild(file) |
| todo.buildArg = file |
| } |
| } |
| return todo, nil |
| } |
| |
| func (fi *fileInfo) appendAndCheck() (fo originalFile, err error) { |
| // Read original file |
| b, err := ioutil.ReadFile(fi.filename) |
| if err == nil { |
| fo.filename = fi.filename |
| fo.contents = b |
| } else if !os.IsNotExist(err) { |
| return originalFile{}, err |
| } |
| |
| if !bytes.HasSuffix(b, []byte("\n")) { |
| b = append(b, '\n') |
| } |
| b = append(b, fi.contents.Bytes()...) |
| |
| b, err = format.Source(b) |
| if err != nil { |
| return originalFile{}, err |
| } |
| |
| if err = ioutil.WriteFile(fi.filename, b, 0644); err != nil { |
| // Just in case, attempt to restore original file. |
| _ = fo.restore() |
| return originalFile{}, err |
| } |
| |
| return fo, nil |
| } |