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  }