From 76b819ae3b47d86bd52418d7c647c344f4b72134 Mon Sep 17 00:00:00 2001 From: John Guo Date: Mon, 24 Jan 2022 14:41:23 +0800 Subject: [PATCH] move gf cli and gdb drivers into gf repo. --- .github/workflows/go.yml | 1 + .gitignore | 3 +- command/gf/LICENSE | 21 + command/gf/Makefile | 18 + command/gf/README.MD | 88 ++ command/gf/go.mod | 16 + command/gf/go.sum | 170 ++++ command/gf/internal/cmd/cmd.go | 64 ++ command/gf/internal/cmd/cmd_build.go | 304 +++++++ command/gf/internal/cmd/cmd_docker.go | 109 +++ command/gf/internal/cmd/cmd_env.go | 61 ++ command/gf/internal/cmd/cmd_gen.go | 30 + command/gf/internal/cmd/cmd_gen_dao.go | 788 ++++++++++++++++++ command/gf/internal/cmd/cmd_gen_pb.go | 78 ++ command/gf/internal/cmd/cmd_gen_pbentity.go | 409 +++++++++ command/gf/internal/cmd/cmd_init.go | 101 +++ command/gf/internal/cmd/cmd_install.go | 26 + command/gf/internal/cmd/cmd_pack.go | 89 ++ command/gf/internal/cmd/cmd_run.go | 161 ++++ command/gf/internal/cmd/cmd_tpl.go | 167 ++++ command/gf/internal/cmd/cmd_version.go | 87 ++ command/gf/internal/consts/consts.go | 5 + .../consts/consts_gen_dao_template_dao.go | 104 +++ .../consts/consts_gen_dao_template_do.go | 14 + .../consts/consts_gen_dao_template_entity.go | 14 + .../consts/consts_gen_pbentity_template.go | 17 + command/gf/internal/packed/packed.go | 1 + command/gf/internal/packed/template-mono.go | 9 + command/gf/internal/packed/template-single.go | 9 + command/gf/internal/service/install.go | 211 +++++ command/gf/internal/utility/allyes/allyes.go | 22 + command/gf/internal/utility/mlog/mlog.go | 67 ++ command/gf/internal/utility/utils/utils.go | 18 + command/gf/main.go | 71 ++ command/gf/test/testdata/tpls/tpl1.yaml | 2 + command/gf/test/testdata/tpls/tpl2.sql | 1 + command/gf/test/testdata/values.json | 8 + contrib/drivers/.gitattributes | 3 + contrib/drivers/.gitignore | 19 + contrib/drivers/LICENSE | 21 + contrib/drivers/README.md | 56 ++ contrib/drivers/clickhouse/clickhouse.go | 12 + contrib/drivers/go.mod | 14 + contrib/drivers/mssql/mssql.go | 327 ++++++++ contrib/drivers/mysql/mysql.go | 23 + contrib/drivers/oracle/oracle.go | 339 ++++++++ contrib/drivers/pgsql/pgsql.go | 245 ++++++ contrib/drivers/sqlite/sqlite.go | 177 ++++ 48 files changed, 4598 insertions(+), 2 deletions(-) create mode 100644 command/gf/LICENSE create mode 100644 command/gf/Makefile create mode 100644 command/gf/README.MD create mode 100644 command/gf/go.mod create mode 100644 command/gf/go.sum create mode 100644 command/gf/internal/cmd/cmd.go create mode 100644 command/gf/internal/cmd/cmd_build.go create mode 100644 command/gf/internal/cmd/cmd_docker.go create mode 100644 command/gf/internal/cmd/cmd_env.go create mode 100644 command/gf/internal/cmd/cmd_gen.go create mode 100644 command/gf/internal/cmd/cmd_gen_dao.go create mode 100644 command/gf/internal/cmd/cmd_gen_pb.go create mode 100644 command/gf/internal/cmd/cmd_gen_pbentity.go create mode 100644 command/gf/internal/cmd/cmd_init.go create mode 100644 command/gf/internal/cmd/cmd_install.go create mode 100644 command/gf/internal/cmd/cmd_pack.go create mode 100644 command/gf/internal/cmd/cmd_run.go create mode 100644 command/gf/internal/cmd/cmd_tpl.go create mode 100644 command/gf/internal/cmd/cmd_version.go create mode 100644 command/gf/internal/consts/consts.go create mode 100644 command/gf/internal/consts/consts_gen_dao_template_dao.go create mode 100644 command/gf/internal/consts/consts_gen_dao_template_do.go create mode 100644 command/gf/internal/consts/consts_gen_dao_template_entity.go create mode 100644 command/gf/internal/consts/consts_gen_pbentity_template.go create mode 100644 command/gf/internal/packed/packed.go create mode 100644 command/gf/internal/packed/template-mono.go create mode 100644 command/gf/internal/packed/template-single.go create mode 100644 command/gf/internal/service/install.go create mode 100644 command/gf/internal/utility/allyes/allyes.go create mode 100644 command/gf/internal/utility/mlog/mlog.go create mode 100644 command/gf/internal/utility/utils/utils.go create mode 100644 command/gf/main.go create mode 100644 command/gf/test/testdata/tpls/tpl1.yaml create mode 100644 command/gf/test/testdata/tpls/tpl2.sql create mode 100644 command/gf/test/testdata/values.json create mode 100644 contrib/drivers/.gitattributes create mode 100644 contrib/drivers/.gitignore create mode 100644 contrib/drivers/LICENSE create mode 100644 contrib/drivers/README.md create mode 100644 contrib/drivers/clickhouse/clickhouse.go create mode 100644 contrib/drivers/go.mod create mode 100644 contrib/drivers/mssql/mssql.go create mode 100644 contrib/drivers/mysql/mysql.go create mode 100644 contrib/drivers/oracle/oracle.go create mode 100644 contrib/drivers/pgsql/pgsql.go create mode 100644 contrib/drivers/sqlite/sqlite.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e406041ad..6159612dc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -7,6 +7,7 @@ on: - develop pull_request: branches: [master, develop] + env: GF_DEBUG: 0 diff --git a/.gitignore b/.gitignore index 5af57e523..f07ed0208 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,4 @@ cbuild **/.DS_Store .vscode/ .test/ -main -gf \ No newline at end of file +main \ No newline at end of file diff --git a/command/gf/LICENSE b/command/gf/LICENSE new file mode 100644 index 000000000..d410ffc3c --- /dev/null +++ b/command/gf/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 john@goframe.org https://goframe.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/command/gf/Makefile b/command/gf/Makefile new file mode 100644 index 000000000..a28ccbda9 --- /dev/null +++ b/command/gf/Makefile @@ -0,0 +1,18 @@ + +pack: pack.template-single pack.template-mono + +pack.template-single: + @rm -fr temp + @mkdir temp || exit 0 + @cd temp && git clone https://github.com/gogf/template-single + @rm -fr temp/template-single/.git + @cd temp && gf pack template-single ../internal/packed/template-single.go -n=packed -y + @rm -fr temp + +pack.template-mono: + @rm -fr temp + @mkdir temp || exit 0 + @cd temp && git clone https://github.com/gogf/template-mono + @rm -fr temp/template-mono/.git + @cd temp && gf pack template-mono ../internal/packed/template-mono.go -n=packed -y + @rm -fr temp \ No newline at end of file diff --git a/command/gf/README.MD b/command/gf/README.MD new file mode 100644 index 000000000..d498c344a --- /dev/null +++ b/command/gf/README.MD @@ -0,0 +1,88 @@ +# GoFrame CLI TOOL + +`gf` is a powerful CLI tool for building [GoFrame](https://goframe.org) application with convenience. + +## 1. Install + +### 1). Manually Install + +> You might need setting the goproxy to make through building. +> Please make sure your Golang version > v1.15. + +1. Latest version + ``` + go install github.com/gogf/gf-cli/v2/gf@master + ``` + +2. Specified version + ``` + go install github.com/gogf/gf-cli/v2/gf@v2.0.0-beta + ``` + +3. Check installation + ``` + gf -v + ``` +### 2). PreBuilt Binary + +You can also install `gf` tool using pre-built binaries: https://github.com/gogf/gf-cli/releases + +After downloads, please use `gf_xxx_xxx install` command to install gf binary to system binary path. + +1. `Mac` + ```shell + wget -O gf https://github.com/gogf/gf-cli/releases/download/v2.0.0-rc/gf_darwin_amd64 && chmod +x gf && ./gf install + ``` + > If you're using `zsh`, you might need rename your alias by command `alias gf=gf` to resolve the conflicts between `gf` and `git fetch`. + +2. `Linux` + ```shell + wget -O gf https://github.com/gogf/gf-cli/releases/download/v2.0.0-rc/gf_linux_amd64 && chmod +x gf && ./gf install + ``` + +3. `Windows` + + Manually download, execute it and then follow the instruction. + +4. 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 [OPTION] + +COMMAND + env show current Golang environment variables + run running go codes with hot-compiled-like feature + gen automatically generate go files for dao/do/entity/pb/pbentity + tpl template parsing and building commands + init create and initialize an empty GoFrame project + pack packing any file/directory to a resource file, or a go file + build cross-building go project for lots of platforms + docker build docker image for current GoFrame project + install install gf binary to system (might need root/admin permission) + version show version information of current binary + +OPTION + -y, --yes all yes for all command without prompt ask + -v, --version show version information of current binary + -d, --debug show internal detailed debugging information + -h, --help more information about this command + +ADDITIONAL + Use "gf COMMAND -h" for details about a command. +``` + +## 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/command/gf/go.mod b/command/gf/go.mod new file mode 100644 index 000000000..82c2d77d8 --- /dev/null +++ b/command/gf/go.mod @@ -0,0 +1,16 @@ +module github.com/gogf/gf/command/gf/v2 + +go 1.14 + +require ( + github.com/denisenkom/go-mssqldb v0.11.0 + github.com/gogf/gf/v2 v2.0.0-rc + github.com/lib/pq v1.10.4 + 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.2.0 // indirect + gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect +) + +replace github.com/gogf/gf/v2 => ../../ \ No newline at end of file diff --git a/command/gf/go.sum b/command/gf/go.sum new file mode 100644 index 000000000..7dfa4dba8 --- /dev/null +++ b/command/gf/go.sum @@ -0,0 +1,170 @@ +github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E= +github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +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.11.0 h1:9rHa233rhdOyrz2GcP9NM+gi2psgJZ4GWDpL/7ND8HI= +github.com/denisenkom/go-mssqldb v0.11.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= +github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogf/gf/v2 v2.0.0-rc.0.20220124023219-43f1354e7983 h1:INOUPZaXysuSYTEwhPgNDZfdmEPqIpAXwcgXlVEAzeQ= +github.com/gogf/gf/v2 v2.0.0-rc.0.20220124023219-43f1354e7983/go.mod h1:apktt6TleWtCIwpz63vBqUnw8MX8gWKoZyxgDpXFtgM= +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/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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.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.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= +github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +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.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= +github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opentelemetry.io/otel v1.0.0/go.mod h1:AjRVh9A5/5DE7S+mZtTR6t8vpKKryam+0lREnfmS4cg= +go.opentelemetry.io/otel v1.2.0 h1:YOQDvxO1FayUcT9MIhJhgMyNO1WqoduiyvQHzGN0kUQ= +go.opentelemetry.io/otel v1.2.0/go.mod h1:aT17Fk0Z1Nor9e0uisf98LrntPGMnk4frBO9+dkf69I= +go.opentelemetry.io/otel/sdk v1.0.0 h1:BNPMYUONPNbLneMttKSjQhOTlFLOD9U22HNG1KrIN2Y= +go.opentelemetry.io/otel/sdk v1.0.0/go.mod h1:PCrDHlSy5x1kjezSdL37PhbFUMjrsLRshJ2zCzeXwbM= +go.opentelemetry.io/otel/trace v1.0.0/go.mod h1:PXTWqayeFUlJV1YDNhsJYB184+IvAH814St6o6ajzIs= +go.opentelemetry.io/otel/trace v1.2.0 h1:Ys3iqbqZhcf28hHzrm5WAquMkDHNZTUkw7KHbuNjej0= +go.opentelemetry.io/otel/trace v1.2.0/go.mod h1:N5FLswTubnxKxOJHM7XZC074qpeEdLy3CgAVsdMucK0= +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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 h1:GLw7MR8AfAG2GmGcmVgObFOHXYypgGjnGno25RDwn3Y= +golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/command/gf/internal/cmd/cmd.go b/command/gf/internal/cmd/cmd.go new file mode 100644 index 000000000..1ad823507 --- /dev/null +++ b/command/gf/internal/cmd/cmd.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "context" + "strings" + + "github.com/gogf/gf/command/gf/v2/internal/service" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/util/gtag" +) + +var ( + GF = cGF{} +) + +type cGF struct { + g.Meta `name:"gf" ad:"{cGFAd}"` +} + +const ( + cGFAd = ` +ADDITIONAL + Use "gf COMMAND -h" for details about a command. +` +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cGFAd`: cGFAd, + }) +} + +type cGFInput struct { + g.Meta `name:"gf"` + Yes bool `short:"y" name:"yes" brief:"all yes for all command without prompt ask" orphan:"true"` + Version bool `short:"v" name:"version" brief:"show version information of current binary" orphan:"true"` + Debug bool `short:"d" name:"debug" brief:"show internal detailed debugging information" orphan:"true"` +} +type cGFOutput struct{} + +func (c cGF) Index(ctx context.Context, in cGFInput) (out *cGFOutput, err error) { + // Version. + if in.Version { + _, err = Version.Index(ctx, cVersionInput{}) + return + } + // No argument or option, do installation checks. + if !service.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") { + if err = service.Install.Run(ctx); err != nil { + return + } + gcmd.Scan("press `Enter` to exit...") + return + } + } + // Print help content. + gcmd.CommandFromCtx(ctx).Print() + return +} diff --git a/command/gf/internal/cmd/cmd_build.go b/command/gf/internal/cmd/cmd_build.go new file mode 100644 index 000000000..b0b9031c8 --- /dev/null +++ b/command/gf/internal/cmd/cmd_build.go @@ -0,0 +1,304 @@ +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + "runtime" + "strings" + + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/encoding/gbase64" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/genv" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gproc" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gtag" +) + +var ( + Build = cBuild{ + nodeNameInConfigFile: "gfcli.build", + packedGoFileName: "build_pack_data.go", + } +) + +type cBuild struct { + g.Meta `name:"build" brief:"{cBuildBrief}" dc:"{cBuildDc}" eg:"{cBuildEg}" ad:"{cBuildAd}"` + nodeNameInConfigFile string // nodeNameInConfigFile is the node name for compiler configurations in configuration file. + packedGoFileName string // packedGoFileName specifies the file name for packing common folders into one single go file. +} + +const ( + cBuildBrief = `cross-building go project for lots of platforms` + cBuildEg = ` +gf build main.go +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 +` + cBuildDc = ` +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. +` + cBuildAd = ` +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 +` + // https://golang.google.cn/doc/install/source + cBuildPlatforms = ` +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 +` +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cBuildBrief`: cBuildBrief, + `cBuildDc`: cBuildDc, + `cBuildEg`: cBuildEg, + `cBuildAd`: cBuildAd, + }) +} + +type cBuildInput struct { + g.Meta `name:"build" config:"gfcli.build"` + File string `name:"FILE" arg:"true" brief:"building file path"` + Name string `short:"n" name:"name" brief:"output binary name"` + Version string `short:"v" name:"version" brief:"output binary version"` + Arch string `short:"a" name:"arch" brief:"output binary architecture, multiple arch separated with ','"` + System string `short:"s" name:"system" brief:"output binary system, multiple os separated with ','"` + Output string `short:"o" name:"output" brief:"output binary path, used when building single binary file"` + Path string `short:"p" name:"path" brief:"output binary directory path, default is './bin'" d:"./bin"` + Extra string `short:"e" name:"extra" brief:"extra custom \"go build\" options"` + Mod string `short:"m" name:"mod" brief:"like \"-mod\" option of \"go build\", use \"-m none\" to disable go module"` + Cgo bool `short:"c" name:"cgo" brief:"enable or disable cgo feature, it's disabled in default" orphan:"true"` + VarMap g.Map `short:"r" name:"varMap" brief:"custom built embedded variable into binary"` + Pack string `name:"pack" brief:"pack specified folder into temporary go file before building and removes it after built"` +} +type cBuildOutput struct{} + +func (c cBuild) Index(ctx context.Context, in cBuildInput) (out *cBuildOutput, err error) { + mlog.SetHeaderPrint(true) + + mlog.Debugf(`build input: %+v`, in) + // Necessary check. + if gproc.SearchBinary("go") == "" { + mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`) + } + + var ( + parser = gcmd.ParserFromCtx(ctx) + file = parser.GetArg(2).String() + ) + 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") + } + } + if in.Name == "" { + in.Name = gfile.Name(file) + } + if len(in.Name) < 1 || in.Name == "*" { + mlog.Fatal("name cannot be empty") + } + if in.Mod != "" && in.Mod != "none" { + mlog.Debugf(`mod is %s`, in.Mod) + if in.Extra == "" { + in.Extra = fmt.Sprintf(`-mod=%s`, in.Mod) + } else { + in.Extra = fmt.Sprintf(`-mod=%s %s`, in.Mod, in.Extra) + } + } + if in.Extra != "" { + in.Extra += " " + } + var ( + customSystems = gstr.SplitAndTrim(in.System, ",") + customArches = gstr.SplitAndTrim(in.Arch, ",") + ) + if len(in.Version) > 0 { + in.Path += "/" + in.Version + } + // System and arch checks. + var ( + spaceRegex = regexp.MustCompile(`\s+`) + platformMap = make(map[string]map[string]bool) + ) + for _, line := range strings.Split(strings.TrimSpace(cBuildPlatforms), "\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 packing. + if len(in.Pack) > 0 { + dataFilePath := fmt.Sprintf(`packed/%s`, c.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`, in.Pack, dataFilePath) + mlog.Print(packCmd) + gproc.MustShellRun(packCmd) + } + + // Injected information by building flags. + ldFlags := fmt.Sprintf(`-X 'github.com/gogf/gf/v2/os/gbuild.builtInVarStr=%v'`, c.getBuildInVarStr(in)) + + // start building + mlog.Print("start building...") + if in.Cgo { + genv.MustSet("CGO_ENABLED", "1") + } else { + genv.MustSet("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(in.Output) > 0 { + output = "-o " + in.Output + ext + } else { + output = "-o " + in.Name + ext + } + cmd = fmt.Sprintf(`go build %s -ldflags "%s" %s %s`, output, ldFlags, in.Extra, file) + } else { + // Cross-building, output the compiled binary to specified path. + if system == "windows" { + ext = ".exe" + } + genv.MustSet("GOOS", system) + genv.MustSet("GOARCH", arch) + cmd = fmt.Sprintf( + `go build -o %s/%s/%s%s -ldflags "%s" %s%s`, + in.Path, system+"_"+arch, in.Name, ext, ldFlags, in.Extra, file, + ) + } + mlog.Debug(cmd) + // It's not necessary printing the complete command string. + cmdShow, _ := gregex.ReplaceString(`\s+(-ldflags ".+?")\s+`, " ", cmd) + mlog.Print(cmdShow) + if result, err := gproc.ShellExec(cmd); err != nil { + mlog.Printf("failed to build, os:%s, arch:%s, error:\n%s\n", system, arch, gstr.Trim(result)) + } else { + mlog.Debug(gstr.Trim(result)) + } + // single binary building. + if len(customSystems) == 0 && len(customArches) == 0 { + goto buildDone + } + } + } +buildDone: + mlog.Print("done!") + return +} + +// getBuildInVarMapJson retrieves and returns the custom build-in variables in configuration +// file as json. +func (c cBuild) getBuildInVarStr(in cBuildInput) string { + buildInVarMap := in.VarMap + if buildInVarMap == nil { + buildInVarMap = make(g.Map) + } + buildInVarMap["builtGit"] = c.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 (c cBuild) getGitCommit() string { + if gproc.SearchBinary("git") == "" { + return "" + } + var ( + cmd = `git log -1 --format="%cd %H" --date=format:"%Y-%m-%d %H:%M:%S"` + s, _ = gproc.ShellExec(cmd) + ) + mlog.Debug(cmd) + if s != "" { + if !gstr.Contains(s, "fatal") { + return gstr.Trim(s) + } + } + return "" +} diff --git a/command/gf/internal/cmd/cmd_docker.go b/command/gf/internal/cmd/cmd_docker.go new file mode 100644 index 000000000..4229d1831 --- /dev/null +++ b/command/gf/internal/cmd/cmd_docker.go @@ -0,0 +1,109 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gproc" + "github.com/gogf/gf/v2/util/gtag" +) + +var ( + Docker = cDocker{} +) + +type cDocker struct { + g.Meta `name:"docker" usage:"{cDockerUsage}" brief:"{cDockerBrief}" eg:"{cDockerEg}" dc:"{cDockerDc}"` +} + +const ( + cDockerUsage = `gf docker [MAIN] [OPTION]` + cDockerBrief = `build docker image for current GoFrame project` + cDockerEg = ` +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 +` + cDockerDc = ` +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. +` + cDockerMainBrief = `main golang file path for "gf build", it's "main.go" in default` + cDockerFileBrief = `file path of the Dockerfile. it's "manifest/docker/Dockerfile" in default` + cDockerShellBrief = `path of the shell file which is executed before docker build` + cDockerPushBrief = `auto push the docker image to docker registry if "-t" option passed` + cDockerTagBrief = `tag name for this docker, which is usually used for docker push` + cDockerExtraBrief = `extra build options passed to "docker image"` +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cDockerUsage`: cDockerUsage, + `cDockerBrief`: cDockerBrief, + `cDockerEg`: cDockerEg, + `cDockerDc`: cDockerDc, + `cDockerMainBrief`: cDockerMainBrief, + `cDockerFileBrief`: cDockerFileBrief, + `cDockerShellBrief`: cDockerShellBrief, + `cDockerPushBrief`: cDockerPushBrief, + `cDockerTagBrief`: cDockerTagBrief, + `cDockerExtraBrief`: cDockerExtraBrief, + }) +} + +type cDockerInput struct { + g.Meta `name:"docker" config:"gfcli.docker"` + Main string `name:"MAIN" arg:"true" brief:"{cDockerMainBrief}" d:"main.go"` + File string `name:"file" short:"f" brief:"{cDockerFileBrief}" d:"manifest/docker/Dockerfile"` + Shell string `name:"shell" short:"s" brief:"{cDockerShellBrief}" d:"manifest/docker/docker.sh"` + Tag string `name:"tag" short:"t" brief:"{cDockerTagBrief}"` + Push bool `name:"push" short:"p" brief:"{cDockerPushBrief}" orphan:"true"` + Extra string `name:"extra" short:"e" brief:"{cDockerExtraBrief}"` +} +type cDockerOutput struct{} + +func (c cDocker) Index(ctx context.Context, in cDockerInput) (out *cDockerOutput, err error) { + // Necessary check. + if gproc.SearchBinary("docker") == "" { + mlog.Fatalf(`command "docker" not found in your environment, please install docker first to proceed this command`) + } + + // Binary build. + if err = gproc.ShellRun(fmt.Sprintf(`gf build %s -a amd64 -s linux`, in.Main)); err != nil { + return + } + // Shell executing. + if gfile.Exists(in.Shell) { + if err = gproc.ShellRun(gfile.GetContents(in.Shell)); err != nil { + return + } + } + // Docker build. + dockerBuildOptions := "" + if in.Tag != "" { + dockerBuildOptions = fmt.Sprintf(`-t %s`, in.Tag) + } + if in.Extra != "" { + dockerBuildOptions = fmt.Sprintf(`%s %s`, dockerBuildOptions, in.Extra) + } + if err = gproc.ShellRun(fmt.Sprintf(`docker build -f %s . %s`, in.File, dockerBuildOptions)); err != nil { + return + } + // Docker push. + if in.Tag == "" || !in.Push { + return + } + if err = gproc.ShellRun(fmt.Sprintf(`docker push %s`, in.Tag)); err != nil { + return + } + return +} diff --git a/command/gf/internal/cmd/cmd_env.go b/command/gf/internal/cmd/cmd_env.go new file mode 100644 index 000000000..21663e34d --- /dev/null +++ b/command/gf/internal/cmd/cmd_env.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "bytes" + "context" + + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gproc" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/olekukonko/tablewriter" +) + +var ( + Env = cEnv{} +) + +type cEnv struct { + g.Meta `name:"env" brief:"show current Golang environment variables"` +} + +type cEnvInput struct { + g.Meta `name:"env"` +} +type cEnvOutput struct{} + +func (c cEnv) Index(ctx context.Context, in cEnvInput) (out *cEnvOutput, err error) { + 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()) + return +} diff --git a/command/gf/internal/cmd/cmd_gen.go b/command/gf/internal/cmd/cmd_gen.go new file mode 100644 index 000000000..e87145074 --- /dev/null +++ b/command/gf/internal/cmd/cmd_gen.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/util/gtag" +) + +var ( + Gen = cGen{} +) + +type cGen struct { + g.Meta `name:"gen" brief:"{cGenBrief}" dc:"{cGenDc}"` +} + +const ( + cGenBrief = `automatically generate go files for dao/do/entity/pb/pbentity` + cGenDc = ` +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" for specified type help. +` +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cGenBrief`: cGenBrief, + `cGenDc`: cGenDc, + }) +} diff --git a/command/gf/internal/cmd/cmd_gen_dao.go b/command/gf/internal/cmd/cmd_gen_dao.go new file mode 100644 index 000000000..098f174de --- /dev/null +++ b/command/gf/internal/cmd/cmd_gen_dao.go @@ -0,0 +1,788 @@ +package cmd + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/gogf/gf/command/gf/v2/internal/consts" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/command/gf/v2/internal/utility/utils" + "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gtag" + "github.com/olekukonko/tablewriter" + + _ "github.com/denisenkom/go-mssqldb" + _ "github.com/lib/pq" + //_ "github.com/mattn/go-oci8" + //_ "github.com/mattn/go-sqlite3" +) + +const ( + defaultDaoPath = `service/internal/dao` + defaultDoPath = `service/internal/do` + defaultEntityPath = `model/entity` + cGenDaoConfig = `gfcli.gen.dao` + cGenDaoUsage = `gf gen dao [OPTION]` + cGenDaoBrief = `automatically generate go files for dao/do/entity` + cGenDaoEg = ` +gf gen dao +gf gen dao -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" +gf gen dao -p ./model -c config.yaml -g user-center -t user,user_detail,user_login +gf gen dao -r user_ +` + + cGenDaoAd = ` +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(config.yaml): + gfcli: + gen: + dao: + - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + tables: "order,products" + jsonCase: "CamelLower" + + - link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary" + path: "./my-app" + prefix: "primary_" + tables: "user, userDetail" +` + cGenDaoBriefPath = `directory path for generated files` + cGenDaoBriefLink = `database configuration, the same as the ORM configuration of GoFrame` + cGenDaoBriefTables = `generate models only for given tables, multiple table names separated with ','` + cGenDaoBriefTablesEx = `generate models excluding given tables, multiple table names separated with ','` + cGenDaoBriefPrefix = `add prefix for all table of specified link/database tables` + cGenDaoBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','` + cGenDaoBriefStdTime = `use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables` + cGenDaoBriefGJsonSupport = `use gJsonSupport to use *gjson.Json instead of string for generated json fields of tables` + cGenDaoBriefImportPrefix = `custom import prefix for generated go files` + cGenDaoBriefOverwriteDao = `overwrite all dao files both inside/outside internal folder` + cGenDaoBriefModelFile = `custom file name for storing generated model content` + cGenDaoBriefModelFileForDao = `custom file name generating model for DAO operations like Where/Data. It's empty in default` + cGenDaoBriefDescriptionTag = `add comment to description tag for each field` + cGenDaoBriefNoJsonTag = `no json tag will be added for each field` + cGenDaoBriefNoModelComment = `no model comment will be added for each field` + cGenDaoBriefGroup = ` +specifying the configuration group name of database for generated ORM instance, +it's not necessary and the default value is "default" +` + cGenDaoBriefJsonCase = ` +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 | +` + + tplVarTableName = `{TplTableName}` + tplVarTableNameCamelCase = `{TplTableNameCamelCase}` + tplVarTableNameCamelLowerCase = `{TplTableNameCamelLowerCase}` + tplVarPackageImports = `{TplPackageImports}` + tplVarImportPrefix = `{TplImportPrefix}` + tplVarStructDefine = `{TplStructDefine}` + tplVarColumnDefine = `{TplColumnDefine}` + tplVarColumnNames = `{TplColumnNames}` + tplVarGroupName = `{TplGroupName}` + tplVarDatetime = `{TplDatetime}` +) + +var ( + createdAt *gtime.Time +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cGenDaoConfig`: cGenDaoConfig, + `cGenDaoUsage`: cGenDaoUsage, + `cGenDaoBrief`: cGenDaoBrief, + `cGenDaoEg`: cGenDaoEg, + `cGenDaoAd`: cGenDaoAd, + `cGenDaoBriefPath`: cGenDaoBriefPath, + `cGenDaoBriefLink`: cGenDaoBriefLink, + `cGenDaoBriefTables`: cGenDaoBriefTables, + `cGenDaoBriefTablesEx`: cGenDaoBriefTablesEx, + `cGenDaoBriefPrefix`: cGenDaoBriefPrefix, + `cGenDaoBriefRemovePrefix`: cGenDaoBriefRemovePrefix, + `cGenDaoBriefStdTime`: cGenDaoBriefStdTime, + `cGenDaoBriefGJsonSupport`: cGenDaoBriefGJsonSupport, + `cGenDaoBriefImportPrefix`: cGenDaoBriefImportPrefix, + `cGenDaoBriefOverwriteDao`: cGenDaoBriefOverwriteDao, + `cGenDaoBriefModelFile`: cGenDaoBriefModelFile, + `cGenDaoBriefModelFileForDao`: cGenDaoBriefModelFileForDao, + `cGenDaoBriefDescriptionTag`: cGenDaoBriefDescriptionTag, + `cGenDaoBriefNoJsonTag`: cGenDaoBriefNoJsonTag, + `cGenDaoBriefNoModelComment`: cGenDaoBriefNoModelComment, + `cGenDaoBriefGroup`: cGenDaoBriefGroup, + `cGenDaoBriefJsonCase`: cGenDaoBriefJsonCase, + }) + + createdAt = gtime.Now() +} + +type ( + cGenDaoInput struct { + g.Meta `name:"dao" config:"{cGenDaoConfig}" usage:"{cGenDaoUsage}" brief:"{cGenDaoBrief}" eg:"{cGenDaoEg}" ad:"{cGenDaoAd}"` + Path string `name:"path" short:"p" brief:"{cGenDaoBriefPath}" d:"internal"` + Link string `name:"link" short:"l" brief:"{cGenDaoBriefLink}"` + Tables string `name:"tables" short:"t" brief:"{cGenDaoBriefTables}"` + TablesEx string `name:"tablesEx" short:"e" brief:"{cGenDaoBriefTablesEx}"` + Group string `name:"group" short:"g" brief:"{cGenDaoBriefGroup}" d:"default"` + Prefix string `name:"prefix" short:"f" brief:"{cGenDaoBriefPrefix}"` + RemovePrefix string `name:"removePrefix" short:"r" brief:"{cGenDaoBriefRemovePrefix}"` + JsonCase string `name:"jsonCase" short:"j" brief:"{cGenDaoBriefJsonCase}" d:"CamelLower"` + ImportPrefix string `name:"importPrefix" short:"i" brief:"{cGenDaoBriefImportPrefix}"` + StdTime bool `name:"stdTime" short:"s" brief:"{cGenDaoBriefStdTime}" orphan:"true"` + GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{cGenDaoBriefGJsonSupport}" orphan:"true"` + OverwriteDao bool `name:"overwriteDao" short:"o" brief:"{cGenDaoBriefOverwriteDao}" orphan:"true"` + DescriptionTag bool `name:"descriptionTag" short:"d" brief:"{cGenDaoBriefDescriptionTag}" orphan:"true"` + NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{cGenDaoBriefNoJsonTag" orphan:"true"` + NoModelComment bool `name:"noModelComment" short:"m" brief:"{cGenDaoBriefNoModelComment}" orphan:"true"` + } + cGenDaoOutput struct{} + + cGenDaoInternalInput struct { + cGenDaoInput + TableName string // TableName specifies the table name of the table. + NewTableName string // NewTableName specifies the prefix-stripped name of the table. + ModName string // ModName specifies the module name of current golang project, which is used for import purpose. + } +) + +func (c cGen) Dao(ctx context.Context, in cGenDaoInput) (out *cGenDaoOutput, err error) { + if g.Cfg().Available(ctx) { + v := g.Cfg().MustGet(ctx, cGenDaoConfig) + if v.IsSlice() { + for i := 0; i < len(v.Interfaces()); i++ { + doGenDaoForArray(ctx, i, in) + } + } else { + doGenDaoForArray(ctx, -1, in) + } + } else { + doGenDaoForArray(ctx, -1, in) + } + mlog.Print("done!") + return +} + +// doGenDaoForArray implements the "gen dao" command for configuration array. +func doGenDaoForArray(ctx context.Context, index int, in cGenDaoInput) { + var ( + err error + db gdb.DB + modName string // Go module name, eg: github.com/gogf/gf. + ) + if index >= 0 { + err = g.Cfg().MustGet( + ctx, + fmt.Sprintf(`%s.%d`, cGenDaoConfig, index), + ).Scan(&in) + if err != nil { + mlog.Fatalf(`invalid configuration of "%s": %+v`, cGenDaoConfig, err) + } + } + if dirRealPath := gfile.RealPath(in.Path); dirRealPath == "" { + mlog.Fatalf(`path "%s" does not exist`, in.Path) + } + removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",") + if in.ImportPrefix == "" { + 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 uses user passed database configuration. + if in.Link != "" { + tempGroup := gtime.TimestampNanoStr() + match, _ := gregex.MatchString(`([a-z]+):(.+)`, in.Link) + if len(match) == 3 { + gdb.AddConfigNode(tempGroup, gdb.ConfigNode{ + Type: gstr.Trim(match[1]), + Link: gstr.Trim(match[2]), + }) + db, _ = gdb.Instance(tempGroup) + } + } else { + db = g.DB(in.Group) + } + if db == nil { + mlog.Fatal("database initialization failed") + } + + var tableNames []string + if in.Tables != "" { + tableNames = gstr.SplitAndTrim(in.Tables, ",") + } else { + tableNames, err = db.Tables(context.TODO()) + if err != nil { + mlog.Fatalf("fetching tables failed: \n %v", err) + } + } + // Table excluding. + if in.TablesEx != "" { + array := garray.NewStrArrayFrom(tableNames) + for _, v := range gstr.SplitAndTrim(in.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) + } + newTableName = in.Prefix + newTableName + newTableNames[i] = newTableName + // Dao. + generateDao(ctx, db, cGenDaoInternalInput{ + cGenDaoInput: in, + TableName: tableName, + NewTableName: newTableName, + ModName: modName, + }) + } + // Do. + generateDo(ctx, db, tableNames, newTableNames, cGenDaoInternalInput{ + cGenDaoInput: in, + ModName: modName, + }) + // Entity. + generateEntity(ctx, db, tableNames, newTableNames, cGenDaoInternalInput{ + cGenDaoInput: in, + ModName: modName, + }) +} + +// generateDaoContentFile generates the dao and model content of given table. +func generateDao(ctx context.Context, db gdb.DB, in cGenDaoInternalInput) { + // Generating table data preparing. + fieldMap, err := db.TableFields(ctx, in.TableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) + } + var ( + dirRealPath = gfile.RealPath(in.Path) + dirPathDao = gfile.Join(in.Path, defaultDaoPath) + tableNameCamelCase = gstr.CaseCamel(in.NewTableName) + tableNameCamelLowerCase = gstr.CaseCamelLower(in.NewTableName) + tableNameSnakeCase = gstr.CaseSnake(in.NewTableName) + importPrefix = in.ImportPrefix + ) + if importPrefix == "" { + if dirRealPath == "" { + dirRealPath = in.Path + importPrefix = dirRealPath + importPrefix = gstr.Trim(dirRealPath, "./") + } else { + importPrefix = gstr.Replace(dirRealPath, gfile.Pwd(), "") + } + importPrefix = gstr.Replace(importPrefix, gfile.Separator, "/") + importPrefix = gstr.Join(g.SliceStr{in.ModName, importPrefix, defaultDaoPath}, "/") + 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, in) + + // dao - internal + generateDaoInternal(tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName, fieldMap, in) +} + +func generateDo(ctx context.Context, db gdb.DB, tableNames, newTableNames []string, in cGenDaoInternalInput) { + var ( + doDirPath = gfile.Join(in.Path, defaultDoPath) + ) + in.NoJsonTag = true + in.DescriptionTag = false + in.NoModelComment = false + // Model content. + for i, tableName := range tableNames { + in.TableName = tableName + fieldMap, err := db.TableFields(ctx, tableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) + } + var ( + newTableName = newTableNames[i] + doFilePath = gfile.Join(doDirPath, gstr.CaseSnake(newTableName)+".go") + structDefinition = generateStructDefinition(generateStructDefinitionInput{ + cGenDaoInternalInput: in, + StructName: gstr.CaseCamel(newTableName), + FieldMap: fieldMap, + IsDo: true, + }) + ) + // replace all types to interface{}. + structDefinition, _ = gregex.ReplaceStringFuncMatch( + "([A-Z]\\w*?)\\s+([\\w\\*\\.]+?)\\s+(//)", + structDefinition, + func(match []string) string { + // If the type is already a pointer/slice/map, it does nothing. + if !gstr.HasPrefix(match[2], "*") && !gstr.HasPrefix(match[2], "[]") && !gstr.HasPrefix(match[2], "map") { + return fmt.Sprintf(`%s interface{} %s`, match[1], match[3]) + } + return match[0] + }, + ) + modelContent := generateDoContent( + tableName, + gstr.CaseCamel(newTableName), + structDefinition, + ) + err = gfile.PutContents(doFilePath, strings.TrimSpace(modelContent)) + if err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", doFilePath, err) + } else { + utils.GoFmt(doFilePath) + mlog.Print("generated:", doFilePath) + } + } +} + +func generateEntity(ctx context.Context, db gdb.DB, tableNames, newTableNames []string, in cGenDaoInternalInput) { + var ( + entityDirPath = gfile.Join(in.Path, defaultEntityPath) + ) + + // Model content. + for i, tableName := range tableNames { + fieldMap, err := db.TableFields(ctx, tableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) + } + var ( + newTableName = newTableNames[i] + entityFilePath = gfile.Join(entityDirPath, gstr.CaseSnake(newTableName)+".go") + entityContent = generateEntityContent( + newTableName, + gstr.CaseCamel(newTableName), + generateStructDefinition(generateStructDefinitionInput{ + cGenDaoInternalInput: in, + StructName: gstr.CaseCamel(newTableName), + FieldMap: fieldMap, + IsDo: false, + }), + ) + ) + err = gfile.PutContents(entityFilePath, strings.TrimSpace(entityContent)) + if err != nil { + mlog.Fatalf("writing content to '%s' failed: %v", entityFilePath, err) + } else { + utils.GoFmt(entityFilePath) + mlog.Print("generated:", entityFilePath) + } + } +} + +func getImportPartContent(source string, isDo bool) string { + var ( + packageImportsArray = garray.NewStrArray() + ) + + if isDo { + packageImportsArray.Append(`"github.com/gogf/gf/v2/frame/g"`) + } + + // Time package recognition. + if strings.Contains(source, "gtime.Time") { + packageImportsArray.Append(`"github.com/gogf/gf/v2/os/gtime"`) + } else if strings.Contains(source, "time.Time") { + packageImportsArray.Append(`"time"`) + } + + // Json type. + if strings.Contains(source, "gjson.Json") { + packageImportsArray.Append(`"github.com/gogf/gf/v2/encoding/gjson"`) + } + + // Generate and write content to golang file. + packageImportsStr := "" + if packageImportsArray.Len() > 0 { + packageImportsStr = fmt.Sprintf("import(\n%s\n)", packageImportsArray.Join("\n")) + } + return packageImportsStr +} + +func generateEntityContent(tableName, tableNameCamelCase, structDefine string) string { + entityContent := gstr.ReplaceByMap(consts.TemplateGenDaoEntityContent, g.MapStrStr{ + tplVarTableName: tableName, + tplVarPackageImports: getImportPartContent(structDefine, false), + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarStructDefine: structDefine, + }) + entityContent = replaceDefaultVar(entityContent) + return entityContent +} + +func generateDoContent(tableName, tableNameCamelCase, structDefine string) string { + doContent := gstr.ReplaceByMap(consts.TemplateGenDaoDoContent, g.MapStrStr{ + tplVarTableName: tableName, + tplVarPackageImports: getImportPartContent(structDefine, true), + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarStructDefine: structDefine, + }) + doContent = replaceDefaultVar(doContent) + return doContent +} + +func generateDaoIndex(tableNameCamelCase, tableNameCamelLowerCase, importPrefix, dirPathDao, fileName string, in cGenDaoInternalInput) { + path := gfile.Join(dirPathDao, fileName+".go") + if in.OverwriteDao || !gfile.Exists(path) { + indexContent := gstr.ReplaceByMap(getTplDaoIndexContent(""), g.MapStrStr{ + tplVarImportPrefix: importPrefix, + tplVarTableName: in.TableName, + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarTableNameCamelLowerCase: tableNameCamelLowerCase, + }) + indexContent = replaceDefaultVar(indexContent) + 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, + in cGenDaoInternalInput, +) { + path := gfile.Join(dirPathDao, "internal", fileName+".go") + modelContent := gstr.ReplaceByMap(getTplDaoInternalContent(""), g.MapStrStr{ + tplVarImportPrefix: importPrefix, + tplVarTableName: in.TableName, + tplVarGroupName: in.Group, + tplVarTableNameCamelCase: tableNameCamelCase, + tplVarTableNameCamelLowerCase: tableNameCamelLowerCase, + tplVarColumnDefine: gstr.Trim(generateColumnDefinitionForDao(fieldMap)), + tplVarColumnNames: gstr.Trim(generateColumnNamesForDao(fieldMap)), + }) + modelContent = replaceDefaultVar(modelContent) + 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 replaceDefaultVar(origin string) string { + return gstr.ReplaceByMap(origin, g.MapStrStr{ + tplVarDatetime: createdAt.String(), + }) +} + +type generateStructDefinitionInput struct { + cGenDaoInternalInput + StructName string // Struct name. + FieldMap map[string]*gdb.TableField // Table field map. + IsDo bool // Is generating DTO struct. +} + +func generateStructDefinition(in generateStructDefinitionInput) string { + buffer := bytes.NewBuffer(nil) + array := make([][]string, len(in.FieldMap)) + names := sortFieldKeyForDao(in.FieldMap) + for index, name := range names { + field := in.FieldMap[name] + array[index] = generateStructFieldDefinition(field, in) + } + 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, " #", "") + stContent = gstr.Replace(stContent, "` ", "`") + stContent = gstr.Replace(stContent, "``", "") + buffer.Reset() + buffer.WriteString(fmt.Sprintf("type %s struct {\n", in.StructName)) + if in.IsDo { + buffer.WriteString(fmt.Sprintf("g.Meta `orm:\"table:%s, do:true\"`\n", in.TableName)) + } + buffer.WriteString(stContent) + buffer.WriteString("}") + return buffer.String() +} + +// generateStructFieldForModel generates and returns the attribute definition for specified field. +func generateStructFieldDefinition(field *gdb.TableField, in generateStructDefinitionInput) []string { + var ( + typeName string + jsonTag = getJsonTagFromCase(field.Name, in.JsonCase) + ) + 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 in.StdTime { + typeName = "time.Time" + } else { + typeName = "*gtime.Time" + } + case "json", "jsonb": + if in.GJsonSupport { + typeName = "*gjson.Json" + } else { + typeName = "string" + } + default: + // Automatically detect its data 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 in.StdTime { + typeName = "time.Time" + } else { + typeName = "*gtime.Time" + } + default: + typeName = "string" + } + } + + var ( + tagKey = "`" + result = []string{ + " #" + gstr.CaseCamel(field.Name), + " #" + typeName, + } + descriptionTag = gstr.Replace(formatComment(field.Comment), `"`, `\"`) + ) + + result = append(result, " #"+fmt.Sprintf(tagKey+`json:"%s"`, jsonTag)) + result = append(result, " #"+fmt.Sprintf(`description:"%s"`+tagKey, descriptionTag)) + result = append(result, " #"+fmt.Sprintf(`// %s`, formatComment(field.Comment))) + + for k, v := range result { + if in.NoJsonTag { + v, _ = gregex.ReplaceString(`json:".+"`, ``, v) + } + if !in.DescriptionTag { + v, _ = gregex.ReplaceString(`description:".*"`, ``, v) + } + if in.NoModelComment { + v, _ = gregex.ReplaceString(`//.+`, ``, v) + } + result[k] = v + } + return result +} + +// 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.Replace(comment, `\n`, " ") + comment = gstr.Trim(comment) + 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 { + var ( + 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 consts.TemplateDaoDaoIndexContent +} + +func getTplDaoInternalContent(tplDaoInternalPath string) string { + if tplDaoInternalPath != "" { + return gfile.GetContents(tplDaoInternalPath) + } + return consts.TemplateDaoDaoInternalContent +} + +// 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 +} diff --git a/command/gf/internal/cmd/cmd_gen_pb.go b/command/gf/internal/cmd/cmd_gen_pb.go new file mode 100644 index 000000000..3ada72667 --- /dev/null +++ b/command/gf/internal/cmd/cmd_gen_pb.go @@ -0,0 +1,78 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/container/gset" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/genv" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gproc" +) + +type ( + cGenPbInput struct { + g.Meta `name:"pb" brief:"parse proto files and generate protobuf go files"` + } + cGenPbOutput struct{} +) + +func (c cGen) Pb(ctx context.Context, in cGenPbInput) (out *cGenPbOutput, err error) { + // Necessary check. + if gproc.SearchBinary("protoc") == "" { + mlog.Fatalf(`command "protoc" not found in your environment, please install protoc first to proceed this command`) + } + + // 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").String(), "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/v2/os/gtime"`, + // }) + // _ = gfile.PutContents(path, content) + // utils.GoFmt(path) + // return path + //}) + mlog.Print("done!") + return +} diff --git a/command/gf/internal/cmd/cmd_gen_pbentity.go b/command/gf/internal/cmd/cmd_gen_pbentity.go new file mode 100644 index 000000000..d7b4f66cd --- /dev/null +++ b/command/gf/internal/cmd/cmd_gen_pbentity.go @@ -0,0 +1,409 @@ +package cmd + +import ( + "bytes" + "context" + "fmt" + "strings" + + "github.com/gogf/gf/command/gf/v2/internal/consts" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" + "github.com/gogf/gf/v2/util/gtag" + "github.com/olekukonko/tablewriter" +) + +const ( + cGenPbEntityConfig = `gfcli.gen.pbentity` + cGenPbEntityBrief = `generate entity message files in protobuf3 format` + cGenPbEntityEg = ` +gf gen pbentity +gf gen pbentity -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test" +gf gen pbentity -p ./protocol/demos/entity -t user,user_detail,user_login +gf gen pbentity -r user_ +` + + cGenPbEntityAd = ` +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(config.yaml): + gfcli: + gen: + - pbentity: + link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + path: "protocol/demos/entity" + tables: "order,products" + package: "demos" + - 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"; +` + cGenPbEntityBriefPath = `directory path for generated files` + cGenPbEntityBriefPackage = `package name for all entity proto files` + cGenPbEntityBriefLink = `database configuration, the same as the ORM configuration of GoFrame` + cGenPbEntityBriefTables = `generate models only for given tables, multiple table names separated with ','` + cGenPbEntityBriefPrefix = `add specified prefix for all entity names and entity proto files` + cGenPbEntityBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','` + cGenPbEntityBriefOption = `extra protobuf options` + cGenPbEntityBriefGroup = ` +specifying the configuration group name of database for generated ORM instance, +it's not necessary and the default value is "default" +` + + cGenPbEntityBriefNameCase = ` +case for message attribute names, default is "Camel": +| 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 | +` + + cGenPbEntityBriefJsonCase = ` +case for message json tag, cases are the same as "nameCase", default "CamelLower". +set it to "none" to ignore json tag generating. +` +) + +type ( + cGenPbEntityInput struct { + g.Meta `name:"pbentity" config:"{cGenPbEntityConfig}" brief:"{cGenPbEntityBrief}" eg:"{cGenPbEntityEg}" ad:"{cGenPbEntityAd}"` + Path string `name:"path" short:"p" brief:"{cGenPbEntityBriefPath}"` + Package string `name:"package" short:"k" brief:"{cGenPbEntityBriefPackage}"` + Link string `name:"link" short:"l" brief:"{cGenPbEntityBriefLink}"` + Tables string `name:"tables" short:"t" brief:"{cGenPbEntityBriefTables}"` + Prefix string `name:"prefix" short:"f" brief:"{cGenPbEntityBriefPrefix}"` + RemovePrefix string `name:"removePrefix" short:"r" brief:"{cGenPbEntityBriefRemovePrefix}"` + NameCase string `name:"nameCase" short:"n" brief:"{cGenPbEntityBriefNameCase}" d:"Camel"` + JsonCase string `name:"jsonCase" short:"j" brief:"{cGenPbEntityBriefJsonCase}" d:"CamelLower"` + Option string `name:"option" short:"o" brief:"{cGenPbEntityBriefOption}"` + } + cGenPbEntityOutput struct{} + + cGenPbEntityInternalInput struct { + cGenPbEntityInput + TableName string // TableName specifies the table name of the table. + NewTableName string // NewTableName specifies the prefix-stripped name of the table. + } +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cGenPbEntityConfig`: cGenPbEntityConfig, + `cGenPbEntityBrief`: cGenPbEntityBrief, + `cGenPbEntityEg`: cGenPbEntityEg, + `cGenPbEntityAd`: cGenPbEntityAd, + `cGenPbEntityBriefPath`: cGenPbEntityBriefPath, + `cGenPbEntityBriefPackage`: cGenPbEntityBriefPackage, + `cGenPbEntityBriefLink`: cGenPbEntityBriefLink, + `cGenPbEntityBriefTables`: cGenPbEntityBriefTables, + `cGenPbEntityBriefPrefix`: cGenPbEntityBriefPrefix, + `cGenPbEntityBriefRemovePrefix`: cGenPbEntityBriefRemovePrefix, + `cGenPbEntityBriefGroup`: cGenPbEntityBriefGroup, + `cGenPbEntityBriefNameCase`: cGenPbEntityBriefNameCase, + `cGenPbEntityBriefJsonCase`: cGenPbEntityBriefJsonCase, + `cGenPbEntityBriefOption`: cGenPbEntityBriefOption, + }) +} + +func (c cGen) PbEntity(ctx context.Context, in cGenPbEntityInput) (out *cGenPbEntityOutput, err error) { + var ( + config = g.Cfg() + ) + if config.Available(ctx) { + v := config.MustGet(ctx, cGenPbEntityConfig) + if v.IsSlice() { + for i := 0; i < len(v.Interfaces()); i++ { + doGenPbEntityForArray(ctx, i, in) + } + } else { + doGenPbEntityForArray(ctx, -1, in) + } + } else { + doGenPbEntityForArray(ctx, -1, in) + } + mlog.Print("done!") + return +} + +func doGenPbEntityForArray(ctx context.Context, index int, in cGenPbEntityInput) { + var ( + err error + db gdb.DB + ) + if index >= 0 { + err = g.Cfg().MustGet( + ctx, + fmt.Sprintf(`%s.%d`, cGenDaoConfig, index), + ).Scan(&in) + if err != nil { + mlog.Fatalf(`invalid configuration of "%s": %+v`, cGenDaoConfig, err) + } + } + if in.Package == "" { + mlog.Fatal("package name should not be empty") + } + removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",") + // It uses user passed database configuration. + if in.Link != "" { + var ( + tempGroup = gtime.TimestampNanoStr() + match, _ = gregex.MatchString(`([a-z]+):(.+)`, in.Link) + ) + if len(match) == 3 { + gdb.AddConfigNode(tempGroup, gdb.ConfigNode{ + Type: gstr.Trim(match[1]), + Link: gstr.Trim(match[2]), + }) + db, _ = gdb.Instance(tempGroup) + } + } else { + db = g.DB() + } + if db == nil { + mlog.Fatal("database initialization failed") + } + + tableNames := ([]string)(nil) + if in.Tables != "" { + tableNames = gstr.SplitAndTrim(in.Tables, ",") + } 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) + } + generatePbEntityContentFile(ctx, db, cGenPbEntityInternalInput{ + cGenPbEntityInput: in, + TableName: tableName, + NewTableName: newTableName, + }) + } +} + +// generatePbEntityContentFile generates the protobuf files for given table. +func generatePbEntityContentFile(ctx context.Context, db gdb.DB, in cGenPbEntityInternalInput) { + fieldMap, err := db.TableFields(ctx, in.TableName) + if err != nil { + mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", in.TableName, err) + } + // Change the `newTableName` if `Prefix` is given. + newTableName := "Entity_" + in.Prefix + in.NewTableName + var ( + tableNameCamelCase = gstr.CaseCamel(newTableName) + tableNameSnakeCase = gstr.CaseSnake(newTableName) + entityMessageDefine = generateEntityMessageDefinition(tableNameCamelCase, fieldMap, in) + fileName = gstr.Trim(tableNameSnakeCase, "-_.") + path = gfile.Join(in.Path, fileName+".proto") + ) + entityContent := gstr.ReplaceByMap(getTplPbEntityContent(""), g.MapStrStr{ + "{PackageName}": in.Package, + "{OptionContent}": in.Option, + "{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(entityName string, fieldMap map[string]*gdb.TableField, in cGenPbEntityInternalInput) 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], in) + } + 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", entityName)) + 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, in cGenPbEntityInternalInput) []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, in.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, in.NameCase), + " #= " + gconv.String(index) + jsonTagStr + ";", + " #" + fmt.Sprintf(`// %s`, comment), + } +} + +func getTplPbEntityContent(tplEntityPath string) string { + if tplEntityPath != "" { + return gfile.GetContents(tplEntityPath) + } + return consts.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 +} + +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/command/gf/internal/cmd/cmd_init.go b/command/gf/internal/cmd/cmd_init.go new file mode 100644 index 000000000..b1d2c84f5 --- /dev/null +++ b/command/gf/internal/cmd/cmd_init.go @@ -0,0 +1,101 @@ +package cmd + +import ( + "context" + "fmt" + "strings" + + "github.com/gogf/gf/command/gf/v2/internal/utility/allyes" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gres" + "github.com/gogf/gf/v2/util/gtag" +) + +var ( + Init = cInit{} +) + +type cInit struct { + g.Meta `name:"init" brief:"{cInitBrief}" eg:"{cInitEg}"` +} + +const ( + cInitRepoPrefix = `github.com/gogf/` + cInitMonoRepo = `template-mono` + cInitSingleRepo = `template-single` + cInitBrief = `create and initialize an empty GoFrame project` + cInitEg = ` +gf init my-project +gf init my-mono-repo -m +` + cInitNameBrief = ` +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. +` +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cInitBrief`: cInitBrief, + `cInitEg`: cInitEg, + `cInitNameBrief`: cInitNameBrief, + }) +} + +type cInitInput struct { + g.Meta `name:"init"` + Name string `name:"NAME" arg:"true" v:"required" brief:"{cInitNameBrief}"` + Mono bool `name:"mono" short:"m" brief:"initialize a mono-repo instead a single-repo" orphan:"true"` +} +type cInitOutput struct{} + +func (c cInit) Index(ctx context.Context, in cInitInput) (out *cInitOutput, err error) { + if !gfile.IsEmpty(in.Name) && !allyes.Check() { + s := gcmd.Scanf(`the folder "%s" is not empty, files might be overwrote, continue? [y/n]: `, in.Name) + if strings.EqualFold(s, "n") { + return + } + } + mlog.Print("initializing...") + + // Create project folder and files. + var ( + templateRepoName string + ) + if in.Mono { + templateRepoName = cInitMonoRepo + } else { + templateRepoName = cInitSingleRepo + } + err = gres.Export(templateRepoName, in.Name, gres.ExportOption{ + RemovePrefix: templateRepoName, + }) + if err != nil { + return + } + + // Replace template name to project name. + err = gfile.ReplaceDir( + cInitRepoPrefix+templateRepoName, + gfile.Basename(gfile.RealPath(in.Name)), + in.Name, + "*", + true, + ) + if err != nil { + return + } + + mlog.Print("initialization done! ") + if !in.Mono { + enjoyCommand := `gf run main.go` + if in.Name != "." { + enjoyCommand = fmt.Sprintf(`cd %s && %s`, in.Name, enjoyCommand) + } + mlog.Printf(`you can now run "%s" to start your journey, enjoy!`, enjoyCommand) + } + return +} diff --git a/command/gf/internal/cmd/cmd_install.go b/command/gf/internal/cmd/cmd_install.go new file mode 100644 index 000000000..624b26eae --- /dev/null +++ b/command/gf/internal/cmd/cmd_install.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "context" + + "github.com/gogf/gf/command/gf/v2/internal/service" + "github.com/gogf/gf/v2/frame/g" +) + +var ( + Install = cInstall{} +) + +type cInstall struct { + g.Meta `name:"install" brief:"install gf binary to system (might need root/admin permission)"` +} + +type cInstallInput struct { + g.Meta `name:"install"` +} +type cInstallOutput struct{} + +func (c cInstall) Index(ctx context.Context, in cInstallInput) (out *cInstallOutput, err error) { + err = service.Install.Run(ctx) + return +} diff --git a/command/gf/internal/cmd/cmd_pack.go b/command/gf/internal/cmd/cmd_pack.go new file mode 100644 index 000000000..a9fc28e12 --- /dev/null +++ b/command/gf/internal/cmd/cmd_pack.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "context" + "strings" + + "github.com/gogf/gf/command/gf/v2/internal/utility/allyes" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gres" + "github.com/gogf/gf/v2/util/gtag" +) + +var ( + Pack = cPack{} +) + +type cPack struct { + g.Meta `name:"pack" usage:"{cPackUsage}" brief:"{cPackBrief}" eg:"{cPackEg}"` +} + +const ( + cPackUsage = `gf pack SRC DST` + cPackBrief = `packing any file/directory to a resource file, or a go file` + cPackEg = ` +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 +` + cPackSrcBrief = `source path for packing, which can be multiple source paths.` + cPackDstBrief = ` +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. +` + cPackNameBrief = `package name for output go file, it's set as its directory name if no name passed` + cPackPrefixBrief = `prefix for each file packed into the resource file` +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cPackUsage`: cPackUsage, + `cPackBrief`: cPackBrief, + `cPackEg`: cPackEg, + `cPackSrcBrief`: cPackSrcBrief, + `cPackDstBrief`: cPackDstBrief, + `cPackNameBrief`: cPackNameBrief, + `cPackPrefixBrief`: cPackPrefixBrief, + }) +} + +type cPackInput struct { + g.Meta `name:"pack"` + Src string `name:"SRC" arg:"true" v:"required" brief:"{cPackSrcBrief}"` + Dst string `name:"DST" arg:"true" v:"required" brief:"{cPackDstBrief}"` + Name string `name:"name" short:"n" brief:"{cPackNameBrief}"` + Prefix string `name:"prefix" short:"p" brief:"{cPackPrefixBrief}"` +} +type cPackOutput struct{} + +func (c cPack) Index(ctx context.Context, in cPackInput) (out *cPackOutput, err error) { + if gfile.Exists(in.Dst) && gfile.IsDir(in.Dst) { + mlog.Fatalf("DST path '%s' cannot be a directory", in.Dst) + } + if !gfile.IsEmpty(in.Dst) && !allyes.Check() { + s := gcmd.Scanf("path '%s' is not empty, files might be overwrote, continue? [y/n]: ", in.Dst) + if strings.EqualFold(s, "n") { + return + } + } + if in.Name == "" && gfile.ExtName(in.Dst) == "go" { + in.Name = gfile.Basename(gfile.Dir(in.Dst)) + } + if in.Name != "" { + if err = gres.PackToGoFile(in.Src, in.Dst, in.Name, in.Prefix); err != nil { + mlog.Fatalf("pack failed: %v", err) + } + } else { + if err = gres.PackToFile(in.Src, in.Dst, in.Prefix); err != nil { + mlog.Fatalf("pack failed: %v", err) + } + } + mlog.Print("done!") + return +} diff --git a/command/gf/internal/cmd/cmd_run.go b/command/gf/internal/cmd/cmd_run.go new file mode 100644 index 000000000..5826580b7 --- /dev/null +++ b/command/gf/internal/cmd/cmd_run.go @@ -0,0 +1,161 @@ +package cmd + +import ( + "context" + "fmt" + "runtime" + + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/container/gtype" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/os/gfsnotify" + "github.com/gogf/gf/v2/os/gproc" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/os/gtimer" + "github.com/gogf/gf/v2/util/gtag" +) + +var ( + Run = cRun{} +) + +type cRun struct { + g.Meta `name:"run" usage:"{cRunUsage}" brief:"{cRunBrief}" eg:"{cRunEg}" dc:"{cRunDc}"` +} + +type cRunApp struct { + File string // Go run file name. + Path string // Directory storing built binary. + Options string // Extra "go run" options. + Args string // Custom arguments. +} + +const ( + cRunUsage = `gf run FILE [OPTION]` + cRunBrief = `running go codes with hot-compiled-like feature` + cRunEg = ` +gf run main.go +gf run main.go --args "server -p 8080" +gf run main.go -mod=vendor +` + cRunDc = ` +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. +` + cRunFileBrief = `building file path.` + cRunPathBrief = `output directory path for built binary file. it's "manifest/output" in default` + cRunExtraBrief = `the same options as "go run"/"go build" except some options as follows defined` +) + +var ( + process *gproc.Process +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cRunUsage`: cRunUsage, + `cRunBrief`: cRunBrief, + `cRunEg`: cRunEg, + `cRunDc`: cRunDc, + `cRunFileBrief`: cRunFileBrief, + `cRunPathBrief`: cRunPathBrief, + `cRunExtraBrief`: cRunExtraBrief, + }) +} + +type ( + cRunInput struct { + g.Meta `name:"run"` + File string `name:"FILE" arg:"true" brief:"{cRunFileBrief}" v:"required"` + Path string `name:"path" short:"p" brief:"{cRunPathBrief}" d:"./"` + Extra string `name:"extra" short:"e" brief:"{cRunExtraBrief}"` + } + cRunOutput struct{} +) + +func (c cRun) Index(ctx context.Context, in cRunInput) (out *cRunOutput, err error) { + // Necessary check. + if gproc.SearchBinary("go") == "" { + mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`) + } + + app := &cRunApp{ + File: in.File, + Path: in.Path, + Options: in.Extra, + } + dirty := gtype.NewBool() + _, err = gfsnotify.Add(gfile.RealPath("."), func(event *gfsnotify.Event) { + if gfile.ExtName(event.Path) != "go" { + return + } + // Variable `dirty` 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(ctx, 1500*gtime.MS, func(ctx context.Context) { + 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 *cRunApp) Run() { + // Rebuild and run the codes. + renamePath := "" + mlog.Printf("build: %s", app.File) + outputPath := gfile.Join(app.Path, 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) + } + } + } + // 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/command/gf/internal/cmd/cmd_tpl.go b/command/gf/internal/cmd/cmd_tpl.go new file mode 100644 index 000000000..51db53ac1 --- /dev/null +++ b/command/gf/internal/cmd/cmd_tpl.go @@ -0,0 +1,167 @@ +package cmd + +import ( + "context" + + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gtag" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + Tpl = cTpl{} +) + +type cTpl struct { + g.Meta `name:"tpl" brief:"{cTplBrief}" dc:"{cTplDc}"` +} + +const ( + cTplBrief = `template parsing and building commands` + cTplDc = ` +The "tpl" command is used for template parsing and building purpose. +It can parse either template file or folder with multiple types of values support, +like json/xml/yaml/toml/ini. +` + cTplParseBrief = `parse either template file or folder with multiple types of values` + cTplParseEg = ` +gf tpl parse -p ./template -v values.json -r +gf tpl parse -p ./template -v values.json -n *.tpl -r +gf tpl parse -p ./template -v values.json -d '${,}}' -r +gf tpl parse -p ./template -v values.json -o ./template.parsed +` + cTplSupportValuesFilePattern = `*.json,*.xml,*.yaml,*.yml,*.toml,*.ini` +) + +type ( + cTplParseInput struct { + g.Meta `name:"parse" brief:"{cTplParseBrief}" eg:"{cTplParseEg}"` + Path string `name:"path" short:"p" brief:"template file or folder path" v:"required"` + Pattern string `name:"pattern" short:"n" brief:"template file pattern when path is a folder, default is:*" d:"*"` + Recursive bool `name:"recursive" short:"c" brief:"recursively parsing files if path is folder, default is:true" d:"true"` + Values string `name:"values" short:"v" brief:"template values file/folder, support file types like: json/xml/yaml/toml/ini" v:"required"` + Output string `name:"output" short:"o" brief:"output file/folder path"` + Delimiters string `name:"delimiters" short:"d" brief:"delimiters for template content parsing, default is:{{,}}" d:"{{,}}"` + Replace bool `name:"replace" short:"r" brief:"replace original files" orphan:"true"` + } + cTplParseOutput struct{} +) + +func init() { + gtag.Sets(g.MapStrStr{ + `cTplBrief`: cTplBrief, + `cTplDc`: cTplDc, + `cTplParseEg`: cTplParseEg, + `cTplParseBrief`: cTplParseBrief, + }) +} + +func (c *cTpl) Parse(ctx context.Context, in cTplParseInput) (out *cTplParseOutput, err error) { + if in.Output == "" && in.Replace == false { + return nil, gerror.New(`parameter output and replace should not be both empty`) + } + delimiters := gstr.SplitAndTrim(in.Delimiters, ",") + mlog.Debugf("delimiters input:%s, parsed:%#v", in.Delimiters, delimiters) + if len(delimiters) != 2 { + return nil, gerror.Newf(`invalid delimiters: %s`, in.Delimiters) + } + g.View().SetDelimiters(delimiters[0], delimiters[1]) + valuesMap, err := c.loadValues(ctx, in.Values) + if err != nil { + return nil, err + } + if len(valuesMap) == 0 { + return nil, gerror.Newf(`empty values loaded from values file/folder "%s"`, in.Values) + } + err = c.parsePath(ctx, valuesMap, in) + if err == nil { + mlog.Print("done!") + } + return +} + +func (c *cTpl) parsePath(ctx context.Context, values g.Map, in cTplParseInput) (err error) { + if !gfile.Exists(in.Path) { + return gerror.Newf(`path "%s" does not exist`, in.Path) + } + var ( + path string + files []string + relativePath string + outputPath string + ) + path = gfile.RealPath(in.Path) + if gfile.IsDir(path) { + files, err = gfile.ScanDirFile(path, in.Pattern, in.Recursive) + if err != nil { + return err + } + for _, file := range files { + relativePath = gstr.Replace(file, path, "") + if in.Output != "" { + outputPath = gfile.Join(in.Output, relativePath) + } + if err = c.parseFile(ctx, file, outputPath, values, in); err != nil { + return + } + } + return + } + if in.Output != "" { + outputPath = in.Output + } + err = c.parseFile(ctx, path, outputPath, values, in) + return +} + +func (c *cTpl) parseFile(ctx context.Context, file string, output string, values g.Map, in cTplParseInput) (err error) { + output = gstr.ReplaceByMap(output, g.MapStrStr{ + `\\`: `\`, + `//`: `/`, + }) + content, err := g.View().Parse(ctx, file, values) + if err != nil { + return err + } + if output != "" { + mlog.Printf(`parse file "%s" to "%s"`, file, output) + return gfile.PutContents(output, content) + } + if in.Replace { + mlog.Printf(`parse and replace file "%s"`, file) + return gfile.PutContents(file, content) + } + return nil +} + +func (c *cTpl) loadValues(ctx context.Context, valuesPath string) (data g.Map, err error) { + if !gfile.Exists(valuesPath) { + return nil, gerror.Newf(`values file/folder "%s" does not exist`, valuesPath) + } + var j *gjson.Json + if gfile.IsDir(valuesPath) { + var valueFiles []string + valueFiles, err = gfile.ScanDirFile(valuesPath, cTplSupportValuesFilePattern, true) + if err != nil { + return nil, err + } + data = make(g.Map) + for _, file := range valueFiles { + if j, err = gjson.Load(file); err != nil { + return nil, err + } + gutil.MapMerge(data, j.Map()) + } + return + } + if j, err = gjson.Load(valuesPath); err != nil { + return nil, err + } + data = j.Map() + return +} diff --git a/command/gf/internal/cmd/cmd_version.go b/command/gf/internal/cmd/cmd_version.go new file mode 100644 index 000000000..a96998f6c --- /dev/null +++ b/command/gf/internal/cmd/cmd_version.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "context" + "fmt" + + "github.com/gogf/gf/command/gf/v2/internal/consts" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gbuild" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +var ( + Version = cVersion{} +) + +type cVersion struct { + g.Meta `name:"version" brief:"show version information of current binary"` +} + +type cVersionInput struct { + g.Meta `name:"version"` +} +type cVersionOutput struct{} + +func (c cVersion) Index(ctx context.Context, in cVersionInput) (*cVersionOutput, error) { + info := gbuild.Info() + if info["git"] == "" { + info["git"] = "none" + } + mlog.Printf(`GoFrame CLI Tool %s, https://goframe.org`, consts.Version) + gfVersion, err := c.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 nil, nil + } + + 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"]))) + return nil, nil +} + +// getGFVersionOfCurrentProject checks and returns the GoFrame version current project using. +func (c cVersion) getGFVersionOfCurrentProject() (string, error) { + goModPath := gfile.Join(gfile.Pwd(), "go.mod") + if gfile.Exists(goModPath) { + lines := gstr.SplitAndTrim(gfile.GetContents(goModPath), "\n") + for _, line := range lines { + line = gstr.Trim(line) + // Version 1. + match, err := gregex.MatchString(`^github\.com/gogf/gf\s+(.+)$`, line) + if err != nil { + return "", err + } + if len(match) <= 1 { + // Version > 1. + match, err = gregex.MatchString(`^github\.com/gogf/gf/v\d\s+(.+)$`, line) + if err != nil { + return "", err + } + } + if len(match) > 1 { + return gstr.Trim(match[1]), nil + } + } + + return "", gerror.New("cannot find goframe requirement in go.mod") + } else { + return "", gerror.New("cannot find go.mod") + } +} diff --git a/command/gf/internal/consts/consts.go b/command/gf/internal/consts/consts.go new file mode 100644 index 000000000..99460e74c --- /dev/null +++ b/command/gf/internal/consts/consts.go @@ -0,0 +1,5 @@ +package consts + +const ( + Version = `v2.0.0-rc` +) diff --git a/command/gf/internal/consts/consts_gen_dao_template_dao.go b/command/gf/internal/consts/consts_gen_dao_template_dao.go new file mode 100644 index 000000000..c287cd759 --- /dev/null +++ b/command/gf/internal/consts/consts_gen_dao_template_dao.go @@ -0,0 +1,104 @@ +package consts + +const TemplateDaoDaoIndexContent = ` +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package dao + +import ( + "{TplImportPrefix}/internal" +) + +// {TplTableNameCamelLowerCase}Dao is the data access object for table {TplTableName}. +// 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{ + internal.New{TplTableNameCamelCase}Dao(), + } +) + +// Fill with you ideas below. + +` + +const TemplateDaoDaoInternalContent = ` +// ========================================================================== +// Code generated by GoFrame CLI tool. DO NOT EDIT. Created at {TplDatetime} +// ========================================================================== + +package internal + +import ( + "context" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// {TplTableNameCamelCase}Dao is the data access object for table {TplTableName}. +type {TplTableNameCamelCase}Dao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of current DAO. + columns {TplTableNameCamelCase}Columns // columns contains all the column names of Table for convenient usage. +} + +// {TplTableNameCamelCase}Columns defines and stores column names for table {TplTableName}. +type {TplTableNameCamelCase}Columns struct { + {TplColumnDefine} +} + +// {TplTableNameCamelLowerCase}Columns holds the columns for table {TplTableName}. +var {TplTableNameCamelLowerCase}Columns = {TplTableNameCamelCase}Columns{ + {TplColumnNames} +} + +// New{TplTableNameCamelCase}Dao creates and returns a new DAO object for table data access. +func New{TplTableNameCamelCase}Dao() *{TplTableNameCamelCase}Dao { + return &{TplTableNameCamelCase}Dao{ + group: "{TplGroupName}", + table: "{TplTableName}", + columns: {TplTableNameCamelLowerCase}Columns, + } +} + +// DB retrieves and returns the underlying raw database management object of current DAO. +func (dao *{TplTableNameCamelCase}Dao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of current dao. +func (dao *{TplTableNameCamelCase}Dao) Table() string { + return dao.table +} + +// Columns returns all column names of current dao. +func (dao *{TplTableNameCamelCase}Dao) Columns() {TplTableNameCamelCase}Columns { + return dao.columns +} + +// Group returns the configuration group name of database of current dao. +func (dao *{TplTableNameCamelCase}Dao) Group() string { + return dao.group +} + +// Ctx creates and returns the Model for current DAO, It automatically sets the context for current operation. +func (dao *{TplTableNameCamelCase}Dao) Ctx(ctx context.Context) *gdb.Model { + return dao.DB().Model(dao.table).Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rollbacks the transaction and returns the error from function f if it returns non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note that, you should not Commit or Rollback the transaction in function f +// as it is automatically handled by this function. +func (dao *{TplTableNameCamelCase}Dao) Transaction(ctx context.Context, f func(ctx context.Context, tx *gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} +` diff --git a/command/gf/internal/consts/consts_gen_dao_template_do.go b/command/gf/internal/consts/consts_gen_dao_template_do.go new file mode 100644 index 000000000..9b63433af --- /dev/null +++ b/command/gf/internal/consts/consts_gen_dao_template_do.go @@ -0,0 +1,14 @@ +package consts + +const TemplateGenDaoDoContent = ` +// ================================================================================= +// Code generated by GoFrame CLI tool. DO NOT EDIT. Created at {TplDatetime} +// ================================================================================= + +package do + +{TplPackageImports} + +// {TplTableNameCamelCase} is the golang structure of table {TplTableName} for DAO operations like Where/Data. +{TplStructDefine} +` diff --git a/command/gf/internal/consts/consts_gen_dao_template_entity.go b/command/gf/internal/consts/consts_gen_dao_template_entity.go new file mode 100644 index 000000000..6232b7d74 --- /dev/null +++ b/command/gf/internal/consts/consts_gen_dao_template_entity.go @@ -0,0 +1,14 @@ +package consts + +const TemplateGenDaoEntityContent = ` +// ================================================================================= +// Code generated by GoFrame CLI tool. DO NOT EDIT. Created at {TplDatetime} +// ================================================================================= + +package entity + +{TplPackageImports} + +// {TplTableNameCamelCase} is the golang structure for table {TplTableName}. +{TplStructDefine} +` diff --git a/command/gf/internal/consts/consts_gen_pbentity_template.go b/command/gf/internal/consts/consts_gen_pbentity_template.go new file mode 100644 index 000000000..bbb184a38 --- /dev/null +++ b/command/gf/internal/consts/consts_gen_pbentity_template.go @@ -0,0 +1,17 @@ +package consts + +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/command/gf/internal/packed/packed.go b/command/gf/internal/packed/packed.go new file mode 100644 index 000000000..e20ab1e95 --- /dev/null +++ b/command/gf/internal/packed/packed.go @@ -0,0 +1 @@ +package packed diff --git a/command/gf/internal/packed/template-mono.go b/command/gf/internal/packed/template-mono.go new file mode 100644 index 000000000..ee6a597cd --- /dev/null +++ b/command/gf/internal/packed/template-mono.go @@ -0,0 +1,9 @@ +package packed + +import "github.com/gogf/gf/v2/os/gres" + +func init() { + if err := gres.Add("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"); err != nil { + panic("add binary content to resource manager failed: " + err.Error()) + } +} diff --git a/command/gf/internal/packed/template-single.go b/command/gf/internal/packed/template-single.go new file mode 100644 index 000000000..d0cde0ca6 --- /dev/null +++ b/command/gf/internal/packed/template-single.go @@ -0,0 +1,9 @@ +package packed + +import "github.com/gogf/gf/v2/os/gres" + +func init() { + if err := gres.Add("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"); err != nil { + panic("add binary content to resource manager failed: " + err.Error()) + } +} diff --git a/command/gf/internal/service/install.go b/command/gf/internal/service/install.go new file mode 100644 index 000000000..27211cb4d --- /dev/null +++ b/command/gf/internal/service/install.go @@ -0,0 +1,211 @@ +package service + +import ( + "context" + "runtime" + "strings" + + "github.com/gogf/gf/command/gf/v2/internal/utility/allyes" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/container/garray" + "github.com/gogf/gf/v2/container/gset" + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/genv" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +var ( + Install = serviceInstall{} +) + +type serviceInstall struct{} + +type serviceInstallAvailablePath struct { + dirPath string + filePath string + writable bool + installed bool +} + +func (s serviceInstall) Run(ctx context.Context) (err error) { + // Ask where to install. + paths := s.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.dirPath) { + continue + } + mlog.Printf(" %2d | %8t | %9t | %s", id, aPath.writable, aPath.installed, aPath.dirPath) + 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.dirPath, 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 { + for { + // Get input and update selectedID. + var ( + inputID int + input = gcmd.Scanf("please choose one installation destination [default %d]: ", selectedID) + ) + if input != "" { + inputID = gconv.Int(input) + } + // Check if out of range. + if inputID >= len(paths) || inputID < 0 { + mlog.Printf("invalid install destination Id: %d", inputID) + continue + } + selectedID = inputID + break + } + } + + // Get selected destination path. + dstPath := paths[selectedID] + + // Install the new binary. + err = gfile.CopyFile(gfile.SelfPath(), dstPath.filePath) + if err != nil { + mlog.Printf("install gf binary to '%s' failed: %v", dstPath.dirPath, err) + mlog.Printf("you can manually install gf by copying the binary to folder: %s", dstPath.dirPath) + } else { + mlog.Printf("gf binary is successfully installed to: %s", dstPath.dirPath) + } + + // Uninstall the old binary. + for _, aPath := range paths { + // Do not delete myself. + if aPath.filePath != "" && aPath.filePath != dstPath.filePath && gfile.SelfPath() != aPath.filePath { + _ = gfile.Remove(aPath.filePath) + } + } + return +} + +// IsInstalled checks and returns whether the binary is installed. +func (s serviceInstall) IsInstalled() bool { + paths := s.getInstallPathsData() + for _, aPath := range paths { + if aPath.installed { + return true + } + } + return false +} + +// GetInstallPathsData returns the installation paths data for the binary. +func (s serviceInstall) getInstallPathsData() []serviceInstallAvailablePath { + var folderPaths []serviceInstallAvailablePath + // Pre generate binaryFileName. + binaryFileName := "gf" + gfile.Ext(gfile.SelfPath()) + switch runtime.GOOS { + case "darwin": + darwinInstallationCheckPaths := []string{"/usr/local/bin"} + for _, v := range darwinInstallationCheckPaths { + folderPaths = s.checkAndAppendToAvailablePath( + folderPaths, v, binaryFileName, + ) + } + fallthrough + + default: + // $GOPATH/bin + gopath := gfile.Join(runtime.GOROOT(), "bin") + folderPaths = s.checkAndAppendToAvailablePath( + folderPaths, gopath, binaryFileName, + ) + // Search and find the writable directory path. + envPath := genv.Get("PATH", genv.Get("Path").String()).String() + if gstr.Contains(envPath, ";") { + for _, v := range gstr.SplitAndTrim(envPath, ";") { + folderPaths = s.checkAndAppendToAvailablePath( + folderPaths, v, binaryFileName, + ) + } + } else if gstr.Contains(envPath, ":") { + for _, v := range gstr.SplitAndTrim(envPath, ":") { + folderPaths = s.checkAndAppendToAvailablePath( + folderPaths, v, binaryFileName, + ) + } + } else if envPath != "" { + folderPaths = s.checkAndAppendToAvailablePath( + folderPaths, envPath, binaryFileName, + ) + } else { + folderPaths = s.checkAndAppendToAvailablePath( + folderPaths, "/usr/local/bin", binaryFileName, + ) + } + } + return folderPaths +} + +// checkAndAppendToAvailablePath checks if `path` is writable and already installed. +// It adds the `path` to `folderPaths` if it is writable or already installed, or else it ignores the `path`. +func (s serviceInstall) checkAndAppendToAvailablePath(folderPaths []serviceInstallAvailablePath, dirPath string, binaryFileName string) []serviceInstallAvailablePath { + var ( + filePath = gfile.Join(dirPath, binaryFileName) + writable = gfile.IsWritable(dirPath) + installed = gfile.Exists(filePath) + ) + if !writable && !installed { + return folderPaths + } + return append( + folderPaths, + serviceInstallAvailablePath{ + dirPath: dirPath, + writable: writable, + filePath: filePath, + installed: installed, + }) +} diff --git a/command/gf/internal/utility/allyes/allyes.go b/command/gf/internal/utility/allyes/allyes.go new file mode 100644 index 000000000..146eda4b4 --- /dev/null +++ b/command/gf/internal/utility/allyes/allyes.go @@ -0,0 +1,22 @@ +package allyes + +import ( + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/genv" +) + +const ( + EnvName = "GF_CLI_ALL_YES" +) + +// Init initializes the package manually. +func Init() { + if gcmd.GetOpt("y") != nil { + genv.MustSet(EnvName, "1") + } +} + +// Check checks whether option allow all yes for command. +func Check() bool { + return genv.Get(EnvName).String() == "1" +} diff --git a/command/gf/internal/utility/mlog/mlog.go b/command/gf/internal/utility/mlog/mlog.go new file mode 100644 index 000000000..4a2f146a8 --- /dev/null +++ b/command/gf/internal/utility/mlog/mlog.go @@ -0,0 +1,67 @@ +package mlog + +import ( + "context" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/genv" + "github.com/gogf/gf/v2/os/glog" +) + +const ( + headerPrintEnvName = "GF_CLI_MLOG_HEADER" +) + +var ( + ctx = context.TODO() + logger = glog.New() +) + +func init() { + logger.SetStack(false) + if genv.Get(headerPrintEnvName).String() == "1" { + logger.SetHeaderPrint(true) + } else { + logger.SetHeaderPrint(false) + } + if gcmd.GetOpt("debug") != nil || gcmd.GetOpt("gf.debug") != nil { + logger.SetDebug(true) + } else { + logger.SetDebug(false) + } +} + +// 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(ctx, v...) +} + +func Printf(format string, v ...interface{}) { + logger.Printf(ctx, format, v...) +} + +func Fatal(v ...interface{}) { + logger.Fatal(ctx, append(g.Slice{"ERROR:"}, v...)...) +} + +func Fatalf(format string, v ...interface{}) { + logger.Fatalf(ctx, "ERROR: "+format, v...) +} + +func Debug(v ...interface{}) { + logger.Debug(ctx, append(g.Slice{"DEBUG:"}, v...)...) +} + +func Debugf(format string, v ...interface{}) { + logger.Debugf(ctx, "DEBUG: "+format, v...) +} diff --git a/command/gf/internal/utility/utils/utils.go b/command/gf/internal/utility/utils/utils.go new file mode 100644 index 000000000..15a689bd8 --- /dev/null +++ b/command/gf/internal/utility/utils/utils.go @@ -0,0 +1,18 @@ +package utils + +import ( + "fmt" + "github.com/gogf/gf/v2/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/command/gf/main.go b/command/gf/main.go new file mode 100644 index 000000000..907e342d0 --- /dev/null +++ b/command/gf/main.go @@ -0,0 +1,71 @@ +package main + +import ( + _ "github.com/gogf/gf/command/gf/v2/internal/packed" + + "github.com/gogf/gf/command/gf/v2/internal/cmd" + "github.com/gogf/gf/command/gf/v2/internal/utility/allyes" + "github.com/gogf/gf/command/gf/v2/internal/utility/mlog" + "github.com/gogf/gf/v2/os/gcmd" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" +) + +func main() { + defer func() { + if exception := recover(); exception != nil { + if err, ok := exception.(error); ok { + mlog.Print(err.Error()) + } else { + panic(exception) + } + } + }() + + // zsh alias "git fetch" conflicts checks. + handleZshAlias() + + // -y option checks. + allyes.Init() + + var ( + ctx = gctx.New() + ) + command, err := gcmd.NewFromObject(cmd.GF) + if err != nil { + panic(err) + } + err = command.AddObject( + cmd.Env, + cmd.Run, + cmd.Gen, + cmd.Tpl, + cmd.Init, + cmd.Pack, + cmd.Build, + cmd.Docker, + cmd.Install, + cmd.Version, + ) + if err != nil { + panic(err) + } + command.Run(ctx) +} + +// zsh alias "git fetch" conflicts checks. +func handleZshAlias() { + if home, err := gfile.Home(); err == nil { + zshPath := gfile.Join(home, ".zshrc") + if gfile.Exists(zshPath) { + var ( + aliasCommand = `alias gf=gf` + content = gfile.GetContents(zshPath) + ) + if !gstr.Contains(content, aliasCommand) { + _ = gfile.PutContentsAppend(zshPath, "\n"+aliasCommand+"\n") + } + } + } +} diff --git a/command/gf/test/testdata/tpls/tpl1.yaml b/command/gf/test/testdata/tpls/tpl1.yaml new file mode 100644 index 000000000..70f899ff8 --- /dev/null +++ b/command/gf/test/testdata/tpls/tpl1.yaml @@ -0,0 +1,2 @@ +server: + address: {{.server.address}} \ No newline at end of file diff --git a/command/gf/test/testdata/tpls/tpl2.sql b/command/gf/test/testdata/tpls/tpl2.sql new file mode 100644 index 000000000..1f5298e2c --- /dev/null +++ b/command/gf/test/testdata/tpls/tpl2.sql @@ -0,0 +1 @@ +insert into {{.sql.table}} \ No newline at end of file diff --git a/command/gf/test/testdata/values.json b/command/gf/test/testdata/values.json new file mode 100644 index 000000000..888740fca --- /dev/null +++ b/command/gf/test/testdata/values.json @@ -0,0 +1,8 @@ +{ + "server": { + "address": "https://goframe.org" + }, + "sql":{ + "table": "table_name" + } +} \ No newline at end of file diff --git a/contrib/drivers/.gitattributes b/contrib/drivers/.gitattributes new file mode 100644 index 000000000..8d6f5cfa6 --- /dev/null +++ b/contrib/drivers/.gitattributes @@ -0,0 +1,3 @@ +*.js linguist-language=GO +*.css linguist-language=GO +*.html linguist-language=GO \ No newline at end of file diff --git a/contrib/drivers/.gitignore b/contrib/drivers/.gitignore new file mode 100644 index 000000000..5af57e523 --- /dev/null +++ b/contrib/drivers/.gitignore @@ -0,0 +1,19 @@ +.buildpath +.hgignore.swp +.project +.orig +.swp +.idea/ +.settings/ +.vscode/ +vendor/ +composer.lock +gitpush.sh +pkg/ +bin/ +cbuild +**/.DS_Store +.vscode/ +.test/ +main +gf \ No newline at end of file diff --git a/contrib/drivers/LICENSE b/contrib/drivers/LICENSE new file mode 100644 index 000000000..0c20e2aaa --- /dev/null +++ b/contrib/drivers/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 john@goframe.org https://goframe.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/contrib/drivers/README.md b/contrib/drivers/README.md new file mode 100644 index 000000000..b9dd59dc1 --- /dev/null +++ b/contrib/drivers/README.md @@ -0,0 +1,56 @@ +# gdb-drivers +Database drivers for package gdb. + +# Installation +Let's take `pgsql` for example. +``` +go get -u github.com/gogf/gf/contrib/drivers/v2 +``` + +Choose and import the driver to your project: +``` +import _ "github.com/gogf/gf/contrib/drivers/v2/pgsql" +``` + +# Supported Drivers + +## MySQL + +BuiltIn supported, nothing todo. + +## SQLite +``` +import _ "github.com/gogf/gf/contrib/drivers/v2/sqlite" +``` +Note: +- It does not support `Save/Replace` features. + +## PostgreSQL +``` +import _ "github.com/gogf/gf/contrib/drivers/v2/pgsql" +``` +Note: +- It does not support `Save/Replace` features. +- It does not support `LastInsertId`. + +## SQL Server +``` +import _ "github.com/gogf/gf/contrib/drivers/v2/mssql" +``` +Note: +- It does not support `Save/Replace` features. +- It does not support `LastInsertId`. +- It supports server version >= `SQL Server2005` + +## Oracle +``` +import _ "github.com/gogf/gf/contrib/drivers/v2/oracle" +``` +Note: +- It does not support `Save/Replace` features. +- It does not support `LastInsertId`. + +# Custom Drivers + +It's quick and easy, please refer to current driver source. +It's quite appreciated if any PR for new drivers support into current repo. diff --git a/contrib/drivers/clickhouse/clickhouse.go b/contrib/drivers/clickhouse/clickhouse.go new file mode 100644 index 000000000..3b77030a2 --- /dev/null +++ b/contrib/drivers/clickhouse/clickhouse.go @@ -0,0 +1,12 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +// Package clickhouse implements gdb.Driver, which supports operations for ClickHouse. +package clickhouse + +import ( + _ "github.com/ClickHouse/clickhouse-go" +) diff --git a/contrib/drivers/go.mod b/contrib/drivers/go.mod new file mode 100644 index 000000000..a55066c81 --- /dev/null +++ b/contrib/drivers/go.mod @@ -0,0 +1,14 @@ +module github.com/gogf/gf/contrib/drivers/v2 + +go 1.15 + +require ( + github.com/ClickHouse/clickhouse-go v1.5.2 + github.com/denisenkom/go-mssqldb v0.11.0 + github.com/gogf/gf/v2 v2.0.0-rc + github.com/lib/pq v1.10.4 + github.com/mattn/go-oci8 v0.1.1 + github.com/mattn/go-sqlite3 v1.14.10 +) + +replace github.com/gogf/gf/v2 => ../../ \ No newline at end of file diff --git a/contrib/drivers/mssql/mssql.go b/contrib/drivers/mssql/mssql.go new file mode 100644 index 000000000..1c440efa3 --- /dev/null +++ b/contrib/drivers/mssql/mssql.go @@ -0,0 +1,327 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. +// +// Note: +// 1. It needs manually import: _ "github.com/denisenkom/go-mssqldb" +// 2. It does not support Save/Replace features. +// 3. It does not support LastInsertId. + +// Package mssql implements gdb.Driver, which supports operations for MSSql. +package mssql + +import ( + _ "github.com/denisenkom/go-mssqldb" + + "context" + "database/sql" + "fmt" + "strconv" + "strings" + + "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +// DriverMssql is the driver for SQL server database. +type DriverMssql struct { + *gdb.Core +} + +var ( + // tableFieldsMap caches the table information retrieved from database. + tableFieldsMap = gmap.New(true) +) + +func init() { + if err := gdb.Register(`mssql`, New()); err != nil { + panic(err) + } +} + +// New create and returns a driver that implements gdb.Driver, which supports operations for Mssql. +func New() gdb.Driver { + return &DriverMssql{} +} + +// New creates and returns a database object for SQL server. +// It implements the interface of gdb.Driver for extra database driver installation. +func (d *DriverMssql) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { + return &DriverMssql{ + Core: core, + }, nil +} + +// Open creates and returns an underlying sql.DB object for mssql. +func (d *DriverMssql) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "sqlserver" + ) + if config.Link != "" { + source = config.Link + } else { + source = fmt.Sprintf( + "user id=%s;password=%s;server=%s;port=%s;database=%s;encrypt=disable", + config.User, config.Pass, config.Host, config.Port, config.Name, + ) + } + + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} + +// FilteredLink retrieves and returns filtered `linkInfo` that can be using for +// logging or tracing purpose. +func (d *DriverMssql) FilteredLink() string { + linkInfo := d.GetConfig().Link + if linkInfo == "" { + return "" + } + s, _ := gregex.ReplaceString( + `(.+);\s*password=(.+);\s*server=(.+)`, + `$1;password=xxx;server=$3`, + d.GetConfig().Link, + ) + return s +} + +// GetChars returns the security char for this type of database. +func (d *DriverMssql) GetChars() (charLeft string, charRight string) { + return "\"", "\"" +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *DriverMssql) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { + defer func() { + newSql, newArgs, err = d.Core.DoFilter(ctx, link, newSql, newArgs) + }() + var index int + // Convert placeholder char '?' to string "@px". + str, _ := gregex.ReplaceStringFunc("\\?", sql, func(s string) string { + index++ + return fmt.Sprintf("@p%d", index) + }) + str, _ = gregex.ReplaceString("\"", "", str) + return d.parseSql(str), args, nil +} + +// parseSql does some replacement of the sql before commits it to underlying driver, +// for support of microsoft sql server. +func (d *DriverMssql) parseSql(sql string) string { + // SELECT * FROM USER WHERE ID=1 LIMIT 1 + if m, _ := gregex.MatchString(`^SELECT(.+)LIMIT 1$`, sql); len(m) > 1 { + return fmt.Sprintf(`SELECT TOP 1 %s`, m[1]) + } + // SELECT * FROM USER WHERE AGE>18 ORDER BY ID DESC LIMIT 100, 200 + patten := `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,\s*(\d+))` + if gregex.IsMatchString(patten, sql) == false { + return sql + } + res, err := gregex.MatchAllString(patten, sql) + if err != nil { + return "" + } + index := 0 + keyword := strings.TrimSpace(res[index][0]) + keyword = strings.ToUpper(keyword) + index++ + switch keyword { + case "SELECT": + // LIMIT statement checks. + if len(res) < 2 || + (strings.HasPrefix(res[index][0], "LIMIT") == false && + strings.HasPrefix(res[index][0], "limit") == false) { + break + } + if gregex.IsMatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) == false { + break + } + // ORDER BY statement checks. + selectStr := "" + orderStr := "" + haveOrder := gregex.IsMatchString("((?i)SELECT)(.+)((?i)ORDER BY)", sql) + if haveOrder { + queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)ORDER BY)", sql) + if len(queryExpr) != 4 || + strings.EqualFold(queryExpr[1], "SELECT") == false || + strings.EqualFold(queryExpr[3], "ORDER BY") == false { + break + } + selectStr = queryExpr[2] + orderExpr, _ := gregex.MatchString("((?i)ORDER BY)(.+)((?i)LIMIT)", sql) + if len(orderExpr) != 4 || + strings.EqualFold(orderExpr[1], "ORDER BY") == false || + strings.EqualFold(orderExpr[3], "LIMIT") == false { + break + } + orderStr = orderExpr[2] + } else { + queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) + if len(queryExpr) != 4 || + strings.EqualFold(queryExpr[1], "SELECT") == false || + strings.EqualFold(queryExpr[3], "LIMIT") == false { + break + } + selectStr = queryExpr[2] + } + first, limit := 0, 0 + for i := 1; i < len(res[index]); i++ { + if len(strings.TrimSpace(res[index][i])) == 0 { + continue + } + + if strings.HasPrefix(res[index][i], "LIMIT") || + strings.HasPrefix(res[index][i], "limit") { + first, _ = strconv.Atoi(res[index][i+1]) + limit, _ = strconv.Atoi(res[index][i+2]) + break + } + } + if haveOrder { + sql = fmt.Sprintf( + "SELECT * FROM "+ + "(SELECT ROW_NUMBER() OVER (ORDER BY %s) as ROWNUMBER_, %s ) as TMP_ "+ + "WHERE TMP_.ROWNUMBER_ > %d AND TMP_.ROWNUMBER_ <= %d", + orderStr, selectStr, first, first+limit, + ) + } else { + if first == 0 { + first = limit + } + sql = fmt.Sprintf( + "SELECT * FROM (SELECT TOP %d * FROM (SELECT TOP %d %s) as TMP1_ ) as TMP2_ ", + limit, first+limit, selectStr, + ) + } + default: + } + return sql +} + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *DriverMssql) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result gdb.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + + result, err = d.DoGetAll(ctx, link, `SELECT NAME FROM SYSOBJECTS WHERE XTYPE='U' AND STATUS >= 0 ORDER BY NAME`) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} + +// TableFields retrieves and returns the fields information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *DriverMssql) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*gdb.TableField, err error) { + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) + if gstr.Contains(table, " ") { + return nil, gerror.NewCode(gcode.CodeInvalidParameter, "function TableFields supports only single table operations") + } + useSchema := d.GetSchema() + if len(schema) > 0 && schema[0] != "" { + useSchema = schema[0] + } + v := tableFieldsMap.GetOrSetFuncLock( + fmt.Sprintf(`mssql_table_fields_%s_%s@group:%s`, table, useSchema, d.GetGroup()), + func() interface{} { + var ( + result gdb.Result + link gdb.Link + ) + if link, err = d.SlaveLink(useSchema); err != nil { + return nil + } + structureSql := fmt.Sprintf(` +SELECT + a.name Field, + CASE b.name + WHEN 'datetime' THEN 'datetime' + WHEN 'numeric' THEN b.name + '(' + convert(varchar(20), a.xprec) + ',' + convert(varchar(20), a.xscale) + ')' + WHEN 'char' THEN b.name + '(' + convert(varchar(20), a.length)+ ')' + WHEN 'varchar' THEN b.name + '(' + convert(varchar(20), a.length)+ ')' + ELSE b.name + '(' + convert(varchar(20),a.length)+ ')' END AS Type, + CASE WHEN a.isnullable=1 THEN 'YES' ELSE 'NO' end AS [Null], + CASE WHEN exists ( + SELECT 1 FROM sysobjects WHERE xtype='PK' AND name IN ( + SELECT name FROM sysindexes WHERE indid IN ( + SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid + ) + ) + ) THEN 'PRI' ELSE '' END AS [Key], + CASE WHEN COLUMNPROPERTY(a.id,a.name,'IsIdentity')=1 THEN 'auto_increment' ELSE '' END Extra, + isnull(e.text,'') AS [Default], + isnull(g.[value],'') AS [Comment] +FROM syscolumns a +LEFT JOIN systypes b ON a.xtype=b.xtype AND a.xusertype=b.xusertype +INNER JOIN sysobjects d ON a.id=d.id AND d.xtype='U' AND d.name<>'dtproperties' +LEFT JOIN syscomments e ON a.cdefault=e.id +LEFT JOIN sys.extended_properties g ON a.id=g.major_id AND a.colid=g.minor_id +LEFT JOIN sys.extended_properties f ON d.id=f.major_id AND f.minor_id =0 +WHERE d.name='%s' +ORDER BY a.id,a.colorder`, + strings.ToUpper(table), + ) + structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) + result, err = d.DoGetAll(ctx, link, structureSql) + if err != nil { + return nil + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + fields[strings.ToLower(m["Field"].String())] = &gdb.TableField{ + Index: i, + Name: strings.ToLower(m["Field"].String()), + Type: strings.ToLower(m["Type"].String()), + Null: m["Null"].Bool(), + Key: m["Key"].String(), + Default: m["Default"].Val(), + Extra: m["Extra"].String(), + Comment: m["Comment"].String(), + } + } + return fields + }, + ) + if v != nil { + fields = v.(map[string]*gdb.TableField) + } + return +} + +// DoInsert is not supported in mssql. +func (d *DriverMssql) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionSave: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Save operation is not supported by mssql driver`) + + case gdb.InsertOptionReplace: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Replace operation is not supported by mssql driver`) + + default: + return d.Core.DoInsert(ctx, link, table, list, option) + } +} diff --git a/contrib/drivers/mysql/mysql.go b/contrib/drivers/mysql/mysql.go new file mode 100644 index 000000000..eeced454a --- /dev/null +++ b/contrib/drivers/mysql/mysql.go @@ -0,0 +1,23 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +// Package mysql implements gdb.Driver, which supports operations for MySQL. +package mysql + +import ( + "github.com/gogf/gf/v2/database/gdb" +) + +func init() { + if err := gdb.Register(`mysql`, New()); err != nil { + panic(err) + } +} + +// New create and returns a driver that implements gdb.Driver, which supports operations for MySQL. +func New() gdb.Driver { + return &gdb.DriverMysql{} +} diff --git a/contrib/drivers/oracle/oracle.go b/contrib/drivers/oracle/oracle.go new file mode 100644 index 000000000..581a8ef62 --- /dev/null +++ b/contrib/drivers/oracle/oracle.go @@ -0,0 +1,339 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. +// +// Note: +// 1. It needs manually import: _ "github.com/mattn/go-oci8" +// 2. It does not support Save/Replace features. +// 3. It does not support LastInsertId. + +// Package oracle implements gdb.Driver, which supports operations for Oracle. +package oracle + +import ( + _ "github.com/mattn/go-oci8" + + "context" + "database/sql" + "fmt" + "reflect" + "strconv" + "strings" + "time" + + "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// DriverOracle is the driver for oracle database. +type DriverOracle struct { + *gdb.Core +} + +var ( + // tableFieldsMap caches the table information retrieved from database. + tableFieldsMap = gmap.New(true) +) + +func init() { + if err := gdb.Register(`oracle`, New()); err != nil { + panic(err) + } +} + +// New create and returns a driver that implements gdb.Driver, which supports operations for Oracle. +func New() gdb.Driver { + return &DriverOracle{} +} + +// New creates and returns a database object for oracle. +// It implements the interface of gdb.Driver for extra database driver installation. +func (d *DriverOracle) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { + return &DriverOracle{ + Core: core, + }, nil +} + +// Open creates and returns an underlying sql.DB object for oracle. +func (d *DriverOracle) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "oci8" + ) + if config.Link != "" { + source = config.Link + } else { + source = fmt.Sprintf( + "%s/%s@%s:%s/%s", + config.User, config.Pass, config.Host, config.Port, config.Name, + ) + } + + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} + +// FilteredLink retrieves and returns filtered `linkInfo` that can be using for +// logging or tracing purpose. +func (d *DriverOracle) FilteredLink() string { + linkInfo := d.GetConfig().Link + if linkInfo == "" { + return "" + } + s, _ := gregex.ReplaceString( + `(.+?)\s*/\s*(.+)\s*@\s*(.+)\s*:\s*(\d+)\s*/\s*(.+)`, + `$1/xxx@$3:$4/$5`, + linkInfo, + ) + return s +} + +// GetChars returns the security char for this type of database. +func (d *DriverOracle) GetChars() (charLeft string, charRight string) { + return "\"", "\"" +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *DriverOracle) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { + defer func() { + newSql, newArgs, err = d.Core.DoFilter(ctx, link, newSql, newArgs) + }() + + var index int + // Convert placeholder char '?' to string ":vx". + newSql, _ = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { + index++ + return fmt.Sprintf(":v%d", index) + }) + newSql, _ = gregex.ReplaceString("\"", "", newSql) + // Handle string datetime argument. + for i, v := range args { + if reflect.TypeOf(v).Kind() == reflect.String { + valueStr := gconv.String(v) + if gregex.IsMatchString(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`, valueStr) { + // args[i] = fmt.Sprintf(`TO_DATE('%s','yyyy-MM-dd HH:MI:SS')`, valueStr) + args[i], _ = time.ParseInLocation("2006-01-02 15:04:05", valueStr, time.Local) + } + } + } + newSql = d.parseSql(newSql) + newArgs = args + return +} + +// parseSql does some replacement of the sql before commits it to underlying driver, +// for support of oracle server. +func (d *DriverOracle) parseSql(sql string) string { + var ( + patten = `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,{0,1}\s*(\d*))` + allMatch, _ = gregex.MatchAllString(patten, sql) + ) + if len(allMatch) == 0 { + return sql + } + var ( + index = 0 + keyword = strings.ToUpper(strings.TrimSpace(allMatch[index][0])) + ) + index++ + switch keyword { + case "SELECT": + if len(allMatch) < 2 || strings.HasPrefix(allMatch[index][0], "LIMIT") == false { + break + } + if gregex.IsMatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) == false { + break + } + queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) + if len(queryExpr) != 4 || + strings.EqualFold(queryExpr[1], "SELECT") == false || + strings.EqualFold(queryExpr[3], "LIMIT") == false { + break + } + first, limit := 0, 0 + for i := 1; i < len(allMatch[index]); i++ { + if len(strings.TrimSpace(allMatch[index][i])) == 0 { + continue + } + + if strings.HasPrefix(allMatch[index][i], "LIMIT") { + if allMatch[index][i+2] != "" { + first, _ = strconv.Atoi(allMatch[index][i+1]) + limit, _ = strconv.Atoi(allMatch[index][i+2]) + } else { + limit, _ = strconv.Atoi(allMatch[index][i+1]) + } + break + } + } + sql = fmt.Sprintf( + "SELECT * FROM "+ + "(SELECT GFORM.*, ROWNUM ROWNUM_ FROM (%s %s) GFORM WHERE ROWNUM <= %d)"+ + " WHERE ROWNUM_ >= %d", + queryExpr[1], queryExpr[2], limit, first, + ) + } + return sql +} + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +// Note that it ignores the parameter `schema` in oracle database, as it is not necessary. +func (d *DriverOracle) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result gdb.Result + result, err = d.DoGetAll(ctx, nil, "SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME") + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *DriverOracle) TableFields( + ctx context.Context, table string, schema ...string, +) (fields map[string]*gdb.TableField, err error) { + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) + if gstr.Contains(table, " ") { + return nil, gerror.NewCode( + gcode.CodeInvalidParameter, + "function TableFields supports only single table operations", + ) + } + useSchema := d.GetSchema() + if len(schema) > 0 && schema[0] != "" { + useSchema = schema[0] + } + v := tableFieldsMap.GetOrSetFuncLock( + fmt.Sprintf(`oracle_table_fields_%s_%s@group:%s`, table, useSchema, d.GetGroup()), + func() interface{} { + var ( + result gdb.Result + link gdb.Link + structureSql = fmt.Sprintf(` +SELECT + COLUMN_NAME AS FIELD, + CASE DATA_TYPE + WHEN 'NUMBER' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')' + WHEN 'FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')' + ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE +FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID`, + strings.ToUpper(table), + ) + ) + if link, err = d.SlaveLink(useSchema); err != nil { + return nil + } + structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) + result, err = d.DoGetAll(ctx, link, structureSql) + if err != nil { + return nil + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + fields[strings.ToLower(m["FIELD"].String())] = &gdb.TableField{ + Index: i, + Name: strings.ToLower(m["FIELD"].String()), + Type: strings.ToLower(m["TYPE"].String()), + } + } + return fields + }, + ) + if v != nil { + fields = v.(map[string]*gdb.TableField) + } + return +} + +// DoInsert inserts or updates data for given table. +// This function is usually used for custom interface definition, you do not need call it manually. +// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. +// Eg: +// Data(g.Map{"uid": 10000, "name":"john"}) +// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) +// +// The parameter `option` values are as follows: +// 0: insert: just insert, if there's unique/primary key in the data, it returns error; +// 1: replace: if there's unique/primary key in the data, it deletes it from table and inserts a new one; +// 2: save: if there's unique/primary key in the data, it updates it or else inserts a new one; +// 3: ignore: if there's unique/primary key in the data, it ignores the inserting; +func (d *DriverOracle) DoInsert( + ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption, +) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionSave: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Save operation is not supported by mssql driver`) + + case gdb.InsertOptionReplace: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Replace operation is not supported by mssql driver`) + } + + var ( + keys []string + values []string + params []interface{} + ) + // Retrieve the table fields and length. + var ( + listLength = len(list) + valueHolder = make([]string, 0) + ) + for k := range list[0] { + keys = append(keys, k) + valueHolder = append(valueHolder, "?") + } + var ( + batchResult = new(gdb.SqlResult) + charL, charR = d.GetChars() + keyStr = charL + strings.Join(keys, charL+","+charR) + charR + valueHolderStr = strings.Join(valueHolder, ",") + ) + // Format "INSERT...INTO..." statement. + intoStr := make([]string, 0) + for i := 0; i < len(list); i++ { + for _, k := range keys { + params = append(params, list[i][k]) + } + values = append(values, valueHolderStr) + intoStr = append(intoStr, fmt.Sprintf("INTO %s(%s) VALUES(%s)", table, keyStr, valueHolderStr)) + if len(intoStr) == option.BatchCount || (i == listLength-1 && len(valueHolder) > 0) { + r, err := d.DoExec(ctx, link, fmt.Sprintf( + "INSERT ALL %s SELECT * FROM DUAL", + strings.Join(intoStr, " "), + ), params...) + if err != nil { + return r, err + } + if n, err := r.RowsAffected(); err != nil { + return r, err + } else { + batchResult.Result = r + batchResult.Affected += n + } + params = params[:0] + intoStr = intoStr[:0] + } + } + return batchResult, nil +} diff --git a/contrib/drivers/pgsql/pgsql.go b/contrib/drivers/pgsql/pgsql.go new file mode 100644 index 000000000..5ed9af2f6 --- /dev/null +++ b/contrib/drivers/pgsql/pgsql.go @@ -0,0 +1,245 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. +// +// Note: +// 1. It needs manually import: _ "github.com/lib/pq" +// 2. It does not support Save/Replace features. +// 3. It does not support LastInsertId. + +// Package pgsql implements gdb.Driver, which supports operations for PostgreSql. +package pgsql + +import ( + _ "github.com/lib/pq" + + "context" + "database/sql" + "database/sql/driver" + "fmt" + "strings" + + "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +// DriverPgsql is the driver for postgresql database. +type DriverPgsql struct { + *gdb.Core +} + +var ( + // tableFieldsMap caches the table information retrieved from database. + tableFieldsMap = gmap.New(true) +) + +func init() { + if err := gdb.Register(`pgsql`, New()); err != nil { + panic(err) + } +} + +// New create and returns a driver that implements gdb.Driver, which supports operations for PostgreSql. +func New() gdb.Driver { + return &DriverPgsql{} +} + +// New creates and returns a database object for postgresql. +// It implements the interface of gdb.Driver for extra database driver installation. +func (d *DriverPgsql) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { + return &DriverPgsql{ + Core: core, + }, nil +} + +// Open creates and returns an underlying sql.DB object for pgsql. +func (d *DriverPgsql) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "postgres" + ) + if config.Link != "" { + source = config.Link + } else { + source = fmt.Sprintf( + "user=%s password=%s host=%s port=%s dbname=%s sslmode=disable", + config.User, config.Pass, config.Host, config.Port, config.Name, + ) + if config.Timezone != "" { + source = fmt.Sprintf("%s timezone=%s", source, config.Timezone) + } + } + + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} + +// FilteredLink retrieves and returns filtered `linkInfo` that can be using for +// logging or tracing purpose. +func (d *DriverPgsql) FilteredLink() string { + linkInfo := d.GetConfig().Link + if linkInfo == "" { + return "" + } + s, _ := gregex.ReplaceString( + `(.+?)\s*password=(.+)\s*host=(.+)`, + `$1 password=xxx host=$3`, + linkInfo, + ) + return s +} + +// GetChars returns the security char for this type of database. +func (d *DriverPgsql) GetChars() (charLeft string, charRight string) { + return "\"", "\"" +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *DriverPgsql) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { + defer func() { + newSql, newArgs, err = d.Core.DoFilter(ctx, link, newSql, newArgs) + }() + + var index int + // Convert placeholder char '?' to string "$x". + sql, _ = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { + index++ + return fmt.Sprintf("$%d", index) + }) + newSql, _ = gregex.ReplaceString(` LIMIT (\d+),\s*(\d+)`, ` LIMIT $2 OFFSET $1`, sql) + return newSql, args, nil +} + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *DriverPgsql) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result gdb.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + query := "SELECT TABLENAME FROM PG_TABLES WHERE SCHEMANAME = 'public' ORDER BY TABLENAME" + if len(schema) > 0 && schema[0] != "" { + query = fmt.Sprintf("SELECT TABLENAME FROM PG_TABLES WHERE SCHEMANAME = '%s' ORDER BY TABLENAME", schema[0]) + } + result, err = d.DoGetAll(ctx, link, query) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *DriverPgsql) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*gdb.TableField, err error) { + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) + if gstr.Contains(table, " ") { + return nil, gerror.NewCode(gcode.CodeInvalidParameter, "function TableFields supports only single table operations") + } + table, _ = gregex.ReplaceString("\"", "", table) + useSchema := d.GetSchema() + if len(schema) > 0 && schema[0] != "" { + useSchema = schema[0] + } + v := tableFieldsMap.GetOrSetFuncLock( + fmt.Sprintf(`pgsql_table_fields_%s_%s@group:%s`, table, useSchema, d.GetGroup()), + func() interface{} { + var ( + result gdb.Result + link gdb.Link + structureSql = fmt.Sprintf(` +SELECT a.attname AS field, t.typname AS type,a.attnotnull as null, + (case when d.contype is not null then 'pri' else '' end) as key + ,ic.column_default as default_value,b.description as comment + ,coalesce(character_maximum_length, numeric_precision, -1) as length + ,numeric_scale as scale +FROM pg_attribute a + left join pg_class c on a.attrelid = c.oid + left join pg_constraint d on d.conrelid = c.oid and a.attnum = d.conkey[1] + left join pg_description b ON a.attrelid=b.objoid AND a.attnum = b.objsubid + left join pg_type t ON a.atttypid = t.oid + left join information_schema.columns ic on ic.column_name = a.attname and ic.table_name = c.relname +WHERE c.relname = '%s' and a.attisdropped is false and a.attnum > 0 +ORDER BY a.attnum`, + strings.ToLower(table), + ) + ) + if link, err = d.SlaveLink(useSchema); err != nil { + return nil + } + structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) + result, err = d.DoGetAll(ctx, link, structureSql) + if err != nil { + return nil + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + fields[m["field"].String()] = &gdb.TableField{ + Index: i, + Name: m["field"].String(), + Type: m["type"].String(), + Null: m["null"].Bool(), + Key: m["key"].String(), + Default: m["default_value"].Val(), + Comment: m["comment"].String(), + } + } + return fields + }, + ) + if v != nil { + fields = v.(map[string]*gdb.TableField) + } + return +} + +// DoInsert is not supported in pgsql. +func (d *DriverPgsql) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionSave: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Save operation is not supported by pgsql driver`) + + case gdb.InsertOptionReplace: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Replace operation is not supported by pgsql driver`) + + default: + return d.Core.DoInsert(ctx, link, table, list, option) + } +} + +// ConvertDataForRecord converting for any data that will be inserted into table/collection as a record. +func (d *DriverPgsql) ConvertDataForRecord(ctx context.Context, value interface{}) map[string]interface{} { + data := gdb.DataToMapDeep(value) + var err error + for k, v := range data { + if valuer, ok := v.(driver.Valuer); ok { + data[k], err = valuer.Value() + if err != nil { + return nil + } + } else { + data[k] = d.Core.ConvertDataForRecordValue(ctx, v) + } + } + + return data +} diff --git a/contrib/drivers/sqlite/sqlite.go b/contrib/drivers/sqlite/sqlite.go new file mode 100644 index 000000000..08cbdc339 --- /dev/null +++ b/contrib/drivers/sqlite/sqlite.go @@ -0,0 +1,177 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. +// +// Note: +// 1. It needs manually import: _ "github.com/mattn/go-sqlite3" +// 2. It does not support Save/Replace features. + +// Package sqlite implements gdb.Driver, which supports operations for SQLite. +package sqlite + +import ( + _ "github.com/mattn/go-sqlite3" + + "context" + "database/sql" + "fmt" + "strings" + + "github.com/gogf/gf/v2/container/gmap" + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" +) + +// DriverSqlite is the driver for sqlite database. +type DriverSqlite struct { + *gdb.Core +} + +var ( + // tableFieldsMap caches the table information retrieved from database. + tableFieldsMap = gmap.New(true) +) + +func init() { + if err := gdb.Register(`sqlite`, New()); err != nil { + panic(err) + } +} + +// New create and returns a driver that implements gdb.Driver, which supports operations for SQLite. +func New() gdb.Driver { + return &DriverSqlite{} +} + +// New creates and returns a database object for sqlite. +// It implements the interface of gdb.Driver for extra database driver installation. +func (d *DriverSqlite) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { + return &DriverSqlite{ + Core: core, + }, nil +} + +// Open creates and returns a underlying sql.DB object for sqlite. +func (d *DriverSqlite) Open(config *gdb.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "sqlite3" + ) + if config.Link != "" { + source = config.Link + } else { + source = config.Name + } + // It searches the source file to locate its absolute path.. + if absolutePath, _ := gfile.Search(source); absolutePath != "" { + source = absolutePath + } + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} + +// FilteredLink retrieves and returns filtered `linkInfo` that can be using for +// logging or tracing purpose. +func (d *DriverSqlite) FilteredLink() string { + return d.GetConfig().Link +} + +// GetChars returns the security char for this type of database. +func (d *DriverSqlite) GetChars() (charLeft string, charRight string) { + return "`", "`" +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *DriverSqlite) DoFilter(ctx context.Context, link gdb.Link, sql string, args []interface{}) (newSql string, newArgs []interface{}, err error) { + return d.Core.DoFilter(ctx, link, sql, args) +} + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *DriverSqlite) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result gdb.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + + result, err = d.DoGetAll(ctx, link, `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME`) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *DriverSqlite) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*gdb.TableField, err error) { + charL, charR := d.GetChars() + table = gstr.Trim(table, charL+charR) + if gstr.Contains(table, " ") { + return nil, gerror.NewCode(gcode.CodeInvalidParameter, "function TableFields supports only single table operations") + } + useSchema := d.GetSchema() + if len(schema) > 0 && schema[0] != "" { + useSchema = schema[0] + } + v := tableFieldsMap.GetOrSetFuncLock( + fmt.Sprintf(`sqlite_table_fields_%s_%s@group:%s`, table, useSchema, d.GetGroup()), + func() interface{} { + var ( + result gdb.Result + link gdb.Link + ) + if link, err = d.SlaveLink(useSchema); err != nil { + return nil + } + result, err = d.DoGetAll(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, table)) + if err != nil { + return nil + } + fields = make(map[string]*gdb.TableField) + for i, m := range result { + fields[strings.ToLower(m["name"].String())] = &gdb.TableField{ + Index: i, + Name: strings.ToLower(m["name"].String()), + Type: strings.ToLower(m["type"].String()), + } + } + return fields + }, + ) + if v != nil { + fields = v.(map[string]*gdb.TableField) + } + return +} + +// DoInsert is not supported in sqlite. +func (d *DriverSqlite) DoInsert(ctx context.Context, link gdb.Link, table string, list gdb.List, option gdb.DoInsertOption) (result sql.Result, err error) { + switch option.InsertOption { + case gdb.InsertOptionSave: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Save operation is not supported by sqlite driver`) + + case gdb.InsertOptionReplace: + return nil, gerror.NewCode(gcode.CodeNotSupported, `Replace operation is not supported by sqlite driver`) + + default: + return d.Core.DoInsert(ctx, link, table, list, option) + } +}