cuelang.org/go@v0.13.0/internal/filetypes/filetypes.go (about) 1 // Copyright 2020 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 15 package filetypes 16 17 import ( 18 "iter" 19 "path/filepath" 20 "strconv" 21 "strings" 22 23 "cuelang.org/go/cue/build" 24 "cuelang.org/go/cue/errors" 25 "cuelang.org/go/cue/token" 26 "cuelang.org/go/internal/filetypes/internal" 27 ) 28 29 // Mode indicate the base mode of operation and indicates a different set of 30 // defaults. 31 type Mode int 32 33 const ( 34 Input Mode = iota // The default 35 Export 36 Def 37 Eval 38 NumModes 39 ) 40 41 func (m Mode) String() string { 42 switch m { 43 default: 44 return "input" 45 case Eval: 46 return "eval" 47 case Export: 48 return "export" 49 case Def: 50 return "def" 51 } 52 } 53 54 type FileInfo = internal.FileInfo 55 56 // ParseArgs converts a sequence of command line arguments representing 57 // files into a sequence of build file specifications. 58 // 59 // The arguments are of the form 60 // 61 // file* (spec: file+)* 62 // 63 // where file is a filename and spec is itself of the form 64 // 65 // tag[=value]('+'tag[=value])* 66 // 67 // A file type spec applies to all its following files and until a next spec 68 // is found. 69 // 70 // Examples: 71 // 72 // json: foo.data bar.data json+schema: bar.schema 73 func ParseArgs(args []string) (files []*build.File, err error) { 74 qualifier := "" 75 hasFiles := false 76 77 sc := &scope{} 78 for i, s := range args { 79 a := strings.Split(s, ":") 80 switch { 81 case len(a) == 1 || len(a[0]) == 1: // filename 82 if s == "" { 83 return nil, errors.Newf(token.NoPos, "empty file name") 84 } 85 f, err := toFile(Input, sc, s) 86 if err != nil { 87 return nil, err 88 } 89 files = append(files, f) 90 hasFiles = true 91 92 case len(a) > 2 || a[0] == "": 93 return nil, errors.Newf(token.NoPos, 94 "unsupported file name %q: may not have ':'", s) 95 96 case a[1] != "": 97 return nil, errors.Newf(token.NoPos, "cannot combine scope with file") 98 99 default: // scope 100 switch { 101 case i == len(args)-1: 102 qualifier = a[0] 103 fallthrough 104 case qualifier != "" && !hasFiles: 105 return nil, errors.Newf(token.NoPos, "scoped qualifier %q without file", qualifier+":") 106 } 107 sc, err = parseScope(a[0]) 108 if err != nil { 109 return nil, err 110 } 111 qualifier = a[0] 112 hasFiles = false 113 } 114 } 115 116 return files, nil 117 } 118 119 // DefaultTagsForInterpretation returns any tags that would be set by default 120 // in the given interpretation in the given mode. 121 func DefaultTagsForInterpretation(interp build.Interpretation, mode Mode) map[string]bool { 122 if interp == "" { 123 return nil 124 } 125 126 // This should never fail if called with a legitimate build.Interpretation constant. 127 f, err := toFile(mode, &scope{ 128 topLevel: map[string]bool{ 129 string(interp): true, 130 }, 131 }, "-") 132 if err != nil { 133 panic(err) 134 } 135 return f.BoolTags 136 } 137 138 // ParseFile parses a single-argument file specifier, such as when a file is 139 // passed to a command line argument. 140 // 141 // Example: 142 // 143 // cue eval -o yaml:foo.data 144 func ParseFile(s string, mode Mode) (*build.File, error) { 145 scope := "" 146 file := s 147 148 if p := strings.LastIndexByte(s, ':'); p >= 0 { 149 scope = s[:p] 150 file = s[p+1:] 151 if scope == "" { 152 return nil, errors.Newf(token.NoPos, "unsupported file name %q: may not have ':", s) 153 } 154 } 155 156 if file == "" { 157 if s != "" { 158 return nil, errors.Newf(token.NoPos, "empty file name in %q", s) 159 } 160 return nil, errors.Newf(token.NoPos, "empty file name") 161 } 162 163 return ParseFileAndType(file, scope, mode) 164 } 165 166 // ParseFileAndType parses a file and type combo. 167 func ParseFileAndType(file, scope string, mode Mode) (*build.File, error) { 168 sc, err := parseScope(scope) 169 if err != nil { 170 return nil, err 171 } 172 return toFile(mode, sc, file) 173 } 174 175 // scope holds attributes that influence encoding and decoding. 176 // Together with the mode and the file name, they determine 177 // a number of properties of the encoding process. 178 type scope struct { 179 topLevel map[string]bool 180 subsidiaryBool map[string]bool 181 subsidiaryString map[string]string 182 } 183 184 func parseScope(scopeStr string) (*scope, error) { 185 if scopeStr == "" { 186 return &scope{}, nil 187 } 188 sc := scope{ 189 topLevel: make(map[string]bool), 190 subsidiaryBool: make(map[string]bool), 191 subsidiaryString: make(map[string]string), 192 } 193 for _, tag := range strings.Split(scopeStr, "+") { 194 tagName, tagVal, hasValue := strings.Cut(tag, "=") 195 switch tagTypes[tagName] { 196 case TagTopLevel: 197 if hasValue { 198 return nil, errors.Newf(token.NoPos, "cannot specify value for tag %q", tagName) 199 } 200 sc.topLevel[tagName] = true 201 case TagSubsidiaryBool: 202 if hasValue { 203 t, err := strconv.ParseBool(tagVal) 204 if err != nil { 205 return nil, errors.Newf(token.NoPos, "invalid boolean value for tag %q", tagName) 206 } 207 sc.subsidiaryBool[tagName] = t 208 } else { 209 sc.subsidiaryBool[tagName] = true 210 } 211 case TagSubsidiaryString: 212 if !hasValue { 213 return nil, errors.Newf(token.NoPos, "tag %q must have value (%s=<value>)", tagName, tagName) 214 } 215 sc.subsidiaryString[tagName] = tagVal 216 default: 217 return nil, errors.Newf(token.NoPos, "unknown filetype %s", tagName) 218 } 219 } 220 return &sc, nil 221 } 222 223 // fileExt is like filepath.Ext except we don't treat file names starting with "." as having an extension 224 // unless there's also another . in the name. 225 // 226 // It also treats "-" as a special case, so we treat stdin/stdout as 227 // a regular file. 228 func fileExt(f string) string { 229 if f == "-" { 230 return "-" 231 } 232 e := filepath.Ext(f) 233 if e == "" || e == filepath.Base(f) { 234 return "" 235 } 236 return e 237 } 238 239 func seqConcat[T any](iters ...iter.Seq[T]) iter.Seq[T] { 240 return func(yield func(T) bool) { 241 for _, it := range iters { 242 for x := range it { 243 if !yield(x) { 244 return 245 } 246 } 247 } 248 } 249 }