dati.go (7502B)
1 package main 2 3 /* 4 Copyright (C) 2023 gearsix <gearsix@tuta.io> 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 at your option) any later version. 10 11 This program is distributed in the hope that it will be useful, 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU General Public License for more details. 15 16 You should have received a copy of the GNU General Public License 17 along with this program. If not, see <https://www.gnu.org/licenses/>. 18 */ 19 20 import ( 21 "bufio" 22 "bytes" 23 "fmt" 24 "os" 25 "path/filepath" 26 "strings" 27 28 "notabug.org/gearsix/dati" 29 ) 30 31 // Data is just a generic map for key/value data 32 type Data map[string]interface{} 33 34 type options struct { 35 RootPath string 36 PartialPaths []string 37 GlobalDataPaths []string 38 DataPaths []string 39 DataKey string 40 SortData string 41 ConfigFile string 42 } 43 44 var opts options 45 var cwd string 46 47 func warn(err error, msg string, args ...interface{}) { 48 warning := "WARNING " 49 if len(msg) > 0 { 50 warning += strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n") 51 if err != nil { 52 warning += ": " 53 } 54 } 55 if err != nil { 56 warning += err.Error() 57 } 58 fmt.Println(warning) 59 } 60 61 func assert(err error, msg string, args ...interface{}) { 62 if err != nil { 63 fmt.Printf("ERROR %s\n%s\n", strings.TrimSuffix(fmt.Sprintf(msg, args...), "\n"), err) 64 os.Exit(1) 65 } 66 } 67 68 func basedir(path string) string { 69 if !filepath.IsAbs(path) { 70 path = filepath.Join(cwd, path) 71 } 72 return path 73 } 74 75 func init() { 76 if len(os.Args) <= 1 { 77 fmt.Println("nothing to do") 78 os.Exit(0) 79 } 80 81 opts = parseArgs(os.Args[1:], options{}) 82 if len(opts.ConfigFile) != 0 { 83 cwd = filepath.Dir(opts.ConfigFile) 84 opts = parseConfig(opts.ConfigFile, opts) 85 } 86 opts = setDefaultOptions(opts) 87 } 88 89 func main() { 90 var err error 91 var global Data 92 var data []Data 93 var template dati.Template 94 var out bytes.Buffer 95 96 opts.GlobalDataPaths = loadFilePaths(opts.GlobalDataPaths...) 97 for _, path := range opts.GlobalDataPaths { 98 var d Data 99 err = dati.LoadDataFile(path, &d) 100 assert(err, "failed to load global data '%s'", path) 101 data = append(data, d) 102 } 103 global = mergeData(data) 104 105 opts.DataPaths = loadFilePaths(opts.DataPaths...) 106 opts.DataPaths, err = dati.SortFileList(opts.DataPaths, opts.SortData) 107 if err != nil { 108 warn(err, "failed to sort data files") 109 } 110 data = make([]Data, 0) 111 for _, path := range opts.DataPaths { 112 var d Data 113 err = dati.LoadDataFile(path, &d) 114 assert(err, "failed to load data '%s'", path) 115 data = append(data, d) 116 } 117 global[opts.DataKey] = data 118 119 template, err = dati.LoadTemplateFile(opts.RootPath, opts.PartialPaths...) 120 assert(err, "unable to load templates") 121 122 out, err = template.Execute(global) 123 assert(err, "failed to execute template '%s'", opts.RootPath) 124 fmt.Print(out.String()) 125 126 return 127 } 128 129 func help() { 130 fmt.Print("Usage: dati [OPTIONS]\n\n") 131 132 fmt.Print("Options") 133 fmt.Print(` 134 -r path, -root path 135 path of template file to execute against. 136 137 -p path..., -partial path... 138 path of (multiple) template files that are called upon by at least one 139 root template. If a directory is passed then all files within that 140 directory will (recursively) be loaded. 141 142 -gd path..., -global-data path... 143 path of (multiple) data files to load as "global data". If a directory is 144 passed then all files within that directory will (recursively) be loaded. 145 146 -d path..., -data path... 147 path of (multiple) data files to load as "data". If a directory is passed 148 then all files within that directory will (recursively) be loaded. 149 150 -dk name, -data-key name 151 set the name of the key used for the generated array of data (default: 152 "data") 153 154 -sd attribute, -sort-data attribute 155 The file attribute to order data files by. If no value is provided, the data 156 will be provided in the order it's loaded. 157 Accepted values: "filename", "modified". 158 A suffix can be appended to each value to set the sort order: "-asc" (for 159 ascending), "-desc" (for descending). If not specified, this defaults to 160 "-asc". 161 -cfg file, -config file 162 A data file to provide default values for the above options (see CONFIG). 163 164 `) 165 } 166 167 // custom arg parser because golang.org/pkg/flag doesn't support list args 168 func parseArgs(args []string, existing options) (o options) { 169 o = existing 170 var flag string 171 for a := 0; a < len(args); a++ { 172 arg := args[a] 173 if arg[0] == '-' && flag != "--" { 174 flag = arg 175 ndelims := 0 176 for len(flag) > 0 && flag[0] == '-' { 177 flag = flag[1:] 178 ndelims++ 179 } 180 181 if ndelims > 2 { 182 warn(nil, "bad flag syntax: '%s'", arg) 183 flag = "" 184 } 185 186 if strings.Contains(flag, "=") { 187 split := strings.SplitN(flag, "=", 2) 188 flag = split[0] 189 args[a] = split[1] 190 a-- 191 } 192 193 // set valid any flags that don't take arguments here 194 if flag == "h" || flag == "help" { 195 help() 196 os.Exit(0) 197 } 198 } else if (flag == "r" || flag == "root") && len(o.RootPath) == 0 { 199 o.RootPath = basedir(arg) 200 } else if flag == "p" || flag == "partial" { 201 o.PartialPaths = append(o.PartialPaths, basedir(arg)) 202 } else if flag == "gd" || flag == "globaldata" { 203 o.GlobalDataPaths = append(o.GlobalDataPaths, basedir(arg)) 204 } else if flag == "d" || flag == "data" { 205 o.DataPaths = append(o.DataPaths, basedir(arg)) 206 } else if flag == "dk" || flag == "datakey" && len(o.DataKey) == 0 { 207 o.DataKey = arg 208 } else if flag == "sd" || flag == "sortdata" && len(o.SortData) == 0 { 209 o.SortData = arg 210 } else if flag == "cfg" || flag == "config" && len(o.ConfigFile) == 0 { 211 o.ConfigFile = basedir(arg) 212 } else if len(flag) == 0 { 213 // skip unknown flag arguments 214 } else { 215 warn(nil, "ignoring flag: '%s'", flag) 216 flag = "" 217 } 218 } 219 220 return 221 } 222 223 func parseConfig(fpath string, existing options) options { 224 var err error 225 var cfgf *os.File 226 if cfgf, err = os.Open(fpath); err != nil { 227 warn(err, "error loading config file '%s'", fpath) 228 } 229 defer cfgf.Close() 230 231 var args []string 232 scanf := bufio.NewScanner(cfgf) 233 for scanf.Scan() { 234 for i, arg := range strings.Split(scanf.Text(), "=") { 235 arg = strings.TrimSpace(arg) 236 if i == 0 { 237 arg = "-" + arg 238 } 239 args = append(args, arg) 240 } 241 } 242 return parseArgs(args, existing) 243 } 244 245 func setDefaultOptions(o options) options { 246 if len(o.SortData) == 0 { 247 o.SortData = "filename" 248 } 249 if len(o.DataKey) == 0 { 250 o.DataKey = "data" 251 } 252 return o 253 } 254 255 // load glob & dir filepaths as individual filepaths 256 func loadFilePaths(paths ...string) (filepaths []string) { 257 for _, path := range paths { 258 var err error 259 if strings.Contains(path, "*") { 260 var glob []string 261 glob, err = filepath.Glob(path) 262 assert(err, "failed to glob '%s'", path) 263 for _, p := range glob { 264 filepaths = append(filepaths, p) 265 } 266 } else { 267 err = filepath.Walk(path, 268 func(p string, info os.FileInfo, e error) error { 269 if e == nil && !info.IsDir() { 270 filepaths = append(filepaths, p) 271 } 272 return e 273 }) 274 } 275 if err != nil { 276 assert(err, "failed to load filepaths for '%s'", path) 277 } 278 } 279 return 280 } 281 282 func mergeData(data []Data) (merged Data) { 283 merged = make(Data) 284 for _, d := range data { 285 for key, val := range d { 286 if merged[key] == nil { 287 merged[key] = val 288 } else { 289 warn(nil, "merge conflict for global data key: '%s'", key) 290 } 291 } 292 } 293 return 294 }