dati

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

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 }