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