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 (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 }