1// Copyright 2019 The CUE Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package cmd 16 17import ( 18 "bytes" 19 "errors" 20 "fmt" 21 "html/template" 22 "io" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 "strings" 27 28 "github.com/spf13/cobra" 29 30 "cuelang.org/go/cue" 31 "cuelang.org/go/cue/build" 32 "cuelang.org/go/cue/format" 33 "cuelang.org/go/cue/load" 34 "cuelang.org/go/cue/parser" 35 "cuelang.org/go/internal" 36) 37 38func newAddCmd(c *Command) *cobra.Command { 39 cmd := &cobra.Command{ 40 // TODO: this command is still experimental, don't show it in 41 // the documentation just yet. 42 Hidden: true, 43 44 Use: "add <glob> [--list]", 45 Short: "bulk append to CUE files", 46 Long: `Append a common snippet of CUE to many files and commit atomically. 47`, 48 RunE: mkRunE(c, runAdd), 49 } 50 51 f := cmd.Flags() 52 f.Bool(string(flagList), false, 53 "text executed as Go template with instance info") 54 f.BoolP(string(flagDryrun), "n", false, 55 "only run simulation") 56 f.StringP(string(flagPackage), "p", "", "package to append to") 57 58 return cmd 59} 60 61func runAdd(cmd *Command, args []string) (err error) { 62 return doAdd(cmd, args) 63} 64 65func doAdd(cmd *Command, args []string) (err error) { 66 // Offsets at which to restore original files, if any, if any of the 67 // appends fail. 68 // Ideally this is placed below where it is used, but we want to make 69 // absolutely sure that the error variable used in defer is the named 70 // returned value and not some shadowed value. 71 72 originals := []originalFile{} 73 defer func() { 74 if err != nil { 75 restoreOriginals(cmd, originals) 76 } 77 }() 78 79 // build instance cache 80 builds := map[string]*build.Instance{} 81 82 getBuild := func(path string) *build.Instance { 83 if b, ok := builds[path]; ok { 84 return b 85 } 86 b := load.Instances([]string{path}, nil)[0] 87 builds[path] = b 88 return b 89 } 90 91 // determine file set. 92 93 todo := []*fileInfo{} 94 95 done := map[string]bool{} 96 97 for _, arg := range args { 98 dir, base := filepath.Split(arg) 99 dir = filepath.Clean(dir) 100 matches, err := filepath.Glob(dir) 101 if err != nil { 102 return err 103 } 104 for _, m := range matches { 105 if fi, err := os.Stat(m); err != nil || !fi.IsDir() { 106 continue 107 } 108 file := filepath.Join(m, base) 109 if done[file] { 110 continue 111 } 112 if s := filepath.ToSlash(file); strings.HasPrefix(s, "pkg/") || strings.Contains(s, "/pkg/") { 113 continue 114 } 115 done[file] = true 116 fi, err := initFile(cmd, file, getBuild) 117 if err != nil { 118 return err 119 } 120 todo = append(todo, fi) 121 b := fi.build 122 if flagList.Bool(cmd) && (b == nil) { 123 return fmt.Errorf("instance info not available for %s", fi.filename) 124 } 125 } 126 } 127 128 // Read text to be appended. 129 text, err := ioutil.ReadAll(cmd.InOrStdin()) 130 if err != nil { 131 return err 132 } 133 134 var tmpl *template.Template 135 if flagList.Bool(cmd) { 136 tmpl, err = template.New("append").Parse(string(text)) 137 if err != nil { 138 return err 139 } 140 } 141 142 for _, fi := range todo { 143 if tmpl == nil { 144 fi.contents.Write(text) 145 continue 146 } 147 if err := tmpl.Execute(fi.contents, fi.build); err != nil { 148 return err 149 } 150 } 151 152 if flagDryrun.Bool(cmd) { 153 stdout := cmd.OutOrStdout() 154 for _, fi := range todo { 155 fmt.Fprintln(stdout, "---", fi.filename) 156 _, _ = io.Copy(stdout, fi.contents) 157 } 158 return nil 159 } 160 161 // All verified. Execute the todo plan 162 for _, fi := range todo { 163 fo, err := fi.appendAndCheck() 164 if err != nil { 165 return err 166 } 167 originals = append(originals, fo) 168 } 169 170 // Verify resulting builds 171 for _, fi := range todo { 172 builds = map[string]*build.Instance{} 173 174 b := getBuild(fi.buildArg) 175 if b.Err != nil { 176 return b.Err 177 } 178 i := cue.Build([]*build.Instance{b})[0] 179 if i.Err != nil { 180 return i.Err 181 } 182 if err := i.Value().Validate(); err != nil { 183 return i.Err 184 } 185 } 186 187 return nil 188} 189 190type originalFile struct { 191 filename string 192 contents []byte 193} 194 195func restoreOriginals(cmd *Command, originals []originalFile) { 196 for _, fo := range originals { 197 if err := fo.restore(); err != nil { 198 fmt.Fprintln(cmd.Stderr(), "Error restoring file: ", err) 199 } 200 } 201} 202 203func (fo *originalFile) restore() error { 204 if len(fo.contents) == 0 { 205 return os.Remove(fo.filename) 206 } 207 return ioutil.WriteFile(fo.filename, fo.contents, 0644) 208} 209 210type fileInfo struct { 211 filename string 212 buildArg string 213 contents *bytes.Buffer 214 build *build.Instance 215} 216 217func initFile(cmd *Command, file string, getBuild func(path string) *build.Instance) (todo *fileInfo, err error) { 218 defer func() { 219 if err != nil { 220 err = fmt.Errorf("init file: %v", err) 221 } 222 }() 223 dir := filepath.Dir(file) 224 todo = &fileInfo{file, dir, &bytes.Buffer{}, nil} 225 226 if fi, err := os.Stat(file); err != nil { 227 if !os.IsNotExist(err) { 228 return nil, err 229 } 230 // File does not exist 231 b := getBuild(dir) 232 todo.build = b 233 pkg := flagPackage.String(cmd) 234 if pkg != "" { 235 // TODO: do something more intelligent once the package name is 236 // computed on a module basis, even for empty directories. 237 b.PkgName = pkg 238 b.Err = nil 239 } else { 240 pkg = b.PkgName 241 } 242 if pkg == "" { 243 return nil, errors.New("must specify package using -p for new files") 244 } 245 todo.buildArg = file 246 fmt.Fprintf(todo.contents, "package %s\n\n", pkg) 247 } else { 248 if fi.IsDir() { 249 return nil, fmt.Errorf("cannot append to directory %s", file) 250 } 251 252 f, err := parser.ParseFile(file, nil) 253 if err != nil { 254 return nil, err 255 } 256 if _, pkgName, _ := internal.PackageInfo(f); pkgName != "" { 257 if pkg := flagPackage.String(cmd); pkg != "" && pkgName != pkg { 258 return nil, fmt.Errorf("package mismatch (%s vs %s) for file %s", pkgName, pkg, file) 259 } 260 todo.build = getBuild(dir) 261 } else { 262 if pkg := flagPackage.String(cmd); pkg != "" { 263 return nil, fmt.Errorf("file %s has no package clause but package %s requested", file, pkg) 264 } 265 todo.build = getBuild(file) 266 todo.buildArg = file 267 } 268 } 269 return todo, nil 270} 271 272func (fi *fileInfo) appendAndCheck() (fo originalFile, err error) { 273 // Read original file 274 b, err := ioutil.ReadFile(fi.filename) 275 if err == nil { 276 fo.filename = fi.filename 277 fo.contents = b 278 } else if !os.IsNotExist(err) { 279 return originalFile{}, err 280 } 281 282 if !bytes.HasSuffix(b, []byte("\n")) { 283 b = append(b, '\n') 284 } 285 b = append(b, fi.contents.Bytes()...) 286 287 b, err = format.Source(b) 288 if err != nil { 289 return originalFile{}, err 290 } 291 292 if err = ioutil.WriteFile(fi.filename, b, 0644); err != nil { 293 // Just in case, attempt to restore original file. 294 _ = fo.restore() 295 return originalFile{}, err 296 } 297 298 return fo, nil 299} 300