github.com/naoina/kocha@v0.7.1-0.20171129072645-78c7a531f799/util/util.go (about) 1 package util 2 3 import ( 4 "bufio" 5 "bytes" 6 "compress/gzip" 7 "crypto/rand" 8 "fmt" 9 htmltemplate "html/template" 10 "io" 11 "io/ioutil" 12 "os" 13 "os/exec" 14 "path" 15 "path/filepath" 16 "reflect" 17 "regexp" 18 "sort" 19 "strconv" 20 "strings" 21 "text/template" 22 "time" 23 "unicode" 24 25 "go/build" 26 "go/format" 27 28 "github.com/jessevdk/go-flags" 29 "github.com/mattn/go-colorable" 30 "github.com/mattn/go-isatty" 31 "github.com/naoina/go-stringutil" 32 ) 33 34 const ( 35 TemplateSuffix = ".tmpl" 36 ) 37 38 var ( 39 // Now returns current time. This is for mock in tests. 40 Now = func() time.Time { return time.Now() } 41 42 // for test. 43 ImportDir = build.ImportDir 44 45 printColor = func(_, format string, a ...interface{}) { fmt.Printf(format, a...) } 46 ) 47 48 func ToCamelCase(s string) string { 49 return stringutil.ToUpperCamelCase(s) 50 } 51 52 func ToSnakeCase(s string) string { 53 return stringutil.ToSnakeCase(s) 54 } 55 56 func NormPath(p string) string { 57 result := path.Clean(p) 58 // path.Clean() truncate the trailing slash but add it. 59 if p[len(p)-1] == '/' && result != "/" { 60 result += "/" 61 } 62 return result 63 } 64 65 type Error struct { 66 Usager usager 67 Message string 68 } 69 70 func (e Error) Error() string { 71 return e.Message 72 } 73 74 type usager interface { 75 Usage() string 76 } 77 78 type fileStatus uint8 79 80 const ( 81 fileStatusConflict fileStatus = iota + 1 82 fileStatusNoConflict 83 fileStatusIdentical 84 ) 85 86 func CopyTemplate(srcPath, dstPath string, data map[string]interface{}) error { 87 tmpl, err := template.ParseFiles(srcPath) 88 if err != nil { 89 return fmt.Errorf("kocha: failed to parse template: %v: %v", srcPath, err) 90 } 91 var bufFrom bytes.Buffer 92 if err := tmpl.Execute(&bufFrom, data); err != nil { 93 return fmt.Errorf("kocha: failed to process template: %v: %v", srcPath, err) 94 } 95 buf := bufFrom.Bytes() 96 if strings.HasSuffix(srcPath, ".go"+TemplateSuffix) { 97 if buf, err = format.Source(buf); err != nil { 98 return fmt.Errorf("kocha: failed to gofmt: %v: %v", srcPath, err) 99 } 100 } 101 dstDir := filepath.Dir(dstPath) 102 if _, err := os.Stat(dstDir); os.IsNotExist(err) { 103 PrintCreateDirectory(dstDir) 104 if err := os.MkdirAll(dstDir, 0755); err != nil { 105 return fmt.Errorf("kocha: failed to create directory: %v: %v", dstDir, err) 106 } 107 } 108 printFunc := PrintCreate 109 status, err := detectConflict(buf, dstPath) 110 if err != nil { 111 return err 112 } 113 switch status { 114 case fileStatusConflict: 115 PrintConflict(dstPath) 116 if !confirmOverwrite(dstPath) { 117 PrintSkip(dstPath) 118 return nil 119 } 120 printFunc = PrintOverwrite 121 case fileStatusIdentical: 122 PrintIdentical(dstPath) 123 return nil 124 } 125 dstFile, err := os.Create(dstPath) 126 if err != nil { 127 return fmt.Errorf("kocha: failed to create file: %v: %v", dstPath, err) 128 } 129 defer dstFile.Close() 130 if _, err := io.Copy(dstFile, bytes.NewBuffer(buf)); err != nil { 131 return fmt.Errorf("kocha: failed to output file: %v: %v", dstPath, err) 132 } 133 printFunc(dstPath) 134 return nil 135 } 136 137 func detectConflict(src []byte, dstPath string) (fileStatus, error) { 138 if _, err := os.Stat(dstPath); os.IsNotExist(err) { 139 return fileStatusNoConflict, nil 140 } 141 dstBuf, err := ioutil.ReadFile(dstPath) 142 if err != nil { 143 return 0, fmt.Errorf("kocha: failed to read file: %v", err) 144 } 145 if bytes.Equal(src, dstBuf) { 146 return fileStatusIdentical, nil 147 } 148 return fileStatusConflict, nil 149 } 150 151 func confirmOverwrite(dstPath string) bool { 152 reader := bufio.NewReader(os.Stdin) 153 for { 154 fmt.Printf("Overwrite %v? [Yn] ", dstPath) 155 yesno, err := reader.ReadString('\n') 156 if err != nil { 157 panic(err) 158 } 159 switch strings.ToUpper(strings.TrimSpace(yesno)) { 160 case "", "YES", "Y": 161 return true 162 case "NO", "N": 163 return false 164 } 165 } 166 } 167 168 func makePrintColor(w io.Writer, color, format string, a ...interface{}) { 169 fmt.Fprintf(w, "\x1b[%s;1m", color) 170 fmt.Fprintf(w, format, a...) 171 fmt.Fprint(w, "\x1b[0m") 172 } 173 174 func PrintGreen(s string, a ...interface{}) { 175 printColor("32", s, a...) 176 } 177 178 func PrintIdentical(path string) { 179 printPathStatus("34", "identical", path) // Blue. 180 } 181 182 func PrintConflict(path string) { 183 printPathStatus("31", "conflict", path) // Red. 184 } 185 186 func PrintSkip(path string) { 187 printPathStatus("36", "skip", path) // Cyan. 188 } 189 190 func PrintOverwrite(path string) { 191 printPathStatus("36", "overwrite", path) // Cyan. 192 } 193 194 func PrintCreate(path string) { 195 printPathStatus("32", "create", path) // Green. 196 } 197 198 func PrintCreateDirectory(path string) { 199 printPathStatus("32", "create directory", path) // Green. 200 } 201 202 func printPathStatus(color, message, s string) { 203 printColor(color, "%20s", message) 204 fmt.Println("", s) 205 } 206 207 // GoString returns Go-syntax representation of the value. 208 // It returns compilable Go-syntax that different with "%#v" format for fmt package. 209 func GoString(i interface{}) string { 210 switch t := i.(type) { 211 case *regexp.Regexp: 212 return fmt.Sprintf(`regexp.MustCompile(%q)`, t) 213 case *htmltemplate.Template: 214 var buf bytes.Buffer 215 for _, t := range t.Templates() { 216 if t.Name() == "content" { 217 continue 218 } 219 if _, err := buf.WriteString(reflect.ValueOf(t).Elem().FieldByName("text").Elem().FieldByName("text").String()); err != nil { 220 panic(err) 221 } 222 } 223 return fmt.Sprintf(`template.Must(template.New(%q).Funcs(kocha.TemplateFuncs).Parse(util.Gunzip(%q)))`, t.Name(), Gzip(buf.String())) 224 case fmt.GoStringer: 225 return t.GoString() 226 case nil: 227 return "nil" 228 } 229 v := reflect.ValueOf(i) 230 var name string 231 if v.Kind() == reflect.Ptr { 232 if v = v.Elem(); !v.IsValid() { 233 return "nil" 234 } 235 name = "&" 236 } 237 name += v.Type().String() 238 var ( 239 tmplStr string 240 fields interface{} 241 ) 242 switch v.Kind() { 243 case reflect.Struct: 244 f := make(map[string]interface{}) 245 for i := 0; i < v.NumField(); i++ { 246 if tf := v.Type().Field(i); !tf.Anonymous && v.Field(i).CanInterface() { 247 f[tf.Name] = GoString(v.Field(i).Interface()) 248 } 249 } 250 tmplStr = ` 251 {{.name}}{ 252 {{range $name, $value := .fields}} 253 {{$name}}: {{$value}}, 254 {{end}} 255 }` 256 fields = f 257 case reflect.Slice: 258 f := make([]string, v.Len()) 259 for i := 0; i < v.Len(); i++ { 260 f[i] = GoString(v.Index(i).Interface()) 261 } 262 tmplStr = ` 263 {{.name}}{ 264 {{range $value := .fields}} 265 {{$value}}, 266 {{end}} 267 }` 268 fields = f 269 case reflect.Map: 270 f := make(map[interface{}]interface{}) 271 for _, k := range v.MapKeys() { 272 f[k.Interface()] = GoString(v.MapIndex(k).Interface()) 273 } 274 tmplStr = ` 275 {{.name}}{ 276 {{range $name, $value := .fields}} 277 {{$name|printf "%q"}}: {{$value}}, 278 {{end}} 279 }` 280 fields = f 281 default: 282 return fmt.Sprintf("%#v", v.Interface()) 283 } 284 t := template.Must(template.New(name).Parse(tmplStr)) 285 var buf bytes.Buffer 286 if err := t.Execute(&buf, map[string]interface{}{ 287 "name": name, 288 "fields": fields, 289 }); err != nil { 290 panic(err) 291 } 292 return buf.String() 293 } 294 295 // Gzip returns gzipped string. 296 func Gzip(raw string) string { 297 var gzipped bytes.Buffer 298 w, err := gzip.NewWriterLevel(&gzipped, gzip.BestCompression) 299 if err != nil { 300 panic(err) 301 } 302 if _, err := w.Write([]byte(raw)); err != nil { 303 panic(err) 304 } 305 if err := w.Close(); err != nil { 306 panic(err) 307 } 308 return gzipped.String() 309 } 310 311 // Gunzip returns unzipped string. 312 func Gunzip(gz string) string { 313 r, err := gzip.NewReader(bytes.NewReader([]byte(gz))) 314 if err != nil { 315 panic(err) 316 } 317 result, err := ioutil.ReadAll(r) 318 if err != nil { 319 panic(err) 320 } 321 return string(result) 322 } 323 324 var settingEnvRegexp = regexp.MustCompile(`\bkocha\.Getenv\(\s*(.+?)\s*,\s*(.+?)\s*\)`) 325 326 // FindEnv returns map of environment variables. 327 // Key of map is key of environment variable, Value of map is value of 328 // environment variable. 329 func FindEnv(basedir string) (map[string]string, error) { 330 if basedir == "" { 331 pwd, err := os.Getwd() 332 if err != nil { 333 return nil, err 334 } 335 basedir = pwd 336 } 337 env := make(map[string]string) 338 if err := filepath.Walk(basedir, func(path string, info os.FileInfo, err error) error { 339 if err != nil { 340 return err 341 } 342 switch info.Name()[0] { 343 case '.', '_': 344 if info.IsDir() { 345 return filepath.SkipDir 346 } 347 return nil 348 } 349 if info.IsDir() { 350 return nil 351 } 352 if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { 353 return nil 354 } 355 body, err := ioutil.ReadFile(path) 356 if err != nil { 357 return err 358 } 359 matches := settingEnvRegexp.FindAllStringSubmatch(string(body), -1) 360 if matches == nil { 361 return nil 362 } 363 for _, m := range matches { 364 key, err := strconv.Unquote(m[1]) 365 if err != nil { 366 continue 367 } 368 value, err := strconv.Unquote(m[2]) 369 if err != nil { 370 value = "WILL BE SET IN RUNTIME" 371 } 372 env[key] = value 373 } 374 return nil 375 }); err != nil { 376 return nil, err 377 } 378 return env, nil 379 } 380 381 // FindAppDir returns application directory. (aka import path) 382 // An application directory retrieves from current working directory. 383 // For example, if current working directory is 384 // "/path/to/gopath/src/github.com/naoina/myapp", FindAppDir returns 385 // "github.com/naoina/myapp". 386 func FindAppDir() (string, error) { 387 dir, err := os.Getwd() 388 if err != nil { 389 return "", err 390 } 391 bp, err := filepath.EvalSymlinks(filepath.Join(filepath.SplitList(build.Default.GOPATH)[0], "src")) 392 if err != nil { 393 return "", err 394 } 395 return filepath.ToSlash(dir)[len(bp)+1:], nil 396 } 397 398 // FindAbsDir returns an absolute path of importPath in GOPATH. 399 // For example, if importPath is "github.com/naoina/myapp", 400 // and GOPATH is "/path/to/gopath", FindAbsDir returns 401 // "/path/to/gopath/src/github.com/naoina/myapp". 402 func FindAbsDir(importPath string) (string, error) { 403 if importPath == "" { 404 return os.Getwd() 405 } 406 dir := filepath.FromSlash(importPath) 407 for _, gopath := range filepath.SplitList(build.Default.GOPATH) { 408 candidate := filepath.Join(gopath, "src", dir) 409 if info, err := os.Stat(candidate); err == nil && info.IsDir() { 410 return candidate, nil 411 } 412 } 413 return "", fmt.Errorf("package `%s' not found in GOPATH", importPath) 414 } 415 416 // IsUnexportedField returns whether the field is unexported. 417 // This function is to avoid the bug in versions older than Go1.3. 418 // See following links: 419 // https://code.google.com/p/go/issues/detail?id=7247 420 // http://golang.org/ref/spec#Exported_identifiers 421 func IsUnexportedField(field reflect.StructField) bool { 422 return !(field.PkgPath == "" && unicode.IsUpper(rune(field.Name[0]))) 423 } 424 425 // Generate a random bytes. 426 func GenerateRandomKey(length int) []byte { 427 result := make([]byte, length) 428 if _, err := io.ReadFull(rand.Reader, result); err != nil { 429 panic(err) 430 } 431 return result 432 } 433 434 func PrintEnv(basedir string) error { 435 envMap, err := FindEnv(basedir) 436 if err != nil { 437 return err 438 } 439 envKeys := make([]string, 0, len(envMap)) 440 for k := range envMap { 441 envKeys = append(envKeys, k) 442 } 443 sort.Strings(envKeys) 444 var buf bytes.Buffer 445 fmt.Fprintf(&buf, "kocha: you can be setting for your app by the following environment variables at the time of launching the app:\n\n") 446 for _, k := range envKeys { 447 v := os.Getenv(k) 448 if v == "" { 449 v = envMap[k] 450 } 451 fmt.Fprintf(&buf, "%4s%v=%v\n", "", k, strconv.Quote(v)) 452 } 453 fmt.Println(buf.String()) 454 return nil 455 } 456 457 type Commander interface { 458 Run(args []string) error 459 Name() string 460 Usage() string 461 Option() interface{} 462 } 463 464 func RunCommand(cmd Commander) { 465 parser := flags.NewNamedParser(cmd.Name(), flags.PrintErrors|flags.PassDoubleDash|flags.PassAfterNonOption) 466 if _, err := parser.AddGroup("", "", cmd.Option()); err != nil { 467 panic(err) 468 } 469 args, err := parser.Parse() 470 if err != nil { 471 fmt.Fprint(os.Stderr, cmd.Usage()) 472 os.Exit(1) 473 } 474 opt := reflect.ValueOf(cmd.Option()) 475 for opt.Kind() == reflect.Ptr { 476 opt = opt.Elem() 477 } 478 h := opt.FieldByName("Help") 479 if h.IsValid() && h.Kind() == reflect.Bool && h.Bool() { 480 fmt.Fprint(os.Stderr, cmd.Usage()) 481 os.Exit(0) 482 } 483 if err := cmd.Run(args); err != nil { 484 if _, ok := err.(*exec.ExitError); !ok { 485 fmt.Fprintf(os.Stderr, "%s: %v\n", cmd.Name(), err) 486 fmt.Fprint(os.Stderr, cmd.Usage()) 487 } 488 os.Exit(1) 489 } 490 } 491 492 func init() { 493 if isatty.IsTerminal(os.Stdout.Fd()) { 494 w := colorable.NewColorableStdout() 495 printColor = func(color, format string, a ...interface{}) { 496 fmt.Fprintf(w, "\x1b[%s;1m", color) 497 fmt.Fprintf(w, format, a...) 498 fmt.Fprint(w, "\x1b[0m") 499 } 500 } 501 }