github.com/suntong/easygen@v5.3.0+incompatible/easygen.go (about) 1 //////////////////////////////////////////////////////////////////////////// 2 // Package: easygen 3 // Purpose: Easy to use universal code/text generator 4 // Authors: Tong Sun (c) 2015-2021, All rights reserved 5 //////////////////////////////////////////////////////////////////////////// 6 7 /* 8 9 Package easygen is an easy to use universal code/text generator library. 10 11 It can be used as a text or html generator for arbitrary purposes with arbitrary data and templates. 12 13 It can be used as a code generator, or anything that is structurally repetitive. Some command line parameter handling code generator are provided as examples, including the Go's built-in flag package, and the viper & cobra package. 14 15 Many examples have been provided to showcase its functionality, and different ways to use it. 16 17 */ 18 package easygen 19 20 import ( 21 "encoding/json" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "os" 26 "path/filepath" 27 "regexp" 28 "strings" 29 30 "gopkg.in/yaml.v3" 31 ) 32 33 //////////////////////////////////////////////////////////////////////////// 34 // Constant and data type/structure definitions 35 36 //////////////////////////////////////////////////////////////////////////// 37 // Global variables definitions 38 39 // EgData, EasyGen key type 40 type EgKey = string 41 42 // EgData, EasyGen driven Data 43 type EgData map[EgKey]interface{} 44 45 // Opts holds the actual values from the command line parameters 46 var Opts = Options{ExtYaml: ".yaml", ExtJson: ".json", ExtTmpl: ".tmpl"} 47 48 //////////////////////////////////////////////////////////////////////////// 49 // Function definitions 50 51 // ReadDataFiles reads in the driving data from the given file, which can 52 // be optionally without the defined extension, and can be a comma-separated 53 // string for multiple data files. 54 func ReadDataFiles(fileName string) EgData { 55 var m EgData 56 for _, dataFn := range strings.Split(fileName, ",") { 57 m = ReadDataFile(dataFn, m) 58 if Opts.Debug >= 1 { 59 fmt.Fprintf(os.Stderr, "[%s] After reading file %s:\n %+v\n", progname, dataFn, m) 60 } 61 } 62 return m 63 } 64 65 // ReadDataFile reads in the driving data from the given file, which can 66 // be optionally without the defined extension 67 func ReadDataFile(fileName string, ms ...EgData) EgData { 68 if IsExist(fileName + Opts.ExtYaml) { 69 return ReadYamlFile(fileName+Opts.ExtYaml, ms...) 70 } else if IsExist(fileName + Opts.ExtJson) { 71 return ReadJsonFile(fileName+Opts.ExtJson, ms...) 72 } else if IsExist(fileName) { 73 verbose("Reading exist Data File", 3) 74 fext := filepath.Ext(fileName) 75 fext = fext[1:] // ignore the leading "." 76 if regexp.MustCompile(`(?i)^y`).MatchString(fext) { 77 verbose("Reading YAML file", 3) 78 return ReadYamlFile(fileName, ms...) 79 } else if regexp.MustCompile(`(?i)^j`).MatchString(fext) { 80 return ReadJsonFile(fileName, ms...) 81 } else { 82 checkError(fmt.Errorf("Unsupported file extension for DataFile '%s'", fileName)) 83 } 84 } else if fileName == "-" { 85 // from stdin 86 // Yaml format is a superset of JSON, it read Json file just as fine 87 return ReadYamlFile(fileName) 88 } 89 checkError(fmt.Errorf("DataFile '%s' cannot be found", fileName)) 90 return nil 91 } 92 93 // ReadYamlFile reads given YAML file as EgData 94 func ReadYamlFile(fileName string, ms ...EgData) EgData { 95 var source []byte 96 var err error 97 if fileName == "-" { 98 source, err = ioutil.ReadAll(os.Stdin) 99 checkError(err) 100 } else { 101 source, err = ioutil.ReadFile(fileName) 102 checkError(err) 103 } 104 105 m := EgData{} 106 if len(ms) > 0 { 107 m = ms[0] 108 } 109 110 err = yaml.Unmarshal(source, &m) 111 checkError(err) 112 113 return m 114 } 115 116 // ReadJsonFile reads given JSON file as EgData 117 func ReadJsonFile(fileName string, ms ...EgData) EgData { 118 source, err := ioutil.ReadFile(fileName) 119 checkError(err) 120 121 m := EgData{} 122 if len(ms) > 0 { 123 m = ms[0] 124 } 125 126 err = json.Unmarshal(source, &m) 127 checkError(err) 128 129 //fmt.Printf("] Input %v\n", m) 130 return m 131 } 132 133 // IsExist checks if the given file exist 134 func IsExist(fileName string) bool { 135 //fmt.Printf("] Checking %s\n", fileName) 136 _, err := os.Stat(fileName) 137 return err == nil || os.IsExist(err) 138 // CAUTION! os.IsExist(err) != !os.IsNotExist(err) 139 // https://gist.github.com/mastef/05f46d3ab2f5ed6a6787#file-isexist_vs_isnotexist-go-L35-L56 140 } 141 142 // Process will process the standard easygen input: the `fileName` is for both template and data file name, and produce output from the template according to the corresponding driving data. 143 // Process() is using the V3's calling convention and *only* works properly in V4+ in the case that there is only one fileName passed to it. If need to pass more files, use Process2() instead. 144 func Process(t Template, wr io.Writer, fileNames ...string) error { 145 return Process2(t, wr, fileNames[0], fileNames[:1]...) 146 } 147 148 // Process2 will process the case that *both* template and data file names are given, and produce output according to the given template and driving data files, 149 // specified via fileNameTempl and fileNames respectively. 150 // fileNameTempl can be a comma-separated string giving many template files 151 func Process2(t Template, wr io.Writer, fileNameTempl string, fileNames ...string) error { 152 for _, dataFn := range fileNames { 153 for _, templateFn := range strings.Split(fileNameTempl, ",") { 154 err := Process1(t, wr, templateFn, dataFn) 155 checkError(err) 156 } 157 } 158 return nil 159 } 160 161 // Process1 will process a *single* case where both template and data file names are given, and produce output according to the given template and driving data files, 162 // specified via fileNameTempl and fileName respectively. 163 // fileNameTempl is not a comma-separated string, but for a single template file. 164 // However, the fileName can be a comma-separated string for multiple data files. 165 func Process1(t Template, wr io.Writer, fileNameTempl string, fileName string) error { 166 m := ReadDataFiles(fileName) 167 168 // template file 169 fileName = fileNameTempl 170 fileNameT := fileNameTempl 171 if IsExist(fileName + Opts.ExtTmpl) { 172 fileNameT = fileName + Opts.ExtTmpl 173 } else { 174 // guard against that fileNameTempl passed with Opts.ExtYaml extension 175 if fileName[len(fileName)-len(Opts.ExtYaml):] == Opts.ExtYaml { 176 idx := strings.LastIndex(fileName, ".") 177 fileName = fileName[:idx] 178 if IsExist(fileName + Opts.ExtTmpl) { 179 fileNameT = fileName + Opts.ExtTmpl 180 } 181 } else if IsExist(fileName) { 182 // fileNameTempl passed with Opts.ExtTmpl already 183 fileNameT = fileName 184 } 185 } 186 187 return Execute(t, wr, fileNameT, m) 188 } 189 190 // Execute0 will execute the Template given as strTempl with the given data map `m` (i.e., no template file and no data file). 191 // It parses text template strTempl then applies it to to the specified data 192 // object m, and writes the output to wr. If an error occurs executing the 193 // template or writing its output, execution stops, but partial results may 194 // already have been written to the output writer. A template may be 195 // executed safely in parallel, although if parallel executions share a 196 // Writer the output may be interleaved. 197 func Execute0(t Template, wr io.Writer, strTempl string, m EgData) error { 198 verbose("Execute with template string: "+strTempl, 2) 199 tmpl, err := t.Parse(strTempl) 200 checkError(err) 201 return tmpl.Execute(wr, m) 202 } 203 204 // Execute will execute the Template from fileNameT on the given data map `m`. 205 func Execute(t Template, wr io.Writer, fileNameT string, m EgData) error { 206 // 1. Check locally 207 verbose("Checking for template locally: "+fileNameT, 2) 208 if !IsExist(fileNameT) { 209 // 2. Check under /etc/ 210 command := filepath.Base(os.Args[0]) 211 templateFile := fmt.Sprintf("/etc/%s/%s", command, fileNameT) 212 verbose("Checking at "+templateFile, 2) 213 if IsExist(templateFile) { 214 fileNameT = templateFile 215 } else { 216 // 3. Check where executable is 217 ex, e := os.Executable() 218 if e != nil { 219 return e 220 } 221 fileNameT = filepath.Dir(ex) + string(filepath.Separator) + fileNameT 222 verbose("Checking at "+fileNameT, 2) 223 if !IsExist(fileNameT) { 224 checkError(fmt.Errorf("Template file '%s' cannot be found", fileNameT)) 225 } 226 } 227 } 228 229 tn, err := t.ParseFiles(fileNameT) 230 checkError(err) 231 232 return tn.ExecuteTemplate(wr, filepath.Base(fileNameT), m) 233 } 234 235 // Process0 will produce output according to the driving data *without* a template file, using the string from strTempl as the template 236 func Process0(t Template, wr io.Writer, strTempl string, fileNames ...string) error { 237 fileName := fileNames[0] 238 m := ReadDataFiles(fileName) 239 240 tmpl, err := t.Parse(strTempl) 241 checkError(err) 242 return tmpl.Execute(wr, m) 243 } 244 245 //////////////////////////////////////////////////////////////////////////// 246 // Support Function definitions 247 248 // Exit if error occurs 249 func checkError(err error) { 250 if err != nil { 251 fmt.Fprintf(os.Stderr, "[%s] Fatal error - %s\n", progname, err) 252 os.Exit(1) 253 } 254 } 255 256 // verbose will print info to stderr according to the verbose level setting 257 func verbose(step string, level int) { 258 if Opts.Debug >= level { 259 print("[", progname, "] ", step, "\n") 260 } 261 }