diff --git a/go.mod b/go.mod index 549075d90..25bb74983 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,20 @@ go 1.14 require ( github.com/BurntSushi/toml v0.3.1 github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.4.9 github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579 github.com/gomodule/redigo v2.0.0+incompatible - github.com/gorilla/websocket v1.4.1 - github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf + github.com/gorilla/websocket v1.4.2 + github.com/grokify/html-strip-tags-go v0.0.0-20200322061010-ea0c1cf2f119 github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/olekukonko/tablewriter v0.0.5 go.opentelemetry.io/otel v1.0.0-RC1 go.opentelemetry.io/otel/oteltest v1.0.0-RC1 go.opentelemetry.io/otel/trace v1.0.0-RC1 golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 golang.org/x/text v0.3.4 + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c ) diff --git a/go.sum b/go.sum index 45d8f1356..61103d738 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 h1:LdXxtjzvZYhhUao github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579 h1:pP/uEy52biKDytlgK/ug8kiYPAiYu6KajKVUHfGrtyw= @@ -12,13 +14,18 @@ github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNu github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf h1:wIOAyJMMen0ELGiFzlmqxdcV1yGbkyHBAB6PolcNbLA= -github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.0.0-20200322061010-ea0c1cf2f119 h1:h3iGUlU8HyW4baKd6D+h1mwOHnM2kwskSuG6Bv4tSbc= +github.com/grokify/html-strip-tags-go v0.0.0-20200322061010-ea0c1cf2f119/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -35,6 +42,7 @@ go.opentelemetry.io/otel/oteltest v1.0.0-RC1/go.mod h1:+eoIG0gdEOaPNftuy1YScLr1G go.opentelemetry.io/otel/trace v1.0.0-RC1 h1:jrjqKJZEibFrDz+umEASeU3LvdVyWKlnTh7XEfwrT58= go.opentelemetry.io/otel/trace v1.0.0-RC1/go.mod h1:86UHmyHWFEtWjfWPSbu0+d0Pf9Q6e1U+3ViBOc+NXAg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= @@ -53,5 +61,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tool/gf/README.MD b/tool/gf/README.MD new file mode 100644 index 000000000..5345ea818 --- /dev/null +++ b/tool/gf/README.MD @@ -0,0 +1,63 @@ +# GF-CLI +English | [简体中文](README_ZH.MD) + +`gf` is a powerful CLI tool for building [GoFrame](https://goframe.org) application with convenience. + +## 1. Install + +> You might need setting the goproxy to make through building. + +1. Latest version + ``` + go install github.com/gogf/gf/tool/gf@latest + ``` +1. Specified version + ``` + go install github.com/gogf/gf/tool/gf@v1.16.0 + ``` +1. Database `sqlite` and `oracle` are not support in `gf gen` command in default as it needs `cgo` and `gcc`, you can manually make some changes to the source codes and do the building. + +## 2. Commands +```html +$ gf +USAGE + gf COMMAND [ARGUMENT] [OPTION] + +COMMAND + env show current Golang environment variables + get install or update GF to system in default... + gen automatically generate go files for ORM models... + mod extra features for go modules... + run running go codes with hot-compiled-like feature... + init initialize an empty GF project at current working directory... + help show more information about a specified command + pack packing any file/directory to a resource file, or a go file... + build cross-building go project for lots of platforms... + docker create a docker image for current GF project... + swagger swagger feature for current project... + update update current gf binary to latest one (might need root/admin permission) + install install gf binary to system (might need root/admin permission) + version show current binary version info + +OPTION + -y all yes for all command without prompt ask + -?,-h show this help or detail for specified command + -v,-i show version information + +ADDITIONAL + Use 'gf help COMMAND' or 'gf COMMAND -h' for detail about a command, which has '...' + in the tail of their comments. +``` + +## 3. FAQ + +### 1). Command `gf run` returns `pipe: too many open files` + +Please use `ulimit -n 65535` to enlarge your system configuration for max open files for current terminal shell session, and then `gf run`. + + + + + + + diff --git a/tool/gf/README_ZH.MD b/tool/gf/README_ZH.MD new file mode 100644 index 000000000..7ad47d8f0 --- /dev/null +++ b/tool/gf/README_ZH.MD @@ -0,0 +1,65 @@ +# GF-CLI +[English](README.MD) | 简体中文 + +`gf`是一款强大的[GoFrame](https://goframe.org)开发工具链,使得我们开发基于`GoFrame`框架的项目更加便捷。 + +## 1. 安装 + +1. 最新版本 + ``` + go install github.com/gogf/gf/tool/gf@latest + ``` +1. 指定版本 + ``` + go install github.com/gogf/gf/tool/gf@v1.16.0 + ``` +> 注意:在`gf gen`命令中,由于`sqlite`和`oracle`数据库需要`cgo`和`gcc`支持,因此预编译的二进制中不提供对这两个数据库的支持,您需要手动修改源码,去掉对应源码文件中指定数据库类型的`import`注释后手动编译支持。 + +## 2. 命令 +```html +$ gf +USAGE + gf COMMAND [ARGUMENT] [OPTION] + +COMMAND + env show current Golang environment variables + get install or update GF to system in default... + gen automatically generate go files for ORM models... + mod extra features for go modules... + run running go codes with hot-compiled-like feature... + init initialize an empty GF project at current working directory... + help show more information about a specified command + pack packing any file/directory to a resource file, or a go file... + build cross-building go project for lots of platforms... + docker create a docker image for current GF project... + swagger swagger feature for current project... + update update current gf binary to latest one (might need root/admin permission) + install install gf binary to system (might need root/admin permission) + version show current binary version info + +OPTION + -y all yes for all command without prompt ask + -?,-h show this help or detail for specified command + -v,-i show version information + +ADDITIONAL + Use 'gf help COMMAND' or 'gf COMMAND -h' for detail about a command, which has '...' + in the tail of their comments. +``` + +## 3. 文档 + +完善详尽的中文文档请参考`GoFrame`官网板块:[开发工具](https://itician.org/pages/viewpage.action?pageId=1114260) + +## 4. FAQ + +### 1). `gf run` 命令报错 `pipe: too many open files` + +请执行`ulimit -n 65535`命令扩展您当前终端会话支持的最大文件打开数,随后再执行`gf run`。需要注意的是该命令仅对当前终端会话有效。 + + + + + + + diff --git a/tool/gf/boot/boot.go b/tool/gf/boot/boot.go new file mode 100644 index 000000000..ef11ced62 --- /dev/null +++ b/tool/gf/boot/boot.go @@ -0,0 +1,31 @@ +package boot + +import ( + "github.com/gogf/gf/os/genv" + _ "github.com/gogf/gf/tool/gf/packed" + + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/text/gstr" +) + +func init() { + // Force using configuration file in current working directory. + // In case of source environment. + genv.Set("GF_GCFG_PATH", gfile.Pwd()) + handleZshAlias() +} + +// zsh alias "git fetch" conflicts checks. +func handleZshAlias() { + home, err := gfile.Home() + if err == nil { + zshPath := gfile.Join(home, ".zshrc") + if gfile.Exists(zshPath) { + aliasCommand := `alias gf=gf` + content := gfile.GetContents(zshPath) + if !gstr.Contains(content, aliasCommand) { + _ = gfile.PutContentsAppend(zshPath, "\n"+aliasCommand+"\n") + } + } + } +} diff --git a/tool/gf/commands/build/build.go b/tool/gf/commands/build/build.go new file mode 100644 index 000000000..f8b157a60 --- /dev/null +++ b/tool/gf/commands/build/build.go @@ -0,0 +1,341 @@ +package build + +import ( + "encoding/json" + "fmt" + "github.com/gogf/gf/encoding/gbase64" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/genv" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gproc" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" + "regexp" + "runtime" + "strings" +) + +// https://golang.google.cn/doc/install/source +const platforms = ` + darwin amd64 + darwin arm64 + ios amd64 + ios arm64 + freebsd 386 + freebsd amd64 + freebsd arm + linux 386 + linux amd64 + linux arm + linux arm64 + linux ppc64 + linux ppc64le + linux mips + linux mipsle + linux mips64 + linux mips64le + netbsd 386 + netbsd amd64 + netbsd arm + openbsd 386 + openbsd amd64 + openbsd arm + windows 386 + windows amd64 + android arm + dragonfly amd64 + plan9 386 + plan9 amd64 + solaris amd64 +` + +const ( + nodeNameInConfigFile = "gfcli.build" // nodeNameInConfigFile is the node name for compiler configurations in configuration file. + packedGoFileName = "build_pack_data.go" // packedGoFileName specifies the file name for packing common folders into one single go file. +) + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf build FILE [OPTION] + +ARGUMENT + FILE building file path. + +OPTION + -n, --name output binary name + -v, --version output binary version + -a, --arch output binary architecture, multiple arch separated with ',' + -s, --system output binary system, multiple os separated with ',' + -o, --output output binary path, used when building single binary file + -p, --path output binary directory path, default is './bin' + -e, --extra extra custom "go build" options + -m, --mod like "-mod" option of "go build", use "-m none" to disable go module + -c, --cgo enable or disable cgo feature, it's disabled in default + --pack pack specified folder into packed/data.go before building. + --swagger auto parse and pack swagger into packed/swagger.go before building. + +EXAMPLES + gf build main.go + gf build main.go --swagger + gf build main.go --pack public,template + gf build main.go --cgo + gf build main.go -m none + gf build main.go -n my-app -a all -s all + gf build main.go -n my-app -a amd64,386 -s linux -p . + gf build main.go -n my-app -v 1.0 -a amd64,386 -s linux,windows,darwin -p ./docker/bin + +DESCRIPTION + The "build" command is most commonly used command, which is designed as a powerful wrapper for + "go build" command for convenience cross-compiling usage. + It provides much more features for building binary: + 1. Cross-Compiling for many platforms and architectures. + 2. Configuration file support for compiling. + 3. Build-In Variables. + +PLATFORMS + darwin amd64,arm64 + freebsd 386,amd64,arm + linux 386,amd64,arm,arm64,ppc64,ppc64le,mips,mipsle,mips64,mips64le + netbsd 386,amd64,arm + openbsd 386,amd64,arm + windows 386,amd64 +`)) +} + +func Run() { + mlog.SetHeaderPrint(true) + parser, err := gcmd.Parse(g.MapStrBool{ + "n,name": true, + "v,version": true, + "a,arch": true, + "s,system": true, + "o,output": true, + "p,path": true, + "e,extra": true, + "m,mod": true, + "pack": true, + "c,cgo": false, + "swagger": false, + }) + if err != nil { + mlog.Fatal(err) + } + file := parser.GetArg(2) + if len(file) < 1 { + // Check and use the main.go file. + if gfile.Exists("main.go") { + file = "main.go" + } else { + mlog.Fatal("build file path cannot be empty") + } + } + path := getOption(parser, "path", "./bin") + name := getOption(parser, "name", gfile.Name(file)) + if len(name) < 1 || name == "*" { + mlog.Fatal("name cannot be empty") + } + var ( + mod = getOption(parser, "mod") + extra = getOption(parser, "extra") + ) + if mod != "" && mod != "none" { + mlog.Debugf(`mod is %s`, mod) + if extra == "" { + extra = fmt.Sprintf(`-mod=%s`, mod) + } else { + extra = fmt.Sprintf(`-mod=%s %s`, mod, extra) + } + } + if extra != "" { + extra += " " + } + var ( + cgoEnabled = gconv.Bool(getOption(parser, "cgo")) + version = getOption(parser, "version") + outputPath = getOption(parser, "output") + archOption = getOption(parser, "arch") + systemOption = getOption(parser, "system") + packStr = getOption(parser, "pack") + customSystems = gstr.SplitAndTrim(systemOption, ",") + customArches = gstr.SplitAndTrim(archOption, ",") + ) + if !cgoEnabled { + cgoEnabled = parser.ContainsOpt("cgo") + } + if len(version) > 0 { + path += "/" + version + } + // System and arch checks. + var ( + spaceRegex = regexp.MustCompile(`\s+`) + platformMap = make(map[string]map[string]bool) + ) + for _, line := range strings.Split(strings.TrimSpace(platforms), "\n") { + line = gstr.Trim(line) + line = spaceRegex.ReplaceAllString(line, " ") + var ( + array = strings.Split(line, " ") + system = strings.TrimSpace(array[0]) + arch = strings.TrimSpace(array[1]) + ) + if platformMap[system] == nil { + platformMap[system] = make(map[string]bool) + } + platformMap[system][arch] = true + } + // Auto swagger. + if containsOption(parser, "swagger") { + if err := gproc.ShellRun(`gf swagger`); err != nil { + return + } + if gfile.Exists("swagger") { + packCmd := fmt.Sprintf(`gf pack %s packed/%s`, "swagger", packedGoFileName) + mlog.Print(packCmd) + if err := gproc.ShellRun(packCmd); err != nil { + return + } + } + } + + // Auto packing. + if len(packStr) > 0 { + dataFilePath := fmt.Sprintf(`packed/%s`, packedGoFileName) + if !gfile.Exists(dataFilePath) { + // Remove the go file that is automatically packed resource. + defer func() { + gfile.Remove(dataFilePath) + mlog.Printf(`remove the automatically generated resource go file: %s`, dataFilePath) + }() + } + packCmd := fmt.Sprintf(`gf pack %s %s`, packStr, dataFilePath) + mlog.Print(packCmd) + gproc.ShellRun(packCmd) + } + + // Injected information by building flags. + ldFlags := fmt.Sprintf(`-X 'github.com/gogf/gf/os/gbuild.builtInVarStr=%v'`, getBuildInVarStr()) + + // start building + mlog.Print("start building...") + if cgoEnabled { + genv.Set("CGO_ENABLED", "1") + } else { + genv.Set("CGO_ENABLED", "0") + } + var ( + cmd = "" + ext = "" + ) + for system, item := range platformMap { + cmd = "" + ext = "" + if len(customSystems) > 0 && customSystems[0] != "all" && !gstr.InArray(customSystems, system) { + continue + } + for arch, _ := range item { + if len(customArches) > 0 && customArches[0] != "all" && !gstr.InArray(customArches, arch) { + continue + } + if len(customSystems) == 0 && len(customArches) == 0 { + if runtime.GOOS == "windows" { + ext = ".exe" + } + // Single binary building, output the binary to current working folder. + output := "" + if len(outputPath) > 0 { + output = "-o " + outputPath + ext + } else { + output = "-o " + name + ext + } + cmd = fmt.Sprintf(`go build %s -ldflags "%s" %s %s`, output, ldFlags, extra, file) + } else { + // Cross-building, output the compiled binary to specified path. + if system == "windows" { + ext = ".exe" + } + genv.Set("GOOS", system) + genv.Set("GOARCH", arch) + cmd = fmt.Sprintf( + `go build -o %s/%s/%s%s -ldflags "%s" %s%s`, + path, system+"_"+arch, name, ext, ldFlags, extra, file, + ) + } + // It's not necessary printing the complete command string. + cmdShow, _ := gregex.ReplaceString(`\s+(-ldflags ".+?")\s+`, " ", cmd) + mlog.Print(cmdShow) + if _, err := gproc.ShellExec(cmd); err != nil { + mlog.Printf("failed to build, os:%s, arch:%s", system, arch) + } + // single binary building. + if len(customSystems) == 0 && len(customArches) == 0 { + goto buildDone + } + } + } +buildDone: + mlog.Print("done!") +} + +// getOption retrieves option value from parser and configuration file. +// It returns the default value specified by parameter is no value found. +func getOption(parser *gcmd.Parser, name string, value ...string) (result string) { + result = parser.GetOpt(name) + if result == "" && g.Config().Available() { + result = g.Config().GetString(nodeNameInConfigFile + "." + name) + } + if result == "" && len(value) > 0 { + result = value[0] + } + return +} + +// containsOption checks whether the command option or the configuration file containing +// given option name. +func containsOption(parser *gcmd.Parser, name string) bool { + result := parser.ContainsOpt(name) + if !result && g.Config().Available() { + result = g.Config().Contains(nodeNameInConfigFile + "." + name) + } + return result +} + +// getBuildInVarMapJson retrieves and returns the custom build-in variables in configuration +// file as json. +func getBuildInVarStr() string { + buildInVarMap := g.Map{} + if g.Config().Available() { + configMap := g.Config().GetMap(nodeNameInConfigFile) + if len(configMap) > 0 { + _, v := gutil.MapPossibleItemByKey(configMap, "VarMap") + if v != nil { + buildInVarMap = gconv.Map(v) + } + } + } + buildInVarMap["builtGit"] = getGitCommit() + buildInVarMap["builtTime"] = gtime.Now().String() + b, err := json.Marshal(buildInVarMap) + if err != nil { + mlog.Fatal(err) + } + return gbase64.EncodeToString(b) +} + +// getGitCommit retrieves and returns the latest git commit hash string if present. +func getGitCommit() string { + if gproc.SearchBinary("git") == "" { + return "" + } + if s, _ := gproc.ShellExec("git rev-list -1 HEAD"); s != "" { + if !gstr.Contains(s, " ") && !gstr.Contains(s, "fatal") { + return gstr.Trim(s) + } + } + return "" +} diff --git a/tool/gf/commands/docker/docker.go b/tool/gf/commands/docker/docker.go new file mode 100644 index 000000000..69e0c9d95 --- /dev/null +++ b/tool/gf/commands/docker/docker.go @@ -0,0 +1,99 @@ +package docker + +import ( + "fmt" + "github.com/gogf/gf/container/garray" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gproc" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" + "os" + "strings" +) + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf docker [FILE] [OPTION] + +ARGUMENT + FILE file path for "gf build", it's "main.go" in default. + OPTION the same options as "docker build" except some options as follows defined + +OPTION + -p, --push auto push the docker image to docker registry if "-t" option passed + +EXAMPLES + gf docker + gf docker -t hub.docker.com/john/image:tag + gf docker -p -t hub.docker.com/john/image:tag + gf docker main.go + gf docker main.go -t hub.docker.com/john/image:tag + gf docker main.go -t hub.docker.com/john/image:tag + gf docker main.go -p -t hub.docker.com/john/image:tag + +DESCRIPTION + The "docker" command builds the GF project to a docker images. + It runs "gf build" firstly to compile the project to binary file. + It then runs "docker build" command automatically to generate the docker image. + You should have docker installed, and there must be a Dockerfile in the root of the project. + +`)) +} + +func Run() { + var err error + autoPush := false + array := garray.NewStrArrayFromCopy(os.Args) + index := array.Search("--push") + if index < 0 { + index = array.Search("-p") + } + if index != -1 { + array.Remove(index) + autoPush = true + } + file := "main.go" + extraOptions := "" + if array.Len() > 2 { + v, _ := array.Get(2) + if gfile.ExtName(v) == "go" { + file, _ = array.Get(2) + if array.Len() > 3 { + extraOptions = strings.Join(array.SubSlice(3), " ") + } + } else { + extraOptions = strings.Join(array.SubSlice(2), " ") + } + } + // Binary build. + err = gproc.ShellRun(fmt.Sprintf(`gf build %s -a amd64 -s linux`, file)) + if err != nil { + return + } + // Docker build. + err = gproc.ShellRun(fmt.Sprintf(`docker build . %s`, extraOptions)) + if err != nil { + return + } + // Docker push. + if !autoPush { + return + } + parser, err := gcmd.Parse(g.MapStrBool{ + "t,tag": true, + }) + if err != nil { + mlog.Fatal(err) + } + tag := parser.GetOpt("t") + if tag == "" { + return + } + err = gproc.ShellRun(fmt.Sprintf(`docker push %s`, tag)) + if err != nil { + return + } +} diff --git a/tool/gf/commands/env/env.go b/tool/gf/commands/env/env.go new file mode 100644 index 000000000..06a163b92 --- /dev/null +++ b/tool/gf/commands/env/env.go @@ -0,0 +1,44 @@ +package env + +import ( + "bytes" + "github.com/gogf/gf/os/gproc" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" + "github.com/olekukonko/tablewriter" +) + +func Run() { + result, err := gproc.ShellExec("go env") + if err != nil { + mlog.Fatal(err) + } + if result == "" { + mlog.Fatal(`retrieving Golang environment variables failed, did you install Golang?`) + } + var ( + lines = gstr.Split(result, "\n") + buffer = bytes.NewBuffer(nil) + ) + array := make([][]string, 0) + for _, line := range lines { + line = gstr.Trim(line) + if line == "" { + continue + } + if gstr.Pos(line, "set ") == 0 { + line = line[4:] + } + match, _ := gregex.MatchString(`(.+?)=(.*)`, line) + if len(match) < 3 { + mlog.Fatalf(`invalid Golang environment variable: "%s"`, line) + } + array = append(array, []string{gstr.Trim(match[1]), gstr.Trim(match[2])}) + } + tw := tablewriter.NewWriter(buffer) + tw.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}) + tw.AppendBulk(array) + tw.Render() + mlog.Print(buffer.String()) +} diff --git a/tool/gf/commands/fix/fix.go b/tool/gf/commands/fix/fix.go new file mode 100644 index 000000000..1bcfa7463 --- /dev/null +++ b/tool/gf/commands/fix/fix.go @@ -0,0 +1,7 @@ +package fix + +import "github.com/gogf/gf/tool/gf/library/mlog" + +func Run() { + mlog.Print("this feature is not completed yet") +} diff --git a/tool/gf/commands/gen/gen.go b/tool/gf/commands/gen/gen.go new file mode 100644 index 000000000..1a5a03d27 --- /dev/null +++ b/tool/gf/commands/gen/gen.go @@ -0,0 +1,54 @@ +package gen + +import ( + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" +) + +func Help() { + switch gcmd.GetArg(2) { + case "dao": + HelpDao() + + case "pb": + HelpPb() + + case "pbentity": + HelpPbEntity() + + default: + mlog.Print(gstr.TrimLeft(` +USAGE + gf gen TYPE [OPTION] + +TYPE + dao generate dao and model files. + pb parse proto files and generate protobuf go files. + pbentity generate entity message files in protobuf3 format. + +DESCRIPTION + The "gen" command is designed for multiple generating purposes. + It's currently supporting generating go files for ORM models, protobuf and protobuf entity files. + Please use "gf gen dao -h" or "gf gen model -h" for specified type help. +`)) + } +} + +func Run() { + genType := gcmd.GetArg(2) + if genType == "" { + mlog.Print("generating type cannot be empty") + return + } + switch genType { + case "dao": + doGenDao() + + case "pb": + doGenPb() + + case "pbentity": + doGenPbEntity() + } +} diff --git a/tool/gf/commands/gen/gen_dao.go b/tool/gf/commands/gen/gen_dao.go new file mode 100644 index 000000000..73701fef7 --- /dev/null +++ b/tool/gf/commands/gen/gen_dao.go @@ -0,0 +1,699 @@ +package gen + +import ( + "bytes" + "context" + "fmt" + "github.com/gogf/gf/container/garray" + "github.com/gogf/gf/database/gdb" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" + "github.com/gogf/gf/tool/gf/library/utils" + "github.com/gogf/gf/util/gconv" + "github.com/olekukonko/tablewriter" + "strings" + + _ "github.com/denisenkom/go-mssqldb" + _ "github.com/lib/pq" + //_ "github.com/mattn/go-oci8" + //_ "github.com/mattn/go-sqlite3" +) + +// generateDaoReq is the input parameter for generating dao. +type generateDaoReq struct { + TableName string // TableName specifies the table name of the table. + NewTableName string // NewTableName specifies the prefix-stripped name of the table. + PrefixName string // PrefixName specifies the custom prefix name for generated dao and model struct. + GroupName string // GroupName specifies the group name of database configuration node for generated DAO. + ModName string // ModName specifies the module name of current golang project, which is used for import purpose. + JsonCase string // JsonCase specifies the case of generated 'json' tag for model struct, value from gstr.Case* function names. + DirPath string // DirPath specifies the directory path for generated files. + StdTime bool // StdTime defines using time.Time from stdlib instead of gtime.Time for generated time/date fields of tables. + ModelIndexFileName string // Custom name for storing generated model content. + TplDaoIndexPath string // TplDaoIndexPath specifies the file path for generating dao index files. + TplDaoInternalPath string // TplDaoInternalPath specifies the file path for generating dao internal files. + TplModelIndexPath string // TplModelIndexPath specifies the file path for generating model index content. + TplModelStructPath string // TplModelStructPath specifies the file path for generating model struct content. +} + +const ( + genDaoDefaultPath = "./app" + nodeNameGenDaoInConfigFile = "gfcli.gen.dao" + defaultModelIndexFileName = "model.go" +) + +func HelpDao() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf gen dao [OPTION] + +OPTION + -/--path directory path for generated files. + -l, --link database configuration, the same as the ORM configuration of GoFrame. + -t, --tables generate models only for given tables, multiple table names separated with ',' + -e, --tablesEx generate models excluding given tables, multiple table names separated with ',' + -g, --group specifying the configuration group name of database for generated ORM instance, + it's not necessary and the default value is "default" + -p, --prefix add prefix for all table of specified link/database tables. + -r, --removePrefix remove specified prefix of the table, multiple prefix separated with ',' + -m, --mod module name for generated golang file imports. + -j, --jsonCase generated json tag case for model struct, cases are as follows: + | Case | Example | + |---------------- |--------------------| + | Camel | AnyKindOfString | + | CamelLower | anyKindOfString | default + | Snake | any_kind_of_string | + | SnakeScreaming | ANY_KIND_OF_STRING | + | SnakeFirstUpper | rgb_code_md5 | + | Kebab | any-kind-of-string | + | KebabScreaming | ANY-KIND-OF-STRING | + -/--stdTime use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables. + -/--modelFile custom file name for storing generated model content. + -/--tplDaoIndex template content for Dao index files generating. + -/--tplDaoInternal template content for Dao internal files generating. + -/--tplModelIndex template content for Model index files generating. + -/--tplModelStruct template content for Model internal files generating. + +CONFIGURATION SUPPORT + Options are also supported by configuration file. + It's suggested using configuration file instead of command line arguments making producing. + The configuration node name is "gf.gen.dao", which also supports multiple databases, for example: + [gfcli] + [[gfcli.gen.dao]] + link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + tables = "order,products" + jsonCase = "CamelLower" + [[gfcli.gen.dao]] + link = "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" + path = "./my-app" + prefix = "primary_" + tables = "user, userDetail" + +EXAMPLES + gf gen dao + gf gen dao -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + gf gen dao -path ./model -c config.yaml -g user-center -t user,user_detail,user_login + gf gen dao -r user_ +`)) +} + +// doGenDao implements the "gen dao" command. +func doGenDao() { + parser, err := gcmd.Parse(g.MapStrBool{ + "path": true, + "m,mod": true, + "l,link": true, + "t,tables": true, + "e,tablesEx": true, + "g,group": true, + "c,config": true, + "p,prefix": true, + "r,removePrefix": true, + "j,jsonCase": true, + "stdTime": false, + "modelFile": true, + "tplDaoIndex": true, + "tplDaoInternal": true, + "tplModelIndex": true, + "tplModelStruct": true, + }) + if err != nil { + mlog.Fatal(err) + } + config := g.Cfg() + if config.Available() { + v := config.GetVar(nodeNameGenDaoInConfigFile) + if v.IsEmpty() && g.IsEmpty(parser.GetOptAll()) { + mlog.Fatal(`command arguments and configurations not found for generating dao files`) + } + if v.IsSlice() { + for i := 0; i < len(v.Interfaces()); i++ { + doGenDaoForArray(i, parser) + } + } else { + doGenDaoForArray(-1, parser) + } + } else { + doGenDaoForArray(-1, parser) + } + mlog.Print("done!") +} + +// doGenDaoForArray implements the "gen dao" command for configuration array. +func doGenDaoForArray(index int, parser *gcmd.Parser) { + var ( + err error + db gdb.DB + modName = getOptionOrConfigForDao(index, parser, "mod") // Go module name, eg: github.com/gogf/gf. + dirPath = getOptionOrConfigForDao(index, parser, "path", genDaoDefaultPath) // Generated directory path. + tablesStr = getOptionOrConfigForDao(index, parser, "tables") // Tables that will be generated. + tablesEx = getOptionOrConfigForDao(index, parser, "tablesEx") // Tables that will be excluded for generating. + prefixName = getOptionOrConfigForDao(index, parser, "prefix") // Add prefix to DAO and Model struct name. + linkInfo = getOptionOrConfigForDao(index, parser, "link") // Custom database link. + configPath = getOptionOrConfigForDao(index, parser, "config") // Config file path, eg: ./config/db.toml. + configGroup = getOptionOrConfigForDao(index, parser, "group", "default") // Group name of database configuration node for generated DAO. + removePrefix = getOptionOrConfigForDao(index, parser, "removePrefix") // Remove prefix from table name. + jsonCase = getOptionOrConfigForDao(index, parser, "jsonCase", "CamelLower") // Case configuration for 'json' tag. + stdTime = getOptionOrConfigForDao(index, parser, "stdTime", "false") // Use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables. + modelFileName = getOptionOrConfigForDao(index, parser, "modelFile", defaultModelIndexFileName) // Custom file name for storing generated model content. + tplDaoIndexPath = getOptionOrConfigForDao(index, parser, "tplDaoIndex") // Template file path for generating dao index files. + tplDaoInternalPath = getOptionOrConfigForDao(index, parser, "tplDaoInternal") // Template file path for generating dao internal files. + tplModelIndexPath = getOptionOrConfigForDao(index, parser, "tplModelIndex") // Template file path for generating model index files. + tplModelStructPath = getOptionOrConfigForDao(index, parser, "tplModelStruct") // Template file path for generating model internal files. + ) + if tplDaoIndexPath != "" && (!gfile.Exists(tplDaoIndexPath) || !gfile.IsReadable(tplDaoIndexPath)) { + mlog.Fatalf("template file for dao index files generating does not exist or is not readable: %s", tplDaoIndexPath) + } + if tplDaoInternalPath != "" && (!gfile.Exists(tplDaoInternalPath) || !gfile.IsReadable(tplDaoInternalPath)) { + mlog.Fatalf("template internal for dao internal files generating does not exist or is not readable: %s: %s", tplDaoInternalPath) + } + if tplModelIndexPath != "" && (!gfile.Exists(tplModelIndexPath) || !gfile.IsReadable(tplModelIndexPath)) { + mlog.Fatalf("template file for model index files generating does not exist or is not readable: %s: %s", tplModelIndexPath) + } + if tplModelStructPath != "" && (!gfile.Exists(tplModelStructPath) || !gfile.IsReadable(tplModelStructPath)) { + mlog.Fatalf("template file for model internal files generating does not exist or is not readable: %s: %s", tplModelStructPath) + } + // Make it compatible with old CLI version for option name: remove-prefix + if removePrefix == "" { + removePrefix = getOptionOrConfigForDao(index, parser, "remove-prefix") + } + removePrefixArray := gstr.SplitAndTrim(removePrefix, ",") + if modName == "" { + if !gfile.Exists("go.mod") { + mlog.Fatal("go.mod does not exist in current working directory") + } + var ( + goModContent = gfile.GetContents("go.mod") + match, _ = gregex.MatchString(`^module\s+(.+)\s*`, goModContent) + ) + if len(match) > 1 { + modName = gstr.Trim(match[1]) + } else { + mlog.Fatal("module name does not found in go.mod") + } + } + // It reads database configuration from project configuration file. + if configPath != "" { + path, err := gfile.Search(configPath) + if err != nil { + mlog.Fatalf("search configuration file '%s' failed: %v", configPath, err) + } + if err := g.Cfg().SetPath(gfile.Dir(path)); err != nil { + mlog.Fatalf("set configuration path '%s' failed: %v", path, err) + } + g.Cfg().SetFileName(gfile.Basename(path)) + } + // It uses user passed database configuration. + if linkInfo != "" { + tempGroup := gtime.TimestampNanoStr() + match, _ := gregex.MatchString(`([a-z]+):(.+)`, linkInfo) + if len(match) == 3 { + gdb.AddConfigNode(tempGroup, gdb.ConfigNode{ + Type: gstr.Trim(match[1]), + LinkInfo: gstr.Trim(match[2]), + }) + db, _ = gdb.Instance(tempGroup) + } + } else { + db = g.DB(configGroup) + } + if db == nil { + mlog.Fatal("database initialization failed") + } + + var tableNames []string + if tablesStr != "" { + tableNames = gstr.SplitAndTrim(tablesStr, ",") + } else { + tableNames, err = db.Tables(context.TODO()) + if err != nil { + mlog.Fatalf("fetching tables failed: \n %v", err) + } + } + // Table excluding. + if tablesEx != "" { + array := garray.NewStrArrayFrom(tableNames) + for _, v := range gstr.SplitAndTrim(tablesEx, ",") { + array.RemoveValue(v) + } + tableNames = array.Slice() + } + + // Generating dao & model go files one by one according to given table name. + newTableNames := make([]string, len(tableNames)) + for i, tableName := range tableNames { + newTableName := tableName + for _, v := range removePrefixArray { + newTableName = gstr.TrimLeftStr(newTableName, v, 1) + } + newTableNames[i] = newTableName + generateDaoContentFile(db, generateDaoReq{ + TableName: tableName, + NewTableName: newTableName, + PrefixName: prefixName, + GroupName: configGroup, + ModName: modName, + JsonCase: jsonCase, + DirPath: dirPath, + StdTime: gconv.Bool(stdTime), + TplDaoIndexPath: tplDaoIndexPath, + TplDaoInternalPath: tplDaoInternalPath, + TplModelIndexPath: tplModelIndexPath, + TplModelStructPath: tplModelStructPath, + }) + } + generateDaoModelContentFile(db, tableNames, newTableNames, generateDaoReq{ + JsonCase: jsonCase, + DirPath: dirPath, + StdTime: gconv.Bool(stdTime), + ModelIndexFileName: modelFileName, + TplModelIndexPath: tplModelIndexPath, + TplModelStructPath: tplModelStructPath, + }) +} + +// generateDaoContentFile generates the dao and model content of given table. +func generateDaoContentFile(db gdb.DB, req generateDaoReq) { + // Generating table data preparing. + fieldMap, err := db.TableFields(context.TODO(), req.TableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", req.TableName, err) + } + // Change the `newTableName` if `prefixName` is given. + newTableName := req.PrefixName + req.NewTableName + var ( + dirPathDao = gstr.Trim(gfile.Join(req.DirPath, "dao"), "./") + tableNameCamelCase = gstr.CaseCamel(newTableName) + tableNameCamelLowerCase = gstr.CaseCamelLower(newTableName) + tableNameSnakeCase = gstr.CaseSnake(newTableName) + importPrefix = "" + dirRealPath = gfile.RealPath(req.DirPath) + ) + if dirRealPath == "" { + dirRealPath = req.DirPath + importPrefix = dirRealPath + importPrefix = gstr.Trim(dirRealPath, "./") + } else { + importPrefix = gstr.Replace(dirRealPath, gfile.Pwd(), "") + } + importPrefix = gstr.Replace(importPrefix, gfile.Separator, "/") + importPrefix = gstr.Join(g.SliceStr{req.ModName, importPrefix}, "/") + importPrefix, _ = gregex.ReplaceString(`\/{2,}`, `/`, gstr.Trim(importPrefix, "/")) + + fileName := gstr.Trim(tableNameSnakeCase, "-_.") + if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" { + // Add suffix to avoid the table name which contains "_test", + // which would make the go file a testing file. + fileName += "_table" + } + + // dao - index + generateDaoIndex(tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName, req) + + // dao - internal + generateDaoInternal(tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName, fieldMap, req) +} + +func generateDaoModelContentFile(db gdb.DB, tableNames, newTableNames []string, req generateDaoReq) { + var ( + modelContent string + packageImports string + dirPathModel = gstr.Trim(gfile.Join(req.DirPath, "model"), "./") + ) + + // Model content. + for i, tableName := range tableNames { + fieldMap, err := db.TableFields(context.TODO(), tableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", req.TableName, err) + } + modelContent += generateDaoModelStructContent( + tableName, + gstr.CaseCamel(newTableNames[i]), + req.TplModelStructPath, + generateStructDefinitionForModel(gstr.CaseCamel(newTableNames[i]), fieldMap, req), + ) + modelContent += "\n" + } + + // Time package recognition. + if strings.Contains(modelContent, "gtime.Time") { + packageImports = gstr.Trim(` +import ( + "github.com/gogf/gf/os/gtime" +)`) + } else if strings.Contains(modelContent, "time.Time") { + packageImports = gstr.Trim(` +import ( + "time" +)`) + } else { + packageImports = "" + } + + // Generate and write content to golang file. + modelContent = gstr.ReplaceByMap(getTplModelIndexContent(req.TplModelIndexPath), g.MapStrStr{ + "{TplPackageImports}": packageImports, + "{TplModelStructs}": modelContent, + }) + path := gfile.Join(dirPathModel, req.ModelIndexFileName) + if err := gfile.PutContents(path, strings.TrimSpace(modelContent)); err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", path, err) + } else { + utils.GoFmt(path) + mlog.Print("generated:", path) + } +} + +func generateDaoModelStructContent(tableName, tableNameCamelCase, tplModelStructPath, structDefine string) string { + return gstr.ReplaceByMap(getTplModelStructContent(tplModelStructPath), g.MapStrStr{ + "{TplTableName}": tableName, + "{TplTableNameCamelCase}": tableNameCamelCase, + "{TplStructDefine}": structDefine, + }) +} + +func generateDaoIndex(tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName string, req generateDaoReq) { + path := gfile.Join(dirPathDao, fileName+".go") + if !gfile.Exists(path) { + indexContent := gstr.ReplaceByMap(getTplDaoIndexContent(req.TplDaoIndexPath), g.MapStrStr{ + "{TplImportPrefix}": importPrefix, + "{TplTableName}": req.TableName, + "{TplTableNameCamelCase}": tableNameCamelCase, + "{TplTableNameCamelLowerCase}": tableNameCamelLowerCase, + }) + if err := gfile.PutContents(path, strings.TrimSpace(indexContent)); err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", path, err) + } else { + utils.GoFmt(path) + mlog.Print("generated:", path) + } + } +} + +func generateDaoInternal( + tableNameCamelCase, tableNameCamelLowerCase, importPrefix string, + dirPathDao, fileName string, + fieldMap map[string]*gdb.TableField, + req generateDaoReq, +) { + path := gfile.Join(dirPathDao, "internal", fileName+".go") + modelContent := gstr.ReplaceByMap(getTplDaoInternalContent(req.TplDaoInternalPath), g.MapStrStr{ + "{TplImportPrefix}": importPrefix, + "{TplTableName}": req.TableName, + "{TplGroupName}": req.GroupName, + "{TplTableNameCamelCase}": tableNameCamelCase, + "{TplTableNameCamelLowerCase}": tableNameCamelLowerCase, + "{TplColumnDefine}": gstr.Trim(generateColumnDefinitionForDao(fieldMap)), + "{TplColumnNames}": gstr.Trim(generateColumnNamesForDao(fieldMap)), + }) + if err := gfile.PutContents(path, strings.TrimSpace(modelContent)); err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", path, err) + } else { + utils.GoFmt(path) + mlog.Print("generated:", path) + } +} + +// generateStructDefinitionForModel generates and returns the struct definition for specified table. +func generateStructDefinitionForModel(structName string, fieldMap map[string]*gdb.TableField, req generateDaoReq) string { + buffer := bytes.NewBuffer(nil) + array := make([][]string, len(fieldMap)) + names := sortFieldKeyForDao(fieldMap) + for index, name := range names { + field := fieldMap[name] + array[index] = generateStructFieldForModel(field, req) + } + tw := tablewriter.NewWriter(buffer) + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetAutoWrapText(false) + tw.SetColumnSeparator("") + tw.AppendBulk(array) + tw.Render() + stContent := buffer.String() + // Let's do this hack of table writer for indent! + stContent = gstr.Replace(stContent, " #", "") + buffer.Reset() + buffer.WriteString(fmt.Sprintf("type %s struct {\n", structName)) + buffer.WriteString(stContent) + buffer.WriteString("}") + return buffer.String() +} + +// generateStructFieldForModel generates and returns the attribute definition for specified field. +func generateStructFieldForModel(field *gdb.TableField, req generateDaoReq) []string { + var typeName, ormTag, jsonTag string + t, _ := gregex.ReplaceString(`\(.+\)`, "", field.Type) + t = gstr.Split(gstr.Trim(t), " ")[0] + t = gstr.ToLower(t) + switch t { + case "binary", "varbinary", "blob", "tinyblob", "mediumblob", "longblob": + typeName = "[]byte" + + case "bit", "int", "int2", "tinyint", "small_int", "smallint", "medium_int", "mediumint", "serial": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "uint" + } else { + typeName = "int" + } + + case "int4", "int8", "big_int", "bigint", "bigserial": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "uint64" + } else { + typeName = "int64" + } + + case "real": + typeName = "float32" + + case "float", "double", "decimal", "smallmoney", "numeric": + typeName = "float64" + + case "bool": + typeName = "bool" + + case "datetime", "timestamp", "date", "time": + if req.StdTime { + typeName = "time.Time" + } else { + typeName = "*gtime.Time" + } + + default: + // Auto detecting type. + switch { + case strings.Contains(t, "int"): + typeName = "int" + case strings.Contains(t, "text") || strings.Contains(t, "char"): + typeName = "string" + case strings.Contains(t, "float") || strings.Contains(t, "double"): + typeName = "float64" + case strings.Contains(t, "bool"): + typeName = "bool" + case strings.Contains(t, "binary") || strings.Contains(t, "blob"): + typeName = "[]byte" + case strings.Contains(t, "date") || strings.Contains(t, "time"): + if req.StdTime { + typeName = "time.Time" + } else { + typeName = "*gtime.Time" + } + default: + typeName = "string" + } + } + ormTag = field.Name + jsonTag = getJsonTagFromCase(field.Name, req.JsonCase) + if gstr.ContainsI(field.Key, "pri") { + ormTag += ",primary" + } + if gstr.ContainsI(field.Key, "uni") { + ormTag += ",unique" + } + return []string{ + " #" + gstr.CaseCamel(field.Name), + " #" + typeName, + " #" + fmt.Sprintf("`"+`orm:"%s"`, ormTag), + " #" + fmt.Sprintf(`json:"%s"`+"`", jsonTag), + " #" + fmt.Sprintf(`// %s`, formatComment(field.Comment)), + } +} + +// formatComment formats the comment string to fit the golang code without any lines. +func formatComment(comment string) string { + comment = gstr.ReplaceByArray(comment, g.SliceStr{ + "\n", " ", + "\r", " ", + }) + comment = gstr.Trim(comment) + comment = gstr.Replace(comment, `\n`, " ") + return comment +} + +// generateColumnDefinitionForDao generates and returns the column names definition for specified table. +func generateColumnDefinitionForDao(fieldMap map[string]*gdb.TableField) string { + var ( + buffer = bytes.NewBuffer(nil) + array = make([][]string, len(fieldMap)) + names = sortFieldKeyForDao(fieldMap) + ) + for index, name := range names { + field := fieldMap[name] + comment := gstr.Trim(gstr.ReplaceByArray(field.Comment, g.SliceStr{ + "\n", " ", + "\r", " ", + })) + array[index] = []string{ + " #" + gstr.CaseCamel(field.Name), + " # " + "string", + " #" + fmt.Sprintf(`// %s`, comment), + } + } + tw := tablewriter.NewWriter(buffer) + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetAutoWrapText(false) + tw.SetColumnSeparator("") + tw.AppendBulk(array) + tw.Render() + defineContent := buffer.String() + // Let's do this hack of table writer for indent! + defineContent = gstr.Replace(defineContent, " #", "") + buffer.Reset() + buffer.WriteString(defineContent) + return buffer.String() +} + +// generateColumnNamesForDao generates and returns the column names assignment content of column struct +// for specified table. +func generateColumnNamesForDao(fieldMap map[string]*gdb.TableField) string { + var ( + buffer = bytes.NewBuffer(nil) + array = make([][]string, len(fieldMap)) + names = sortFieldKeyForDao(fieldMap) + ) + for index, name := range names { + field := fieldMap[name] + array[index] = []string{ + " #" + gstr.CaseCamel(field.Name) + ":", + fmt.Sprintf(` #"%s",`, field.Name), + } + } + tw := tablewriter.NewWriter(buffer) + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetAutoWrapText(false) + tw.SetColumnSeparator("") + tw.AppendBulk(array) + tw.Render() + namesContent := buffer.String() + // Let's do this hack of table writer for indent! + namesContent = gstr.Replace(namesContent, " #", "") + buffer.Reset() + buffer.WriteString(namesContent) + return buffer.String() +} + +func getTplDaoIndexContent(tplDaoIndexPath string) string { + if tplDaoIndexPath != "" { + return gfile.GetContents(tplDaoIndexPath) + } + return templateDaoDaoIndexContent +} + +func getTplDaoInternalContent(tplDaoInternalPath string) string { + if tplDaoInternalPath != "" { + return gfile.GetContents(tplDaoInternalPath) + } + return templateDaoDaoInternalContent +} + +func getTplModelIndexContent(tplModelIndexPath string) string { + if tplModelIndexPath != "" { + return gfile.GetContents(tplModelIndexPath) + } + return templateDaoModelIndexContent +} + +func getTplModelStructContent(tplModelStructPath string) string { + if tplModelStructPath != "" { + return gfile.GetContents(tplModelStructPath) + } + return templateDaoModelStructContent +} + +// getJsonTagFromCase call gstr.Case* function to convert the s to specified case. +func getJsonTagFromCase(str, caseStr string) string { + switch gstr.ToLower(caseStr) { + case gstr.ToLower("Camel"): + return gstr.CaseCamel(str) + + case gstr.ToLower("CamelLower"): + return gstr.CaseCamelLower(str) + + case gstr.ToLower("Kebab"): + return gstr.CaseKebab(str) + + case gstr.ToLower("KebabScreaming"): + return gstr.CaseKebabScreaming(str) + + case gstr.ToLower("Snake"): + return gstr.CaseSnake(str) + + case gstr.ToLower("SnakeFirstUpper"): + return gstr.CaseSnakeFirstUpper(str) + + case gstr.ToLower("SnakeScreaming"): + return gstr.CaseSnakeScreaming(str) + } + return str +} + +func sortFieldKeyForDao(fieldMap map[string]*gdb.TableField) []string { + names := make(map[int]string) + for _, field := range fieldMap { + names[field.Index] = field.Name + } + var ( + i = 0 + j = 0 + result = make([]string, len(names)) + ) + for { + if len(names) == 0 { + break + } + if val, ok := names[i]; ok { + result[j] = val + j++ + delete(names, i) + } + i++ + } + return result +} + +// getOptionOrConfigForDao retrieves option value from parser and configuration file. +// It returns the default value specified by parameter is no value found. +func getOptionOrConfigForDao(index int, parser *gcmd.Parser, name string, defaultValue ...string) (result string) { + result = parser.GetOpt(name) + if result == "" && g.Config().Available() { + g.Cfg().SetViolenceCheck(true) + if index >= 0 { + result = g.Cfg().GetString(fmt.Sprintf(`%s.%d.%s`, nodeNameGenDaoInConfigFile, index, name)) + } else { + result = g.Cfg().GetString(fmt.Sprintf(`%s.%s`, nodeNameGenDaoInConfigFile, name)) + } + } + if result == "" && len(defaultValue) > 0 { + result = defaultValue[0] + } + return +} diff --git a/tool/gf/commands/gen/gen_dao_template_dao.go b/tool/gf/commands/gen/gen_dao_template_dao.go new file mode 100644 index 000000000..879875c07 --- /dev/null +++ b/tool/gf/commands/gen/gen_dao_template_dao.go @@ -0,0 +1,73 @@ +package gen + +const templateDaoDaoIndexContent = ` +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package dao + +import ( + "{TplImportPrefix}/dao/internal" +) + +// {TplTableNameCamelLowerCase}Dao is the manager for logic model data accessing and custom defined data operations functions management. +// You can define custom methods on it to extend its functionality as you wish. +type {TplTableNameCamelLowerCase}Dao struct { + *internal.{TplTableNameCamelCase}Dao +} + +var ( + // {TplTableNameCamelCase} is globally public accessible object for table {TplTableName} operations. + {TplTableNameCamelCase} {TplTableNameCamelLowerCase}Dao +) + +func init() { + {TplTableNameCamelCase} = {TplTableNameCamelLowerCase}Dao{ + internal.New{TplTableNameCamelCase}Dao(), + } +} + +// Fill with you ideas below. + +` + +const templateDaoDaoInternalContent = ` +// ========================================================================== +// Code generated by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "github.com/gogf/gf/database/gdb" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/frame/gmvc" +) + +// {TplTableNameCamelCase}Dao is the manager for logic model data accessing and custom defined data operations functions management. +type {TplTableNameCamelCase}Dao struct { + gmvc.M // M is the core and embedded struct that inherits all chaining operations from gdb.Model. + C {TplTableNameCamelLowerCase}Columns // C is the short type for Columns, which contains all the column names of Table for convenient usage. + DB gdb.DB // DB is the raw underlying database management object. + Table string // Table is the underlying table name of the DAO. +} + +// {TplTableNameCamelCase}Columns defines and stores column names for table {TplTableName}. +type {TplTableNameCamelLowerCase}Columns struct { + {TplColumnDefine} +} + +// New{TplTableNameCamelCase}Dao creates and returns a new DAO object for table data access. +func New{TplTableNameCamelCase}Dao() *{TplTableNameCamelCase}Dao { + columns := {TplTableNameCamelLowerCase}Columns{ + {TplColumnNames} + } + return &{TplTableNameCamelCase}Dao{ + C: columns, + M: g.DB("{TplGroupName}").Model("{TplTableName}").Safe(), + DB: g.DB("{TplGroupName}"), + Table: "{TplTableName}", + } +} +` diff --git a/tool/gf/commands/gen/gen_dao_template_model.go b/tool/gf/commands/gen/gen_dao_template_model.go new file mode 100644 index 000000000..3eef8d4c0 --- /dev/null +++ b/tool/gf/commands/gen/gen_dao_template_model.go @@ -0,0 +1,18 @@ +package gen + +const templateDaoModelIndexContent = ` +// ================================================================================= +// Code generated by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package model + +{TplPackageImports} + +{TplModelStructs} +` + +const templateDaoModelStructContent = ` +// {TplTableNameCamelCase} is the golang structure for table {TplTableName}. +{TplStructDefine} +` diff --git a/tool/gf/commands/gen/gen_pb.go b/tool/gf/commands/gen/gen_pb.go new file mode 100644 index 000000000..81ee6f1be --- /dev/null +++ b/tool/gf/commands/gen/gen_pb.go @@ -0,0 +1,77 @@ +package gen + +import ( + "fmt" + "github.com/gogf/gf/container/gset" + "github.com/gogf/gf/os/genv" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gproc" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" +) + +func HelpPb() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf gen pb + +`)) +} + +// doGenPb parses current `proto` files in folder `protocol` and generates `pb` files to `protobuf`. +func doGenPb() { + // protoc search. + protocBinPath := gproc.SearchBinary("protoc") + if protocBinPath == "" { + mlog.Fatal(`"protoc" command not found, install it first to proceed proto files parsing`) + } + // protocol fold checks. + protoFolder := "protocol" + if !gfile.Exists(protoFolder) { + mlog.Fatalf(`proto files folder "%s" does not exist`, protoFolder) + } + // folder scanning. + files, err := gfile.ScanDirFile(protoFolder, "*.proto", true) + if err != nil { + mlog.Fatal(err) + } + if len(files) == 0 { + mlog.Fatalf(`no proto files found in folder "%s"`, protoFolder) + } + dirSet := gset.NewStrSet() + for _, file := range files { + dirSet.Add(gfile.Dir(file)) + } + var ( + servicePath = gfile.RealPath(".") + goPathSrc = gfile.RealPath(gfile.Join(genv.Get("GOPATH"), "src")) + ) + dirSet.Iterator(func(protoDirPath string) bool { + parsingCommand := fmt.Sprintf( + "protoc --gofast_out=plugins=grpc:. %s/*.proto -I%s", + protoDirPath, + servicePath, + ) + if goPathSrc != "" { + parsingCommand += " -I" + goPathSrc + } + mlog.Print(parsingCommand) + if output, err := gproc.ShellExec(parsingCommand); err != nil { + mlog.Print(output) + mlog.Fatal(err) + } + return true + }) + // Custom replacement. + //pbFolder := "protobuf" + //_, _ = gfile.ScanDirFileFunc(pbFolder, "*.go", true, func(path string) string { + // content := gfile.GetContents(path) + // content = gstr.ReplaceByArray(content, g.SliceStr{ + // `gtime "gtime"`, `gtime "github.com/gogf/gf/os/gtime"`, + // }) + // _ = gfile.PutContents(path, content) + // utils.GoFmt(path) + // return path + //}) + mlog.Print("done!") +} diff --git a/tool/gf/commands/gen/gen_pbentity.go b/tool/gf/commands/gen/gen_pbentity.go new file mode 100644 index 000000000..8c270f9e7 --- /dev/null +++ b/tool/gf/commands/gen/gen_pbentity.go @@ -0,0 +1,447 @@ +package gen + +import ( + "bytes" + "context" + "fmt" + _ "github.com/denisenkom/go-mssqldb" + "github.com/gogf/gf/database/gdb" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" + "github.com/gogf/gf/util/gconv" + _ "github.com/lib/pq" + //_ "github.com/mattn/go-oci8" + //_ "github.com/mattn/go-sqlite3" + "github.com/olekukonko/tablewriter" + "strings" +) + +// generatePbEntityReq is the input parameter for generating entity protobuf files. +type generatePbEntityReq struct { + TableName string // TableName specifies the table name of the table. + NewTableName string // NewTableName specifies the prefix-stripped name of the table. + PrefixName string // PrefixName specifies the custom prefix name for generated protobuf entity. + GroupName string // GroupName specifies the group name of database configuration node for generated protobuf entity. + PkgName string // PkgName specifies package name for generated protobuf. + NameCase string // NameCase specifies the case of generated attribute name for entity message, value from gstr.Case* function names. + JsonCase string // JsonCase specifies the case of json tag for attribute name of entity message, value from gstr.Case* function names. + DirPath string // DirPath specifies the directory path for generated files. + OptionContent string // OptionContent specifies the extra option configuration content for protobuf. + TplEntityPath string // TplEntityPath specifies the file path for generating protobuf entity files. +} + +const ( + nodeNameGenPbEntityInConfigFile = "gfcli.gen.pbentity" +) + +func HelpPbEntity() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf gen pbentity [OPTION] + +OPTION + -/--path directory path for generated files. + -/--package package name for all entity proto files. + -l, --link database configuration, the same as the ORM configuration of GoFrame. + -t, --tables generate models only for given tables, multiple table names separated with ',' + -c, --config used to specify the configuration file for database, it's commonly not necessary. + If "-l" is not passed, it will search "./config.toml" and "./config/config.toml" + in current working directory in default. + -p, --prefix add specified prefix for all entity names and entity proto files. + -r, --removePrefix remove specified prefix of the table, multiple prefix separated with ',' + -n, --nameCase case for message attribute names, default is "Camel": + | Case | Example | + |---------------- |--------------------| + | Camel | AnyKindOfString | default + | CamelLower | anyKindOfString | + | Snake | any_kind_of_string | + | SnakeScreaming | ANY_KIND_OF_STRING | + | SnakeFirstUpper | rgb_code_md5 | + | Kebab | any-kind-of-string | + | KebabScreaming | ANY-KIND-OF-STRING | + -j, --jsonCase case for message json tag, cases are the same as "nameCase", default "CamelLower". + set it to "none" to ignore json tag generating. + -o, --option extra protobuf options. + -/--tplEntity template content for protobuf entity files generating. + +CONFIGURATION SUPPORT + Options are also supported by configuration file. + It's suggested using configuration file instead of command line arguments making producing. + The configuration node name is "gf.gen.pbentity", which also supports multiple databases, for example: + [gfcli] + [[gfcli.gen.pbentity]] + link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + path = "protocol/demos/entity" + tables = "order,products" + package = "demos" + [[gfcli.gen.pbentity]] + link = "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" + path = "protocol/demos/entity" + prefix = "primary_" + tables = "user, userDetail" + package = "demos" + option = """ +option go_package = "protobuf/demos"; +option java_package = "protobuf/demos"; +option php_namespace = "protobuf/demos"; +""" + +EXAMPLES + gf gen pbentity + gf gen pbentity -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + gf gen pbentity -path ./protocol/demos/entity -c config.yaml -g user-center -t user,user_detail,user_login + gf gen pbentity -r user_ +`)) +} + +// doGenPbEntity implements the "gen pbentity" command. +func doGenPbEntity() { + parser, err := gcmd.Parse(g.MapStrBool{ + "path": true, + "package": true, + "l,link": true, + "t,tables": true, + "c,config": true, + "p,prefix": true, + "r,removePrefix": true, + "o,option": true, + "n,nameCase": true, + "j,jsonCase": true, + "tplEntity": true, + }) + if err != nil { + mlog.Fatal(err) + } + config := g.Cfg() + if config.Available() { + v := config.GetVar(nodeNameGenPbEntityInConfigFile) + if v.IsEmpty() && g.IsEmpty(parser.GetOptAll()) { + mlog.Fatal(`command arguments and configurations not found for generating protobuf entity files`) + } + if v.IsSlice() { + for i := 0; i < len(v.Interfaces()); i++ { + doGenPbEntityForArray(i, parser) + } + } else { + doGenPbEntityForArray(-1, parser) + } + } else { + doGenPbEntityForArray(-1, parser) + } + mlog.Print("done!") +} + +// doGenPbEntityForArray implements the "gen pbentity" command for configuration array. +func doGenPbEntityForArray(index int, parser *gcmd.Parser) { + var ( + err error + db gdb.DB + dirPath = getOptionOrConfigForPbEntity(index, parser, "path") // Generated directory path. + pkgName = getOptionOrConfigForPbEntity(index, parser, "package") // Package name for protobuf. + tablesStr = getOptionOrConfigForPbEntity(index, parser, "tables") // Tables that will be generated. + prefixName = getOptionOrConfigForPbEntity(index, parser, "prefix") // Add prefix to entity name. + linkInfo = getOptionOrConfigForPbEntity(index, parser, "link") // Custom database link. + configPath = getOptionOrConfigForPbEntity(index, parser, "config") // Config file path, eg: ./config/db.toml. + configGroup = getOptionOrConfigForPbEntity(index, parser, "group", "default") // Group name of database configuration node for generated protobuf entity. + removePrefix = getOptionOrConfigForPbEntity(index, parser, "removePrefix") // Remove prefix from table name. + nameCase = getOptionOrConfigForPbEntity(index, parser, "nameCase", "Camel") // Case configuration for message name. + jsonCase = getOptionOrConfigForPbEntity(index, parser, "jsonCase", "CamelLower") // Case configuration for message json tag. + optionContent = getOptionOrConfigForPbEntity(index, parser, "option") // Option content for protobuf. + tplEntityPath = getOptionOrConfigForPbEntity(index, parser, "tplEntity") // Specifies the file path for generating protobuf entity files. + ) + if tplEntityPath != "" && (!gfile.Exists(tplEntityPath) || !gfile.IsReadable(tplEntityPath)) { + mlog.Fatalf("template file for entity files generating does not exist or is not readable: %s", tplEntityPath) + } + // Make it compatible with old CLI version for option name: remove-prefix + if removePrefix == "" { + removePrefix = getOptionOrConfigForPbEntity(index, parser, "remove-prefix") + } + removePrefixArray := gstr.SplitAndTrim(removePrefix, ",") + if pkgName == "" { + mlog.Fatal("package name should not be empty") + } + // It reads database configuration from project configuration file. + if configPath != "" { + path, err := gfile.Search(configPath) + if err != nil { + mlog.Fatalf("search configuration file '%s' failed: %v", configPath, err) + } + if err := g.Cfg().SetPath(gfile.Dir(path)); err != nil { + mlog.Fatalf("set configuration path '%s' failed: %v", path, err) + } + g.Cfg().SetFileName(gfile.Basename(path)) + } + // It uses user passed database configuration. + if linkInfo != "" { + tempGroup := gtime.TimestampNanoStr() + match, _ := gregex.MatchString(`([a-z]+):(.+)`, linkInfo) + if len(match) == 3 { + gdb.AddConfigNode(tempGroup, gdb.ConfigNode{ + Type: gstr.Trim(match[1]), + LinkInfo: gstr.Trim(match[2]), + }) + db, _ = gdb.Instance(tempGroup) + } + } else { + db = g.DB(configGroup) + } + if db == nil { + mlog.Fatal("database initialization failed") + } + + tableNames := ([]string)(nil) + if tablesStr != "" { + tableNames = gstr.SplitAndTrim(tablesStr, ",") + } else { + tableNames, err = db.Tables(context.TODO()) + if err != nil { + mlog.Fatalf("fetching tables failed: \n %v", err) + } + } + + for _, tableName := range tableNames { + newTableName := tableName + for _, v := range removePrefixArray { + newTableName = gstr.TrimLeftStr(newTableName, v, 1) + } + req := &generatePbEntityReq{ + TableName: tableName, + NewTableName: newTableName, + PrefixName: prefixName, + GroupName: configGroup, + PkgName: pkgName, + NameCase: nameCase, + JsonCase: jsonCase, + DirPath: dirPath, + OptionContent: gstr.Trim(optionContent), + TplEntityPath: tplEntityPath, + } + generatePbEntityContentFile(db, req) + } +} + +// generatePbEntityContentFile generates the protobuf files for given table. +func generatePbEntityContentFile(db gdb.DB, req *generatePbEntityReq) { + fieldMap, err := db.TableFields(db.GetCtx(), req.TableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", req.TableName, err) + } + // Change the `newTableName` if `prefixName` is given. + newTableName := "Entity_" + req.PrefixName + req.NewTableName + var ( + tableNameCamelCase = gstr.CaseCamel(newTableName) + tableNameSnakeCase = gstr.CaseSnake(newTableName) + entityMessageDefine = generateEntityMessageDefinition(tableNameCamelCase, fieldMap, req) + fileName = gstr.Trim(tableNameSnakeCase, "-_.") + path = gfile.Join(req.DirPath, fileName+".proto") + ) + entityContent := gstr.ReplaceByMap(getTplPbEntityContent(req.TplEntityPath), g.MapStrStr{ + "{PackageName}": req.PkgName, + "{OptionContent}": req.OptionContent, + "{EntityMessage}": entityMessageDefine, + }) + if err := gfile.PutContents(path, strings.TrimSpace(entityContent)); err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", path, err) + } else { + mlog.Print("generated:", path) + } +} + +// generateEntityMessageDefinition generates and returns the message definition for specified table. +func generateEntityMessageDefinition(name string, fieldMap map[string]*gdb.TableField, req *generatePbEntityReq) string { + var ( + buffer = bytes.NewBuffer(nil) + array = make([][]string, len(fieldMap)) + names = sortFieldKeyForPbEntity(fieldMap) + ) + for index, name := range names { + array[index] = generateMessageFieldForPbEntity(index+1, fieldMap[name], req) + } + tw := tablewriter.NewWriter(buffer) + tw.SetBorder(false) + tw.SetRowLine(false) + tw.SetAutoWrapText(false) + tw.SetColumnSeparator("") + tw.AppendBulk(array) + tw.Render() + stContent := buffer.String() + // Let's do this hack of table writer for indent! + stContent = gstr.Replace(stContent, " #", "") + buffer.Reset() + buffer.WriteString(fmt.Sprintf("message %s {\n", name)) + buffer.WriteString(stContent) + buffer.WriteString("}") + return buffer.String() +} + +// generateMessageFieldForPbEntity generates and returns the message definition for specified field. +func generateMessageFieldForPbEntity(index int, field *gdb.TableField, req *generatePbEntityReq) []string { + var ( + typeName string + comment string + jsonTagStr string + ) + t, _ := gregex.ReplaceString(`\(.+\)`, "", field.Type) + t = gstr.Split(gstr.Trim(t), " ")[0] + t = gstr.ToLower(t) + switch t { + case "binary", "varbinary", "blob", "tinyblob", "mediumblob", "longblob": + typeName = "bytes" + + case "bit", "int", "tinyint", "small_int", "smallint", "medium_int", "mediumint", "serial": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "uint32" + } else { + typeName = "int32" + } + + case "int8", "big_int", "bigint", "bigserial": + if gstr.ContainsI(field.Type, "unsigned") { + typeName = "uint64" + } else { + typeName = "int64" + } + + case "real": + typeName = "float" + + case "float", "double", "decimal", "smallmoney": + typeName = "double" + + case "bool": + typeName = "bool" + + case "datetime", "timestamp", "date", "time": + typeName = "int64" + + default: + // Auto detecting type. + switch { + case strings.Contains(t, "int"): + typeName = "int" + case strings.Contains(t, "text") || strings.Contains(t, "char"): + typeName = "string" + case strings.Contains(t, "float") || strings.Contains(t, "double"): + typeName = "double" + case strings.Contains(t, "bool"): + typeName = "bool" + case strings.Contains(t, "binary") || strings.Contains(t, "blob"): + typeName = "bytes" + case strings.Contains(t, "date") || strings.Contains(t, "time"): + typeName = "int64" + default: + typeName = "string" + } + } + comment = gstr.ReplaceByArray(field.Comment, g.SliceStr{ + "\n", " ", + "\r", " ", + }) + comment = gstr.Trim(comment) + comment = gstr.Replace(comment, `\n`, " ") + comment, _ = gregex.ReplaceString(`\s{2,}`, ` `, comment) + if jsonTagName := formatCase(field.Name, req.JsonCase); jsonTagName != "" { + jsonTagStr = fmt.Sprintf(`[(gogoproto.jsontag) = "%s"]`, jsonTagName) + // beautiful indent. + if index < 10 { + // 3 spaces + jsonTagStr = " " + jsonTagStr + } else if index < 100 { + // 2 spaces + jsonTagStr = " " + jsonTagStr + } else { + // 1 spaces + jsonTagStr = " " + jsonTagStr + } + } + return []string{ + " #" + typeName, + " #" + formatCase(field.Name, req.NameCase), + " #= " + gconv.String(index) + jsonTagStr + ";", + " #" + fmt.Sprintf(`// %s`, comment), + } +} + +func getTplPbEntityContent(tplEntityPath string) string { + if tplEntityPath != "" { + return gfile.GetContents(tplEntityPath) + } + return templatePbEntityMessageContent +} + +// formatCase call gstr.Case* function to convert the s to specified case. +func formatCase(str, caseStr string) string { + switch gstr.ToLower(caseStr) { + case gstr.ToLower("Camel"): + return gstr.CaseCamel(str) + + case gstr.ToLower("CamelLower"): + return gstr.CaseCamelLower(str) + + case gstr.ToLower("Kebab"): + return gstr.CaseKebab(str) + + case gstr.ToLower("KebabScreaming"): + return gstr.CaseKebabScreaming(str) + + case gstr.ToLower("Snake"): + return gstr.CaseSnake(str) + + case gstr.ToLower("SnakeFirstUpper"): + return gstr.CaseSnakeFirstUpper(str) + + case gstr.ToLower("SnakeScreaming"): + return gstr.CaseSnakeScreaming(str) + + case "none": + return "" + } + return str +} + +// getOptionOrConfigForPbEntity retrieves option value from parser and configuration file. +// It returns the default value specified by parameter is no value found. +func getOptionOrConfigForPbEntity(index int, parser *gcmd.Parser, name string, defaultValue ...string) (result string) { + result = parser.GetOpt(name) + if result == "" && g.Config().Available() { + g.Cfg().SetViolenceCheck(true) + if index >= 0 { + result = g.Cfg().GetString(fmt.Sprintf(`%s.%d.%s`, nodeNameGenPbEntityInConfigFile, index, name)) + } else { + result = g.Cfg().GetString(fmt.Sprintf(`%s.%s`, nodeNameGenPbEntityInConfigFile, name)) + } + } + if result == "" && len(defaultValue) > 0 { + result = defaultValue[0] + } + return +} + +func sortFieldKeyForPbEntity(fieldMap map[string]*gdb.TableField) []string { + names := make(map[int]string) + for _, field := range fieldMap { + names[field.Index] = field.Name + } + var ( + result = make([]string, len(names)) + i = 0 + j = 0 + ) + for { + if len(names) == 0 { + break + } + if val, ok := names[i]; ok { + result[j] = val + j++ + delete(names, i) + } + i++ + } + return result +} diff --git a/tool/gf/commands/gen/gen_pbentity_template.go b/tool/gf/commands/gen/gen_pbentity_template.go new file mode 100644 index 000000000..f73d5c55e --- /dev/null +++ b/tool/gf/commands/gen/gen_pbentity_template.go @@ -0,0 +1,17 @@ +package gen + +const templatePbEntityMessageContent = ` +// ========================================================================== +// Code generated by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +syntax = "proto3"; + +package {PackageName}; + +import "github.com/gogo/protobuf/gogoproto/gogo.proto"; + +{OptionContent} + +{EntityMessage} +` diff --git a/tool/gf/commands/get/get.go b/tool/gf/commands/get/get.go new file mode 100644 index 000000000..584b5cf7e --- /dev/null +++ b/tool/gf/commands/get/get.go @@ -0,0 +1,33 @@ +package get + +import ( + "fmt" + "github.com/gogf/gf/os/gproc" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" + "os" +) + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf get PACKAGE + +ARGUMENT + PACKAGE remote golang package path, eg: github.com/gogf/gf + +EXAMPLES + gf get github.com/gogf/gf + gf get github.com/gogf/gf@latest + gf get github.com/gogf/gf@master + gf get golang.org/x/sys +`)) +} + +func Run() { + if len(os.Args) > 2 { + gproc.ShellRun(fmt.Sprintf(`go get -u %s`, gstr.Join(os.Args[2:], " "))) + } else { + mlog.Fatal("please input the package path for get") + } +} diff --git a/tool/gf/commands/initialize/initialize.go b/tool/gf/commands/initialize/initialize.go new file mode 100644 index 000000000..a5ddadd0b --- /dev/null +++ b/tool/gf/commands/initialize/initialize.go @@ -0,0 +1,110 @@ +package initialize + +import ( + "github.com/gogf/gf/encoding/gcompress" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/allyes" + "github.com/gogf/gf/tool/gf/library/mlog" + "strings" +) + +const ( + emptyProject = "github.com/gogf/gf-empty" + emptyProjectName = "gf-empty" +) + +var ( + cdnUrl = g.Config("url").GetString("cdn.url") + homeUrl = g.Config("url").GetString("home.url") +) + +func init() { + if cdnUrl == "" { + mlog.Fatal("CDN configuration cannot be empty") + } + if homeUrl == "" { + mlog.Fatal("Home configuration cannot be empty") + } +} + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf init NAME + +ARGUMENT + NAME name for the project. It will create a folder with NAME in current directory. + The NAME will also be the module name for the project. + +EXAMPLES + gf init my-app + gf init my-project-name +`)) +} + +func Run() { + parser, err := gcmd.Parse(nil) + if err != nil { + mlog.Fatal(err) + } + projectName := parser.GetArg(2) + if projectName == "" { + mlog.Fatal("project name should not be empty") + } + dirPath := projectName + if !gfile.IsEmpty(dirPath) && !allyes.Check() { + s := gcmd.Scanf(`the folder "%s" is not empty, files might be overwrote, continue? [y/n]: `, projectName) + if strings.EqualFold(s, "n") { + return + } + } + mlog.Print("initializing...") + // MD5 retrieving. + respMd5, err := g.Client().Get(homeUrl + "/cli/project/md5") + if err != nil { + mlog.Fatalf("get the project zip md5 failed: %s", err.Error()) + } + if respMd5 == nil { + mlog.Fatal("got the project zip md5 failed") + } + defer respMd5.Close() + md5DataStr := respMd5.ReadAllString() + if md5DataStr == "" { + mlog.Fatal("get the project zip md5 failed: empty md5 value. maybe network issue, try again?") + } + + // Zip data retrieving. + respData, err := g.Client().Get(cdnUrl + "/cli/project/zip?" + md5DataStr) + if err != nil { + mlog.Fatalf("got the project zip data failed: %s", err.Error()) + } + if respData == nil { + mlog.Fatal("got the project zip data failed") + } + defer respData.Close() + zipData := respData.ReadAll() + if len(zipData) == 0 { + mlog.Fatal("get the project data failed: empty data value. maybe network issue, try again?") + } + // Current folder. + replacedProjectName := projectName + if replacedProjectName == "." { + replacedProjectName = gfile.Name(gfile.RealPath(".")) + } + // Unzip the zip data. + if err = gcompress.UnZipContent(zipData, dirPath, emptyProjectName+"-master"); err != nil { + mlog.Fatal("unzip project data failed,", err.Error()) + } + // Replace project name. + if err = gfile.ReplaceDir(emptyProject, replacedProjectName, dirPath, "Dockerfile,*.go,*.MD,*.mod", true); err != nil { + mlog.Fatal("content replacing failed,", err.Error()) + } + if err = gfile.ReplaceDir(emptyProjectName, replacedProjectName, dirPath, "Dockerfile,*.go,*.MD,*.mod", true); err != nil { + mlog.Fatal("content replacing failed,", err.Error()) + } + mlog.Print("initialization done! ") + mlog.Print("you can now run 'gf run main.go' to start your journey, enjoy!") +} diff --git a/tool/gf/commands/install/install.go b/tool/gf/commands/install/install.go new file mode 100644 index 000000000..e178c3f3f --- /dev/null +++ b/tool/gf/commands/install/install.go @@ -0,0 +1,192 @@ +package install + +import ( + "github.com/gogf/gf/container/garray" + "github.com/gogf/gf/container/gset" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/genv" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/allyes" + "github.com/gogf/gf/tool/gf/library/mlog" + "github.com/gogf/gf/util/gconv" + "runtime" + "strings" +) + +// installFolderPath contains installFolderPath-related data, +// such as path, writable, binaryFilePath, and installed. +type installFolderPath struct { + path string + writable bool + binaryFilePath string + installed bool +} + +// Run does the installation. +func Run() { + // Ask where to install. + paths := getInstallPathsData() + if len(paths) <= 0 { + mlog.Printf("no path detected, you can manually install gf by copying the binary to path folder.") + return + } + mlog.Printf("I found some installable paths for you(from $PATH): ") + mlog.Printf(" %2s | %8s | %9s | %s", "Id", "Writable", "Installed", "Path") + + // Print all paths status and determine the default selectedID value. + var ( + selectedID = -1 + pathSet = gset.NewStrSet() // Used for repeated items filtering. + ) + for id, aPath := range paths { + if !pathSet.AddIfNotExist(aPath.path) { + continue + } + mlog.Printf(" %2d | %8t | %9t | %s", id, aPath.writable, aPath.installed, aPath.path) + if selectedID == -1 { + // Use the previously installed path as the most priority choice. + if aPath.installed { + selectedID = id + } + } + } + // If there's no previously installed path, use the first writable path. + if selectedID == -1 { + // Order by choosing priority. + commonPaths := garray.NewStrArrayFrom(g.SliceStr{ + `/usr/local/bin`, + `/usr/bin`, + `/usr/sbin`, + `C:\Windows`, + `C:\Windows\system32`, + `C:\Go\bin`, + `C:\Program Files`, + `C:\Program Files (x86)`, + }) + // Check the common installation directories. + commonPaths.Iterator(func(k int, v string) bool { + for id, aPath := range paths { + if strings.EqualFold(aPath.path, v) { + selectedID = id + return false + } + } + return true + }) + if selectedID == -1 { + selectedID = 0 + } + } + + if allyes.Check() { + // Use the default selectedID. + mlog.Printf("please choose one installation destination [default %d]: %d", selectedID, selectedID) + } else { + // Get input and update selectedID. + input := gcmd.Scanf("please choose one installation destination [default %d]: ", selectedID) + if input != "" { + selectedID = gconv.Int(input) + } + } + + // Check if out of range. + if selectedID >= len(paths) || selectedID < 0 { + mlog.Printf("invalid install destination Id: %d", selectedID) + return + } + + // Get selected destination path. + dstPath := paths[selectedID] + + // Install the new binary. + err := gfile.CopyFile(gfile.SelfPath(), dstPath.binaryFilePath) + if err != nil { + mlog.Printf("install gf binary to '%s' failed: %v", dstPath.path, err) + mlog.Printf("you can manually install gf by copying the binary to folder: %s", dstPath.path) + } else { + mlog.Printf("gf binary is successfully installed to: %s", dstPath.path) + } + + // Uninstall the old binary. + for _, aPath := range paths { + // Do not delete myself. + if aPath.binaryFilePath != "" && + aPath.binaryFilePath != dstPath.binaryFilePath && + gfile.SelfPath() != aPath.binaryFilePath { + gfile.Remove(aPath.binaryFilePath) + } + } +} + +// IsInstalled returns whether the binary is installed. +func IsInstalled() bool { + paths := getInstallPathsData() + for _, aPath := range paths { + if aPath.installed { + return true + } + } + return false +} + +// GetInstallPathsData returns the installation paths data for the binary. +func getInstallPathsData() []installFolderPath { + var folderPaths []installFolderPath + // Pre generate binaryFileName. + binaryFileName := "gf" + gfile.Ext(gfile.SelfPath()) + switch runtime.GOOS { + case "darwin": + folderPaths = checkPathAndAppendToInstallFolderPath( + folderPaths, "/usr/local/bin", binaryFileName, + ) + default: + // Search and find the writable directory path. + envPath := genv.Get("PATH", genv.Get("Path")) + if gstr.Contains(envPath, ";") { + for _, v := range gstr.SplitAndTrim(envPath, ";") { + folderPaths = checkPathAndAppendToInstallFolderPath( + folderPaths, v, binaryFileName) + } + } else if gstr.Contains(envPath, ":") { + for _, v := range gstr.SplitAndTrim(envPath, ":") { + folderPaths = checkPathAndAppendToInstallFolderPath( + folderPaths, v, binaryFileName) + } + } else if envPath != "" { + folderPaths = checkPathAndAppendToInstallFolderPath( + folderPaths, envPath, binaryFileName) + } else { + folderPaths = checkPathAndAppendToInstallFolderPath( + folderPaths, "/usr/local/bin", binaryFileName) + } + } + return folderPaths +} + +// checkPathAndAppendToInstallFolderPath checks if is writable and already installed. +// It adds the to if it is writable or already installed, or else it ignores the . +func checkPathAndAppendToInstallFolderPath(folderPaths []installFolderPath, path string, binaryFileName string) []installFolderPath { + var ( + binaryFilePath = gfile.Join(path, binaryFileName) + writable = gfile.IsWritable(path) + installed = isInstalled(binaryFilePath) + ) + if !writable && !installed { + return folderPaths + } + return append( + folderPaths, + installFolderPath{ + path: path, + writable: writable, + binaryFilePath: binaryFilePath, + installed: installed, + }) +} + +// Check if this gf binary path exists. +func isInstalled(path string) bool { + return gfile.Exists(path) +} diff --git a/tool/gf/commands/mod/mod.go b/tool/gf/commands/mod/mod.go new file mode 100644 index 000000000..216ae0302 --- /dev/null +++ b/tool/gf/commands/mod/mod.go @@ -0,0 +1,113 @@ +package mod + +import ( + "fmt" + "github.com/gogf/gf/container/gmap" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/genv" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" +) + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf mod ARGUMENT + +ARGUMENT + path copy all packages with its latest version in Go modules, which does not exist + in GOPATH, to GOPATH. This enables your project using GOPATH building, but you + should have GOPATH environment variable configured. + +EXAMPLES + gf mod path +`)) +} + +func Run() { + argument := gcmd.GetArg(2) + switch argument { + case "path": + doPath() + + default: + mlog.Print("argument cannot be empty") + Help() + } +} + +// doPath copies all packages in Go modules, which does not exist in GOPATH, to GOPATH. +// This enables your project using GOPATH building, but you should have GOPATH +// environment variable configured. +func doPath() { + goPathEnv := genv.Get("GOPATH") + if goPathEnv == "" { + mlog.Fatal("GOPATH is not found in your environment") + } + mlog.Print("scanning...") + var ( + copied = false + haveCount = 0 + ) + for _, goPath := range gstr.SplitAndTrim(goPathEnv, ";") { + goModPath := gfile.Join(goPath, "pkg", "mod") + if !gfile.Exists(goModPath) { + continue + } + pathMap := gmap.NewStrStrMap() + _, err := gfile.ScanDirFunc(goModPath, "*.*", true, func(path string) string { + // Ignore the cache folder. + if gstr.Contains(path, gfile.Join(goModPath, "cache")) { + return "" + } + name := gfile.Name(path) + if name == "" { + return "" + } + if !gstr.Contains(name, "@") { + return "" + } + if n := gstr.Count(path, "@"); n > 1 { + return "" + } + if !gfile.IsDir(path) { + return "" + } + array := gstr.Split(path, "@") + if v := pathMap.Get(array[0]); v == "" { + pathMap.Set(array[0], array[1]) + } else { + if gstr.CompareVersionGo(v, array[1]) < 0 { + pathMap.Set(array[0], array[1]) + } + } + return path + }) + if err != nil { + mlog.Fatal(err) + } + haveCount += pathMap.Size() + pathMap.Iterator(func(k string, v string) bool { + src := fmt.Sprintf(`%s@%s`, k, v) + dst := gfile.Join(goPath, "src", gstr.Trim(gstr.Replace(k, goModPath, ""), "\\/")) + if !gfile.Exists(dst) { + mlog.Printf(`copying %s to %s`, src, dst) + if err := gfile.Copy(src, dst); err != nil { + mlog.Fatal(err) + } + copied = true + } + return true + }) + } + if !copied { + if haveCount > 0 { + mlog.Print(`all packages of go modules already exist in GOPATH`) + } else { + mlog.Printf(`no packages found in go module path: %s`, goPathEnv) + } + return + } + mlog.Print("done!") +} diff --git a/tool/gf/commands/pack/pack.go b/tool/gf/commands/pack/pack.go new file mode 100644 index 000000000..7a3efaa9d --- /dev/null +++ b/tool/gf/commands/pack/pack.go @@ -0,0 +1,80 @@ +package pack + +import ( + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gres" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/allyes" + "github.com/gogf/gf/tool/gf/library/mlog" + "strings" +) + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf pack SRC DST + +ARGUMENT + SRC source path for packing, which can be multiple source paths. + DST destination file path for packed file. if extension of the filename is ".go" and "-n" option is given, + it enables packing SRC to go file, or else it packs SRC into a binary file. + +OPTION + -n, --name package name for output go file, it's set as its directory name if no name passed + -p, --prefix prefix for each file packed into the resource file + +EXAMPLES + gf pack public data.bin + gf pack public,template data.bin + gf pack public,template packed/data.go + gf pack public,template,config packed/data.go + gf pack public,template,config packed/data.go -n=packed -p=/var/www/my-app + gf pack /var/www/public packed/data.go -n=packed +`)) +} + +func Run() { + parser, err := gcmd.Parse(g.MapStrBool{ + "n,name": true, + "p,prefix": true, + }) + if err != nil { + mlog.Fatal(err) + } + srcPath := parser.GetArg(2) + dstPath := parser.GetArg(3) + if srcPath == "" { + mlog.Fatal("SRC path cannot be empty") + } + if dstPath == "" { + mlog.Fatal("DST path cannot be empty") + } + if gfile.Exists(dstPath) && gfile.IsDir(dstPath) { + mlog.Fatalf("DST path '%s' cannot be a directory", dstPath) + } + if !gfile.IsEmpty(dstPath) && !allyes.Check() { + s := gcmd.Scanf("path '%s' is not empty, files might be overwrote, continue? [y/n]: ", dstPath) + if strings.EqualFold(s, "n") { + return + } + } + var ( + name = parser.GetOpt("name") + prefix = parser.GetOpt("prefix") + ) + if name == "" && gfile.ExtName(dstPath) == "go" { + name = gfile.Basename(gfile.Dir(dstPath)) + } + if name != "" { + if err := gres.PackToGoFile(srcPath, dstPath, name, prefix); err != nil { + mlog.Fatalf("pack failed: %v", err) + } + } else { + if err := gres.PackToFile(srcPath, dstPath, prefix); err != nil { + mlog.Fatalf("pack failed: %v", err) + } + } + mlog.Print("done!") +} diff --git a/tool/gf/commands/run/run.go b/tool/gf/commands/run/run.go new file mode 100644 index 000000000..fe9b55d96 --- /dev/null +++ b/tool/gf/commands/run/run.go @@ -0,0 +1,212 @@ +package run + +import ( + "fmt" + "github.com/gogf/gf/container/garray" + "github.com/gogf/gf/container/gtype" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gfsnotify" + "github.com/gogf/gf/os/gproc" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/os/gtimer" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/commands/swagger" + "github.com/gogf/gf/tool/gf/library/mlog" + "os" + "runtime" + "strings" + "time" +) + +type App struct { + File string // Go run file name/path. + Options string // Extra "go run" options. + Args string // Auto parse and pack swagger files. + Swagger bool // Auto parse and pack swagger files. +} + +const ( + gPROXY_CHECK_TIMEOUT = time.Second +) + +var ( + process *gproc.Process + httpClient = ghttp.NewClient() +) + +func init() { + httpClient.SetTimeout(gPROXY_CHECK_TIMEOUT) +} + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf run FILE [OPTION] + +ARGUMENT + FILE building file path. + OPTION the same options as "go run"/"go build" except some options as follows defined + +OPTION + -/--args custom process arguments. + -/--swagger auto parse and pack swagger into packed/data-swagger.go before running. + +EXAMPLES + gf run main.go + gf run main.go --swagger + gf run main.go --args "server -p 8080" + gf run main.go -mod=vendor + +DESCRIPTION + The "run" command is used for running go codes with hot-compiled-like feature, + which compiles and runs the go codes asynchronously when codes change. +`)) +} + +func Run() { + parser, err := gcmd.Parse(g.MapStrBool{ + "args": true, + }) + if err != nil { + mlog.Fatal(err) + } + mlog.SetHeaderPrint(true) + file := gcmd.GetArg(2) + if len(file) < 1 { + mlog.Fatal("file path cannot be empty") + } + app := &App{ + File: file, + } + // ================================================================================ + // This command is very special that it supports options of "go run" and "go build" + // from the third parameter of os.Args. That means, we should filter any parameter + // that "go run" and "go build" do not allow. + // ================================================================================ + // Swagger checks. + array := garray.NewStrArrayFrom(os.Args) + index := array.Search("--swagger") + if index < 0 { + index = array.Search("-swagger") + } + if index != -1 { + app.Swagger = true + array.Remove(index) + } + // args checks. + args := parser.GetOpt("args") + if args != "" { + app.Args = args + index := -1 + array.Iterator(func(k int, v string) bool { + if gstr.Contains(v, "-args") { + index = k + return false + } + return true + }) + if index != -1 { + v, _ := array.Get(index) + if gstr.Contains(v, "=") { + array.Remove(index) + } else { + array.Remove(index) + array.Remove(index) + } + } + } + // -y checks + array.RemoveValue("-y") + array.RemoveValue("--y") + if array.Len() > 3 { + app.Options = strings.Join(array.SubSlice(3), " ") + } + dirty := gtype.NewBool() + _, err = gfsnotify.Add(gfile.RealPath("."), func(event *gfsnotify.Event) { + if gfile.ExtName(event.Path) != "go" { + return + } + // Ignore swagger file. + if gfile.Basename(event.Path) == "data-swagger.go" { + return + } + // Variable is used for running the changes only one in one second. + if !dirty.Cas(false, true) { + return + } + // With some delay in case of multiple code changes in very short interval. + gtimer.SetTimeout(1500*gtime.MS, func() { + defer dirty.Set(false) + mlog.Printf(`go file changes: %s`, event.String()) + app.Run() + }) + }) + if err != nil { + mlog.Fatal(err) + } + go app.Run() + select {} +} + +func (app *App) Run() { + // Rebuild and run the codes. + renamePath := "" + mlog.Printf("build: %s", app.File) + outputPath := gfile.Join("bin", gfile.Name(app.File)) + if runtime.GOOS == "windows" { + outputPath += ".exe" + if gfile.Exists(outputPath) { + renamePath = outputPath + "~" + if err := gfile.Rename(outputPath, renamePath); err != nil { + mlog.Print(err) + } + } + } + // Auto swagger. + if app.Swagger { + if err := gproc.ShellRun(`gf swagger`); err != nil { + return + } + if gfile.Exists("swagger") { + packCmd := fmt.Sprintf(`gf pack %s packed/%s -n packed -y`, "swagger", swagger.PackedGoFileName) + mlog.Print(packCmd) + if err := gproc.ShellRun(packCmd); err != nil { + return + } + } + } + // In case of `pipe: too many open files` error. + // Build the app. + buildCommand := fmt.Sprintf(`go build -o %s %s %s`, outputPath, app.Options, app.File) + mlog.Print(buildCommand) + result, err := gproc.ShellExec(buildCommand) + if err != nil { + mlog.Printf("build error: \n%s%s", result, err.Error()) + return + } + // Kill the old process if build successfully. + if process != nil { + if err := process.Kill(); err != nil { + mlog.Debugf("kill process error: %s", err.Error()) + //return + } + } + // Run the binary file. + runCommand := fmt.Sprintf(`%s %s`, outputPath, app.Args) + mlog.Print(runCommand) + if runtime.GOOS == "windows" { + // Special handling for windows platform. + // DO NOT USE "cmd /c" command. + process = gproc.NewProcess(runCommand, nil) + } else { + process = gproc.NewProcessCmd(runCommand, nil) + } + if pid, err := process.Start(); err != nil { + mlog.Printf("build running error: %s", err.Error()) + } else { + mlog.Printf("build running pid: %d", pid) + } +} diff --git a/tool/gf/commands/swagger/swagger.go b/tool/gf/commands/swagger/swagger.go new file mode 100644 index 000000000..2993f200c --- /dev/null +++ b/tool/gf/commands/swagger/swagger.go @@ -0,0 +1,154 @@ +package swagger + +import ( + "errors" + "fmt" + "github.com/gogf/gf/container/gtype" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gfsnotify" + "github.com/gogf/gf/os/gproc" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/os/gtimer" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/tool/gf/library/mlog" + "github.com/gogf/swagger" +) + +const ( + defaultOutput = "./swagger" + swaggoRepoPath = "github.com/swaggo/swag/cmd/swag" + PackedGoFileName = "swagger.go" +) + +func Help() { + mlog.Print(gstr.TrimLeft(` +USAGE + gf swagger [OPTION] + +OPTION + -s, --server start a swagger server at specified address after swagger files + produced + -o, --output the output directory for storage parsed swagger files, + the default output directory is "./swagger" + -/--pack auto parses and packs swagger into packed/swagger.go. + +EXAMPLES + gf swagger + gf swagger --pack + gf swagger -s 8080 + gf swagger -s 127.0.0.1:8080 + gf swagger -o ./document/swagger + + +DESCRIPTION + The "swagger" command parses the current project and produces swagger API description + files, which can be used in swagger API server. If used with "-s/--server" option, it + watches the changes of go files of current project and reproduces the swagger files, + which is quite convenient for local API development. + If it fails in command "swag", please firstly check your system PATH whether containing + go binary path, or you can install the "swag" tool manually referring to: + https://github.com/swaggo/swag +`)) +} + +func Run() { + mlog.SetHeaderPrint(true) + parser, err := gcmd.Parse(g.MapStrBool{ + "s,server": true, + "o,output": true, + "pack": false, + }) + if err != nil { + mlog.Fatal(err) + } + server := parser.GetOpt("server") + output := parser.GetOpt("output", defaultOutput) + // Generate swagger files. + if err := generateSwaggerFiles(output, parser.ContainsOpt("pack")); err != nil { + mlog.Print(err) + } + // Watch the go file changes and regenerate the swagger files. + dirty := gtype.NewBool() + _, err = gfsnotify.Add(gfile.RealPath("."), func(event *gfsnotify.Event) { + if gfile.ExtName(event.Path) != "go" || gstr.Contains(event.Path, "swagger") { + return + } + // Variable is used for running the changes only one in one second. + if !dirty.Cas(false, true) { + return + } + // With some delay in case of multiple code changes in very short interval. + gtimer.SetTimeout(1500*gtime.MS, func() { + mlog.Printf(`go file changes: %s`, event.String()) + mlog.Print(`reproducing swagger files...`) + if err := generateSwaggerFiles(output, parser.ContainsOpt("pack")); err != nil { + mlog.Print(err) + } else { + mlog.Print(`done!`) + } + dirty.Set(false) + }) + }) + if err != nil { + mlog.Fatal(err) + } + // Swagger server starts. + if server != "" { + if gstr.IsNumeric(server) { + server = ":" + server + } + s := g.Server() + s.Plugin(&swagger.Swagger{}) + s.SetAddr(server) + s.Run() + } +} + +// generateSwaggerFiles generates necessary swagger files. +func generateSwaggerFiles(output string, pack bool) error { + mlog.Print(`producing swagger files...`) + // Temporary storing swagger files directory. + tempOutputPath := gfile.Join(gfile.TempDir(), "swagger") + if gfile.Exists(tempOutputPath) { + gfile.Remove(tempOutputPath) + } + gfile.Mkdir(tempOutputPath) + // Check and install swag tool. + swag := gproc.SearchBinary("swag") + if swag == "" { + err := gproc.ShellRun(fmt.Sprintf(`go get -u -v %s`, swaggoRepoPath)) + if err != nil { + return err + } + } + // Generate swagger files using swag. + command := fmt.Sprintf(`swag init -o %s`, tempOutputPath) + result, err := gproc.ShellExec(command) + if err != nil { + return errors.New(result + err.Error()) + } + if !gfile.Exists(gfile.Join(tempOutputPath, "swagger.json")) { + return errors.New("make swagger files failed") + } + if !gfile.Exists(output) { + gfile.Mkdir(output) + } + if err = gfile.CopyFile( + gfile.Join(tempOutputPath, "swagger.json"), + gfile.Join(output, "swagger.json"), + ); err != nil { + return err + } + mlog.Print(`done!`) + // Auto pack into go file. + if pack && gfile.Exists("swagger") { + packCmd := fmt.Sprintf(`gf pack %s packed/%s -n packed`, "swagger", PackedGoFileName) + mlog.Print(packCmd) + if err := gproc.ShellRun(packCmd); err != nil { + return err + } + } + return nil +} diff --git a/tool/gf/commands/update/update.go b/tool/gf/commands/update/update.go new file mode 100644 index 000000000..919e71e9a --- /dev/null +++ b/tool/gf/commands/update/update.go @@ -0,0 +1,95 @@ +package update + +import ( + "fmt" + "github.com/gogf/gf/crypto/gmd5" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/tool/gf/library/mlog" + "runtime" +) + +var ( + cdnUrl = g.Config("url").GetString("cdn.url") + homeUrl = g.Config("url").GetString("home.url") +) + +func init() { + if cdnUrl == "" { + mlog.Fatal("CDN configuration cannot be empty") + } + if homeUrl == "" { + mlog.Fatal("Home configuration cannot be empty") + } +} + +func Run() { + mlog.Print("checking...") + md5Url := homeUrl + `/cli/binary/md5` + latestMd5 := ghttp.GetContent(md5Url, g.Map{ + "os": runtime.GOOS, + "arch": runtime.GOARCH, + }) + if latestMd5 == "" { + mlog.Fatal("get the latest binary md5 failed, may be network issue") + } + localMd5, err := gmd5.EncryptFile(gfile.SelfPath()) + if err != nil { + mlog.Fatal("calculate local binary md5 failed,", err.Error()) + } + if localMd5 != latestMd5 { + mlog.Print("downloading...") + ext := "" + if runtime.GOOS == "windows" { + ext = ".exe" + } + downloadUrl := fmt.Sprintf( + `%s/cli/binary/%s_%s/gf%s?%s`, + cdnUrl, + runtime.GOOS, + runtime.GOARCH, + ext, + latestMd5, + ) + mlog.Debugf("HTTP GET %s", downloadUrl) + res, err := ghttp.Get(downloadUrl) + if err != nil || res.StatusCode != 200 { + mlog.Fatalf( + "downloading failed for %s %s, may be network issue:\n%s", + runtime.GOOS, runtime.GOARCH, res.ReadAllString(), + ) + } + defer res.Close() + data := res.ReadAll() + mlog.Print("installing...") + var ( + binPath = gfile.SelfPath() + binDirPath = gfile.SelfDir() + renamePath = binPath + "~" + ) + if runtime.GOOS == "windows" { + // Rename myself for windows. + if err := gfile.Rename(binPath, renamePath); err != nil { + mlog.Fatal("rename binary file failed:", err.Error()) + } + defer gfile.Remove(renamePath) + } else { + // Remove the binary for other platforms. + if gfile.IsWritable(binDirPath) { + if err := gfile.Remove(binPath); err != nil { + mlog.Fatal("remove binary failed:", err.Error()) + } + } + } + if err := gfile.PutBytes(binPath, data); err != nil { + mlog.Fatal("install binary failed:", err.Error()) + } + if err := gfile.Chmod(binPath, 0777); err != nil { + mlog.Fatal("chmod binary failed:", err.Error()) + } + mlog.Print("gf binary is now updated to the latest version") + } else { + mlog.Print("it's the latest version, no need updates") + } +} diff --git a/tool/gf/config/url.toml b/tool/gf/config/url.toml new file mode 100644 index 000000000..6f965f328 --- /dev/null +++ b/tool/gf/config/url.toml @@ -0,0 +1,17 @@ +# gf pack config packed/config.go + + +# gf home site cdn. +[cdn] + url = "https://gfcdn.johng.cn" + +# home site url. +[home] + url = "https://goframe.org" + +# go proxy for gf get. +[proxy] + urls = [ + "https://goproxy.io/", + "https://goproxy.cn/" + ] \ No newline at end of file diff --git a/tool/gf/go.mod b/tool/gf/go.mod new file mode 100644 index 000000000..940df97cf --- /dev/null +++ b/tool/gf/go.mod @@ -0,0 +1,15 @@ +module github.com/gogf/gf/tool/gf + +go 1.11 + +require ( + github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e + github.com/gogf/gf v1.16.1 + github.com/gogf/swagger v1.0.4 + github.com/gorilla/websocket v1.4.2 // indirect + github.com/grokify/html-strip-tags-go v0.0.0-20200322061010-ea0c1cf2f119 // indirect + github.com/lib/pq v1.2.0 + github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/olekukonko/tablewriter v0.0.5 + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect +) diff --git a/tool/gf/go.sum b/tool/gf/go.sum new file mode 100644 index 000000000..4d7f691bd --- /dev/null +++ b/tool/gf/go.sum @@ -0,0 +1,93 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 h1:LdXxtjzvZYhhUaonAaAKArG3pyC67kGL3YY+6hGG8G4= +github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e h1:LzwWXEScfcTu7vUZNlDDWDARoSGEtvlDKK2BYHowNeE= +github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gogf/gf v1.13.8-0.20201010060010-09ce105eeeab/go.mod h1:nGAMjE4ohU2bwj4Gk3h25K6rEkPZMDdvsmyifpFcuMQ= +github.com/gogf/gf v1.16.1 h1:J2kcf8ufbuIIGrbeXfMH/CCkH+hUyC9lrQKrLXZrKVg= +github.com/gogf/gf v1.16.1/go.mod h1:5eEgE9fWeRQW8dJE3GLpCy0KkNitXh6POesdJiBE/lw= +github.com/gogf/swagger v1.0.4 h1:MILniFKPh52/26s+z8taSh8thn1tq2RaeWM7rYX1dRw= +github.com/gogf/swagger v1.0.4/go.mod h1:4rD12TCoDz60jmgtuFnx7ZBWUM92tXc/qtrIrkBIp5Q= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gqcn/structs v1.1.1/go.mod h1:/aBhTBSsKQ2Ec9pbnYdGphtdWXHFn4KrCL0fXM/Adok= +github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/grokify/html-strip-tags-go v0.0.0-20200322061010-ea0c1cf2f119 h1:h3iGUlU8HyW4baKd6D+h1mwOHnM2kwskSuG6Bv4tSbc= +github.com/grokify/html-strip-tags-go v0.0.0-20200322061010-ea0c1cf2f119/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng= +go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= +go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg= +go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= +go.opentelemetry.io/otel/oteltest v0.19.0 h1:YVfA0ByROYqTwOxqHVZYZExzEpfZor+MU1rU+ip2v9Q= +go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= +go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc= +go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tool/gf/library/allyes/allyes.go b/tool/gf/library/allyes/allyes.go new file mode 100644 index 000000000..e71c51f79 --- /dev/null +++ b/tool/gf/library/allyes/allyes.go @@ -0,0 +1,22 @@ +package allyes + +import ( + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/genv" +) + +const ( + EnvName = "GF_CLI_ALL_YES" +) + +// Init initializes the package manually. +func Init() { + if gcmd.ContainsOpt("y") { + genv.Set(EnvName, "1") + } +} + +// Check checks whether option allow all yes for command. +func Check() bool { + return genv.Get(EnvName) == "1" +} diff --git a/tool/gf/library/mlog/mlog.go b/tool/gf/library/mlog/mlog.go new file mode 100644 index 000000000..1f4c4fe7e --- /dev/null +++ b/tool/gf/library/mlog/mlog.go @@ -0,0 +1,63 @@ +package mlog + +import ( + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/genv" + "github.com/gogf/gf/os/glog" +) + +const ( + headerPrintEnvName = "GF_CLI_MLOG_HEADER" +) + +var ( + logger = glog.New() +) + +func init() { + logger.SetStack(false) + logger.SetDebug(false) + if genv.Get(headerPrintEnvName) == "1" { + logger.SetHeaderPrint(true) + } else { + logger.SetHeaderPrint(false) + } + if gcmd.ContainsOpt("debug") { + logger.SetDebug(true) + } +} + +// SetHeaderPrint enables/disables header printing to stdout. +func SetHeaderPrint(enabled bool) { + logger.SetHeaderPrint(enabled) + if enabled { + genv.Set(headerPrintEnvName, "1") + } else { + genv.Set(headerPrintEnvName, "0") + } +} + +func Print(v ...interface{}) { + logger.Print(v...) +} + +func Printf(format string, v ...interface{}) { + logger.Printf(format, v...) +} + +func Fatal(v ...interface{}) { + logger.Fatal(append(g.Slice{"Error:"}, v...)...) +} + +func Fatalf(format string, v ...interface{}) { + logger.Fatalf("Error: "+format, v...) +} + +func Debug(v ...interface{}) { + logger.Debug(append(g.Slice{"Debug:"}, v...)...) +} + +func Debugf(format string, v ...interface{}) { + logger.Debugf("Debug: "+format, v...) +} diff --git a/tool/gf/library/proxy/proxy.go b/tool/gf/library/proxy/proxy.go new file mode 100644 index 000000000..bcaba6ea3 --- /dev/null +++ b/tool/gf/library/proxy/proxy.go @@ -0,0 +1,33 @@ +package proxy + +import ( + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/os/genv" + "github.com/gogf/gf/tool/gf/library/mlog" + "time" +) + +var ( + httpClient = ghttp.NewClient() +) + +func init() { + httpClient.SetTimeout(time.Second) +} + +// AutoSet automatically checks and sets the golang proxy. +func AutoSet() { + SetGoModuleEnabled(true) + genv.Set("GOPROXY", "https://goproxy.cn") +} + +// SetGoModuleEnabled enables/disables the go module feature. +func SetGoModuleEnabled(enabled bool) { + if enabled { + mlog.Debug("set GO111MODULE=on") + genv.Set("GO111MODULE", "on") + } else { + mlog.Debug("set GO111MODULE=off") + genv.Set("GO111MODULE", "off") + } +} diff --git a/tool/gf/library/utils/utils.go b/tool/gf/library/utils/utils.go new file mode 100644 index 000000000..ce29dd738 --- /dev/null +++ b/tool/gf/library/utils/utils.go @@ -0,0 +1,18 @@ +package utils + +import ( + "fmt" + "github.com/gogf/gf/os/gproc" +) + +var ( + // gofmtPath is the binary path of command `gofmt`. + gofmtPath = gproc.SearchBinaryPath("gofmt") +) + +// GoFmt formats the source file using command `gofmt -w -s PATH`. +func GoFmt(path string) { + if gofmtPath != "" { + gproc.ShellExec(fmt.Sprintf(`%s -w -s %s`, gofmtPath, path)) + } +} diff --git a/tool/gf/main.go b/tool/gf/main.go new file mode 100644 index 000000000..e985fd2ee --- /dev/null +++ b/tool/gf/main.go @@ -0,0 +1,215 @@ +package main + +import ( + "fmt" + "github.com/gogf/gf" + "github.com/gogf/gf/errors/gerror" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/tool/gf/commands/env" + "github.com/gogf/gf/tool/gf/commands/mod" + "strings" + + "github.com/gogf/gf/os/gbuild" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/text/gstr" + _ "github.com/gogf/gf/tool/gf/boot" + "github.com/gogf/gf/tool/gf/commands/build" + "github.com/gogf/gf/tool/gf/commands/docker" + "github.com/gogf/gf/tool/gf/commands/fix" + "github.com/gogf/gf/tool/gf/commands/gen" + "github.com/gogf/gf/tool/gf/commands/get" + "github.com/gogf/gf/tool/gf/commands/initialize" + "github.com/gogf/gf/tool/gf/commands/install" + "github.com/gogf/gf/tool/gf/commands/pack" + "github.com/gogf/gf/tool/gf/commands/run" + "github.com/gogf/gf/tool/gf/commands/swagger" + "github.com/gogf/gf/tool/gf/commands/update" + "github.com/gogf/gf/tool/gf/library/allyes" + "github.com/gogf/gf/tool/gf/library/mlog" + "github.com/gogf/gf/tool/gf/library/proxy" +) + +func init() { + // Automatically sets the golang proxy for all commands. + proxy.AutoSet() +} + +var ( + helpContent = gstr.TrimLeft(` +USAGE + gf COMMAND [ARGUMENT] [OPTION] + +COMMAND + env show current Golang environment variables + get install or update GF to system in default... + gen automatically generate go files for ORM models... + mod extra features for go modules... + run running go codes with hot-compiled-like feature... + init create and initialize an empty GF project... + help show more information about a specified command + pack packing any file/directory to a resource file, or a go file... + build cross-building go project for lots of platforms... + docker create a docker image for current GF project... + swagger swagger feature for current project... + update update current gf binary to latest one (might need root/admin permission) + install install gf binary to system (might need root/admin permission) + version show current binary version info + +OPTION + -y all yes for all command without prompt ask + -?,-h show this help or detail for specified command + -v,-i show version information + +ADDITIONAL + Use 'gf help COMMAND' or 'gf COMMAND -h' for detail about a command, which has '...' + in the tail of their comments. +`) +) + +func main() { + defer func() { + if exception := recover(); exception != nil { + if err, ok := exception.(error); ok { + mlog.Print(gerror.Current(err).Error()) + } else { + panic(exception) + } + } + }() + + allyes.Init() + + command := gcmd.GetArg(1) + // Help information + if gcmd.ContainsOpt("h") && command != "" { + help(command) + return + } + switch command { + case "help": + help(gcmd.GetArg(2)) + case "version": + version() + case "env": + env.Run() + case "get": + get.Run() + case "gen": + gen.Run() + case "fix": + fix.Run() + case "mod": + mod.Run() + case "init": + initialize.Run() + case "pack": + pack.Run() + case "docker": + docker.Run() + case "swagger": + swagger.Run() + case "update": + update.Run() + case "install": + install.Run() + case "build": + build.Run() + case "run": + run.Run() + default: + for k := range gcmd.GetOptAll() { + switch k { + case "?", "h": + mlog.Print(helpContent) + return + case "i", "v": + version() + return + } + } + // No argument or option, do installation checks. + if !install.IsInstalled() { + mlog.Print("hi, it seams it's the first time you installing gf cli.") + s := gcmd.Scanf("do you want to install gf binary to your system? [y/n]: ") + if strings.EqualFold(s, "y") { + install.Run() + gcmd.Scan("press to exit...") + return + } + } + mlog.Print(helpContent) + } +} + +// help shows more information for specified command. +func help(command string) { + switch command { + case "get": + get.Help() + case "gen": + gen.Help() + case "init": + initialize.Help() + case "docker": + docker.Help() + case "swagger": + swagger.Help() + case "build": + build.Help() + case "pack": + pack.Help() + case "run": + run.Help() + case "mod": + mod.Help() + default: + mlog.Print(helpContent) + } +} + +// version prints the version information of the cli tool. +func version() { + info := gbuild.Info() + if info["git"] == "" { + info["git"] = "none" + } + mlog.Printf(`GoFrame CLI Tool %s, https://goframe.org`, gf.VERSION) + gfVersion, err := getGFVersionOfCurrentProject() + if err != nil { + gfVersion = err.Error() + } else { + gfVersion = gfVersion + " in current go.mod" + } + mlog.Printf(`GoFrame Version: %s`, gfVersion) + mlog.Printf(`CLI Installed At: %s`, gfile.SelfPath()) + if info["gf"] == "" { + mlog.Print(`Current is a custom installed version, no installation information.`) + return + } + + mlog.Print(gstr.Trim(fmt.Sprintf(` +CLI Built Detail: + Go Version: %s + GF Version: %s + Git Commit: %s + Build Time: %s +`, info["go"], info["gf"], info["git"], info["time"]))) +} + +// getGFVersionOfCurrentProject checks and returns the GoFrame version current project using. +func getGFVersionOfCurrentProject() (string, error) { + goModPath := gfile.Join(gfile.Pwd(), "go.mod") + if gfile.Exists(goModPath) { + match, err := gregex.MatchString(`github.com/gogf/gf\s+([\w\d\.]+)`, gfile.GetContents(goModPath)) + if err != nil { + return "", err + } + if len(match) > 1 { + return match[1], nil + } + return "", gerror.New("cannot find goframe requirement in go.mod") + } else { + return "", gerror.New("cannot find go.mod") + } +} diff --git a/tool/gf/packed/config.go b/tool/gf/packed/config.go new file mode 100644 index 000000000..f066cc66e --- /dev/null +++ b/tool/gf/packed/config.go @@ -0,0 +1,9 @@ +package packed + +import "github.com/gogf/gf/os/gres" + +func init() { + if err := gres.Add("H4sIAAAAAAAC/wrwZmYRYeBg4GBIi7kQwIAE+Bk4GZLz89Iy0/VLi3L0SvJzc0JDWBkYC9a9iMvpc+xrNuBxvf5DJH7m2xM3+494KolpqLRpHtFgWbm3dmpI1u6E5+//z3sq73HE7pf0nmU7PGYfTonvdO1cYbg5j/F4ENOfKTPmzL/kwFDwTNT2TMypuj+R2hu//bdST5zvs9X+1cxwtfDPW1cb9kcnnL3kEvgm8j2/y9mv7JytJ7devW1/4XDg9VrZTAb9rLPvQx9ONr3Ce56BgeH//wBvdo4D12YyTmNgYPjFwMAA8xsDw+EMwUBkv7HB/Qb20v4Gq3iQZmQlAd6MTCLMiKBBNhgUNDCwpBFE4goohCnYHQEBAgz/He/ATUFyEisbSJqJgYmhmYGBQZIRxAMEAAD//5uozoWyAQAA"); err != nil { + panic("add binary content to resource manager failed: " + err.Error()) + } +}