dati

A Go library/binary to parse & execute data against template langauges.
git clone git://src.gearsix.net/datidati.zip
Log | Files | Refs | Atom | README | LICENSE

dati.go (raw) (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 }