├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── changelog.yml ├── dependabot.yml └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .nojekyll ├── LICENSE ├── Makefile ├── README.md ├── README.zh-CN.md ├── _examples ├── README.md ├── cliapp │ └── main.go ├── cmd │ ├── color_usage.go │ ├── daemon_run.go │ ├── emoji_demo.go │ ├── env_info.go │ ├── example.go │ ├── git.go │ ├── git_info.go │ ├── git_pull_multi.go │ ├── git_remote.go │ ├── interact_demo.go │ ├── progress_demo.go │ ├── show_demo.go │ └── spinner_demo.go ├── emojitest.go ├── ggit │ └── main.go ├── images │ ├── README.md │ ├── app-version.jpg │ ├── auto-complete-tips.jpg │ ├── cmd-help.png │ ├── cmd-list.png │ ├── color │ │ ├── basic-color.jpg │ │ ├── color-demo.jpg │ │ └── color-tags.jpg │ ├── err-cmd-tips.jpg │ ├── interact │ │ ├── confirm.jpg │ │ ├── m-select.jpg │ │ ├── passwd.jpg │ │ ├── read.jpg │ │ └── select.jpg │ ├── progress │ │ ├── prog-bar.png │ │ ├── prog-bar.svg │ │ ├── prog-other.jpg │ │ ├── prog-rt.jpg │ │ ├── prog-spinner.jpg │ │ ├── prog-spinner1.jpg │ │ └── prog-txt.svg │ ├── run-example.jpg │ └── run-example.png ├── multilevel │ └── main.go ├── navbar.md ├── rawflag.go ├── related_pkg.md ├── serveman │ ├── commands.go │ └── main.go ├── sflag │ ├── README.md │ ├── parser.go │ ├── parser_test.go │ └── value_getter.go └── simpleone │ └── main.go ├── app.go ├── app_test.go ├── base.go ├── base_test.go ├── builtin ├── gen_auto_complete.go ├── gen_emoji_codeMap.go ├── gen_gcli_command.go ├── genac │ ├── gen_bash.go │ ├── gen_zsh.go │ └── genac.go ├── launcheditor │ └── launch_editer.go ├── reverseproxy │ └── reverse_proxy.go └── tcpproxy │ └── tcp_proxy.go ├── cmd.go ├── cmd_test.go ├── docs └── guide │ └── chapter-01.md ├── events └── events.go ├── ext.go ├── ext_test.go ├── gcli.go ├── gcli_test.go ├── gflag ├── README.md ├── args.go ├── args_test.go ├── flags.go ├── gflag.go ├── gflag_test.go ├── help.go ├── opts.go ├── opts_test.go ├── parser.go ├── util.go └── value.go ├── go.mod ├── go.sum ├── help.go ├── helper ├── clog.go ├── download.go ├── utils.go ├── utils_nonwin.go └── utils_windows.go ├── index.html ├── interact ├── README.md ├── base.go ├── collector.go ├── collector_test.go ├── control │ └── control.go ├── cparam │ ├── choices.go │ ├── cparam.go │ ├── multi_choices.go │ └── string.go ├── images │ └── select.png ├── interact.go ├── prompt.go ├── question.go ├── question_test.go ├── read.go ├── read_nonwin.go ├── read_windows.go ├── select.go └── steps.go ├── internal ├── cmd-run-flow.puml └── help_tpl.go ├── progress ├── README.md ├── helper.go ├── progress.go ├── progress_test.go ├── quickstart.go ├── spinner.go ├── spinner_test.go └── widgets.go ├── resource ├── Changelog-TODO.md ├── Dockerfile ├── auto-completion │ ├── about-auto-complete.md │ ├── auto-completion.bash │ ├── auto-completion.zsh │ ├── composer.plg.zsh │ ├── golang.plg.zsh │ └── sf-console.zsh ├── dev.md ├── emojis.txt ├── gcli-cmd-code.tpl └── resource.md ├── show ├── README.md ├── alert.go ├── banner.go ├── base.go ├── emoji │ ├── emoji.go │ ├── emoji_map.go │ ├── simple_emoji.go │ └── some-record.md ├── json.go ├── list.go ├── show.go ├── show_test.go ├── symbols │ └── chars.go ├── table │ ├── style.go │ ├── table.go │ └── table_test.go ├── title.go └── writer.go ├── util.go └── util_test.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: inhere 7 | 8 | --- 9 | 10 | **System (please complete the following information):** 11 | 12 | - OS: `linux` [e.g. linux, macOS] 13 | - GO Version: `1.13` [e.g. `1.13`] 14 | - Pkg Version: `1.1.1` [e.g. `1.1.1`] 15 | 16 | **Describe the bug** 17 | 18 | A clear and concise description of what the bug is. 19 | 20 | **To Reproduce** 21 | 22 | ```go 23 | // go code 24 | ``` 25 | 26 | **Expected behavior** 27 | 28 | A clear and concise description of what you expected to happen. 29 | 30 | **Screenshots** 31 | 32 | If applicable, add screenshots to help explain your problem. 33 | 34 | **Additional context** 35 | 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | title: '## Change Log' 2 | # style allow: simple, markdown(mkdown), ghr(gh-release) 3 | style: gh-release 4 | # group names 5 | names: [Refactor, Fixed, Feature, Update, Other] 6 | # if empty will auto fetch by git remote 7 | #repo_url: https://github.com/gookit/goutil 8 | 9 | filters: 10 | # message length should >= 12 11 | - name: msg_len 12 | min_len: 12 13 | # message words should >= 3 14 | - name: words_len 15 | min_len: 3 16 | - name: keyword 17 | keyword: format code 18 | exclude: true 19 | - name: keywords 20 | keywords: format code, action test 21 | exclude: true 22 | 23 | # group match rules 24 | # not matched will use 'Other' group. 25 | rules: 26 | - name: Refactor 27 | start_withs: [refactor, break] 28 | contains: ['refactor:'] 29 | - name: Fixed 30 | start_withs: [fix] 31 | contains: ['fix:'] 32 | - name: Feature 33 | start_withs: [feat, new] 34 | contains: [feature, 'feat:'] 35 | - name: Update 36 | start_withs: [up] 37 | contains: ['update:', 'up:'] 38 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every weekday 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: action-tests 2 | on: 3 | pull_request: 4 | paths: 5 | - 'go.mod' 6 | - '**.go' 7 | - '**.yml' 8 | push: 9 | paths: 10 | - 'go.mod' 11 | - '**.go' 12 | - '**.yml' 13 | 14 | jobs: 15 | 16 | test: 17 | name: Test on go ${{ matrix.go_version }} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | go_version: [1.18, 1.19, '1.20'] 22 | 23 | steps: 24 | - name: Check out codes 25 | uses: actions/checkout@v4 26 | 27 | - name: Setup Go Faster 28 | uses: WillAbides/setup-go-faster@v1.14.0 29 | timeout-minutes: 3 30 | with: 31 | go-version: ${{ matrix.go_version }} 32 | 33 | - name: Revive check 34 | uses: morphy2k/revive-action@v2.7.5 35 | if: ${{ matrix.os == 'ubuntu-latest' }} 36 | with: 37 | # Exclude patterns, separated by semicolons (optional) 38 | exclude: "./internal/..." 39 | 40 | - name: Run staticcheck 41 | uses: reviewdog/action-staticcheck@v1 42 | if: ${{ github.event_name == 'pull_request'}} 43 | with: 44 | github_token: ${{ secrets.github_token }} 45 | # Change reviewdog reporter if you need [github-pr-check,github-check,github-pr-review]. 46 | reporter: github-pr-check 47 | # Report all results. [added,diff_context,file,nofilter]. 48 | filter_mode: added 49 | # Exit with 1 when it find at least one finding. 50 | fail_on_error: true 51 | 52 | - name: Run tests 53 | run: go test -v -cover ./... 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release new version 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | strategy: 14 | fail-fast: true 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Setup ENV 23 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 24 | run: | 25 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 26 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 27 | 28 | - name: Generate changelog 29 | run: | 30 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 31 | chmod a+x /usr/local/bin/chlog 32 | chlog -c .github/changelog.yml -o changelog.md prev last 33 | 34 | # https://github.com/softprops/action-gh-release 35 | - name: Create release and upload assets 36 | uses: softprops/action-gh-release@v2 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | name: ${{ env.RELEASE_TAG }} 41 | tag_name: ${{ env.RELEASE_TAG }} 42 | body_path: changelog.md 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | # files: macos-chlog.exe 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.swp 3 | .idea 4 | *.patch 5 | ### Go template 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | /alone 19 | /ggit 20 | /cliapp 21 | .DS_Store 22 | # shell script 23 | /*.bash 24 | /*.sh 25 | /*.zsh 26 | /*.pid 27 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/.nojekyll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # link https://github.com/humbug/box/blob/master/Makefile 2 | #SHELL = /bin/sh 3 | .DEFAULT_GOAL := help 4 | # 每行命令之前必须有一个tab键。如果想用其他键,可以用内置变量.RECIPEPREFIX 声明 5 | # mac 下这条声明 没起作用 !! 6 | .RECIPEPREFIX = > 7 | .PHONY: all usage help clean 8 | 9 | # 需要注意的是,每行命令在一个单独的shell中执行。这些Shell之间没有继承关系。 10 | # - 解决办法是将两行命令写在一行,中间用分号分隔。 11 | # - 或者在换行符前加反斜杠转义 \ 12 | 13 | # 接收命令行传入参数 make COMMAND tag=v2.0.4 14 | # TAG=$(tag) 15 | 16 | ##there some make command for the project 17 | ## 18 | 19 | help: 20 | @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | sed -e 's/: / /' 21 | 22 | ##Available Commands: 23 | 24 | clean: ## Clean all created artifacts 25 | clean: 26 | git clean --exclude=.idea/ -fdx 27 | 28 | cs-fix: ## Fix code style for all files 29 | cs-fix: 30 | gofmt -w ./ 31 | 32 | cs-diff: ## Display code style error files 33 | cs-diff: 34 | gofmt -l ./ 35 | -------------------------------------------------------------------------------- /_examples/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | ## run example 4 | 5 | ### cli application 6 | 7 | show help: 8 | 9 | ```bash 10 | go run ./cliapp -h 11 | ``` 12 | 13 | run application: 14 | 15 | ```bash 16 | go run ./cliapp demo 17 | ``` 18 | 19 | ### Only one command 20 | 21 | show help: 22 | 23 | ```bash 24 | go run ./simpleone -h 25 | ``` 26 | 27 | run command: 28 | 29 | ```bash 30 | go run ./simpleone 31 | ``` 32 | 33 | ### Multi level commands 34 | 35 | show help: 36 | 37 | ```bash 38 | go run ./multilevel -h 39 | ``` 40 | 41 | run command: 42 | 43 | ```bash 44 | go run ./multilevel 45 | ``` 46 | 47 | ### Simple git commands 48 | 49 | show help: 50 | 51 | ```bash 52 | go run ./ggit -h 53 | ``` 54 | 55 | run command: 56 | 57 | ```bash 58 | go run ./ggit 59 | ``` 60 | -------------------------------------------------------------------------------- /_examples/cliapp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/color" 7 | "github.com/gookit/gcli/v3" 8 | "github.com/gookit/gcli/v3/_examples/cmd" 9 | "github.com/gookit/gcli/v3/builtin" 10 | "github.com/gookit/gcli/v3/events" 11 | // "github.com/gookit/gcli/v3/builtin/filewatcher" 12 | // "github.com/gookit/gcli/v3/builtin/reverseproxy" 13 | ) 14 | 15 | var customGOpt string 16 | 17 | // local run: 18 | // 19 | // go run ./_examples/cliapp 20 | // go build ./_examples/cliapp && ./cliapp 21 | // 22 | // run on windows(cmd, powerShell): 23 | // 24 | // go run ./_examples/cliapp 25 | // go build ./_examples/cliapp && ./cliapp 26 | func main() { 27 | app := gcli.NewApp(func(app *gcli.App) { 28 | app.Version = "3.0.0" 29 | app.Desc = "this is my cli application" 30 | app.On(gcli.EvtAppInit, func(ctx *gcli.HookCtx) bool { 31 | // do something... 32 | fmt.Println("init app event", ctx.Name()) 33 | return false 34 | }) 35 | 36 | // app.SetVerbose(gcli.VerbDebug) 37 | // app.DefaultCommand("example") 38 | app.Logo.Text = ` ________ _______ 39 | / ____/ / / _/ | ____ ____ 40 | / / / / / // /| | / __ \/ __ \ 41 | / /___/ /____/ // ___ |/ /_/ / /_/ / 42 | \____/_____/___/_/ |_/ .___/ .___/ 43 | /_/ /_/` 44 | }) 45 | 46 | // disable global options 47 | // gcli.GOpts().SetDisable() 48 | 49 | // app.BeforeAddOpts = func(opts *gcli.Flags) { 50 | // opts.StrVar(&customGOpt, &gcli.CliOpt{Name: "custom", Desc: "desc message for the option"}) 51 | // } 52 | 53 | app.On(events.OnAppBindOptsAfter, func(ctx *gcli.HookCtx) (stop bool) { 54 | ctx.App.Flags().StrVar(&customGOpt, &gcli.CliOpt{ 55 | Name: "custom", 56 | Desc: "desc message for the option", 57 | }) 58 | return false 59 | }) 60 | 61 | // app.Strict = true 62 | app.Add(cmd.GitCmd) 63 | 64 | app.Add(cmd.Example) 65 | app.Add(cmd.DaemonRun) 66 | app.Add(cmd.EnvInfo) 67 | app.Add(cmd.CliColor, cmd.EmojiDemo) 68 | app.Add( 69 | cmd.ShowDemo, 70 | cmd.ProgressDemo, 71 | cmd.SpinnerDemo, 72 | cmd.InteractDemo, 73 | ) 74 | 75 | app.Add(builtin.GenEmojiMap) 76 | app.Add(builtin.GenAutoComplete()) 77 | 78 | // app.Add(filewatcher.FileWatcher(nil)) 79 | // app.Add(reverseproxy.ReverseProxyCommand()) 80 | 81 | app.Add(&gcli.Command{ 82 | Name: "test", 83 | Aliases: []string{"ts"}, 84 | Desc: "this is a description message for command {$cmd}", 85 | Func: func(cmd *gcli.Command, args []string) error { 86 | gcli.Print("hello, in the test command\n") 87 | return nil 88 | }, 89 | }) 90 | 91 | // create by func 92 | gcli.NewCommand("test1", "description1", func(c *gcli.Command) { 93 | // some config for the command 94 | }).WithFunc(func(c *gcli.Command, args []string) error { 95 | color.Green.Println("hello, command is: ", c.Name) 96 | return nil 97 | }).AttachTo(app) 98 | 99 | // fmt.Printf("%+v\n", gcli.CommandNames()) 100 | 101 | // running 102 | app.Run(nil) 103 | } 104 | -------------------------------------------------------------------------------- /_examples/cmd/color_usage.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/color" 7 | "github.com/gookit/gcli/v3" 8 | ) 9 | 10 | var colorOpts = struct { 11 | id int 12 | c string 13 | dir string 14 | }{} 15 | 16 | // CliColor command definition 17 | var CliColor = &gcli.Command{ 18 | Name: "color", 19 | Desc: "this is a example for cli color usage", 20 | Aliases: []string{"clr", "colors"}, 21 | Func: colorUsage, 22 | Examples: "{$binName} {$cmd} --id 12 -c val ag0 ag1", 23 | Config: func(c *gcli.Command) { 24 | c.IntOpt(&colorOpts.id, "id", "", 2, "the id option") 25 | c.StrOpt(&colorOpts.c, "c", "", "value", "the config option") 26 | c.StrOpt(&colorOpts.dir, "dir", "", "", "the dir option") 27 | 28 | }, 29 | } 30 | 31 | func colorUsage(_ *gcli.Command, _ []string) error { 32 | // simple usage 33 | color.FgCyan.Printf("Simple to use %s\n", "color") 34 | 35 | // custom color 36 | color.New(color.FgMagenta, color.BgBlack).Println("custom color style") 37 | // can also: 38 | color.Style{color.FgCyan, color.OpBold}.Println("custom color style") 39 | 40 | // use defined color tag 41 | color.Print("hello, welcome\n") 42 | 43 | // use custom color tag 44 | color.Print("hello, welcome\n") 45 | 46 | // set a color tag 47 | color.Tag("info").Println("info style message") 48 | 49 | // prompt message 50 | color.Info.Prompt("prompt style message") 51 | color.Warn.Prompt("prompt style message") 52 | 53 | // tips message 54 | color.Info.Tips("tips style message") 55 | color.Warn.Tips("tips style message") 56 | 57 | i := 0 58 | fmt.Print("\n- All Available color Tags: \n\n") 59 | 60 | for tag := range color.GetColorTags() { 61 | i++ 62 | color.Tag(tag).Print(tag) 63 | 64 | if i%5 == 0 { 65 | fmt.Print("\n") 66 | } else { 67 | fmt.Print(" ") 68 | } 69 | } 70 | fmt.Print("\n") 71 | 72 | return nil 73 | } 74 | 75 | func byte8color() { 76 | 77 | } 78 | -------------------------------------------------------------------------------- /_examples/cmd/daemon_run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "time" 9 | 10 | "github.com/gookit/color" 11 | "github.com/gookit/gcli/v3" 12 | ) 13 | 14 | var bgrOpts = struct { 15 | deamon bool 16 | }{} 17 | 18 | var DaemonRun = &gcli.Command{ 19 | Name: "bgrun", 20 | Desc: "an example for background run program", 21 | Func: handleDaemonRun, 22 | Aliases: []string{"bgr"}, 23 | Config: func(c *gcli.Command) { 24 | c.BoolOpt(&bgrOpts.deamon, "daemon", "d", false, "want background run") 25 | 26 | }, 27 | } 28 | 29 | func handleDaemonRun(c *gcli.Command, _ []string) (err error) { 30 | if bgrOpts.deamon { 31 | newArgs := clearDaemonOpt("--daemon", "-d", c.Ctx.OsArgs()[1:]) 32 | newCmd := exec.Command(c.BinName(), newArgs...) 33 | 34 | if err = newCmd.Start(); err != nil { 35 | return 36 | } 37 | 38 | pid := newCmd.Process.Pid 39 | color.Magenta.Printf("server start, process [PID:%d] running...\n", pid) 40 | 41 | err = ioutil.WriteFile("./server.pid", []byte(fmt.Sprintf("%d", pid)), 0666) 42 | if err != nil { 43 | return 44 | } 45 | 46 | bgrOpts.deamon = false 47 | os.Exit(0) 48 | } 49 | 50 | // block process 51 | for { 52 | fmt.Println(time.Now()) 53 | time.Sleep(time.Second * 2) 54 | } 55 | } 56 | 57 | // newArgs := clearDaemonOpt("--daemon", "-d", c.OsArgs()[1:]) 58 | func clearDaemonOpt(name, short string, args []string) []string { 59 | var newArgs []string 60 | for _, val := range args { 61 | if val == name || val == short { 62 | continue 63 | } 64 | 65 | newArgs = append(newArgs, val) 66 | } 67 | 68 | return newArgs 69 | } 70 | -------------------------------------------------------------------------------- /_examples/cmd/emoji_demo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/color" 7 | "github.com/gookit/gcli/v3" 8 | "github.com/gookit/gcli/v3/show/emoji" 9 | ) 10 | 11 | var EmojiDemo = &gcli.Command{ 12 | Name: "emoji", 13 | Desc: "this is a emoji usage example command", 14 | Aliases: []string{"emoj"}, 15 | // Func: , 16 | Examples: ` 17 | An render example 18 | {$fullCmd} render ":car: a message text, contains emoji :smile:" 19 | An search example 20 | {$fullCmd} search smi`, 21 | Subs: []*gcli.Command{ 22 | { 23 | Name: "render", 24 | Desc: "render given string, will replace special char to emoji", 25 | Aliases: []string{"r"}, 26 | Config: func(c *gcli.Command) { 27 | c.AddArg("msg", "The message string for render", true) 28 | }, 29 | Func: func(c *gcli.Command, args []string) error { 30 | fmt.Println(emoji.Render(c.Arg("msg").String())) 31 | return nil 32 | }, 33 | }, 34 | { 35 | Name: "search", 36 | Desc: "search emojis by given keywords", 37 | Aliases: []string{"s"}, 38 | Config: func(c *gcli.Command) { 39 | c.AddArg("keyword", "The keyword string for search", true) 40 | }, 41 | Func: func(c *gcli.Command, args []string) error { 42 | kw := c.Arg("keyword").String() 43 | 44 | return searchEmoji(kw) 45 | }, 46 | }, 47 | }, 48 | } 49 | 50 | func searchEmoji(kw string) (err error) { 51 | ret := emoji.Search(kw, 15) 52 | if len(ret) == 0 { 53 | color.Note.Tips(":( no matched emoji found! keyword: %s", kw) 54 | return 55 | } 56 | 57 | color.Success.Println("OK, successfully found some emojis:") 58 | for name, code := range ret { 59 | fmt.Println(code, name) 60 | } 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /_examples/cmd/env_info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "github.com/gookit/gcli/v3" 8 | "github.com/gookit/gcli/v3/show" 9 | ) 10 | 11 | // options for the command 12 | var eiOpts = struct { 13 | id int 14 | c string 15 | dir string 16 | opt string 17 | names Names 18 | }{} 19 | 20 | // EnvInfo command 21 | var EnvInfo = &gcli.Command{ 22 | Name: "env", 23 | Desc: "collect project info by git info", 24 | Aliases: []string{"env-info", "ei"}, 25 | Config: func(c *gcli.Command) { 26 | c.IntOpt(&eiOpts.id, "id", "", 0, "the id option") 27 | c.StrOpt(&eiOpts.c, "c", "", "", "the config option") 28 | c.StrOpt(&eiOpts.dir, "dir", "d", "", "the dir option") 29 | 30 | }, 31 | 32 | Func: func(c *gcli.Command, _ []string) error { 33 | eAble, _ := os.Executable() 34 | 35 | data := map[string]any{ 36 | "os": runtime.GOOS, 37 | "binName": c.BinName(), 38 | "workDir": c.WorkDir(), 39 | "rawArgs": os.Args, 40 | "execAble": eAble, 41 | "env": os.Environ(), 42 | } 43 | 44 | show.JSON(&data) 45 | return nil 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /_examples/cmd/example.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/color" 7 | "github.com/gookit/gcli/v3" 8 | "github.com/gookit/goutil/dump" 9 | ) 10 | 11 | // Names The string flag list, implemented flag.Value interface 12 | type Names []string 13 | 14 | func (ns *Names) String() string { 15 | return fmt.Sprint(*ns) 16 | } 17 | 18 | func (ns *Names) Set(value string) error { 19 | *ns = append(*ns, value) 20 | return nil 21 | } 22 | 23 | // options for the command 24 | var exampleOpts = struct { 25 | id int 26 | c string 27 | dir string 28 | opt string 29 | showErr bool 30 | names Names 31 | }{} 32 | 33 | // Example command definition 34 | var Example = &gcli.Command{ 35 | Func: exampleExecute, 36 | Name: "example", 37 | Aliases: []string{"module-exp", "exp", "ex"}, 38 | Desc: "this is command description message", 39 | // {$binName} {$cmd} is help vars. '{$cmd}' will replace to 'example' 40 | Examples: ` 41 | {$binName} {$cmd} --id 12 -c val ag0 ag1 42 | {$fullCmd} --names tom --names john -n c test use special option 43 | `, 44 | Config: func(c *gcli.Command) { 45 | // bind options 46 | c.IntOpt(&exampleOpts.id, "id", "", 2, "the id option") 47 | c.BoolOpt(&exampleOpts.showErr, "err", "e", false, "display error example") 48 | c.StrOpt(&exampleOpts.c, "config", "c", "value", "the config option") 49 | // notice `DIRECTORY` will replace to option value type 50 | c.StrOpt(&exampleOpts.dir, "dir", "d", "", "the `DIRECTORY` option") 51 | // setting option name and short-option name 52 | c.StrOpt(&exampleOpts.opt, "opt", "o", "", "the option message") 53 | // setting a special option var, it must implement the flag.Value interface 54 | c.VarOpt(&exampleOpts.names, "names", "n", "the option message") 55 | 56 | // bind args with names 57 | c.AddArg("arg0", "the first argument, is required", true) 58 | c.AddArg("arg1", "the second argument, is required", true) 59 | c.AddArg("arg2", "the optional argument, is optional") 60 | c.AddArg("arrArg", "the array argument, is array", false, true) 61 | 62 | }, 63 | } 64 | 65 | // command running 66 | // example run: 67 | // 68 | // go run ./_examples/cliapp.go ex -c some.txt -d ./dir --id 34 -n tom -n john val0 val1 val2 arrVal0 arrVal1 arrVal2 69 | func exampleExecute(c *gcli.Command, args []string) error { 70 | color.Infoln("hello, in example command") 71 | 72 | if exampleOpts.showErr { 73 | return c.NewErrf("OO, An error has occurred!!") 74 | } 75 | 76 | magentaln := color.Magenta.Println 77 | 78 | color.Cyanln("All Aptions:") 79 | // fmt.Printf("%+v\n", exampleOpts) 80 | dump.V(exampleOpts) 81 | 82 | color.Cyanln("Remain Args:") 83 | // fmt.Printf("%v\n", args) 84 | dump.P(args) 85 | 86 | magentaln("Get arg by name:") 87 | arr := c.Arg("arg0") 88 | fmt.Printf("named arg '%s', value: %#v\n", arr.Name, arr.Value) 89 | 90 | magentaln("All named args:") 91 | for _, arg := range c.Args() { 92 | fmt.Printf("- named arg '%s': %+v\n", arg.Name, arg.Value) 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /_examples/cmd/git.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/gookit/gcli/v3" 4 | 5 | var GitCmd = &gcli.Command{ 6 | Name: "git", 7 | Desc: "git usage example", 8 | Subs: []*gcli.Command{ 9 | GitInfo, GitPullMulti, GitRemote, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /_examples/cmd/git_info.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gookit/color" 8 | "github.com/gookit/gcli/v3" 9 | "github.com/gookit/goutil/sysutil" 10 | ) 11 | 12 | var gitOpts = struct { 13 | id int 14 | c string 15 | dir string 16 | }{} 17 | 18 | type GitInfoData struct { 19 | Tag string `json:"tag" description:"get tag name"` 20 | Version string `json:"version" description:"git repo version."` 21 | ReleaseAt string `json:"releaseAt" description:"latest commit date"` 22 | } 23 | 24 | // GitInfo git info command 25 | var GitInfo = &gcli.Command{ 26 | Name: "info", 27 | // Aliases: []string{"git-info"}, 28 | Desc: "collect project latest commit info by git log command", 29 | Config: func(c *gcli.Command) { 30 | c.IntOpt(&gitOpts.id, "id", "", 0, "the id option") 31 | c.StrOpt(&gitOpts.c, "c", "", "", "the config option") 32 | c.StrOpt(&gitOpts.dir, "dir", "d", "", "the dir option") 33 | }, 34 | Func: gitExecute, 35 | } 36 | 37 | // arg test: 38 | // go build console/cliapp.go && ./cliapp git --id 12 -c val ag0 ag1 39 | func gitExecute(_ *gcli.Command, _ []string) error { 40 | info := GitInfoData{} 41 | 42 | // latest commit id by: git log --pretty=%H -n1 HEAD 43 | cid, err := sysutil.QuickExec("git log --pretty=%H -n1 HEAD") 44 | if err != nil { 45 | return err 46 | } 47 | 48 | cid = strings.TrimSpace(cid) 49 | fmt.Printf("commit id: %s\n", cid) 50 | info.Version = cid 51 | 52 | // latest commit date by: git log -n1 --pretty=%ci HEAD 53 | cDate, err := sysutil.QuickExec("git log -n1 --pretty=%ci HEAD") 54 | if err != nil { 55 | return err 56 | } 57 | 58 | cDate = strings.TrimSpace(cDate) 59 | info.ReleaseAt = cDate 60 | fmt.Printf("commit date: %s\n", cDate) 61 | 62 | // get tag: git describe --tags --exact-match HEAD 63 | tag, err := sysutil.QuickExec("git describe --tags --exact-match HEAD") 64 | if err != nil { 65 | // get branch: git branch -a | grep "*" 66 | br, err := sysutil.ShellExec(`git branch -a | grep "*"`, "sh") 67 | if err != nil { 68 | return err 69 | } 70 | br = strings.TrimSpace(strings.Trim(br, "*")) 71 | info.Tag = br 72 | fmt.Printf("git branch: %s\n", br) 73 | } else { 74 | tag = strings.TrimSpace(tag) 75 | info.Tag = tag 76 | fmt.Printf("latest tag: %s\n", tag) 77 | } 78 | 79 | color.Println("\nOk, project info collect completed!") 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /_examples/cmd/git_pull_multi.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/gookit/color" 11 | "github.com/gookit/gcli/v3" 12 | "github.com/gookit/goutil/fsutil" 13 | "github.com/gookit/goutil/strutil" 14 | "github.com/gookit/goutil/sysutil" 15 | ) 16 | 17 | // GitPullMulti use git pull for update multi project 18 | var GitPullMulti = &gcli.Command{ 19 | Name: "pull", 20 | Desc: "use git pull for update multi project", 21 | Aliases: []string{"pul"}, 22 | Config: func(c *gcli.Command) { 23 | c.AddArg( 24 | "basePath", 25 | "the base operate dir path. default is current dir", 26 | true, 27 | ). 28 | WithValue("./"). 29 | WithValidator(func(v any) (i any, e error) { 30 | if !fsutil.IsDir(v.(string)) { 31 | return nil, fmt.Errorf("the base path must be an exist dir") 32 | } 33 | return v, nil 34 | }) 35 | 36 | c.AddArg( 37 | "dirNames", 38 | "the operate dir names in the base path, allow multi by spaces", 39 | false, true, 40 | ) 41 | }, 42 | Examples: ` 43 | {$fullCmd} /my/workspace project1 project2 44 | `, 45 | Func: func(c *gcli.Command, _ []string) (err error) { 46 | var ret string 47 | basePath := c.Arg("basePath").String() 48 | dirNames := c.Arg("dirNames").Strings() 49 | 50 | if len(dirNames) == 0 { 51 | dirNames = getSubDirs(basePath) 52 | if len(dirNames) == 0 { 53 | return fmt.Errorf("no valid subdirs in the base path: %s", basePath) 54 | } 55 | } 56 | 57 | color.Green.Println("The operate bash path:", basePath) 58 | fmt.Println("- want updated project dir names:", dirNames) 59 | 60 | for _, name := range dirNames { 61 | ret, err = execCmd("git pull", path.Join(basePath, name)) 62 | if err != nil { 63 | return 64 | } 65 | color.Info.Println("RESULT:") 66 | fmt.Println(ret) 67 | } 68 | 69 | color.Cyan.Println("Update Complete :)") 70 | return 71 | }, 72 | } 73 | 74 | func getSubDirs(basePath string) (dirs []string) { 75 | ss, _ := filepath.Glob(basePath + "/*") 76 | for _, spath := range ss { 77 | if !fsutil.IsDir(spath) { 78 | continue 79 | } 80 | 81 | pos := strings.LastIndexByte(spath, os.PathSeparator) 82 | if pos == 0 { 83 | continue 84 | } 85 | name := strutil.Substr(spath, pos+1, len(spath)-pos) 86 | 87 | // skip like: .git some.txt 88 | if strings.ContainsRune(name, '.') { 89 | continue 90 | } 91 | 92 | dirs = append(dirs, name) 93 | } 94 | return 95 | } 96 | 97 | func execCmd(cmdString, workDir string) (string, error) { 98 | if len(workDir) > 0 { 99 | color.Comment.Println(">", cmdString, "(On:"+workDir+")") 100 | } else { 101 | color.Comment.Println(">", cmdString) 102 | } 103 | 104 | return sysutil.QuickExec(cmdString, workDir) 105 | } 106 | -------------------------------------------------------------------------------- /_examples/cmd/git_remote.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/gookit/gcli/v3" 5 | "github.com/gookit/goutil/dump" 6 | ) 7 | 8 | var gitRmtOPts = struct { 9 | v bool 10 | }{} 11 | 12 | // GitRemote remote command of the git. 13 | var GitRemote = &gcli.Command{ 14 | Name: "remote", 15 | Desc: "remote command of the git", 16 | Aliases: []string{"rmt"}, 17 | Config: func(c *gcli.Command) { 18 | c.BoolOpt(&gitRmtOPts.v, "v", "", false, "option for git remote") 19 | }, 20 | Func: func(c *gcli.Command, args []string) error { 21 | dump.P(c.Path()) 22 | return nil 23 | }, 24 | Subs: []*gcli.Command{ 25 | { 26 | Name: "set-url", 27 | Desc: "set-url command of git remote", 28 | Aliases: []string{"su"}, 29 | Config: func(c *gcli.Command) { 30 | c.AddArg("name", "the remote name", true) 31 | c.AddArg("address", "the remote address", true) 32 | }, 33 | Func: func(c *gcli.Command, args []string) error { 34 | dump.P(c.Path()) 35 | return nil 36 | }, 37 | }, 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /_examples/cmd/interact_demo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | 7 | "github.com/gookit/color" 8 | "github.com/gookit/gcli/v3" 9 | "github.com/gookit/gcli/v3/interact" 10 | "github.com/gookit/gcli/v3/interact/cparam" 11 | "github.com/gookit/gcli/v3/show/emoji" 12 | "github.com/gookit/goutil" 13 | "github.com/gookit/goutil/dump" 14 | "github.com/gookit/goutil/errorx" 15 | "github.com/gookit/goutil/strutil" 16 | ) 17 | 18 | // InteractDemo command 19 | var InteractDemo = &gcli.Command{ 20 | Name: "interact", 21 | Func: interactDemo, 22 | Desc: "the command will show some interactive methods", 23 | Subs: []*gcli.Command{InteractCollectCmd}, 24 | 25 | Aliases: []string{"itt"}, 26 | Config: func(c *gcli.Command) { 27 | c.AddArg("name", "want running interact method name", true) 28 | }, 29 | Examples: `{$fullCmd} confirm 30 | {$fullCmd} select 31 | `, 32 | Help: ` 33 | Supported interactive methods: 34 | read read user input text 35 | answerIsYes check user answer is Yes 36 | confirm confirm message 37 | select select one from given options 38 | password read user hidden input 39 | multiSelect select multi from given options 40 | `, 41 | } 42 | 43 | var funcMap = map[string]func(c *gcli.Command){ 44 | "read": demoReadInput, 45 | "select": demoSelect, 46 | "confirm": demoConfirm, 47 | "password": demoPassword, 48 | 49 | "ms": demoMultiSelect, 50 | 51 | "multiSelect": demoMultiSelect, 52 | "answerIsYes": demoAnswerIsYes, 53 | } 54 | 55 | func demoReadInput(c *gcli.Command) { 56 | ans, _ := interact.ReadLine("Your name?") 57 | 58 | if ans != "" { 59 | color.Println("Your input: ", ans) 60 | } else { 61 | color.Cyan.Println("No input!") 62 | } 63 | } 64 | 65 | func interactDemo(c *gcli.Command, _ []string) error { 66 | name := c.Arg("name").String() 67 | if handler, ok := funcMap[name]; ok { 68 | handler(c) 69 | } else { 70 | return c.NewErrf("want run unknown demo method: %s", name) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func demoSelect(_ *gcli.Command) { 77 | color.Green.Println("Thies's An Select Demo") 78 | fmt.Println("----------------------------------------------------------") 79 | 80 | ans := interact.SelectOne( 81 | "Your city name(use string slice/array)?", 82 | []string{"chengdu", "beijing", "shanghai"}, 83 | "", 84 | ) 85 | color.Info.Println("your select is:", ans) 86 | fmt.Println("----------------------------------------------------------") 87 | 88 | ans1 := interact.Choice( 89 | "Your age(use int slice/array)?", 90 | []int{23, 34, 45}, 91 | "", 92 | ) 93 | color.Info.Println("your select is:", ans1) 94 | 95 | fmt.Println("----------------------------------------------------------") 96 | 97 | ans2 := interact.SingleSelect( 98 | "Your city name(use map)?", 99 | map[string]string{"a": "chengdu", "b": "beijing", "c": "shanghai"}, 100 | "a", 101 | ) 102 | color.Info.Println("your select is:", ans2) 103 | 104 | s := interact.NewSelect("Your city", []string{"chengdu", "beijing", "shanghai"}) 105 | s.DefOpt = "2" 106 | r := s.Run() 107 | color.Info.Println("your select key:", r.K.String()) 108 | color.Info.Println("your select val:", r.String()) 109 | } 110 | 111 | func demoMultiSelect(_ *gcli.Command) { 112 | color.Green.Println("Thies's An MultiSelect Demo") 113 | 114 | ans := interact.MultiSelect( 115 | "Your city name(use array)?", 116 | []string{"chengdu", "beijing", "shanghai"}, 117 | nil, 118 | ) 119 | color.Comment.Println("your select is: ", ans) 120 | fmt.Println("----------------------------------------------------------") 121 | 122 | ans2 := interact.Checkbox( 123 | "Your city name(use map)?", 124 | map[string]string{"a": "chengdu", "b": "beijing", "c": "shanghai"}, 125 | []string{"a"}, 126 | ) 127 | color.Comment.Println("your select is:", ans2) 128 | } 129 | 130 | func demoConfirm(_ *gcli.Command) { 131 | color.Green.Println("Thies's An Confirm Demo") 132 | 133 | if interact.Confirm("Ensure continue") { 134 | fmt.Println(emoji.Render(":smile: Confirmed")) 135 | } else { 136 | color.Warn.Println("Unconfirmed") 137 | } 138 | } 139 | 140 | func demoPassword(_ *gcli.Command) { 141 | color.Green.Println("Thies's An ReadPassword Demo") 142 | // hiddenInputTest() 143 | // return 144 | // pwd := interact.GetHiddenInput("Enter Password:", true) 145 | // color.Comment.Println("you input password is: ", pwd) 146 | 147 | pwd := interact.ReadPassword() 148 | color.Comment.Println("Your input password is:", pwd) 149 | } 150 | 151 | func hiddenInputTest() { 152 | // COMMAND: sh -c 'read -p "Enter Password:" -s user_input && echo $user_input' 153 | // str := fmt.Sprintf(`'read -p "%s" -s user_input && echo $user_input'`, "Enter Password:") 154 | // cmd := exec.CommandContext() 155 | cmd := exec.Command("sh", "-c", `read -p "Enter Password:" -s user_input && echo $user_input`) 156 | err := cmd.Start() 157 | fmt.Println("start", err) 158 | err = cmd.Wait() 159 | fmt.Println("wait", err, cmd.Process.Pid, cmd.ProcessState.Pid()) 160 | 161 | cmd = exec.Command("sh", "./read-pwd.sh") 162 | bs, err := cmd.Output() 163 | fmt.Println(string(bs), err) 164 | } 165 | 166 | func demoAnswerIsYes(_ *gcli.Command) { 167 | 168 | } 169 | 170 | func demoQuestion(_ *gcli.Command) { 171 | ans := interact.Ask("Your name? ", "", nil, 3) 172 | color.Comment.Println("Your answer is:", ans) 173 | } 174 | 175 | // InteractCollectCmd instance. 176 | var InteractCollectCmd = &gcli.Command{ 177 | Name: "collect", 178 | Desc: "collect multi input params at once", 179 | Func: func(c *gcli.Command, args []string) error { 180 | 181 | vc := interact.NewCollector() 182 | err := vc.AddParams( 183 | cparam.NewStringParam("title", "title name").Config(func(p *cparam.StringParam) { 184 | p.ValidFn = func(val string) error { 185 | return goutil.OrError(strutil.IsBlank(val), errorx.Raw("title is required")) 186 | } 187 | }), 188 | cparam.NewChoiceParam("projects", "select projects"). 189 | WithChoices([]string{"user", "order", "goods"}), 190 | ) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | if err = vc.Run(); err != nil { 196 | return err 197 | } 198 | 199 | c.Println("Result:") 200 | dump.P(vc.Results()) 201 | 202 | return nil 203 | }, 204 | } 205 | -------------------------------------------------------------------------------- /_examples/cmd/progress_demo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gookit/color" 8 | "github.com/gookit/gcli/v3" 9 | "github.com/gookit/gcli/v3/progress" 10 | ) 11 | 12 | var pdOpts = struct { 13 | maxSteps int 14 | overwrite, random bool 15 | 16 | handlers map[string]func(int) 17 | }{} 18 | 19 | var ProgressDemo = &gcli.Command{ 20 | Name: "prog", 21 | Desc: "there are some progress bar run demos", 22 | Aliases: []string{"prg-demo", "progress"}, 23 | // Subs: []*gcli.Command{}, 24 | Config: func(c *gcli.Command) { 25 | c.IntOpt(&pdOpts.maxSteps, "max-step", "", 100, "setting the max step value") 26 | c.BoolOpt(&pdOpts.overwrite, "overwrite", "o", true, "setting overwrite progress bar line") 27 | c.BoolVar(&pdOpts.random, &gcli.CliOpt{Name: "random", Desc: "use random style for progress bar"}) 28 | 29 | c.BindArg(&gcli.CliArg{ 30 | Name: "name", 31 | Desc: "progress bar type name. allow: bar,txt,dtxt,loading,roundTrip", 32 | 33 | Required: true, 34 | // Validator: func(val any) (any, error) { 35 | // name := val.(string) 36 | // }, 37 | }) 38 | }, 39 | Examples: `Text progress bar: 40 | {$fullCmd} txt 41 | Image progress bar: 42 | {$fullCmd} bar`, 43 | Func: func(c *gcli.Command, args []string) error { 44 | name := c.Arg("name").String() 45 | max := pdOpts.maxSteps 46 | 47 | color.Infoln("Progress Demo:") 48 | switch name { 49 | case "bar": 50 | showProgressBar(max) 51 | case "bars", "all-bar": 52 | showAllProgressBar(max) 53 | case "dt", "dtxt", "dynamicText": 54 | dynamicTextBar(max) 55 | case "txt", "text": 56 | txtProgressBar(max) 57 | case "spr", "load", "loading", "spinner": 58 | runLoadingBar(max) 59 | case "rt", "roundTrip": 60 | runRoundTripBar(max) 61 | default: 62 | return c.NewErrf("the progress bar type name only allow: bar,txt,dtxt,loading,roundTrip. input is: %s", name) 63 | } 64 | return nil 65 | }, 66 | } 67 | 68 | func showProgressBar(maxStep int) { 69 | cs := progress.BarStyles[3] 70 | if pdOpts.random { 71 | cs = progress.RandomBarStyle() 72 | } 73 | 74 | p := progress.CustomBar(40, cs) 75 | p.MaxSteps = uint(maxStep) 76 | p.Format = progress.FullBarFormat 77 | // p.Overwrite = true 78 | 79 | // p.AddMessage("message", " handling ...") 80 | 81 | // running 82 | runProgressBar(p, maxStep, 60) 83 | p.Finish() 84 | } 85 | 86 | func showAllProgressBar(maxStep int) { 87 | ln := len(progress.BarStyles) 88 | ch := make(chan bool, ln) 89 | 90 | for i, style := range progress.BarStyles { 91 | go func(i int, style progress.BarChars) { 92 | p := progress.CustomBar(40, style) 93 | 94 | // p.Newline = true 95 | p.MaxSteps = uint(maxStep) 96 | // p.Format = progress.FullBarFormat 97 | p.Format = progress.BarFormat 98 | p.AddMessage("message", fmt.Sprintf("Bar %d", i+1)) 99 | 100 | // run 101 | runProgressBar(p, maxStep, 100) 102 | 103 | // end 104 | p.Finish() 105 | ch <- true 106 | }(i, style) // NOTICE: must use arguments 107 | } 108 | 109 | // waiting 110 | for range progress.BarStyles { 111 | <-ch 112 | } 113 | 114 | fmt.Println("- Done with progress, number ", ln) 115 | } 116 | 117 | func runRoundTripBar(max int) { 118 | p := progress.RoundTrip(0).WithMaxSteps(max) 119 | 120 | // running 121 | runProgressBar(p, max, 120) 122 | 123 | p.Finish() 124 | } 125 | 126 | func txtProgressBar(maxStep int) { 127 | txt := progress.Txt(maxStep) 128 | txt.AddMessage("message", "Handling ... ") 129 | // txt.Overwrite = false 130 | // running 131 | runProgressBar(txt, maxStep, 80) 132 | 133 | txt.Finish("Completed") 134 | } 135 | 136 | func dynamicTextBar(maxStep int) { 137 | messages := map[int]string{ 138 | // key is percent, range is 0 - 100. 139 | 20: " Prepare ...", 140 | 40: " Request ...", 141 | 65: " Transport ...", 142 | 95: " Saving ...", 143 | 100: " Handle Complete.", 144 | } 145 | 146 | // maxStep = 10 147 | p := progress.DynamicText(messages, maxStep) 148 | // p.Overwrite = false 149 | 150 | // running 151 | runProgressBar(p, maxStep, 100) 152 | p.Finish() 153 | } 154 | 155 | func runLoadingBar(maxStep int) { 156 | p := progress.LoadingBar(progress.RandomCharsTheme()) 157 | p.MaxSteps = uint(maxStep) 158 | p.AddMessage("message", " data loading ... ...") 159 | 160 | // running 161 | runProgressBar(p, maxStep, 70) 162 | 163 | // p.Finish() 164 | p.Finish("data load complete") 165 | } 166 | 167 | // running 168 | func runProgressBar(p *progress.Progress, maxSteps int, speed int) { 169 | p.Start() 170 | for i := 0; i < maxSteps; i++ { 171 | time.Sleep(time.Duration(speed) * time.Millisecond) 172 | p.Advance() 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /_examples/cmd/show_demo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/gookit/gcli/v3" 4 | 5 | var ShowDemo = &gcli.Command{ 6 | Name: "show", 7 | Func: runShow, 8 | // 9 | Desc: "the command will show some data format methods", 10 | } 11 | 12 | func runShow(c *gcli.Command, _ []string) error { 13 | 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /_examples/cmd/spinner_demo.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gookit/gcli/v3" 7 | "github.com/gookit/gcli/v3/progress" 8 | ) 9 | 10 | type spinnerDemo struct { 11 | speed int 12 | themeNum int 13 | } 14 | 15 | var spOpts = spinnerDemo{} 16 | 17 | var SpinnerDemo = &gcli.Command{ 18 | Name: "spinner", 19 | Desc: "there are some CLI spinner bar run demos", 20 | Aliases: []string{"spr", "spr-demo"}, 21 | // Func: spOpts.Run, 22 | Config: func(c *gcli.Command) { 23 | c.IntOpt(&spOpts.speed, "speed", "s", 100, "setting the spinner running speed") 24 | c.IntOpt(&spOpts.themeNum, "theme-num", "t", 0, "setting the theme numbering. allow: 0 - 16") 25 | 26 | c.AddArg("name", 27 | "spinner type name. allow: loading,roundTrip", 28 | false, 29 | ) 30 | }, 31 | Examples: `Loading spinner: 32 | {$fullCmd} loading 33 | roundTrip spinner: 34 | {$fullCmd} roundTrip`, 35 | Func: func(c *gcli.Command, _ []string) error { 36 | name := c.Arg("name").String() 37 | 38 | switch name { 39 | case "", "spinner", "load", "loading": 40 | spOpts.runLoadingSpinner() 41 | case "rt", "roundTrip": 42 | spOpts.runRoundTripSpinner() 43 | default: 44 | return c.NewErrf("the spinner type name only allow: loading,roundTrip. input is: %s", name) 45 | } 46 | return nil 47 | }, 48 | } 49 | 50 | func (sd *spinnerDemo) runRoundTripSpinner() { 51 | s := progress.RoundTripLoading( 52 | progress.GetCharTheme(sd.themeNum), 53 | time.Duration(sd.speed)*time.Millisecond, 54 | ) 55 | 56 | // s.Start("%s work handling ... ...") 57 | s.Start("[%s] work handling ... ...") 58 | 59 | // Run for some time to simulate work 60 | time.Sleep(4 * time.Second) 61 | s.Stop("work handle complete") 62 | } 63 | 64 | func (sd *spinnerDemo) runLoadingSpinner() { 65 | s := progress.LoadingSpinner( 66 | progress.GetCharsTheme(sd.themeNum), 67 | time.Duration(sd.speed)*time.Millisecond, 68 | ) 69 | 70 | s.Start("%s work handling ... ...") 71 | // Run for some time to simulate work 72 | time.Sleep(4 * time.Second) 73 | s.Stop("work handle complete") 74 | } 75 | -------------------------------------------------------------------------------- /_examples/emojitest.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/gcli/v3/show/emoji" 7 | "github.com/gookit/gcli/v3/show/symbols" 8 | ) 9 | 10 | // go run ./_examples/test.go 11 | func main() { 12 | fmt.Println(symbols.LEFT, emoji.BOX, "\xe2\x8c\x9a", emoji.HEART, "\u2764", "END") 13 | fmt.Println(emoji.HEART, "🚻", "\U0001f44d", "\U0001F17E", "\U00000038\U000020e3", "\U0001f4af") 14 | 15 | fmt.Println("\u2601\U000FE001", emoji.Render("hello :snake: emoji :car:")) 16 | 17 | fmt.Println(emoji.ToUnicode(emoji.HEART), "\U0001F194", emoji.Decode("\U0001f496")) 18 | 19 | ns := emoji.ToUnicode(emoji.HEART) 20 | fmt.Println(ns, "👩🏾👩🏽", "\U0001F469\U0001F3FD", "\U0001f170") 21 | 22 | } 23 | -------------------------------------------------------------------------------- /_examples/ggit/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gookit/gcli/v3/_examples/cmd" 5 | ) 6 | 7 | // test run: 8 | // go build ./_examples/ggit && ./ggit -h 9 | // test run: 10 | // go run ./_examples/ggit 11 | func main() { 12 | // Alone Running 13 | cmd.GitCmd.MustRun(nil) 14 | } 15 | -------------------------------------------------------------------------------- /_examples/images/README.md: -------------------------------------------------------------------------------- 1 | # something 2 | 3 | recording svg: 4 | 5 | ```bash 6 | termtosvg -t solarized_light -g 90x6 _examples/images/progress/bar.svg 7 | ``` 8 | -------------------------------------------------------------------------------- /_examples/images/app-version.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/app-version.jpg -------------------------------------------------------------------------------- /_examples/images/auto-complete-tips.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/auto-complete-tips.jpg -------------------------------------------------------------------------------- /_examples/images/cmd-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/cmd-help.png -------------------------------------------------------------------------------- /_examples/images/cmd-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/cmd-list.png -------------------------------------------------------------------------------- /_examples/images/color/basic-color.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/color/basic-color.jpg -------------------------------------------------------------------------------- /_examples/images/color/color-demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/color/color-demo.jpg -------------------------------------------------------------------------------- /_examples/images/color/color-tags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/color/color-tags.jpg -------------------------------------------------------------------------------- /_examples/images/err-cmd-tips.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/err-cmd-tips.jpg -------------------------------------------------------------------------------- /_examples/images/interact/confirm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/interact/confirm.jpg -------------------------------------------------------------------------------- /_examples/images/interact/m-select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/interact/m-select.jpg -------------------------------------------------------------------------------- /_examples/images/interact/passwd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/interact/passwd.jpg -------------------------------------------------------------------------------- /_examples/images/interact/read.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/interact/read.jpg -------------------------------------------------------------------------------- /_examples/images/interact/select.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/interact/select.jpg -------------------------------------------------------------------------------- /_examples/images/progress/prog-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/progress/prog-bar.png -------------------------------------------------------------------------------- /_examples/images/progress/prog-other.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/progress/prog-other.jpg -------------------------------------------------------------------------------- /_examples/images/progress/prog-rt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/progress/prog-rt.jpg -------------------------------------------------------------------------------- /_examples/images/progress/prog-spinner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/progress/prog-spinner.jpg -------------------------------------------------------------------------------- /_examples/images/progress/prog-spinner1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/progress/prog-spinner1.jpg -------------------------------------------------------------------------------- /_examples/images/run-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/run-example.jpg -------------------------------------------------------------------------------- /_examples/images/run-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/_examples/images/run-example.png -------------------------------------------------------------------------------- /_examples/multilevel/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gookit/gcli/v3" 5 | "github.com/gookit/goutil/dump" 6 | ) 7 | 8 | var opts = struct { 9 | fontName string 10 | visualMode bool 11 | list bool 12 | sample bool 13 | }{} 14 | 15 | var l1sub1opts = struct { 16 | aint int 17 | }{} 18 | var l2sub1opts = struct { 19 | astr string 20 | }{} 21 | 22 | var cmd = gcli.Command{ 23 | Name: "test", 24 | Aliases: []string{"ts"}, 25 | Desc: "this is a description message for {$cmd}", // // {$cmd} will be replace to 'test' 26 | Subs: []*gcli.Command{ 27 | { 28 | Name: "l1sub1", 29 | Desc: "desc message", 30 | Subs: []*gcli.Command{ 31 | { 32 | Name: "l2sub1", 33 | Desc: "desc message", 34 | Config: func(c *gcli.Command) { 35 | c.StrVar(&l2sub1opts.astr, &gcli.CliOpt{ 36 | Name: "astr", 37 | Desc: "desc for astr", 38 | }) 39 | }, 40 | }, 41 | { 42 | Name: "l2sub2", 43 | Desc: "desc message", 44 | }, 45 | }, 46 | Config: func(c *gcli.Command) { 47 | c.IntOpt(&l1sub1opts.aint, "aint", "", 2, "desc for aint") 48 | }, 49 | }, 50 | { 51 | Name: "l1sub2", 52 | Desc: "desc message", 53 | }, 54 | }, 55 | } 56 | 57 | // test run: go build ./_examples/multilevel && ./multilevel -h 58 | // test run: go run ./_examples/multilevel 59 | func main() { 60 | cmd.BoolOpt(&opts.visualMode, "visual", "v", false, "Prints the font name.") 61 | cmd.StrOpt(&opts.fontName, "font", "", "", "Choose a font name. Default is a random font.") 62 | cmd.BoolOpt(&opts.list, "list", "", false, "Lists all available fonts.") 63 | cmd.BoolOpt(&opts.sample, "sample", "", false, "Prints a sample with that font.") 64 | 65 | cmd.Func = func(c *gcli.Command, args []string) error { 66 | c.Infoln("hello, in the alone command:", c.Name) 67 | 68 | dump.Print(args) 69 | dump.Print(opts) 70 | 71 | return nil 72 | } 73 | 74 | // Alone Running 75 | cmd.MustRun(nil) 76 | // cmd.Run(os.Args[1:]) 77 | } 78 | -------------------------------------------------------------------------------- /_examples/navbar.md: -------------------------------------------------------------------------------- 1 | * Translates 2 | * [English](/README.md) 3 | * [中文说明](/README.zh-CN.md) 4 | 5 | * Gookit 6 | * [Cache](https://gookit.github.io/cache/ "cache management") 7 | * [Color](https://gookit.github.io/color/ "console color render") 8 | * [Config](https://gookit.github.io/config/ "config management") 9 | * [Event](https://gookit.github.io/event/ "event management") 10 | * [Filter](https://gookit.github.io/filter/ "data filter and convert") 11 | * [I18n](https://gookit.github.io/i18n/ "i18n management") 12 | * [Ini](https://gookit.github.io/ini/ "ini file parse and management") 13 | * [Gcli](https://gookit.github.io/gcli/ "console application build") 14 | * [Goutil](https://gookit.github.io/goutil/ "Helper utils for go") 15 | * [Rux](https://gookit.github.io/rux/ "Rux is an simple and fast web framework") 16 | * [Slog](https://gookit.github.io/slog/ "Lightweight, extensible logging library") 17 | * [Validate](https://gookit.github.io/validate/ "Data validate for go") 18 | -------------------------------------------------------------------------------- /_examples/rawflag.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | var int0 int 10 | var str0 string 11 | 12 | // go run ./_examples/rawflag.go -int 10 -str abc 13 | // go run ./_examples/rawflag.go --int 10 --str abc 14 | // go run ./_examples/rawflag.go --int=10 --str=abc 15 | func main() { 16 | useNewFlagSet() 17 | 18 | fmt.Println("int:", int0, "str:", str0) 19 | } 20 | 21 | func useDefaultFlag() { 22 | flag.IntVar(&int0, "int", 0, "int opt") 23 | flag.StringVar(&str0, "str", "", "str opt") 24 | 25 | flag.Parse() 26 | } 27 | 28 | func useNewFlagSet() { 29 | f := flag.NewFlagSet("user", flag.ExitOnError) 30 | f.IntVar(&int0, "int", 0, "int opt") 31 | f.StringVar(&str0, "str", "", "str opt") 32 | 33 | _ = f.Parse(os.Args[1:]) 34 | } 35 | -------------------------------------------------------------------------------- /_examples/related_pkg.md: -------------------------------------------------------------------------------- 1 | # related pkg 2 | 3 | - https://github.com/c-bata/go-prompt 4 | - https://github.com/jroimartin/gocui 5 | - https://github.com/nsf/termbox-go 6 | -------------------------------------------------------------------------------- /_examples/serveman/commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os/exec" 7 | 8 | "github.com/gookit/color" 9 | "github.com/gookit/gcli/v3" 10 | ) 11 | 12 | // Config struct 13 | type Config struct { 14 | // will exec command. eg: "go run main.go" 15 | Cmd string `json:"cmd"` 16 | // serve name for will exec command 17 | Name string `json:"name"` 18 | // run in the background 19 | Daemon bool 20 | // the pid file. eg "/var/run/serve.pid" 21 | PidFile string `json:"pidFile"` 22 | // the command run dir. 23 | WorkDir string `json:"workDir"` 24 | } 25 | 26 | var ( 27 | config = new(Config) 28 | // config file 29 | confFile string 30 | ) 31 | 32 | // ServerStart command 33 | var ServerStart = &gcli.Command{ 34 | Name: "start", 35 | Desc: "start server", 36 | Func: func(c *gcli.Command, args []string) error { 37 | return startServer(c.BinName()) 38 | }, 39 | Config: func(c *gcli.Command) { 40 | // c.StrOpt(&config.Pid, "pid", "", "", "the running server PID file") 41 | c.StrOpt(&confFile, "config", "c", "serve-config.json", "the running json config file path") 42 | c.BoolOpt(&config.Daemon, "daemon", "d", false, "the running server PID file") 43 | }, 44 | } 45 | 46 | func startServer(binFile string) (err error) { 47 | if config.Daemon { 48 | cmd := exec.Command(binFile, "start") 49 | if err = cmd.Start(); err != nil { 50 | return 51 | } 52 | 53 | pid := cmd.Process.Pid 54 | color.Greenf("Server start, [PID] %d running...\n", pid) 55 | err = ioutil.WriteFile(config.PidFile, []byte(fmt.Sprintf("%d", pid)), 0666) 56 | config.Daemon = false 57 | return 58 | } 59 | 60 | color.Infoln("Server started") 61 | // front run 62 | // startHttp() 63 | return 64 | } 65 | 66 | var ServerStop = &gcli.Command{ 67 | Name: "stop", 68 | Desc: "stop the running server(by PID file)", 69 | Func: func(_ *gcli.Command, _ []string) error { 70 | return stopServer() 71 | }, 72 | } 73 | 74 | // ServerRestart Server restart 75 | var ServerRestart = &gcli.Command{ 76 | Name: "restart", 77 | Desc: "restart the running server by PID file", 78 | Func: func(c *gcli.Command, _ []string) (err error) { 79 | // c.App().SubRun("stop", []string{"-c", confFile}) 80 | if err = stopServer(); err != nil { 81 | return 82 | } 83 | 84 | return startServer(c.BinName()) 85 | }, 86 | } 87 | 88 | func stopServer() error { 89 | bs, _ := ioutil.ReadFile(config.PidFile) 90 | cmd := exec.Command("kill", string(bs)) 91 | err := cmd.Start() 92 | 93 | color.Success.Println("server stopped") 94 | return err 95 | } 96 | -------------------------------------------------------------------------------- /_examples/serveman/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/gookit/gcli/v3" 4 | 5 | func main() { 6 | app := gcli.NewApp() 7 | app.Version = "1.0.0" 8 | app.Desc = "manage the http server start,stop,restart" 9 | 10 | app.Add(ServerStart, ServerStop, ServerRestart) 11 | app.Run(nil) 12 | } 13 | -------------------------------------------------------------------------------- /_examples/sflag/README.md: -------------------------------------------------------------------------------- 1 | # SFlag 2 | 3 | 简单的选项参数解析器,不依赖go内置的flag包。 4 | 5 | -------------------------------------------------------------------------------- /_examples/sflag/parser_test.go: -------------------------------------------------------------------------------- 1 | package sflag 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestArgsParser_Parse(t *testing.T) { 12 | at := assert.New(t) 13 | 14 | sample := "-n 10 --name tom --debug -aux --age 24 arg0 arg1" 15 | args := strings.Split(sample, " ") 16 | p := &ArgsParser{} 17 | p.Parse(args) 18 | 19 | at.Eq("[arg0 arg1]", fmt.Sprint(p.Args())) 20 | assertOptions := func(str string) { 21 | at.Contains(str, `"n":"10"`) 22 | at.Contains(str, `"name":"tom"`) 23 | at.Contains(str, `"debug":true`) 24 | at.Contains(str, `"age":"24"`) 25 | at.Contains(str, `"a":true`) 26 | at.Contains(str, `"u":true`) 27 | at.Contains(str, `"x":true`) 28 | } 29 | str := p.OptsString() 30 | assertOptions(str) 31 | 32 | // define bool options 33 | sample = "-n 10 --name tom --debug arg0 -aux --age 24 arg1" 34 | args = strings.Split(sample, " ") 35 | 36 | // 加上 []string{"debug"} 解析器就能正确分别 "--debug arg0" 37 | p = NewArgsParser([]string{"debug"}, nil) 38 | p.Parse(args) 39 | at.Eq("[arg0 arg1]", fmt.Sprint(p.Args())) 40 | str = p.OptsString() 41 | assertOptions(str) 42 | 43 | // define array options 44 | sample = "-n 10 --name tom --name john --debug false -aux --age 24 arg0 arg1" 45 | args = strings.Split(sample, " ") 46 | // 加上 []string{"name"} 解析器就能正确解析 "--name --name john" 47 | p = ParseArgs(args, nil, []string{"name"}) 48 | at.Eq("[arg0 arg1]", fmt.Sprint(p.Args())) 49 | str = p.OptsString() 50 | at.Contains(str, `"n":"10"`) 51 | at.Contains(str, `"name":[]string{"tom", "john"}`) 52 | at.Contains(str, `"debug":false`) 53 | at.Contains(str, `"age":"24"`) 54 | at.Contains(str, `"a":true`) 55 | at.Contains(str, `"u":true`) 56 | at.Contains(str, `"x":true`) 57 | 58 | // define bool and array options 59 | sample = "-n 10 --name tom --name john --debug arg0 -aux --age 24 arg1" 60 | args = strings.Split(sample, " ") 61 | 62 | p = ParseArgs(args, []string{"debug"}, []string{"name"}) 63 | at.Eq("[arg0 arg1]", fmt.Sprint(p.Args())) 64 | str = p.OptsString() 65 | at.Contains(str, `"n":"10"`) 66 | at.Contains(str, `"name":[]string{"tom", "john"}`) 67 | at.Contains(str, `"debug":true`) 68 | at.Contains(str, `"age":"24"`) 69 | at.Contains(str, `"a":true`) 70 | at.Contains(str, `"u":true`) 71 | at.Contains(str, `"x":true`) 72 | } 73 | -------------------------------------------------------------------------------- /_examples/sflag/value_getter.go: -------------------------------------------------------------------------------- 1 | package sflag 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/gookit/goutil/arrutil" 7 | ) 8 | 9 | // ValueGetter struct 10 | type ValueGetter struct { 11 | // value store parsed argument data. (type: string, []string) 12 | Value any 13 | // is array 14 | Arrayed bool 15 | } 16 | 17 | // Int argument value to int 18 | func (v *ValueGetter) Int(defVal ...int) int { 19 | def := 0 20 | if len(defVal) == 1 { 21 | def = defVal[0] 22 | } 23 | 24 | if v.Value == nil || v.Arrayed { 25 | return def 26 | } 27 | 28 | if str, ok := v.Value.(string); ok { 29 | val, err := strconv.Atoi(str) 30 | if err != nil { 31 | return val 32 | } 33 | } 34 | 35 | return def 36 | } 37 | 38 | // String argument value to string 39 | func (v *ValueGetter) String(defVal ...string) string { 40 | def := "" 41 | if len(defVal) == 1 { 42 | def = defVal[0] 43 | } 44 | 45 | if v.Value == nil || v.Arrayed { 46 | return def 47 | } 48 | 49 | if str, ok := v.Value.(string); ok { 50 | return str 51 | } 52 | 53 | return def 54 | } 55 | 56 | // Ints value to int slice 57 | func (v *ValueGetter) Ints() (ints []int) { 58 | ints, _ = arrutil.StringsToInts(v.Strings()) 59 | return 60 | } 61 | 62 | // Strings value to string slice, if argument isArray = true. 63 | func (v *ValueGetter) Strings() (ss []string) { 64 | if v.Value != nil && v.Arrayed { 65 | ss = v.Value.([]string) 66 | } 67 | 68 | return 69 | } 70 | 71 | // Array alias of the Strings() 72 | func (v *ValueGetter) Array() (ss []string) { 73 | return v.Strings() 74 | } 75 | 76 | // HasValue value is empty 77 | func (v *ValueGetter) HasValue() bool { 78 | return v.Value != nil 79 | } 80 | -------------------------------------------------------------------------------- /_examples/simpleone/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gookit/gcli/v3" 5 | "github.com/gookit/goutil/dump" 6 | ) 7 | 8 | var opts = struct { 9 | fontName string 10 | visualMode bool 11 | list bool 12 | sample bool 13 | number int 14 | }{} 15 | 16 | // test run: 17 | // go build ./_examples/simpleone && ./simpleone -h 18 | // test run: 19 | // go run ./_examples/simpleone 20 | func main() { 21 | cmd := gcli.Command{ 22 | Name: "test", 23 | Aliases: []string{"ts"}, 24 | Desc: "this is a description message for {$cmd}", // // {$cmd} will be replace to 'test' 25 | } 26 | 27 | cmd.BoolOpt(&opts.visualMode, "visual", "v", false, "Prints the font name.") 28 | cmd.StrOpt(&opts.fontName, "font", "fn", "", "Choose a font name. Default is a random name;true") 29 | cmd.BoolOpt(&opts.list, "list", "", false, "Lists all available fonts.") 30 | cmd.BoolOpt(&opts.sample, "sample", "", false, "Prints a sample with that font.\nmessage at new line") 31 | cmd.IntOpt(&opts.number, "number", "n,num", 0, "a integer option") 32 | 33 | cmd.AddArg("arg1", "this is a argument") 34 | cmd.AddArg("arg2", "this is a argument2") 35 | 36 | cmd.WithConfigFn(func(opt *gcli.FlagsConfig) { 37 | opt.DescNewline = true 38 | }) 39 | 40 | cmd.Func = func(c *gcli.Command, args []string) error { 41 | c.Infoln("hello, in the alone command:", c.Name) 42 | 43 | dump.Print(args) 44 | dump.Print(opts) 45 | 46 | return nil 47 | } 48 | 49 | // Alone Running 50 | cmd.MustRun(nil) 51 | // cmd.Run(os.Args[1:]) 52 | } 53 | -------------------------------------------------------------------------------- /base_test.go: -------------------------------------------------------------------------------- 1 | package gcli_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gookit/gcli/v3" 9 | "github.com/gookit/gcli/v3/events" 10 | "github.com/gookit/goutil/dump" 11 | "github.com/gookit/goutil/testutil/assert" 12 | ) 13 | 14 | var ( 15 | buf = new(bytes.Buffer) 16 | ) 17 | 18 | func newNotExitApp(fns ...func(app *gcli.App)) *gcli.App { 19 | cli := gcli.New(fns...) 20 | cli.ExitOnEnd = false 21 | 22 | return cli 23 | } 24 | 25 | func TestApp_Hooks_EvtAppInit(t *testing.T) { 26 | buf.Reset() 27 | 28 | cli := newNotExitApp() 29 | cli.On(events.OnAppInitAfter, func(ctx *gcli.HookCtx) bool { 30 | buf.WriteString("trigger " + ctx.Name()) 31 | return false 32 | }) 33 | cli.Add(simpleCmd) 34 | assert.Eq(t, "trigger "+events.OnAppInitAfter, buf.String()) 35 | 36 | buf.Reset() 37 | cli.On(events.OnGlobalOptsParsed, func(ctx *gcli.HookCtx) bool { 38 | buf.WriteString("trigger " + ctx.Name() + ", args:" + fmt.Sprintf("%v", ctx.Strings("args"))) 39 | return false 40 | }) 41 | 42 | cli.Run([]string{"simple"}) 43 | assert.Eq(t, "trigger "+events.OnGlobalOptsParsed+", args:[simple]", buf.String()) 44 | } 45 | 46 | func TestApp_Hooks_OnAppCmdAdd(t *testing.T) { 47 | buf.Reset() 48 | 49 | cli := newNotExitApp() 50 | cli.On(events.OnAppCmdAdd, func(ctx *gcli.HookCtx) (stop bool) { 51 | buf.WriteString(ctx.Name()) 52 | buf.WriteString(" - ") 53 | buf.WriteString(ctx.Cmd.Name + ";") 54 | return 55 | }) 56 | 57 | cli.Add(emptyCmd) 58 | assert.Eq(t, "app.cmd.add.before - empty;", buf.String()) 59 | 60 | cli.Add(simpleCmd) 61 | assert.Eq(t, "app.cmd.add.before - empty;app.cmd.add.before - simple;", buf.String()) 62 | } 63 | 64 | func TestCommand_Hooks_EvtCmdOptParsed(t *testing.T) { 65 | buf.Reset() 66 | 67 | cli := newNotExitApp() 68 | cli.Add(&gcli.Command{ 69 | Name: "test", 70 | Desc: "desc", 71 | Config: func(c *gcli.Command) { 72 | buf.WriteString("run config;") 73 | c.On(events.OnCmdOptParsed, func(ctx *gcli.HookCtx) (stop bool) { 74 | dump.P(ctx.Strings("args")) 75 | buf.WriteString(ctx.Name()) 76 | return 77 | }) 78 | }, 79 | }) 80 | assert.Contains(t, buf.String(), "run config;") 81 | 82 | cli.Run([]string{"test"}) 83 | assert.Contains(t, buf.String(), events.OnCmdOptParsed) 84 | } 85 | 86 | func TestApp_On_CmdNotFound(t *testing.T) { 87 | buf.Reset() 88 | 89 | cli := newNotExitApp() 90 | cli.Add(simpleCmd) 91 | 92 | fmt.Println("--------- will print command tips ----------") 93 | cli.On(events.OnCmdNotFound, func(ctx *gcli.HookCtx) bool { 94 | buf.WriteString("trigger: " + events.OnCmdNotFound) 95 | buf.WriteString("; command: " + ctx.Str("name")) 96 | return false 97 | }) 98 | 99 | cli.Run([]string{"top"}) 100 | assert.Eq(t, "trigger: cmd.not.found; command: top", buf.String()) 101 | buf.Reset() 102 | 103 | fmt.Println("--------- dont print command tips ----------") 104 | cli.On(events.OnCmdNotFound, func(ctx *gcli.HookCtx) bool { 105 | buf.WriteString("trigger: " + events.OnCmdNotFound) 106 | buf.WriteString("; command: " + ctx.Str("name")) 107 | return true 108 | }) 109 | 110 | cli.Run([]string{"top"}) 111 | assert.Eq(t, "trigger: cmd.not.found; command: top", buf.String()) 112 | } 113 | 114 | func TestApp_On_CmdNotFound_redirect(t *testing.T) { 115 | buf.Reset() 116 | simpleCmd.Init() 117 | simpleCmd.ResetData() 118 | assert.Eq(t, nil, simpleCmd.Ctx.Get("simple")) 119 | 120 | cli := newNotExitApp() 121 | cli.Add(simpleCmd) 122 | 123 | fmt.Println("--------- redirect to run another command ----------") 124 | cli.On(events.OnCmdNotFound, func(ctx *gcli.HookCtx) bool { 125 | buf.WriteString("trigger:" + events.OnCmdNotFound) 126 | buf.WriteString(" - command:" + ctx.Str("name")) 127 | buf.WriteString("; redirect:simple - ") 128 | 129 | err := ctx.App.Exec(simpleCmd.Name, nil) 130 | assert.NoErr(t, err) 131 | buf.WriteString("value:" + simpleCmd.Ctx.Str("simple")) 132 | return true 133 | }) 134 | 135 | cli.Run([]string{"top"}) 136 | want := "trigger:cmd.not.found - command:top; redirect:simple - value:simple command" 137 | assert.Eq(t, want, buf.String()) 138 | } 139 | -------------------------------------------------------------------------------- /builtin/gen_emoji_codeMap.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/gookit/gcli/v3" 11 | ) 12 | 13 | type genEmojiMap struct { 14 | c *gcli.Command 15 | // muan - https://raw.githubusercontent.com/muan/emoji/gh-pages/javascripts/emojilib/emojis.json 16 | // gemoji - https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json 17 | // unicode - https://unicode.org/Public/emoji/11.0/emoji-test.txt 18 | source string // allow: gemoji 19 | saveDir string 20 | onlyGen bool 21 | } 22 | 23 | // Gemoji definition 24 | type Gemoji struct { 25 | Aliases []string `json:"aliases"` 26 | Description string `json:"description"` 27 | Emoji string `json:"emoji"` 28 | Tags []string `json:"tags"` 29 | } 30 | 31 | var gem = &genEmojiMap{} 32 | 33 | // GenEmojiMap create 34 | var GenEmojiMap = &gcli.Command{ 35 | Name: "gen-emojis", 36 | Aliases: []string{"gen-emj"}, 37 | // handler func 38 | Func: gem.run, 39 | // des 40 | Desc: "fetch emoji codes form data source url, then generate a go file.", 41 | // config options 42 | Config: func(c *gcli.Command) { 43 | gem.c = c 44 | c.StrOpt( 45 | &gem.source, "source", "s", "gemoji", 46 | "the emoji data source, allow: muan, gemoji, unicode", 47 | ) 48 | c.StrOpt(&gem.saveDir, "dir", "d", "./", "the generated go file save `DIR` path") 49 | c.BoolOpt(&gem.onlyGen, "onlyGen", "", false, "whether only generate go file from exists emoji data file") 50 | }, 51 | Help: `source allow: 52 | muan - https://raw.githubusercontent.com/muan/emoji/gh-pages/javascripts/emojilib/emojis.json 53 | gemoji - https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json 54 | unicode - https://unicode.org/Public/emoji/11.0/emoji-test.txt 55 | `, 56 | } 57 | 58 | func (g *genEmojiMap) run(c *gcli.Command, _ []string) error { 59 | c.Infoln("TODO") 60 | return nil 61 | } 62 | 63 | // Download 实现单个文件的下载 64 | func (g *genEmojiMap) Download(remoteFile string, saveAs string) error { 65 | nt := time.Now().Format("2006-01-02 15:04:05") 66 | fmt.Printf("[%s]To download %s\n", nt, remoteFile) 67 | 68 | newFile, err := os.Create(saveAs) 69 | if err != nil { 70 | return err 71 | } 72 | defer newFile.Close() 73 | 74 | client := http.Client{Timeout: 900 * time.Second} 75 | resp, err := client.Get(remoteFile) 76 | if err != nil { 77 | return err 78 | } 79 | defer resp.Body.Close() 80 | 81 | _, err = io.Copy(newFile, resp.Body) 82 | if err != nil { 83 | fmt.Println(err.Error()) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | const templateString = ` 90 | package {{.PkgName}} 91 | // NOTE: THIS FILE WAS PRODUCED BY THE 92 | // EMOJI CODE MAP CODE GENERATION TOOL (https://github.com/gookit/gcli) 93 | // DO NOT EDIT 94 | // Mapping from character to concrete escape code. 95 | var emojiMap = map[string]string{ 96 | {{range $key, $val := .CodeMap}}":{{$key}}:": {{$val}}, 97 | {{end}} 98 | } 99 | ` 100 | -------------------------------------------------------------------------------- /builtin/gen_gcli_command.go: -------------------------------------------------------------------------------- 1 | package builtin 2 | 3 | import "github.com/gookit/gcli/v3" 4 | 5 | // GenCmdCode command 6 | var GenCmdCode = &gcli.Command{ 7 | Name: "gen-cmd", 8 | Desc: "quick generate gcli command code", 9 | } 10 | -------------------------------------------------------------------------------- /builtin/genac/gen_bash.go: -------------------------------------------------------------------------------- 1 | package genac 2 | -------------------------------------------------------------------------------- /builtin/genac/gen_zsh.go: -------------------------------------------------------------------------------- 1 | package genac 2 | -------------------------------------------------------------------------------- /builtin/genac/genac.go: -------------------------------------------------------------------------------- 1 | package genac 2 | -------------------------------------------------------------------------------- /builtin/launcheditor/launch_editer.go: -------------------------------------------------------------------------------- 1 | package launcheditor 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | ) 10 | 11 | // DefaultEditor definition 12 | const DefaultEditor = "vim" 13 | 14 | // GetEditor sets callback to get editor program 15 | var GetEditor func() (string, error) 16 | 17 | func getEditor() (string, error) { 18 | if GetEditor != nil { 19 | return GetEditor() 20 | } 21 | return exec.LookPath(DefaultEditor) 22 | } 23 | 24 | func randomFilename() string { 25 | buf := make([]byte, 16) 26 | if _, err := rand.Read(buf); err != nil { 27 | return "CLI_EDIT_FILE" 28 | } 29 | 30 | return fmt.Sprintf(".%x", buf) 31 | } 32 | 33 | // LaunchEditor launch the specified editor with a random filename 34 | func LaunchEditor(editor string) (content []byte, err error) { 35 | return LaunchWithFilename(editor, randomFilename()) 36 | } 37 | 38 | // LaunchWithFilename launch the specified editor with a filename 39 | func LaunchWithFilename(editor, filename string) (content []byte, err error) { 40 | cmd := exec.Command(editor, filename) 41 | cmd.Stdin = os.Stdin 42 | cmd.Stdout = os.Stdout 43 | cmd.Stderr = os.Stderr 44 | 45 | defer os.Remove(filename) 46 | err = cmd.Run() 47 | 48 | if err != nil { 49 | if _, isExitError := err.(*exec.ExitError); !isExitError { 50 | return 51 | } 52 | } 53 | 54 | content, err = ioutil.ReadFile(filename) 55 | if err != nil { 56 | return []byte{}, nil 57 | } 58 | return 59 | } 60 | -------------------------------------------------------------------------------- /builtin/reverseproxy/reverse_proxy.go: -------------------------------------------------------------------------------- 1 | package reverseproxy 2 | 3 | // ref links: 4 | // https://blog.csdn.net/mengxinghuiku/article/details/65448600 5 | // https://github.com/ilanyu/ReverseProxy 6 | import ( 7 | "fmt" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "net/http/httputil" 12 | "net/url" 13 | 14 | "github.com/gookit/gcli/v3" 15 | ) 16 | 17 | type reverseProxy struct { 18 | listen string 19 | remote string 20 | remoteIP string 21 | } 22 | 23 | var rp = &reverseProxy{} 24 | var dnsServers = []string{ 25 | "114.114.114.114", 26 | "114.114.115.115", 27 | "119.29.29.29", 28 | "223.5.5.5", 29 | "8.8.8.8", 30 | "208.67.222.222", 31 | "208.67.220.220", 32 | } 33 | 34 | // ReverseProxyCmd command 35 | var ReverseProxyCmd = &gcli.Command{ 36 | Name: "proxy", 37 | Func: rp.Run, 38 | Desc: "start a reverse proxy http server", 39 | Config: func(c *gcli.Command) { 40 | c.StrOpt( 41 | &rp.listen, 42 | "listen", "s", "127.0.0.1:1180", 43 | "local proxy server listen address.", 44 | ) 45 | c.StrOpt( 46 | &rp.remote, 47 | "remote", "r", "", 48 | "the remote reverse proxy server `address`. eg http://site.com:80;true", 49 | ) 50 | c.StrOpt( 51 | &rp.remoteIP, 52 | "remoteIP", "", "", 53 | "the remote reverse proxy server IP address.", 54 | ) 55 | 56 | }, 57 | } 58 | 59 | func (rp *reverseProxy) Run(cmd *gcli.Command, args []string) error { 60 | if rp.remote == "" { 61 | return cmd.NewErrf("must be setting the remote server by -r, --remote ") 62 | } 63 | 64 | urlObj, err := url.Parse(rp.remote) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | rpHandler := ReverseProxy(urlObj) 70 | 71 | log.Printf("Listening on %s, forwarding to %s", rp.listen, rp.remote) 72 | log.Fatal(http.ListenAndServe(rp.listen, rpHandler)) 73 | 74 | return nil 75 | } 76 | 77 | /************************************************************* 78 | * Reverse proxy 79 | *************************************************************/ 80 | 81 | // ReverseProxy create a global reverse proxy. 82 | // Usage: 83 | // 84 | // rp := ReverseProxy(&url.URL{ 85 | // Scheme: "http", 86 | // Host: "localhost:9091", 87 | // }, &url.URL{ 88 | // Scheme: "http", 89 | // Host: "localhost:9092", 90 | // }) 91 | // log.Fatal(http.ListenAndServe(":9090", rp)) 92 | func ReverseProxy(targets ...*url.URL) *httputil.ReverseProxy { 93 | if len(targets) == 0 { 94 | panic("Please add at least one remote target server") 95 | } 96 | 97 | var target *url.URL 98 | 99 | // if only one target 100 | if len(targets) == 1 { 101 | target = targets[0] 102 | } 103 | 104 | director := func(req *http.Request) { 105 | if len(targets) > 1 { 106 | target = targets[rand.Int()%len(targets)] 107 | } 108 | 109 | fmt.Printf("Received request %s %s %s\n", req.Method, req.Host, req.RemoteAddr) 110 | 111 | // log.Println(r.RemoteAddr + " " + r.Method + " " + r.URL.String() + " " + r.Proto + " " + r.UserAgent()) 112 | targetQuery := target.RawQuery 113 | 114 | req.URL.Scheme = target.Scheme 115 | req.URL.Host = target.Host 116 | req.URL.Path = target.Path 117 | // req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) 118 | 119 | if targetQuery == "" || req.URL.RawQuery == "" { 120 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 121 | } else { 122 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 123 | } 124 | if _, ok := req.Header["User-Agent"]; !ok { 125 | // explicitly disable User-Agent so it's not set to default value 126 | req.Header.Set("User-Agent", "") 127 | } 128 | } 129 | 130 | return &httputil.ReverseProxy{Director: director} 131 | } 132 | -------------------------------------------------------------------------------- /builtin/tcpproxy/tcp_proxy.go: -------------------------------------------------------------------------------- 1 | package tcpproxy 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gookit/gcli/v3" 7 | ) 8 | 9 | // TCPProxy definition. 10 | // refs: 11 | // https://www.jianshu.com/p/53e219fbf3c5 12 | // https://github.com/yangxikun/gsproxy 13 | type TCPProxy struct { 14 | lock sync.Mutex 15 | } 16 | 17 | // Run server 18 | func (p *TCPProxy) Run() { 19 | 20 | } 21 | 22 | // Handle connection 23 | func (p *TCPProxy) Handle() { 24 | 25 | } 26 | 27 | // var tp = TCPProxy{} 28 | 29 | // TCPProxyCommand command definition 30 | func TCPProxyCommand() *gcli.Command { 31 | cmd := &gcli.Command{ 32 | Func: runServer, 33 | Name: "watch", 34 | 35 | Desc: "file system change notification", 36 | 37 | Aliases: []string{"fwatch", "fswatch"}, 38 | Examples: `watch a dir: 39 | {$fullCmd} -e .git -e .idea -d ./_examples --ext ".go|.md" 40 | watch a file(s): 41 | {$fullCmd} -f _examples/cliapp.go -f app.go 42 | open debug mode: 43 | {$binName} --verbose 4 {$cmd} -e .git -e .idea -d ./_examples --ext ".go|.md" 44 | `, 45 | } 46 | 47 | // cmd.StrOpt(&tp.Dir, "dir", "d", "", "the want watched directory") 48 | // cmd.StrOpt(&opts.Ext, "ext", "", ".go", "the watched file extensions, multi split by '|'") 49 | // cmd.VarOpt(&opts.Files, "files", "f", "the want watched file paths") 50 | // cmd.StrOpt(&opts.Config, "config", "c", "", "load options from a json config") 51 | // cmd.VarOpt(&opts.Exclude, "exclude", "e", "the ignored directory or files") 52 | 53 | return cmd 54 | } 55 | 56 | func runServer(c *gcli.Command, _ []string) error { 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /docs/guide/chapter-01.md: -------------------------------------------------------------------------------- 1 | # chapter 2 | 3 | -------------------------------------------------------------------------------- /events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // constants for hooks event, there are default allowed event names 4 | const ( 5 | // OnAppInitBefore On app init before 6 | OnAppInitBefore = "app.init.before" 7 | // OnAppInitAfter On app init after 8 | OnAppInitAfter = "app.init.after" 9 | // OnAppExit On app exit before 10 | OnAppExit = "app.exit" 11 | 12 | // OnAppBindOptsBefore before bind app options 13 | OnAppBindOptsBefore = "app.bind.opts.before" 14 | // OnAppBindOptsAfter after bind app options. 15 | // 16 | // support binding custom global options 17 | OnAppBindOptsAfter = "app.bind.opts.after" 18 | 19 | // OnAppCmdAdd on app cmd add 20 | OnAppCmdAdd = "app.cmd.add.before" 21 | 22 | // OnAppCmdAdded on app cmd added 23 | OnAppCmdAdded = "app.cmd.added" 24 | 25 | // OnAppOptsParsed event 26 | // 27 | // Data: 28 | // {args: app-args} 29 | OnAppOptsParsed = "app.opts.parsed" 30 | 31 | // OnAppPrepared prepare for run, after the OnAppOptsParsed 32 | OnAppPrepared = "app.run.prepared" 33 | 34 | // OnAppRunBefore app run before, after the OnAppPrepared 35 | OnAppRunBefore = "app.run.before" 36 | OnAppRunAfter = "app.run.after" 37 | OnAppRunError = "app.run.error" 38 | 39 | OnCmdInitBefore = "cmd.init.before" 40 | OnCmdInitAfter = "cmd.init.after" 41 | 42 | // OnCmdNotFound on top-command or subcommand not found. 43 | // 44 | // Ctx: 45 | // {"name": name, "args": []string} 46 | OnCmdNotFound = "cmd.not.found" 47 | 48 | // OnAppCmdNotFound on top command not found. 49 | // ctx: {"name": name, "args": []string} 50 | OnAppCmdNotFound = "app.cmd.not.found" 51 | // OnCmdSubNotFound on subcommand not found. 52 | // ctx: {"name": name, "args": []string} 53 | OnCmdSubNotFound = "cmd.sub.not.found" 54 | 55 | // OnCmdOptParsed event 56 | // 57 | // Data: 58 | // {args: command-args} 59 | OnCmdOptParsed = "cmd.opts.parsed" 60 | 61 | // OnCmdRunBefore cmd run, flags has been parsed. 62 | OnCmdRunBefore = "cmd.run.before" 63 | // OnCmdRunAfter after cmd success run 64 | OnCmdRunAfter = "cmd.run.after" 65 | OnCmdRunError = "cmd.run.error" 66 | 67 | // OnCmdExecBefore cmd exec 68 | OnCmdExecBefore = "cmd.exec.before" 69 | OnCmdExecAfter = "cmd.exec.after" 70 | OnCmdExecError = "cmd.exec.error" 71 | 72 | // OnGlobalOptsParsed app or cmd parsed the global options 73 | // 74 | // Data: 75 | // {args: remain-args} 76 | OnGlobalOptsParsed = "gcli.gopts.parsed" 77 | ) 78 | -------------------------------------------------------------------------------- /ext_test.go: -------------------------------------------------------------------------------- 1 | package gcli_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/gcli/v3" 7 | "github.com/gookit/goutil/byteutil" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestHelpReplacer(t *testing.T) { 12 | is := assert.New(t) 13 | vs := gcli.HelpReplacer{} 14 | 15 | vs.AddReplaces(map[string]string{ 16 | "key0": "val0", 17 | "key1": "val1", 18 | }) 19 | 20 | is.Len(vs.Replaces(), 2) 21 | is.Contains(vs.Replaces(), "key0") 22 | 23 | vs.AddReplaces(map[string]string{"key2": "val2"}) 24 | vs.AddReplace("key3", "val3") 25 | 26 | is.Eq("val3", vs.GetReplace("key3")) 27 | is.Eq("", vs.GetReplace("not-exist")) 28 | 29 | is.Eq("hello val0", vs.ReplacePairs("hello {$key0}")) 30 | is.Eq("hello val0 val2", vs.ReplacePairs("hello {$key0} {$key2}")) 31 | // invalid input 32 | is.Eq("hello {key0}", vs.ReplacePairs("hello {key0}")) 33 | } 34 | 35 | func TestHooks_Fire(t *testing.T) { 36 | is := assert.New(t) 37 | buf := byteutil.NewBuffer() 38 | hooks := gcli.Hooks{} 39 | 40 | hooks.AddHook("test", func(ctx *gcli.HookCtx) bool { 41 | buf.WriteString("fire the test hook") 42 | return false 43 | }) 44 | 45 | hooks.Fire("test", nil) 46 | is.Eq("fire the test hook", buf.ResetGet()) 47 | 48 | hooks.Fire("not-exist", nil) 49 | hooks.On("*", func(ctx *gcli.HookCtx) bool { 50 | buf.WriteString("fire the * hook") 51 | return false 52 | }) 53 | 54 | // add prefix hook 55 | hooks.On("app.test.*", func(ctx *gcli.HookCtx) bool { 56 | buf.WriteString("fire the app.test.* hook") 57 | return false 58 | }) 59 | 60 | hooks.Fire("app.test.init", nil) 61 | 62 | s := buf.ResetGet() 63 | is.StrContains(s, "fire the app.test.* hook") 64 | is.StrContains(s, "fire the * hook") 65 | } 66 | -------------------------------------------------------------------------------- /gcli_test.go: -------------------------------------------------------------------------------- 1 | package gcli_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/gookit/gcli/v3" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestGcliBasic(t *testing.T) { 12 | is := assert.New(t) 13 | is.NotEmpty(gcli.Version()) 14 | is.NotEmpty(gcli.CommitID()) 15 | } 16 | 17 | func TestVerbose(t *testing.T) { 18 | is := assert.New(t) 19 | 20 | old := gcli.Verbose() 21 | is.Eq(gcli.VerbError, old) 22 | is.False(gcli.GOpts().NoColor) 23 | 24 | gcli.SetDebugMode() 25 | is.Eq(gcli.VerbDebug, gcli.Verbose()) 26 | 27 | gcli.SetQuietMode() 28 | is.Eq(gcli.VerbQuiet, gcli.Verbose()) 29 | 30 | info := gcli.VerbInfo 31 | gcli.SetVerbose(info) 32 | is.Eq(info, gcli.Verbose()) 33 | is.Eq(3, info.Int()) 34 | is.Eq("info", info.Name()) 35 | is.Eq("INFO", info.Upper()) 36 | 37 | gcli.SetVerbose(old) 38 | is.Eq(gcli.VerbError, gcli.Verbose()) 39 | } 40 | 41 | func TestVerbLevel(t *testing.T) { 42 | is := assert.New(t) 43 | 44 | verb := gcli.VerbLevel(23) 45 | is.Eq("unknown", verb.Name()) 46 | is.Eq(23, verb.Int()) 47 | 48 | err := verb.Set("2") 49 | is.NoErr(err) 50 | is.Eq(gcli.VerbWarn, verb) 51 | is.Eq("warn", verb.Name()) 52 | 53 | err = verb.Set("debug") 54 | is.NoErr(err) 55 | is.Eq(gcli.VerbDebug, verb) 56 | is.Eq("debug", verb.Name()) 57 | 58 | err = verb.Set("30") 59 | is.NoErr(err) 60 | is.Eq(gcli.VerbCrazy, verb) 61 | is.Eq("crazy", verb.Name()) 62 | } 63 | 64 | func TestStrictMode(t *testing.T) { 65 | is := assert.New(t) 66 | 67 | old := gcli.StrictMode() 68 | defer func() { 69 | gcli.SetStrictMode(old) 70 | }() 71 | 72 | gcli.SetStrictMode(false) 73 | is.False(gcli.StrictMode()) 74 | 75 | gcli.SetStrictMode(true) 76 | is.True(gcli.StrictMode()) 77 | } 78 | 79 | func TestNewCtx(t *testing.T) { 80 | is := assert.New(t) 81 | 82 | ctx := gcli.NewCtx() 83 | ctx.InitCtx() 84 | 85 | is.True(ctx.PID() > 0) 86 | is.Eq(runtime.GOOS, ctx.OsName()) 87 | is.NotEmpty(ctx.BinName()) 88 | is.NotEmpty(ctx.WorkDir()) 89 | 90 | args := ctx.OsArgs() 91 | is.NotEmpty(args) 92 | 93 | if len(args) > 1 { 94 | is.NotEmpty(ctx.ArgLine()) 95 | } else { 96 | is.Empty(ctx.ArgLine()) 97 | } 98 | } 99 | 100 | func TestSetStrictMode(t *testing.T) { 101 | stm := gcli.StrictMode() 102 | defer gcli.SetStrictMode(stm) 103 | 104 | opts := struct { 105 | name string 106 | ok, bl bool 107 | }{} 108 | 109 | // gcli.SetVerbose(gcli.VerbDebug) 110 | app := gcli.NewApp(gcli.NotExitOnEnd()) 111 | app.Add(&gcli.Command{ 112 | Name: "test", 113 | Config: func(c *gcli.Command) { 114 | c.StrOpt(&opts.name, "name", "n", "", "1") 115 | c.BoolOpt(&opts.ok, "ok", "o", true, "2") 116 | c.BoolOpt(&opts.bl, "bl", "b", false, "3") 117 | }, 118 | Func: func(c *gcli.Command, _ []string) error { 119 | return nil 120 | }, 121 | }) 122 | 123 | app.Run([]string{"test", "-o", "-n", "inhere"}) 124 | assert.Eq(t, "inhere", opts.name) 125 | assert.True(t, opts.ok) 126 | 127 | app.Run([]string{"test", "-o=false", "-n=inhere"}) 128 | assert.Eq(t, "inhere", opts.name) 129 | assert.False(t, opts.ok) 130 | 131 | app.Run([]string{"test", "-ob"}) 132 | // assert.StrContains(t, errMsg, "ddd") 133 | 134 | gcli.SetStrictMode(true) 135 | app.Run([]string{"test", "-ob"}) 136 | assert.True(t, opts.ok) 137 | assert.True(t, opts.bl) 138 | } 139 | 140 | func TestString(t *testing.T) { 141 | s := gcli.String("ab,cd") 142 | assert.Eq(t, "ab,cd", s.String()) 143 | assert.Eq(t, []string{"ab", "cd"}, s.Split(",")) 144 | } 145 | -------------------------------------------------------------------------------- /gflag/README.md: -------------------------------------------------------------------------------- 1 | # Gflag 2 | 3 | `gflag` provide command line options and arguments parse, binding, management. 4 | 5 | ## GoDoc 6 | 7 | Please see https://pkg.go.dev/github.com/gookit/gcli/v3 8 | 9 | ## Install 10 | 11 | ```shell 12 | go get github.com/gookit/gcli/v3/gflag 13 | ``` 14 | 15 | ## Flags 16 | 17 | - `options` - start with `-` or `--`, and the first character must be a letter 18 | - `arguments` - not start with `-` or `--`, and after options. 19 | 20 | ### Flag Options 21 | 22 | - Support long option. eg: `--long` `--long value` 23 | - Support short option. eg: `-s -a value` 24 | - Support define array option 25 | - eg: `--tag php --tag go` will get `tag: [php, go]` 26 | 27 | ### Flag Arguments 28 | 29 | - Support binding named argument 30 | - Support define array argument 31 | 32 | ## Usage 33 | 34 | ```go file="demo.go" 35 | package main 36 | 37 | import ( 38 | "fmt" 39 | "os" 40 | 41 | "github.com/gookit/gcli/v3/gflag" 42 | "github.com/gookit/goutil" 43 | ) 44 | 45 | var name string 46 | 47 | func main() { 48 | gf := gflag.New("testFlags") 49 | gf.StrOpt(&name, "name", "n", "", "") 50 | 51 | gf.SetHandle(func(p *gflag.Parser) error { 52 | fmt.Println(p.Name()) 53 | return nil 54 | }) 55 | 56 | goutil.MustOK(gf.Parse(os.Args[1:])) 57 | } 58 | ``` 59 | 60 | ### Run 61 | 62 | ```shell 63 | go run demo.go 64 | ``` 65 | 66 | ## Binding methods 67 | 68 | ### Binding cli options 69 | 70 | ```go 71 | Bool(name, shorts string, defVal bool, desc string) *bool 72 | BoolOpt(ptr *bool, name, shorts string, defVal bool, desc string) 73 | BoolVar(ptr *bool, opt *CliOpt) 74 | Float64Opt(p *float64, name, shorts string, defVal float64, desc string) 75 | Float64Var(ptr *float64, opt *CliOpt) 76 | 77 | Int(name, shorts string, defValue int, desc string) *int 78 | Int64(name, shorts string, defValue int64, desc string) *int64 79 | Int64Opt(ptr *int64, name, shorts string, defValue int64, desc string) 80 | Int64Var(ptr *int64, opt *CliOpt) 81 | IntOpt(ptr *int, name, shorts string, defValue int, desc string) 82 | IntVar(ptr *int, opt *CliOpt) 83 | 84 | Str(name, shorts string, defValue, desc string) *string 85 | StrOpt(p *string, name, shorts, defValue, desc string) 86 | StrVar(p *string, opt *CliOpt) 87 | 88 | Uint(name, shorts string, defVal uint, desc string) *uint 89 | Uint64(name, shorts string, defVal uint64, desc string) *uint64 90 | Uint64Opt(ptr *uint64, name, shorts string, defVal uint64, desc string) 91 | Uint64Var(ptr *uint64, opt *CliOpt) 92 | UintOpt(ptr *uint, name, shorts string, defValue uint, desc string) 93 | UintVar(ptr *uint, opt *CliOpt) 94 | 95 | Var(ptr flag.Value, opt *CliOpt) 96 | VarOpt(v flag.Value, name, shorts, desc string) 97 | ``` 98 | 99 | ### Binding cli arguments 100 | 101 | ```go 102 | AddArg(name, desc string, requiredAndArrayed ...bool) *CliArg 103 | AddArgByRule(name, rule string) *CliArg 104 | AddArgument(arg *CliArg) *CliArg 105 | BindArg(arg *CliArg) *CliArg 106 | ``` 107 | -------------------------------------------------------------------------------- /gflag/args_test.go: -------------------------------------------------------------------------------- 1 | package gflag_test 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/gookit/gcli/v3/gflag" 9 | "github.com/gookit/goutil/testutil/assert" 10 | ) 11 | 12 | func TestArguments_AddArgByRule(t *testing.T) { 13 | is := assert.New(t) 14 | ags := gflag.Arguments{} 15 | 16 | arg := ags.AddArgByRule("arg2", "arg2 desc;false;23") 17 | is.Eq("arg2 desc", arg.Desc) 18 | is.Eq(23, arg.Int()) 19 | is.Eq(false, arg.Arrayed) 20 | } 21 | 22 | func TestArguments_BindArg(t *testing.T) { 23 | is := assert.New(t) 24 | ags := gflag.Arguments{} 25 | 26 | ags.BindArg(&gflag.Argument{Name: "ag0"}) 27 | is.True(ags.HasArg("ag0")) 28 | } 29 | 30 | func TestArgument(t *testing.T) { 31 | is := assert.New(t) 32 | arg := gflag.NewArgument("arg0", "arg desc") 33 | 34 | is.False(arg.Arrayed) 35 | is.False(arg.Required) 36 | is.True(arg.IsEmpty()) 37 | is.False(arg.HasValue()) 38 | 39 | is.Eq("arg0", arg.Name) 40 | is.Eq("arg desc", arg.Desc) 41 | is.Eq(0, arg.Index()) 42 | 43 | // no value 44 | is.Nil(arg.Strings()) 45 | is.Nil(arg.GetValue()) 46 | is.Nil(arg.SplitToStrings()) 47 | is.Eq(0, arg.Int()) 48 | is.Eq("", arg.String()) 49 | is.Eq("ab", arg.WithValue("ab").String()) 50 | is.Eq("abc", arg.WithDefault("abc").String()) 51 | 52 | // add value 53 | err := arg.SetValue("ab,cd") 54 | is.NoErr(err) 55 | 56 | is.Eq(0, arg.Int()) 57 | is.Eq("ab,cd", arg.String()) 58 | is.Eq([]string{"ab", "cd"}, arg.Array()) 59 | is.Eq([]string{"ab", "cd"}, arg.SplitToStrings(",")) 60 | 61 | // int value 62 | is.NoErr(arg.SetValue(23)) 63 | is.Eq(23, arg.Int()) 64 | is.Eq("23", arg.String()) 65 | 66 | // string int value 67 | err = arg.SetValue("23") 68 | is.NoErr(err) 69 | is.Eq(23, arg.Int()) 70 | is.Eq("23", arg.String()) 71 | 72 | // array value 73 | arg.WithArrayed() 74 | is.NoErr(arg.SetValue([]string{"a", "b"})) 75 | is.True(arg.Arrayed) 76 | is.Eq(0, arg.Int()) 77 | is.Eq("[a b]", arg.String()) 78 | is.Eq([]string{"a", "b"}, arg.Array()) 79 | 80 | arg = gflag.NewArgument("arg0", "arg desc").SetArrayed() 81 | is.True(arg.Arrayed) 82 | 83 | // required and is-array 84 | arg = gflag.NewArgument("arg1", "arg desc", true, true) 85 | arg.Init() 86 | is.True(arg.Arrayed) 87 | is.True(arg.Required) 88 | is.Eq("arg1...", arg.HelpName()) 89 | } 90 | 91 | func TestArgument_GetValue(t *testing.T) { 92 | arg := gflag.NewArgument("arg0", "arg desc") 93 | 94 | // custom handler 95 | assert.NoErr(t, arg.SetValue("a-b-c")) 96 | arg.Handler = func(value any) any { 97 | str := value.(string) 98 | return strings.SplitN(str, "-", 2) 99 | } 100 | assert.Eq(t, []string{"a", "b-c"}, arg.GetValue()) 101 | } 102 | 103 | var str2int = func(val any) (any, error) { 104 | return strconv.Atoi(val.(string)) 105 | } 106 | 107 | func TestArgument_WithConfig(t *testing.T) { 108 | arg := gflag.NewArgument("arg0", "arg desc").WithFn(func(arg *gflag.CliArg) { 109 | _ = arg.SetValue(23) 110 | arg.Init() 111 | }) 112 | 113 | assert.Eq(t, 23, arg.Val()) 114 | assert.Eq(t, "arg0", arg.HelpName()) 115 | } 116 | 117 | func TestArgument_SetValue(t *testing.T) { 118 | arg := gflag.NewArgument("arg0", "arg desc") 119 | // convert "12" to 12 120 | arg.Validator = str2int 121 | 122 | err := arg.SetValue("12") 123 | assert.NoErr(t, err) 124 | assert.Eq(t, 12, arg.Val()) 125 | arg.Set(nil) // reset value 126 | 127 | err = arg.SetValue("abc") 128 | assert.Err(t, err) 129 | assert.Nil(t, arg.Val()) 130 | 131 | // convert "12" to 12 132 | arg = gflag.NewArgument("arg0", "arg desc").WithValidator(str2int) 133 | err = arg.SetValue("12") 134 | assert.NoErr(t, err) 135 | assert.Eq(t, 12, arg.Val()) 136 | } 137 | 138 | func TestCliArgs_AddArg_panic(t *testing.T) { 139 | is := assert.New(t) 140 | c := gflag.Arguments{} 141 | c.SetName("test") 142 | 143 | arg := c.AddArg("arg0", "arg desc", true) 144 | is.Eq(0, arg.Index()) 145 | 146 | ret := c.ArgByIndex(0) 147 | is.Eq(ret, arg) 148 | 149 | assert.PanicsMsg(t, func() { 150 | c.ArgByIndex(1) 151 | }, "gflag: get not exists argument #1") 152 | 153 | arg = c.AddArg("arg1", "arg1 desc") 154 | is.Eq(1, arg.Index()) 155 | 156 | ret = c.Arg("arg1") 157 | is.Eq(ret, arg) 158 | 159 | is.PanicsMsg(func() { 160 | c.Arg("not-exist") 161 | }, "gflag: get not exists argument 'not-exist'") 162 | 163 | is.Len(c.Args(), 2) 164 | 165 | is.PanicsMsg(func() { 166 | c.AddArg("", "desc") 167 | }, "gflag: the command argument name cannot be empty") 168 | 169 | is.PanicsMsg(func() { 170 | c.AddArg(":)&dfd", "desc") 171 | }, "gflag: the argument name ':)&dfd' is invalid, must match: ^[a-zA-Z][\\w-]*$") 172 | 173 | is.PanicsMsg(func() { 174 | c.AddArg("arg1", "desc") 175 | }, "gflag: the argument name 'arg1' already exists in command 'test'") 176 | is.PanicsMsg(func() { 177 | c.AddArg("arg2", "arg2 desc", true) 178 | }, "gflag: required argument 'arg2' cannot be defined after optional argument") 179 | 180 | c.AddArg("arg3", "arg3 desc", false, true) 181 | is.PanicsMsg(func() { 182 | c.AddArg("argN", "desc", true) 183 | }, "gflag: have defined an array argument, you cannot add argument 'argN'") 184 | } 185 | -------------------------------------------------------------------------------- /gflag/gflag.go: -------------------------------------------------------------------------------- 1 | // Package gflag provide command line options and arguments binding, parse, management. 2 | package gflag 3 | 4 | import ( 5 | "github.com/gookit/goutil/cflag" 6 | "github.com/gookit/goutil/strutil" 7 | ) 8 | 9 | const ( 10 | // AlignLeft Align right, padding left 11 | AlignLeft = strutil.PosRight 12 | // AlignRight Align left, padding right 13 | AlignRight = strutil.PosLeft 14 | // default desc 15 | defaultDesc = "No description" 16 | ) 17 | 18 | const ( 19 | // TagRuleNamed struct tag use named k-v rule. 20 | // 21 | // eg: 22 | // `flag:"name=int0;shorts=i;required=true;desc=int option message"` 23 | // // name contains short name 24 | // `flag:"name=int0,i;required=true;desc=int option message"` 25 | TagRuleNamed uint8 = iota 26 | 27 | // TagRuleSimple struct tag use simple rule. 28 | // format: "desc;required;default;shorts" 29 | // 30 | // eg: `flag:"int option message;required;;i"` 31 | TagRuleSimple 32 | 33 | // TagRuleField struct tag use field name as flag setting name. TODO 34 | // 35 | // eg: `flag:"name,n" desc:"int option message" required:"true" default:"0"` 36 | TagRuleField 37 | ) 38 | 39 | // FlagTagName default tag name on struct 40 | var FlagTagName = "flag" 41 | 42 | // ConfigFunc config func for parser 43 | type ConfigFunc func(cfg *Config) 44 | 45 | // Config for render help information 46 | type Config struct { 47 | // WithoutType don't display flag data type on print help 48 | WithoutType bool 49 | // DescNewline flag desc at new line on print help 50 | DescNewline bool 51 | // Alignment flag name align left or right. default is: left 52 | Alignment strutil.PosFlag 53 | // TagName on struct. default is FlagTagName 54 | TagName string 55 | // TagRuleType for struct tag value. default is TagRuleNamed 56 | TagRuleType uint8 57 | // DisableArg disable binding arguments. 58 | DisableArg bool 59 | // IndentLongOpt indent long option name on print help 60 | IndentLongOpt bool 61 | // EnhanceShort enhance short option parse. TODO 62 | // 63 | // 0 - none 64 | // 1 - multi short bool options. eg: `-aux` = `-a -u -x` 65 | // 2 - allow name with value as one node. eg: `-Ostdout` = `-O stdout` 66 | EnhanceShort uint8 67 | } 68 | 69 | // GetTagName get tag name, default is FlagTagName 70 | func (c *Config) GetTagName() string { 71 | if c.TagName == "" { 72 | c.TagName = FlagTagName 73 | } 74 | return c.TagName 75 | } 76 | 77 | // WithIndentLongOpt on print help 78 | func WithIndentLongOpt(yes bool) ConfigFunc { 79 | return func(cfg *Config) { 80 | cfg.IndentLongOpt = yes 81 | } 82 | } 83 | 84 | // OptCategory struct 85 | type OptCategory struct { 86 | Name, Title string 87 | OptNames []string 88 | } 89 | 90 | // Ints The int flag list, implemented flag.Value interface 91 | type Ints = cflag.Ints 92 | 93 | // IntsString implemented flag.Value interface 94 | type IntsString = cflag.IntsString 95 | 96 | // String The special string flag, implemented flag.Value interface 97 | type String = cflag.String 98 | 99 | // Strings The string flag list, implemented flag.Value interface 100 | type Strings = cflag.Strings 101 | 102 | // Booleans The bool flag list, implemented flag.Value interface 103 | type Booleans = cflag.Booleans 104 | 105 | // EnumString The string flag list, implemented flag.Value interface 106 | type EnumString = cflag.EnumString 107 | 108 | // KVString The key-value string flag, repeatable. 109 | type KVString = cflag.KVString 110 | 111 | // ConfString The config-string flag, INI format, like nginx-config. 112 | type ConfString = cflag.ConfString 113 | -------------------------------------------------------------------------------- /gflag/help.go: -------------------------------------------------------------------------------- 1 | package gflag 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/gookit/color" 9 | "github.com/gookit/goutil/cflag" 10 | "github.com/gookit/goutil/strutil" 11 | ) 12 | 13 | /*********************************************************************** 14 | * Flags: 15 | * - render help message 16 | ***********************************************************************/ 17 | 18 | // SetHelpRender set the raw *flag.FlagSet.Usage 19 | func (p *Parser) SetHelpRender(fn func()) { 20 | p.fSet.Usage = fn 21 | } 22 | 23 | // PrintHelpPanel for all options to the gf.out 24 | func (p *Parser) PrintHelpPanel() { 25 | color.Fprint(p.out, p.String()) 26 | } 27 | 28 | // String for all flag options 29 | func (p *Parser) String() string { 30 | return p.BuildHelp() 31 | } 32 | 33 | // BuildHelp string for all flag options 34 | func (p *Parser) BuildHelp() string { 35 | if p.buf == nil { 36 | p.buf = new(bytes.Buffer) 37 | } 38 | 39 | // repeat call the method 40 | if p.buf.Len() < 1 { 41 | p.buf.WriteString("Options:\n") 42 | p.buf.WriteString(p.BuildOptsHelp()) 43 | p.buf.WriteByte('\n') 44 | 45 | if p.HasArgs() { 46 | p.buf.WriteString("Arguments:\n") 47 | p.buf.WriteString(p.BuildArgsHelp()) 48 | p.buf.WriteByte('\n') 49 | } 50 | } 51 | 52 | return p.buf.String() 53 | } 54 | 55 | // BuildOptsHelp string. 56 | func (p *Parser) BuildOptsHelp() string { 57 | var sb strings.Builder 58 | 59 | p.fSet.VisitAll(func(f *Flag) { 60 | line := p.formatOneFlag(f) 61 | if line != "" { 62 | sb.WriteString(line) 63 | sb.WriteByte('\n') 64 | } 65 | }) 66 | 67 | return sb.String() 68 | } 69 | 70 | func (p *Parser) formatOneFlag(f *Flag) (s string) { 71 | // Skip render: 72 | // - opt is not exists(Has ensured that it is not a short name) 73 | // - it is hidden flag option 74 | // - flag desc is empty 75 | opt, has := p.opts[f.Name] 76 | if !has || opt.Hidden { 77 | return 78 | } 79 | 80 | var fullName string 81 | name := f.Name 82 | // eg: "-V, --version" length is: 13 83 | nameLen := p.names[name] 84 | // display description on new line 85 | descNl := p.cfg.DescNewline 86 | 87 | var nlIndent string 88 | if descNl { 89 | nlIndent = "\n " 90 | } else { 91 | nlIndent = "\n " + strings.Repeat(" ", p.optMaxLen) 92 | } 93 | 94 | // add prefix '-' to option 95 | fullName = cflag.AddPrefixes2(name, opt.Shorts, true) 96 | if p.hasShort && p.cfg.IndentLongOpt && fullName[1] == '-' { 97 | nameLen += 4 98 | fullName = " " + fullName 99 | } 100 | 101 | s = fmt.Sprintf(" %s", fullName) 102 | 103 | // - build flag type info 104 | typeName, desc := UnquoteUsage(f) 105 | // typeName: option value data type: int, string, ..., bool value will return "" 106 | if !p.cfg.WithoutType && len(typeName) > 0 { 107 | typeLen := len(typeName) + 1 108 | if !descNl && nameLen+typeLen > p.optMaxLen { 109 | descNl = true 110 | } else { 111 | nameLen += typeLen 112 | } 113 | 114 | s += fmt.Sprintf(" %s", typeName) 115 | } 116 | 117 | if descNl { 118 | s += nlIndent 119 | } else { 120 | // padding space to optMaxLen width. 121 | if padLen := p.optMaxLen - nameLen; padLen > 0 { 122 | s += strings.Repeat(" ", padLen) 123 | } 124 | s += " " 125 | } 126 | 127 | // --- build description 128 | if desc == "" { 129 | desc = defaultDesc 130 | } else { 131 | desc = strings.Replace(strutil.UpperFirst(desc), "\n", nlIndent, -1) 132 | } 133 | 134 | s += requiredMark(opt.Required) + desc 135 | 136 | // ---- append default value 137 | if isZero, isStr := cflag.IsZeroValue(f, f.DefValue); !isZero { 138 | if isStr { 139 | s += fmt.Sprintf(" (default %q)", f.DefValue) 140 | } else { 141 | s += fmt.Sprintf(" (default %v)", f.DefValue) 142 | } 143 | } 144 | 145 | // arrayed, repeatable 146 | if _, ok := f.Value.(cflag.RepeatableFlag); ok { 147 | s += " (repeatable)" 148 | } 149 | return s 150 | } 151 | -------------------------------------------------------------------------------- /gflag/opts_test.go: -------------------------------------------------------------------------------- 1 | package gflag_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/gookit/gcli/v3/gflag" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestCliOpts_useShorts(t *testing.T) { 12 | co := gflag.CliOpts{} 13 | co.SetName("opts") 14 | co.InitFlagSet() 15 | 16 | var str string 17 | co.StrOpt(&str, "str", "s", "a string option") 18 | 19 | err := co.ParseOpts([]string{"-s", "val"}) 20 | assert.NoErr(t, err) 21 | assert.Eq(t, "val", str) 22 | } 23 | 24 | func TestCliOpt_basic(t *testing.T) { 25 | opt := gflag.NewOpt("opt1", "a option", "") 26 | opt.WithOptFns(gflag.WithDefault("abc"), gflag.WithRequired(), gflag.WithShorts("o", "a")) 27 | 28 | assert.True(t, opt.Required) 29 | assert.Eq(t, "abc", opt.DefVal) 30 | assert.Eq(t, "o,a", opt.Shorts2String()) 31 | 32 | opt = gflag.NewOpt("opt1", "a option", "") 33 | opt.WithOptFns(gflag.WithShortcut("o")) 34 | 35 | assert.False(t, opt.Required) 36 | assert.Eq(t, "o", opt.Shorts2String()) 37 | } 38 | 39 | func TestCliOpt_Validate(t *testing.T) { 40 | fm := gflag.CliOpt{Name: "opt1"} 41 | assert.False(t, fm.Required) 42 | 43 | fm.WithOptFns(gflag.WithRequired(), gflag.WithValidator(func(val string) error { 44 | if len(val) < 5 { 45 | return errors.New("flag value min len is 5") 46 | } 47 | return nil 48 | })) 49 | 50 | assert.True(t, fm.Required) 51 | 52 | err := fm.Validate("") 53 | assert.Err(t, err) 54 | assert.Eq(t, "option 'opt1' is required", err.Error()) 55 | 56 | err = fm.Validate("val") 57 | assert.Err(t, err) 58 | assert.Eq(t, "flag value min len is 5", err.Error()) 59 | 60 | err = fm.Validate("value") 61 | assert.NoErr(t, err) 62 | } 63 | -------------------------------------------------------------------------------- /gflag/util.go: -------------------------------------------------------------------------------- 1 | package gflag 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gookit/goutil/arrutil" 7 | "github.com/gookit/goutil/cflag" 8 | "github.com/gookit/goutil/comdef" 9 | "github.com/gookit/goutil/strutil" 10 | ) 11 | 12 | func sepStr(seps []string) string { 13 | if len(seps) > 0 { 14 | return seps[0] 15 | } 16 | return comdef.DefaultSep 17 | } 18 | 19 | func requiredMark(must bool) string { 20 | if must { 21 | return "*" 22 | } 23 | return "" 24 | } 25 | 26 | // panicf message 27 | func panicf(format string, v ...any) { 28 | panic(fmt.Sprintf("gflag: "+format, v...)) 29 | } 30 | 31 | // allowed keys on struct tag. 32 | // 33 | // Parse named rule: parse tag named k-v value. item split by ';' 34 | // 35 | // eg: "name=int0;shorts=i;required=true;desc=int option message" 36 | // 37 | // Supported field name: 38 | // 39 | // name 40 | // desc 41 | // shorts 42 | // required 43 | // default 44 | var ( 45 | flagTagKeys = arrutil.Strings{"name", "desc", "required", "default", "shorts"} 46 | flagTagKeys1 = arrutil.Strings{"desc", "required", "default", "shorts"} 47 | flagArgKeys = arrutil.Strings{"desc", "required", "default"} 48 | ) 49 | 50 | // struct tag value use simple rule. each item split by ';' 51 | // 52 | // - format: "name;desc;required;default;shorts" 53 | // - format: "desc;required;default;shorts" 54 | // 55 | // eg: 56 | // 57 | // "int option message;required;i" 58 | // "opt-name;int option message;;a,b" 59 | // "int option message;;a,b;23" 60 | // 61 | // Returns field name: 62 | // 63 | // name 64 | // desc 65 | // shorts 66 | // required 67 | // default 68 | func parseSimpleRule(rule string) (mp map[string]string) { 69 | ss := strutil.SplitNTrimmed(rule, ";", 5) 70 | ln := len(ss) 71 | if ln == 0 { 72 | return 73 | } 74 | 75 | mp = make(map[string]string, ln) 76 | if ln == 1 { 77 | mp["desc"] = ss[0] 78 | return 79 | } 80 | 81 | // first is name 82 | if cflag.IsGoodName(ss[0]) { 83 | return arrutil.CombineToSMap(flagTagKeys, ss) 84 | } 85 | return arrutil.CombineToSMap(flagTagKeys1, ss) 86 | } 87 | 88 | // UnquoteUsage extracts a back-quoted name from the usage 89 | // string for a flag and returns it and the un-quoted usage. 90 | // Given "a `name` to show" it returns ("name", "a name to show"). 91 | // If there are no back quotes, the name is an educated guess of the 92 | // type of the flag's value, or the empty string if the flag is boolean. 93 | // 94 | // Note: from go flag.UnquoteUsage() 95 | func UnquoteUsage(flag *Flag) (name string, usage string) { 96 | // Look for a back-quoted name, but avoid the strings package. 97 | usage = flag.Usage 98 | for i := 0; i < len(usage); i++ { 99 | if usage[i] == '`' { 100 | for j := i + 1; j < len(usage); j++ { 101 | if usage[j] == '`' { 102 | name = usage[i+1 : j] 103 | usage = usage[:i] + name + usage[j+1:] 104 | return name, usage 105 | } 106 | } 107 | break // Only one back quote; use type name. 108 | } 109 | } 110 | 111 | // No explicit name, so use type if we can find one. 112 | name = "value" 113 | switch fv := flag.Value.(type) { 114 | case boolFlag: 115 | if fv.IsBoolFlag() { 116 | name = "" 117 | } 118 | case *durationValue: 119 | name = "duration" 120 | case *float64Value: 121 | name = "float" 122 | case *intValue, *int64Value: 123 | name = "int" 124 | case *stringValue: 125 | name = "string" 126 | case *uintValue, *uint64Value: 127 | name = "uint" 128 | } 129 | return 130 | } 131 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gookit/gcli/v3 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gookit/color v1.5.4 7 | github.com/gookit/goutil v0.6.18 8 | golang.org/x/crypto v0.36.0 9 | ) 10 | 11 | require ( 12 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 13 | golang.org/x/sync v0.12.0 // indirect 14 | golang.org/x/sys v0.31.0 // indirect 15 | golang.org/x/term v0.30.0 // indirect 16 | golang.org/x/text v0.23.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 3 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 4 | github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw= 5 | github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 8 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 9 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 10 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 11 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 12 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 13 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 14 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 15 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 16 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 17 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 18 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 19 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 20 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 21 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 22 | -------------------------------------------------------------------------------- /helper/clog.go: -------------------------------------------------------------------------------- 1 | package helper 2 | -------------------------------------------------------------------------------- /helper/download.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net/http" 8 | "os" 9 | "path" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/gookit/gcli/v3/progress" 15 | "github.com/gookit/goutil/fmtutil" 16 | ) 17 | 18 | // Downloader struct definition. 19 | // refer: https://gist.github.com/albulescu/e61979cc852e4ee8f49c 20 | type Downloader struct { 21 | saveAs string // build by SaveDir + Filename 22 | 23 | FileURL string 24 | SaveDir string 25 | Filename string // save file name. 26 | Progress bool // display progress info 27 | } 28 | 29 | // Download begin 30 | func (d *Downloader) Download() error { 31 | return nil 32 | } 33 | 34 | // show download progress info 35 | func (d *Downloader) showProgress() { 36 | } 37 | 38 | // Download file from remote URL. 39 | // from https://gist.github.com/albulescu/e61979cc852e4ee8f49c 40 | func Download(url, saveDir string, rename ...string) error { 41 | filename := path.Base(url) 42 | fmt.Printf("Downloading file from %s\n", url) 43 | 44 | saveDir = strings.TrimRight(saveDir, "/") 45 | saveAs := saveDir + "/" + filename 46 | if len(rename) > 0 { 47 | saveAs = saveDir + "/" + rename[0] 48 | } 49 | 50 | exist := true 51 | if _, err := os.Stat(saveAs); err != nil { 52 | if os.IsNotExist(err) { 53 | exist = false 54 | } 55 | } 56 | 57 | if exist { 58 | fmt.Printf("Remove old file %s\n", saveAs) 59 | if err := os.Remove(saveAs); err != nil { 60 | return err 61 | } 62 | } 63 | 64 | start := time.Now() 65 | outFile, err := os.Create(saveAs) 66 | if err != nil { 67 | fmt.Println(saveAs) 68 | panic(err) 69 | } 70 | //noinspection GoUnhandledErrorResult 71 | defer outFile.Close() 72 | 73 | headResp, err := http.Head(url) 74 | if err != nil { 75 | panic(err) 76 | } 77 | //noinspection GoUnhandledErrorResult 78 | defer headResp.Body.Close() 79 | 80 | // get remote file size from header. 81 | length := headResp.Header.Get("Content-Length") 82 | size, err := strconv.Atoi(length) 83 | if err != nil { // no "Content-Length" 84 | size = 0 85 | } 86 | 87 | done := make(chan int64) 88 | go printDownloadPercent(done, saveAs, int64(size)) 89 | 90 | resp, err := http.Get(url) 91 | if err != nil { 92 | panic(err) 93 | } 94 | //noinspection GoUnhandledErrorResult 95 | defer resp.Body.Close() 96 | 97 | if resp.StatusCode != 200 { 98 | log.Fatalf("status code error: %d %s", resp.StatusCode, resp.Status) 99 | } 100 | 101 | n, err := io.Copy(outFile, resp.Body) 102 | if err != nil { 103 | panic(err) 104 | } 105 | 106 | done <- n 107 | 108 | elapsed := time.Since(start) 109 | fmt.Printf("Download completed in %s\n", elapsed) 110 | return nil 111 | } 112 | 113 | func printDownloadPercent(done chan int64, path string, total int64) { 114 | fmtSize := "unknown" 115 | if total > 0 { 116 | fmtSize = fmtutil.DataSize(uint64(total)) 117 | } 118 | 119 | fmt.Printf("Download total size: %s\n", fmtSize) 120 | 121 | for { 122 | select { 123 | case <-done: // end, output newline 124 | fmt.Println() 125 | return 126 | default: 127 | file, err := os.Open(path) 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | fi, err := file.Stat() 133 | if err != nil { 134 | log.Fatal(err) 135 | } 136 | 137 | size := fi.Size() 138 | if size == 0 { 139 | size = 1 140 | } 141 | 142 | // move to begin of the line and clear line text. 143 | fmt.Print("\x0D\x1B[2K") 144 | fmt.Printf("Downloaded %s", fmtutil.DataSize(uint64(size))) 145 | 146 | if total > 0 { 147 | percent := float64(size) / float64(total) * 100 148 | fmt.Printf(", Progress %.0f%%", percent) 149 | } 150 | } 151 | 152 | time.Sleep(time.Second) 153 | } 154 | } 155 | 156 | // SimpleDownload simple download 157 | func SimpleDownload(url, saveAs string) (err error) { 158 | newFile, err := os.Create(saveAs) 159 | if err != nil { 160 | return err 161 | } 162 | //noinspection GoUnhandledErrorResult 163 | defer newFile.Close() 164 | 165 | s := progress.LoadingSpinner( 166 | progress.GetCharsTheme(18), 167 | time.Duration(100)*time.Millisecond, 168 | ) 169 | 170 | s.Start("%s Downloading ... ...") 171 | 172 | client := http.Client{Timeout: 300 * time.Second} 173 | // Request the remote url. 174 | resp, err := client.Get(url) 175 | if err != nil { 176 | log.Fatal(err) 177 | } 178 | //noinspection GoUnhandledErrorResult 179 | defer resp.Body.Close() 180 | 181 | if resp.StatusCode != 200 { 182 | log.Fatalf("status code error: %d %s", resp.StatusCode, resp.Status) 183 | } 184 | 185 | _, err = io.Copy(newFile, resp.Body) 186 | if err != nil { 187 | return 188 | } 189 | 190 | s.Stop("Download completed") 191 | return 192 | } 193 | -------------------------------------------------------------------------------- /helper/utils.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/gookit/goutil/strutil" 11 | ) 12 | 13 | const ( 14 | // RegGoodName match a good option, argument name 15 | RegGoodName = `^[a-zA-Z][\w-]*$` 16 | // RegGoodCmdName match a good command name 17 | RegGoodCmdName = `^[a-zA-Z][\w-]*$` 18 | // RegGoodCmdId match command id. eg: "self:init" 19 | RegGoodCmdId = `^[a-zA-Z][\w:-]*$` 20 | // match command path. eg: "self init" 21 | // RegGoodCmdPath = `^[a-zA-Z][\w -]*$` 22 | ) 23 | 24 | var ( 25 | // GoodName good name for option and argument 26 | goodName = regexp.MustCompile(RegGoodName) 27 | // GoodCmdId match a good command name 28 | goodCmdId = regexp.MustCompile(RegGoodCmdId) 29 | // GoodCmdName match a good command name 30 | goodCmdName = regexp.MustCompile(RegGoodCmdName) 31 | ) 32 | 33 | // IsGoodName check 34 | func IsGoodName(name string) bool { 35 | return goodName.MatchString(name) 36 | } 37 | 38 | // IsGoodCmdId check 39 | func IsGoodCmdId(name string) bool { 40 | return goodCmdId.MatchString(name) 41 | } 42 | 43 | // IsGoodCmdName check 44 | func IsGoodCmdName(name string) bool { 45 | return goodCmdName.MatchString(name) 46 | } 47 | 48 | // Panicf message 49 | func Panicf(format string, v ...any) { 50 | panic(fmt.Sprintf("GCli: "+format, v...)) 51 | } 52 | 53 | // RenderText render text template with data. TODO use strutil.RenderText() 54 | func RenderText(input string, data any, fns template.FuncMap, isFile ...bool) string { 55 | t := template.New("cli") 56 | t.Funcs(template.FuncMap{ 57 | // don't escape content 58 | "raw": func(s string) string { 59 | return s 60 | }, 61 | "trim": strings.TrimSpace, 62 | // join strings. usage {{ join .Strings ","}} 63 | "join": func(ss []string, sep string) string { 64 | return strings.Join(ss, sep) 65 | }, 66 | // lower first char 67 | "lcFirst": strutil.LowerFirst, 68 | // upper first char 69 | "ucFirst": strutil.UpperFirst, 70 | }) 71 | 72 | // custom add template functions 73 | if len(fns) > 0 { 74 | t.Funcs(fns) 75 | } 76 | 77 | if len(isFile) > 0 && isFile[0] { 78 | template.Must(t.ParseFiles(input)) 79 | } else { 80 | template.Must(t.Parse(input)) 81 | } 82 | 83 | // use buffer receive rendered content 84 | var buf bytes.Buffer 85 | if err := t.Execute(&buf, data); err != nil { 86 | panic(err) 87 | } 88 | 89 | return buf.String() 90 | } 91 | -------------------------------------------------------------------------------- /helper/utils_nonwin.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package helper 4 | -------------------------------------------------------------------------------- /helper/utils_windows.go: -------------------------------------------------------------------------------- 1 | package helper 2 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GCli - command-line application and tool library written in Golang. 9 | 10 | 11 |
12 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /interact/README.md: -------------------------------------------------------------------------------- 1 | # Interactive 2 | 3 | command-line interactive util methods 4 | 5 | - `ReadInput` 6 | - `ReadLine` 7 | - `ReadFirst` 8 | - `Prompt` 9 | - `Confirm` 10 | - `Query/Question/Ask` 11 | - `Select/Choice` 12 | - `MultiSelect/Checkbox` 13 | - `ReadPassword` 14 | 15 | ## GoDoc 16 | 17 | Please see https://pkg.go.dev/github.com/gookit/gcli/v3/interact 18 | 19 | ## Install 20 | 21 | ```shell 22 | go get github.com/gookit/gcli/v3/interact 23 | ``` 24 | 25 | ## Select & Choice 26 | 27 | Usage: 28 | 29 | ```go 30 | package main 31 | 32 | import ( 33 | "fmt" 34 | "os/exec" 35 | 36 | "github.com/gookit/color" 37 | "github.com/gookit/gcli/v3/interact" 38 | ) 39 | 40 | func main() { 41 | color.Green.Println("This's An Select Demo") 42 | fmt.Println("----------------------------------------------------------") 43 | 44 | ans := interact.SelectOne( 45 | "Your city name(use string slice/array)?", 46 | []string{"chengdu", "beijing", "shanghai"}, 47 | "", 48 | ) 49 | color.Info.Println("your select is:", ans) 50 | fmt.Println("----------------------------------------------------------") 51 | 52 | ans1 := interact.Choice( 53 | "Your age(use int slice/array)?", 54 | []int{23, 34, 45}, 55 | "", 56 | ) 57 | color.Info.Println("your select is:", ans1) 58 | 59 | fmt.Println("----------------------------------------------------------") 60 | 61 | ans2 := interact.SingleSelect( 62 | "Your city name(use map)?", 63 | map[string]string{"a": "chengdu", "b": "beijing", "c": "shanghai"}, 64 | "a", 65 | ) 66 | color.Info.Println("your select is:", ans2) 67 | 68 | s := interact.NewSelect("Your city", []string{"chengdu", "beijing", "shanghai"}) 69 | s.DefOpt = "2" 70 | r := s.Run() 71 | color.Info.Println("your select key:", r.K.String()) 72 | color.Info.Println("your select val:", r.String()) 73 | } 74 | ``` 75 | 76 | Preview: 77 | 78 | ![](images/select.png) 79 | 80 | ## Refers 81 | 82 | - https://github.com/manifoldco/promptui 83 | - https://github.com/chzyer/readline -------------------------------------------------------------------------------- /interact/base.go: -------------------------------------------------------------------------------- 1 | // Package interact collect some interactive methods for CLI 2 | package interact 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | 8 | "github.com/gookit/color" 9 | "github.com/gookit/goutil/structs" 10 | ) 11 | 12 | const ( 13 | // OK success exit code 14 | OK = 0 15 | // ERR error exit code 16 | ERR = 2 17 | ) 18 | 19 | // ComOptions struct 20 | type ComOptions struct { 21 | // ValidFn check input value 22 | ValidFn func(val any) (any, error) 23 | } 24 | 25 | // Value alias of structs.Value 26 | type Value = structs.Value 27 | 28 | // RunFace for interact methods 29 | type RunFace interface { 30 | Run() *Value 31 | } 32 | 33 | // ItemFace for interact methods 34 | type ItemFace interface { 35 | Name() string 36 | Value() string 37 | } 38 | 39 | /************************************************************* 40 | * value for select 41 | *************************************************************/ 42 | 43 | // SelectResult data store 44 | type SelectResult struct { 45 | Value // V the select value(s) 46 | // K the select key(s) 47 | K Value 48 | } 49 | 50 | // create SelectResult create 51 | func newSelectResult(key, val any) *SelectResult { 52 | return &SelectResult{ 53 | K: Value{V: key}, 54 | Value: Value{V: val}, 55 | } 56 | } 57 | 58 | // KeyString get 59 | func (sv *SelectResult) KeyString() string { 60 | return sv.K.String() 61 | } 62 | 63 | // KeyStrings get 64 | func (sv *SelectResult) KeyStrings() []string { 65 | return sv.K.Strings() 66 | } 67 | 68 | // Key value get 69 | func (sv *SelectResult) Key() any { 70 | return sv.K.Val() 71 | } 72 | 73 | // WithKey value 74 | func (sv *SelectResult) WithKey(key any) *SelectResult { 75 | sv.K.Set(key) 76 | return sv 77 | } 78 | 79 | /************************************************************* 80 | * helper methods 81 | *************************************************************/ 82 | 83 | func exitWithErr(format string, v ...any) { 84 | color.Error.Tips(format, v...) 85 | os.Exit(ERR) 86 | } 87 | 88 | func exitWithMsg(exitCode int, messages ...any) { 89 | fmt.Println(messages...) 90 | os.Exit(exitCode) 91 | } 92 | -------------------------------------------------------------------------------- /interact/collector.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "github.com/gookit/goutil/arrutil" 5 | "github.com/gookit/goutil/errorx" 6 | "github.com/gookit/goutil/maputil" 7 | "github.com/gookit/goutil/structs" 8 | "github.com/gookit/goutil/strutil" 9 | ) 10 | 11 | // InputParameter interface 12 | type InputParameter interface { 13 | Type() string 14 | Name() string 15 | Desc() string 16 | Value() structs.Value 17 | Set(v string) error 18 | Run() error 19 | } 20 | 21 | // Collector information collector 信息收集者 22 | // cli input values collector 23 | type Collector struct { 24 | // input parameters 25 | ps map[string]InputParameter 26 | ret maputil.Data 27 | err error 28 | 29 | ns []string 30 | } 31 | 32 | // NewCollector instance 33 | func NewCollector() *Collector { 34 | return &Collector{ 35 | ps: make(map[string]InputParameter), 36 | ret: make(maputil.Data), 37 | } 38 | } 39 | 40 | // AddParams definitions at once. 41 | func (c *Collector) AddParams(ps ...InputParameter) error { 42 | for _, p := range ps { 43 | if err := c.AddParam(p); err != nil { 44 | return err 45 | } 46 | } 47 | return nil 48 | } 49 | 50 | // Param get from collector 51 | func (c *Collector) Param(name string) (InputParameter, bool) { 52 | p, ok := c.ps[name] 53 | return p, ok 54 | } 55 | 56 | // MustParam get from collector 57 | func (c *Collector) MustParam(name string) InputParameter { 58 | p, ok := c.ps[name] 59 | if !ok { 60 | panic("not found the param: " + name) 61 | } 62 | 63 | return p 64 | } 65 | 66 | // AddParam to collector 67 | func (c *Collector) AddParam(p InputParameter) error { 68 | if strutil.IsBlank(p.Name()) { 69 | return errorx.Raw("input parameter name cannot be empty") 70 | } 71 | 72 | name := p.Name() 73 | if arrutil.Contains(c.ns, name) { 74 | return errorx.Rawf("input parameter name %s has been exists", name) 75 | } 76 | 77 | c.ns = append(c.ns, name) 78 | c.ps[name] = p 79 | 80 | return nil 81 | } 82 | 83 | // Results for collector 84 | func (c *Collector) Results() maputil.Data { 85 | return c.ret 86 | } 87 | 88 | // Run collector 89 | func (c *Collector) Run() error { 90 | if len(c.ns) == 0 { 91 | return errorx.Raw("empty params definitions") 92 | } 93 | 94 | for _, name := range c.ns { 95 | p := c.ps[name] 96 | 97 | // has input value 98 | if c.ret.Has(name) { 99 | err := p.Set(c.ret.Str(name)) 100 | if err != nil { 101 | return err 102 | } 103 | continue 104 | } 105 | 106 | // require input value 107 | if err := p.Run(); err != nil { 108 | return err 109 | } 110 | 111 | c.ret.Set(name, p.Value().V) 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /interact/collector_test.go: -------------------------------------------------------------------------------- 1 | package interact_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/gcli/v3/interact" 7 | "github.com/gookit/gcli/v3/interact/cparam" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestCollector_Run(t *testing.T) { 12 | c := interact.NewCollector() 13 | err := c.AddParams( 14 | cparam.NewStringParam("title", "title name"), 15 | cparam.NewChoiceParam("projects", "select projects"), 16 | ) 17 | 18 | assert.NoErr(t, err) 19 | } 20 | -------------------------------------------------------------------------------- /interact/control/control.go: -------------------------------------------------------------------------------- 1 | package control 2 | -------------------------------------------------------------------------------- /interact/cparam/choices.go: -------------------------------------------------------------------------------- 1 | package cparam 2 | 3 | import ( 4 | "github.com/gookit/gcli/v3/interact" 5 | "github.com/gookit/goutil/errorx" 6 | ) 7 | 8 | // ChoiceParam definition 9 | type ChoiceParam struct { 10 | InputParam 11 | // Choices for select 12 | Choices []string 13 | selected string 14 | } 15 | 16 | // NewChoiceParam instance 17 | func NewChoiceParam(name, desc string) *ChoiceParam { 18 | return &ChoiceParam{ 19 | InputParam: InputParam{ 20 | typ: TypeChoicesParam, 21 | name: name, 22 | desc: desc, 23 | }, 24 | } 25 | } 26 | 27 | // WithChoices to param definition 28 | func (p *ChoiceParam) WithChoices(Choices []string) *ChoiceParam { 29 | p.Choices = Choices 30 | return p 31 | } 32 | 33 | // Selected values get 34 | func (p *ChoiceParam) Selected() string { 35 | return p.val.String() 36 | } 37 | 38 | // Set value 39 | func (p *ChoiceParam) Set(v string) error { 40 | if err := p.Valid(v); err != nil { 41 | return err 42 | } 43 | 44 | p.selected = v 45 | p.val.Set(p.selected) 46 | return nil 47 | } 48 | 49 | // Run param and get user input 50 | func (p *ChoiceParam) Run() (err error) { 51 | if len(p.Choices) == 0 { 52 | return errorx.Raw("must provide items for choices") 53 | } 54 | 55 | s := interact.NewSelect(p.Desc(), p.Choices) 56 | // s.EnableMulti() 57 | 58 | sr := s.Run() 59 | p.val.Set(sr.Val()) 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /interact/cparam/cparam.go: -------------------------------------------------------------------------------- 1 | package cparam 2 | 3 | import ( 4 | "github.com/gookit/goutil/errorx" 5 | "github.com/gookit/goutil/structs" 6 | ) 7 | 8 | // params types 9 | const ( 10 | TypeStringParam = "string" 11 | TypeChoicesParam = "choices" 12 | ) 13 | 14 | // RunFn func type 15 | type RunFn func() (val string, err error) 16 | 17 | // InputParam struct 18 | type InputParam struct { 19 | typ string 20 | name string 21 | desc string 22 | // Default value 23 | Default any 24 | ValidFn func(val string) error 25 | runFn func() (val string, err error) 26 | // Value for input 27 | val structs.Value 28 | err error 29 | } 30 | 31 | // NewInputParam instance 32 | func NewInputParam(typ, name, desc string) *InputParam { 33 | return &InputParam{ 34 | typ: typ, 35 | name: name, 36 | desc: desc, 37 | } 38 | } 39 | 40 | // Type name get 41 | func (p *InputParam) Type() string { 42 | return p.typ 43 | } 44 | 45 | // Name get 46 | func (p *InputParam) Name() string { 47 | return p.name 48 | } 49 | 50 | // Desc message 51 | func (p *InputParam) Desc() string { 52 | return p.desc 53 | } 54 | 55 | // Valid value validate 56 | func (p *InputParam) Valid(v string) error { 57 | if p.ValidFn != nil { 58 | return p.ValidFn(v) 59 | } 60 | return nil 61 | } 62 | 63 | // Set value and with validate 64 | func (p *InputParam) Set(v string) error { 65 | if err := p.Valid(v); err != nil { 66 | return err 67 | } 68 | 69 | p.val.Set(v) 70 | return nil 71 | } 72 | 73 | // Value data get 74 | func (p *InputParam) Value() structs.Value { 75 | return p.val 76 | } 77 | 78 | // Value get 79 | func (p *InputParam) String() string { 80 | return p.val.String() 81 | } 82 | 83 | // SetFunc for run 84 | func (p *InputParam) SetFunc(fn RunFn) { 85 | p.runFn = fn 86 | } 87 | 88 | // SetValidFn for run 89 | func (p *InputParam) SetValidFn(fn func(val string) error) { 90 | p.ValidFn = fn 91 | } 92 | 93 | // Run param and get user input 94 | func (p *InputParam) Run() (err error) { 95 | if p.runFn != nil { 96 | val, err := p.runFn() 97 | if err != nil { 98 | return err 99 | } 100 | 101 | err = p.Set(val) 102 | } else { 103 | err = errorx.Raw("please implement me") 104 | } 105 | 106 | return err 107 | } 108 | -------------------------------------------------------------------------------- /interact/cparam/multi_choices.go: -------------------------------------------------------------------------------- 1 | package cparam 2 | 3 | import ( 4 | "github.com/gookit/gcli/v3/interact" 5 | "github.com/gookit/goutil/errorx" 6 | ) 7 | 8 | // ChoicesParam definition 9 | type ChoicesParam struct { 10 | InputParam 11 | // Choices for select 12 | Choices []string 13 | selected []string 14 | } 15 | 16 | // NewChoicesParam instance 17 | func NewChoicesParam(name, desc string) *ChoicesParam { 18 | return &ChoicesParam{ 19 | InputParam: InputParam{ 20 | typ: TypeChoicesParam, 21 | name: name, 22 | desc: desc, 23 | }, 24 | } 25 | } 26 | 27 | // WithChoices to param definition 28 | func (p *ChoicesParam) WithChoices(Choices []string) *ChoicesParam { 29 | p.Choices = Choices 30 | return p 31 | } 32 | 33 | // Selected values get 34 | func (p *ChoicesParam) Selected() []string { 35 | return p.val.Strings() 36 | } 37 | 38 | // Set value 39 | func (p *ChoicesParam) Set(v string) error { 40 | if err := p.Valid(v); err != nil { 41 | return err 42 | } 43 | 44 | p.selected = append(p.selected, v) 45 | p.val.Set(p.selected) 46 | return nil 47 | } 48 | 49 | // Run param and get user input 50 | func (p *ChoicesParam) Run() (err error) { 51 | if len(p.Choices) == 0 { 52 | return errorx.Raw("must provide items for choices") 53 | } 54 | 55 | s := interact.NewSelect(p.Desc(), p.Choices) 56 | s.EnableMulti() 57 | 58 | sr := s.Run() 59 | p.val.Set(sr.Val()) 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /interact/cparam/string.go: -------------------------------------------------------------------------------- 1 | package cparam 2 | 3 | import ( 4 | "github.com/gookit/color" 5 | "github.com/gookit/goutil/cliutil" 6 | ) 7 | 8 | // StringParam definition 9 | type StringParam struct { 10 | InputParam 11 | } 12 | 13 | // NewStringParam instance 14 | func NewStringParam(name, desc string) *StringParam { 15 | return &StringParam{ 16 | InputParam: InputParam{ 17 | typ: TypeStringParam, 18 | name: name, 19 | desc: desc, 20 | }, 21 | } 22 | } 23 | 24 | // Config param 25 | func (p *StringParam) Config(fn func(p *StringParam)) *StringParam { 26 | fn(p) 27 | return p 28 | } 29 | 30 | // Run param and get user input 31 | func (p *StringParam) Run() (err error) { 32 | var val string 33 | if p.runFn != nil { 34 | val, err = p.runFn() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return p.Set(val) 40 | } 41 | 42 | val, err = cliutil.ReadLine(color.WrapTag(p.desc+"? ", "yellow")) 43 | if err != nil { 44 | return err 45 | } 46 | return p.Set(val) 47 | } 48 | -------------------------------------------------------------------------------- /interact/images/select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gookit/gcli/e461875ac4dfa5cbf8e736165316da14b44d69b9/interact/images/select.png -------------------------------------------------------------------------------- /interact/interact.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/gookit/color" 8 | ) 9 | 10 | // the global input output stream 11 | var ( 12 | Input io.Reader = os.Stdin 13 | Output io.Writer = os.Stdout 14 | ) 15 | 16 | // SetInput stream 17 | func SetInput(in io.Reader) { Input = in } 18 | 19 | // SetOutput stream 20 | func SetOutput(out io.Writer) { Output = out } 21 | 22 | // ResetIO stream 23 | func ResetIO() { 24 | Input = os.Stdin 25 | Output = os.Stdout 26 | } 27 | 28 | // Interactive definition 29 | type Interactive struct { 30 | Name string 31 | } 32 | 33 | // New Interactive instance 34 | func New(name string) *Interactive { 35 | return &Interactive{Name: name} 36 | } 37 | 38 | // Options definition 39 | type Options struct { 40 | Quit bool 41 | // default value 42 | DefVal string 43 | } 44 | 45 | // Confirm a question, returns bool 46 | func Confirm(message string, defVal ...bool) bool { 47 | color.Print(message) 48 | return AnswerIsYes(defVal...) 49 | } 50 | 51 | // Unconfirmed a question, returns bool 52 | func Unconfirmed(message string, defVal ...bool) bool { 53 | return !Confirm(message, defVal...) 54 | } 55 | 56 | // Ask a question and return the result of the input. 57 | // 58 | // Usage: 59 | // 60 | // answer := Ask("Your name?", "", nil) 61 | // answer := Ask("Your name?", "tom", nil) 62 | // answer := Ask("Your name?", "", nil, 3) 63 | func Ask(question, defVal string, fn func(ans string) error, maxTimes ...int) string { 64 | q := &Question{Q: question, Func: fn, DefVal: defVal} 65 | if len(maxTimes) > 0 { 66 | q.MaxTimes = maxTimes[0] 67 | } 68 | 69 | return q.Run().String() 70 | } 71 | 72 | // Query is alias of method Ask() 73 | func Query(question, defVal string, fn func(ans string) error, maxTimes ...int) string { 74 | return Ask(question, defVal, fn, maxTimes...) 75 | } 76 | 77 | // Choice is alias of method SelectOne() 78 | func Choice(title string, options any, defOpt string, allowQuit ...bool) string { 79 | return SelectOne(title, options, defOpt, allowQuit...) 80 | } 81 | 82 | // SingleSelect is alias of method SelectOne() 83 | func SingleSelect(title string, options any, defOpt string, allowQuit ...bool) string { 84 | return SelectOne(title, options, defOpt, allowQuit...) 85 | } 86 | 87 | // SelectOne select one of the options, returns selected option value 88 | // 89 | // Map options: 90 | // 91 | // { 92 | // // option value => option name 93 | // 'a' => 'chengdu', 94 | // 'b' => 'beijing' 95 | // } 96 | // 97 | // Array options: 98 | // 99 | // { 100 | // // only name, value will use index 101 | // 'chengdu', 102 | // 'beijing' 103 | // } 104 | func SelectOne(title string, options any, defOpt string, allowQuit ...bool) string { 105 | s := &Select{Title: title, Options: options, DefOpt: defOpt} 106 | 107 | if len(allowQuit) > 0 { 108 | s.DisableQuit = !allowQuit[0] 109 | } 110 | 111 | return s.Run().String() 112 | } 113 | 114 | // Checkbox is alias of method MultiSelect() 115 | func Checkbox(title string, options any, defOpts []string, allowQuit ...bool) []string { 116 | return MultiSelect(title, options, defOpts, allowQuit...) 117 | } 118 | 119 | // MultiSelect select multi of the options, returns selected option values. 120 | // like SingleSelect(), but allow select multi option 121 | func MultiSelect(title string, options any, defOpts []string, allowQuit ...bool) []string { 122 | s := &Select{Title: title, Options: options, DefOpts: defOpts, MultiSelect: true} 123 | 124 | if len(allowQuit) > 0 { 125 | s.DisableQuit = !allowQuit[0] 126 | } 127 | 128 | return s.Run().Strings() 129 | } 130 | -------------------------------------------------------------------------------- /interact/prompt.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | type result struct { 11 | answer string 12 | err error 13 | } 14 | 15 | // Prompt query and read user answer. 16 | // 17 | // Usage: 18 | // 19 | // answer,err := Prompt(context.Background(), "your name?", "") 20 | // 21 | // from package golang.org/x/tools/cmd/getgo 22 | func Prompt(ctx context.Context, query, defaultAnswer string) (string, error) { 23 | _, _ = fmt.Fprintf(Output, "%s [%s]: ", query, defaultAnswer) 24 | 25 | ch := make(chan result, 1) 26 | go func() { 27 | s := bufio.NewScanner(Input) 28 | if !s.Scan() { // reading 29 | ch <- result{"", s.Err()} 30 | return 31 | } 32 | 33 | answer := strings.TrimSpace(s.Text()) 34 | if answer == "" { 35 | answer = defaultAnswer 36 | } 37 | ch <- result{answer, nil} 38 | }() 39 | 40 | select { 41 | case r := <-ch: 42 | return r.answer, r.err 43 | case <-ctx.Done(): 44 | return "", ctx.Err() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /interact/question.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gookit/color" 8 | ) 9 | 10 | // Question definition 11 | type Question struct { 12 | // Q the question message 13 | Q string 14 | // Func validate user input answer is right. 15 | // if not set, will only check answer is empty. 16 | Func func(ans string) error 17 | // DefVal default value 18 | DefVal string 19 | // MaxTimes maximum allowed number of errors, 0 is don't limited 20 | MaxTimes int 21 | errTimes int 22 | } 23 | 24 | // NewQuestion instance. 25 | // 26 | // Usage: 27 | // 28 | // q := NewQuestion("Please input your name?") 29 | // ans := q.Run().String() 30 | func NewQuestion(q string, defVal ...string) *Question { 31 | if len(defVal) > 0 { 32 | return &Question{Q: q, DefVal: defVal[0]} 33 | } 34 | return &Question{Q: q} 35 | } 36 | 37 | // Run and returns value 38 | func (q *Question) Run() *Value { 39 | q.render() 40 | echoErr := color.Error.Println 41 | 42 | DoASK: 43 | ans, err := ReadLine("A: ") 44 | if err != nil { 45 | exitWithErr("(interact.Question) %s", err.Error()) 46 | } 47 | 48 | // don't input 49 | if ans == "" { 50 | if q.DefVal != "" { // has default value 51 | return &Value{V: q.DefVal} 52 | } 53 | 54 | q.checkErrTimes() 55 | echoErr("A value is required.") 56 | goto DoASK 57 | } 58 | 59 | // has validator func 60 | if q.Func != nil { 61 | if err := q.Func(ans); err != nil { 62 | q.checkErrTimes() 63 | echoErr(err.Error()) 64 | goto DoASK 65 | } 66 | } 67 | 68 | return &Value{V: ans} 69 | } 70 | 71 | func (q *Question) render() { 72 | q.Q = strings.TrimSpace(q.Q) 73 | if q.Q == "" { 74 | exitWithErr("(interact.Question) must provide question message") 75 | } 76 | 77 | var defMsg string 78 | 79 | q.DefVal = strings.TrimSpace(q.DefVal) 80 | if q.DefVal != "" { 81 | defMsg = fmt.Sprintf("[default:%s]", color.Green.Render(q.DefVal)) 82 | } 83 | 84 | // print question 85 | fmt.Printf("%s%s\n", color.Comment.Render(q.Q), defMsg) 86 | } 87 | 88 | func (q *Question) checkErrTimes() { 89 | if q.MaxTimes <= 0 { 90 | return 91 | } 92 | 93 | // limit error times 94 | if q.MaxTimes == q.errTimes { 95 | times := color.Magenta.Render(q.MaxTimes) 96 | exitWithMsg(0, "\n You've entered incorrectly", times, "times. Bye!") 97 | } 98 | 99 | q.errTimes++ 100 | } 101 | -------------------------------------------------------------------------------- /interact/question_test.go: -------------------------------------------------------------------------------- 1 | package interact_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestQuestion_Run(t *testing.T) { 8 | // q := interact.NewQuestion("your name") 9 | // q.Run() 10 | } 11 | -------------------------------------------------------------------------------- /interact/read.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "strings" 9 | 10 | "github.com/gookit/color" 11 | "github.com/gookit/goutil/cliutil" 12 | "github.com/gookit/goutil/envutil" 13 | ) 14 | 15 | // ReadInput read user input form Stdin 16 | func ReadInput(question string) (string, error) { 17 | if len(question) > 0 { 18 | color.Fprint(Output, question) 19 | } 20 | 21 | scanner := bufio.NewScanner(Input) 22 | if !scanner.Scan() { // reading 23 | return "", scanner.Err() 24 | } 25 | 26 | return strings.TrimSpace(scanner.Text()), nil 27 | } 28 | 29 | // ReadLine read one line from user input. 30 | // 31 | // Usage: 32 | // 33 | // in := ReadLine("") 34 | // ans, _ := ReadLine("your name?") 35 | func ReadLine(question string) (string, error) { 36 | if len(question) > 0 { 37 | color.Fprint(Output, question) 38 | } 39 | 40 | reader := bufio.NewReader(Input) 41 | answer, _, err := reader.ReadLine() 42 | return strings.TrimSpace(string(answer)), err 43 | } 44 | 45 | // ReadFirst read first char 46 | func ReadFirst(question string) (string, error) { 47 | answer, err := ReadLine(question) 48 | if len(answer) == 0 { 49 | return "", err 50 | } 51 | 52 | return string(answer[0]), err 53 | } 54 | 55 | // AnswerIsYes check user inputted answer is right 56 | // 57 | // Usage: 58 | // 59 | // fmt.Print("are you OK?") 60 | // ok := AnswerIsYes() 61 | // ok := AnswerIsYes(true) 62 | func AnswerIsYes(defVal ...bool) bool { 63 | mark := " [yes|no]: " 64 | if len(defVal) > 0 { 65 | var defShow string 66 | if defVal[0] { 67 | defShow = "yes" 68 | } else { 69 | defShow = "no" 70 | } 71 | 72 | mark = fmt.Sprintf(" [yes|no](default %s): ", defShow) 73 | } 74 | 75 | // _, err := fmt.Scanln(&answer) 76 | // _, err := fmt.Scan(&answer) 77 | fChar, err := ReadFirst(mark) 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | if len(fChar) > 0 { 83 | fChar := strings.ToLower(fChar) 84 | if fChar == "y" { 85 | return true 86 | } 87 | if fChar == "n" { 88 | return false 89 | } 90 | } else if len(defVal) > 0 { // has default value 91 | return defVal[0] 92 | } 93 | 94 | _, _ = fmt.Fprint(Output, "Please try again") 95 | return AnswerIsYes() 96 | } 97 | 98 | // GetHiddenInput interactively prompts for input without echoing to the terminal. 99 | // 100 | // Usage: 101 | // 102 | // // askPassword 103 | // pwd := GetHiddenInput("Enter Password:") 104 | func GetHiddenInput(message string, trimmed bool) string { 105 | var err error 106 | var input string 107 | var hasResult bool 108 | 109 | // like *nix, git-bash ... 110 | if envutil.HasShellEnv("sh") { 111 | // COMMAND: sh -c 'read -p "Enter Password:" -s user_input && echo $user_input' 112 | cmd := fmt.Sprintf(`'read -p "%s" -s user_input && echo $user_input'`, message) 113 | input, err = cliutil.ShellExec(cmd) 114 | if err != nil { 115 | fmt.Println("error:", err) 116 | return "" 117 | } 118 | 119 | println() // new line 120 | hasResult = true 121 | } else if envutil.IsWin() { // at windows cmd.exe 122 | // create a temp VB script file 123 | vbFile, err := ioutil.TempFile("", "gcli-pwd") 124 | if err != nil { 125 | return "" 126 | } 127 | defer func() { 128 | // delete file 129 | vbFile.Close() 130 | _ = os.Remove(vbFile.Name()) 131 | }() 132 | 133 | script := fmt.Sprintf(`wscript.echo(InputBox("%s", "", "password here"))`, message) 134 | _, _ = vbFile.WriteString(script) 135 | hasResult = true 136 | 137 | // exec VB script 138 | // COMMAND: cscript //nologo vbFile.Name() 139 | input, err = cliutil.ExecCmd("cscript", []string{"//nologo", vbFile.Name()}) 140 | if err != nil { 141 | return "" 142 | } 143 | } 144 | 145 | if hasResult { 146 | if trimmed { 147 | return strings.TrimSpace(input) 148 | } 149 | return input 150 | } 151 | 152 | panic("current env is not support the method") 153 | } 154 | -------------------------------------------------------------------------------- /interact/read_nonwin.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package interact 4 | 5 | import ( 6 | "syscall" 7 | 8 | "golang.org/x/crypto/ssh/terminal" 9 | ) 10 | 11 | // ReadPassword from terminal 12 | func ReadPassword(question ...string) string { 13 | if len(question) > 0 { 14 | print(question[0]) 15 | } else { 16 | print("Enter Password: ") 17 | } 18 | 19 | bs, err := terminal.ReadPassword(syscall.Stdin) 20 | if err != nil { 21 | return "" 22 | } 23 | 24 | println() // new line 25 | return string(bs) 26 | } 27 | -------------------------------------------------------------------------------- /interact/read_windows.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "syscall" 5 | 6 | "golang.org/x/crypto/ssh/terminal" 7 | ) 8 | 9 | // ReadPassword from terminal 10 | func ReadPassword(question ...string) string { 11 | if len(question) > 0 { 12 | print(question[0]) 13 | } else { 14 | print("Enter Password: ") 15 | } 16 | 17 | // on Windows, must convert 'syscall.Stdin' to int 18 | bs, err := terminal.ReadPassword(int(syscall.Stdin)) 19 | if err != nil { 20 | return "" 21 | } 22 | 23 | println() // new line 24 | return string(bs) 25 | } 26 | -------------------------------------------------------------------------------- /interact/select.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/gookit/color" 10 | "github.com/gookit/goutil/arrutil" 11 | "github.com/gookit/goutil/strutil" 12 | ) 13 | 14 | // Select definition 15 | type Select struct { 16 | // Title message for select. e.g "Your city?" 17 | Title string 18 | // Options the items data for select. allow: []int, []string, map[string]string 19 | Options any 20 | // DefOpt default option when not input answer 21 | DefOpt string 22 | // DefOpts use for `MultiSelect` is true 23 | DefOpts []string 24 | // DisableQuit option. if is false, will display "quit" option. default False 25 | DisableQuit bool 26 | // MultiSelect allow multi select. default False 27 | MultiSelect bool 28 | // QuitHandler func() 29 | // parsed options data 30 | // { 31 | // "option value": "option name", 32 | // } 33 | valMap map[string]string 34 | } 35 | 36 | // NewSelect instance. 37 | // 38 | // - items allow: []int, []string, map[string]string 39 | // 40 | // Usage: 41 | // 42 | // s := NewSelect("Your city?", []string{"chengdu", "beijing"}) 43 | // r := s.Run() 44 | // key := r.KeyString() // "1" 45 | // val := r.String() // "beijing" 46 | func NewSelect(title string, items any) *Select { 47 | return &Select{ 48 | Title: title, 49 | Options: items, 50 | } 51 | } 52 | 53 | func (s *Select) prepare() (keys []string) { 54 | s.Title = strings.TrimSpace(s.Title) 55 | if s.Title == "" || s.Options == nil { 56 | exitWithErr("(interact.Select) must provide title and options data") 57 | } 58 | 59 | s.valMap = make(map[string]string) 60 | handleArrItem := func(i int, v any) { 61 | nv := fmt.Sprint(i) 62 | s.valMap[nv] = fmt.Sprint(v) 63 | keys = append(keys, nv) 64 | } 65 | 66 | switch optsData := s.Options.(type) { 67 | case map[string]int: 68 | keys = make([]string, len(optsData)) 69 | i := 0 70 | for v, n := range optsData { 71 | keys[i] = v 72 | s.valMap[v] = fmt.Sprint(n) 73 | i++ 74 | } 75 | 76 | sort.Strings(keys) // sort 77 | case map[string]string: 78 | s.valMap = optsData 79 | keys = make([]string, len(optsData)) 80 | i := 0 81 | for v := range optsData { 82 | keys[i] = v 83 | i++ 84 | } 85 | 86 | sort.Strings(keys) // sort 87 | case string: 88 | ss := strutil.ToArray(optsData, ",") 89 | for i, v := range ss { 90 | handleArrItem(i, v) 91 | } 92 | case []int: 93 | for i, v := range optsData { 94 | handleArrItem(i, v) 95 | } 96 | case []string: 97 | for i, v := range optsData { 98 | handleArrItem(i, v) 99 | } 100 | default: 101 | exitWithErr("(interact.Select) invalid options data for select") 102 | } 103 | 104 | // format some field data 105 | s.DefOpt = strings.TrimSpace(s.DefOpt) 106 | if len(s.DefOpts) > 0 { 107 | s.DefOpts = arrutil.StringsFilter(s.DefOpts) 108 | } 109 | return 110 | } 111 | 112 | // Render select and options to terminal 113 | func (s *Select) render(keys []string) { 114 | buf := new(bytes.Buffer) 115 | green := color.Green.Render 116 | 117 | buf.WriteString(color.Comment.Render(s.Title)) 118 | for _, opt := range keys { 119 | buf.WriteString(fmt.Sprintf("\n %s) %s", green(opt), s.valMap[opt])) 120 | } 121 | 122 | if !s.DisableQuit { 123 | s.valMap["q"] = "quit" 124 | buf.WriteString(fmt.Sprintf("\n %s) quit", green("q"))) 125 | } 126 | 127 | // render select and options message to terminal 128 | color.Println(buf.String()) 129 | buf = nil 130 | } 131 | 132 | func (s *Select) selectOne() *SelectResult { 133 | var has bool 134 | var defVal string 135 | tipsText := "Your choice: " 136 | 137 | // has default opt, check it 138 | if s.DefOpt != "" { 139 | defVal, has = s.valMap[s.DefOpt] 140 | if !has { 141 | exitWithErr("(interact.Select) default option '%s' don't exists", s.DefOpt) 142 | } 143 | 144 | defMsg := fmt.Sprintf("[default:%s]", color.Green.Render(s.DefOpt)) 145 | tipsText = "Your choice" + defMsg + ": " 146 | } 147 | 148 | DoSelect: 149 | key, err := ReadLine(tipsText) 150 | if err != nil { 151 | exitWithErr("(interact.Select) %s", err.Error()) 152 | } 153 | 154 | if key == "" { // empty input 155 | if s.DefOpt != "" { // has default option 156 | return newSelectResult(s.DefOpt, defVal) 157 | } 158 | goto DoSelect // retry ... 159 | } 160 | 161 | // check input 162 | val, has := s.valMap[key] 163 | if !has { 164 | color.Error.Println("Unknown option key:", key) 165 | goto DoSelect // retry ... 166 | } 167 | 168 | // quit select. 169 | if !s.DisableQuit && key == "q" { 170 | exitWithMsg(OK, "\n Quit,ByeBye") 171 | } 172 | 173 | return newSelectResult(key, val) 174 | } 175 | 176 | // for enable MultiSelect 177 | func (s *Select) selectMulti() *SelectResult { 178 | var defValues []string 179 | hasDefault := len(s.DefOpts) > 0 180 | tipsText := "Your choice(multi use , separate): " 181 | if hasDefault { 182 | // check opt is valid. 183 | var defOpts []string 184 | for _, key := range s.DefOpts { 185 | if key = strings.TrimSpace(key); key != "" { 186 | val, has := s.valMap[key] 187 | if !has { 188 | exitWithErr("(interact.Select) default option '%s' don't exists", key) 189 | } 190 | 191 | defOpts = append(defOpts, key) 192 | defValues = append(defValues, val) 193 | } 194 | } 195 | 196 | // override value 197 | s.DefOpts = defOpts 198 | 199 | tipsText = fmt.Sprintf( 200 | "Your choice(multi use , separate)[default:%s]: ", 201 | color.Green.Render(strings.Join(s.DefOpts, ",")), 202 | ) 203 | } 204 | 205 | DoSelect: 206 | ans, err := ReadLine(tipsText) 207 | if err != nil { 208 | exitWithErr("(interact.Select) %s", err.Error()) 209 | } 210 | 211 | keys := strutil.ToSlice(ans, ",") 212 | if len(keys) == 0 { // empty input 213 | // has default options 214 | if hasDefault { 215 | return newSelectResult(s.DefOpts, defValues) 216 | } 217 | 218 | goto DoSelect // retry ... 219 | } 220 | 221 | // check input 222 | var values []string 223 | for _, k := range keys { 224 | v, has := s.valMap[k] 225 | if !has { 226 | color.Error.Println("Unknown option key:", k) 227 | goto DoSelect // retry ... 228 | } 229 | 230 | values = append(values, v) 231 | 232 | // quit select. 233 | if !s.DisableQuit && k == "q" { 234 | exitWithMsg(OK, "\n Quit,ByeBye") 235 | } 236 | } 237 | 238 | return newSelectResult(keys, values) 239 | } 240 | 241 | // EnableMulti select 242 | func (s *Select) EnableMulti() *Select { 243 | s.MultiSelect = true 244 | return s 245 | } 246 | 247 | // Run select and receive use input answer 248 | func (s *Select) Run() *SelectResult { 249 | keys := s.prepare() 250 | // render to console 251 | s.render(keys) 252 | 253 | // if enable MultiSelect 254 | if s.MultiSelect { 255 | return s.selectMulti() 256 | } 257 | return s.selectOne() 258 | } 259 | -------------------------------------------------------------------------------- /interact/steps.go: -------------------------------------------------------------------------------- 1 | package interact 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // StepHandler for steps run 9 | type StepHandler func(ctx context.Context) error 10 | 11 | // StepsRun follow the steps to run 12 | type StepsRun struct { 13 | // record error 14 | err error 15 | // mark is stopped 16 | stopped bool 17 | // steps length 18 | length int 19 | // current step index 20 | current int 21 | // Steps step name and handler define. 22 | // { 23 | // // step 1 24 | // func(ctx context.Context) { do something.} 25 | // // step 2 26 | // func(ctx context.Context) { do something.} 27 | // } 28 | Steps []StepHandler 29 | } 30 | 31 | // Run all steps 32 | func (s *StepsRun) Run() { 33 | if s.stopped { 34 | return 35 | } 36 | 37 | s.length = len(s.Steps) 38 | if s.length == 0 { 39 | s.err = fmt.Errorf("no step handlers need to running") 40 | return 41 | } 42 | 43 | ctx := context.Background() 44 | 45 | for i, handler := range s.Steps { 46 | s.current = i 47 | 48 | err := handler(ctx) 49 | if err != nil { 50 | s.err = err 51 | return 52 | } 53 | } 54 | } 55 | 56 | // Stop set stop run 57 | func (s *StepsRun) Stop() { 58 | s.stopped = true 59 | } 60 | 61 | // Err get error 62 | func (s *StepsRun) Err() error { 63 | return s.err 64 | } 65 | -------------------------------------------------------------------------------- /internal/cmd-run-flow.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | start 3 | :input command line - Entry; 4 | 5 | stop 6 | @enduml -------------------------------------------------------------------------------- /internal/help_tpl.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // var AppHelp 4 | -------------------------------------------------------------------------------- /progress/README.md: -------------------------------------------------------------------------------- 1 | # Progress Display 2 | 3 | Package progress provide terminal progress bar display. Such as: `Txt`, `Bar`, `Loading`, `RoundTrip`, `DynamicText` ... 4 | 5 | - progress bar 6 | - text progress bar 7 | - pending/loading progress bar 8 | - counter 9 | - dynamic Text 10 | 11 | ## GoDoc 12 | 13 | Please see https://pkg.go.dev/github.com/gookit/gcli/v3/progress 14 | 15 | ## Install 16 | 17 | ```bash 18 | go get github.com/gookit/gcli/v3/progress 19 | ``` 20 | 21 | ## Usage 22 | 23 | Examples: 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "time" 30 | 31 | "github.com/gookit/gcli/v3/progress" 32 | ) 33 | 34 | func main() { 35 | speed := 100 36 | maxSteps := 110 37 | p := progress.Bar(maxSteps) 38 | p.Start() 39 | 40 | for i := 0; i < maxSteps; i++ { 41 | time.Sleep(time.Duration(speed) * time.Millisecond) 42 | p.Advance() 43 | } 44 | 45 | p.Finish() 46 | } 47 | ``` 48 | 49 | > More demos please see [progress_demo.go](../_examples/cmd/progress_demo.go) 50 | 51 | run demos: 52 | 53 | ```bash 54 | go run ./_examples/cliapp.go prog txt 55 | go run ./_examples/cliapp.go prog bar 56 | go run ./_examples/cliapp.go prog roundTrip 57 | ``` 58 | 59 | ## Progress Bar 60 | 61 | ### Internal Widgets 62 | 63 | Widget Name | Usage example | Description 64 | -------------|--------------------|------------------------------------------- 65 | `max` | `{@max}` | Display max steps for progress bar 66 | `current` | `{@current}` | Display current steps for progress bar 67 | `percent` | `{@percent:4s}` | Display percent for progress run 68 | `elapsed` | `{@elapsed:7s}` | Display has elapsed time for progress run 69 | `remaining` | `{@remaining:7s}` | Display remaining time 70 | `estimated` | `{@estimated:-7s}` | Display estimated time 71 | `memory` | `{@memory:6s}` | Display memory consumption size 72 | 73 | ### Custom Progress Bar 74 | 75 | Allow you custom progress bar render format. There are internal format for Progress 76 | 77 | ```go 78 | // txt bar 79 | MinFormat = "{@message}{@current}" 80 | TxtFormat = "{@message}{@percent:4s}%({@current}/{@max})" 81 | DefFormat = "{@message}{@percent:4s}%({@current}/{@max})" 82 | FullFormat = "{@percent:4s}%({@current}/{@max}) {@elapsed:7s}/{@estimated:-7s} {@memory:6s}" 83 | 84 | // bar 85 | 86 | DefBarFormat = "{@bar} {@percent:4s}%({@current}/{@max}){@message}" 87 | FullBarFormat = "{@bar} {@percent:4s}%({@current}/{@max}) {@elapsed:7s}/{@estimated:-7s} {@memory:6s}" 88 | ``` 89 | 90 | Examples: 91 | 92 | ```go 93 | package main 94 | import "github.com/gookit/gcli/v3/progress" 95 | 96 | // CustomBar create custom progress bar 97 | func main() { 98 | maxSteps := 100 99 | // use special bar style: [==============>-------------] 100 | // barStyle := progress.BarStyles[0] 101 | // use random bar style 102 | barStyle := progress.RandomBarStyle() 103 | 104 | p: = progress.New(maxSteps). 105 | Config(func(p *Progress) { 106 | p.Format = progress.DefBarFormat 107 | }). 108 | AddWidget("bar", progress.BarWidget(60, barStyle)) 109 | 110 | p.Start() 111 | 112 | for i := 0; i < maxStep; i++ { 113 | time.Sleep(80 * time.Millisecond) 114 | p.Advance() 115 | } 116 | 117 | p.Finish() 118 | } 119 | ``` 120 | 121 | ### Progress Functions 122 | 123 | Quick create progress bar: 124 | 125 | ```text 126 | func Bar(maxSteps ...int) *Progress 127 | func Counter(maxSteps ...int) *Progress 128 | func CustomBar(width int, cs BarChars, maxSteps ...int) *Progress 129 | func DynamicText(messages map[int]string, maxSteps ...int) *Progress 130 | func Full(maxSteps ...int) *Progress 131 | func LoadBar(chars []rune, maxSteps ...int) *Progress 132 | func LoadingBar(chars []rune, maxSteps ...int) *Progress 133 | func New(maxSteps ...int) *Progress 134 | func NewWithConfig(fn func(p *Progress), maxSteps ...int) *Progress 135 | func RoundTrip(char rune, charNumAndBoxWidth ...int) *Progress 136 | func RoundTripBar(char rune, charNumAndBoxWidth ...int) *Progress 137 | func SpinnerBar(chars []rune, maxSteps ...int) *Progress 138 | func Tape(maxSteps ...int) *Progress 139 | func Txt(maxSteps ...int) *Progress 140 | ``` 141 | 142 | ## Spinner Bar 143 | 144 | ### Spinner Functions 145 | 146 | Quick create progress spinner: 147 | 148 | ```text 149 | func LoadingSpinner(chars []rune, speed time.Duration) *SpinnerFactory 150 | func RoundTripLoading(char rune, speed time.Duration, charNumAndBoxWidth ...int) *SpinnerFactory 151 | func RoundTripSpinner(char rune, speed time.Duration, charNumAndBoxWidth ...int) *SpinnerFactory 152 | func Spinner(speed time.Duration) *SpinnerFactory 153 | ``` 154 | 155 | ## Related 156 | 157 | - https://github.com/vbauerster/mpb 158 | - https://github.com/schollz/progressbar 159 | - https://github.com/gosuri/uiprogress -------------------------------------------------------------------------------- /progress/helper.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func repeatRune(char rune, length int) (chars []rune) { 9 | for i := 0; i < length; i++ { 10 | chars = append(chars, char) 11 | } 12 | 13 | return 14 | } 15 | 16 | // CharThemes collection. can use for Progress bar, RoundTripSpinner 17 | var CharThemes = []rune{ 18 | CharEqual, 19 | CharCenter, 20 | CharSquare, 21 | CharSquare1, 22 | CharSquare2, 23 | } 24 | 25 | // GetCharTheme by index number. if index not exist, will return a random theme 26 | func GetCharTheme(index int) rune { 27 | if index > 0 && len(CharThemes) > index { 28 | return CharThemes[index] 29 | } 30 | return RandomCharTheme() 31 | } 32 | 33 | // RandomCharTheme get 34 | func RandomCharTheme() rune { 35 | rand.Seed(time.Now().UnixNano()) 36 | return CharThemes[rand.Intn(len(CharThemes)-1)] 37 | } 38 | 39 | // CharsThemes collection. can use for LoadingBar, LoadingSpinner 40 | var CharsThemes = [][]rune{ 41 | {'卍', '卐'}, 42 | {'☺', '☻'}, 43 | {'░', '▒', '▓'}, 44 | {'⊘', '⊖', '⊕', '⊗'}, 45 | {'◐', '◒', '◓', '◑'}, 46 | {'✣', '✤', '✥', '❉'}, 47 | {'-', '\\', '|', '/'}, 48 | {'▢', '■', '▢', '■'}, 49 | []rune("▖▘▝▗"), 50 | []rune("◢◣◤◥"), 51 | []rune("⌞⌟⌝⌜"), 52 | []rune("◎●◯◌○⊙"), 53 | []rune("◡◡⊙⊙◠◠"), 54 | []rune("⇦⇧⇨⇩"), 55 | []rune("✳✴✵✶✷✸✹"), 56 | []rune("←↖↑↗→↘↓↙"), 57 | []rune("➩➪➫➬➭➮➯➱"), 58 | []rune("①②③④"), 59 | []rune("㊎㊍㊌㊋㊏"), 60 | []rune("⣾⣽⣻⢿⡿⣟⣯⣷"), 61 | []rune("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"), 62 | []rune("▉▊▋▌▍▎▏▎▍▌▋▊▉"), 63 | []rune("🌍🌎🌏"), 64 | []rune("☰☱☲☳☴☵☶☷"), 65 | []rune("⠋⠙⠚⠒⠂⠂⠒⠲⠴⠦⠖⠒⠐⠐⠒⠓⠋"), 66 | []rune("🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛"), 67 | } 68 | 69 | // GetCharsTheme by index number 70 | func GetCharsTheme(index int) []rune { 71 | if index > 0 && len(CharsThemes) > index { 72 | return CharsThemes[index] 73 | } 74 | return RandomCharsTheme() 75 | } 76 | 77 | // RandomCharsTheme get 78 | func RandomCharsTheme() []rune { 79 | rand.Seed(time.Now().UnixNano()) 80 | return CharsThemes[rand.Intn(len(CharsThemes)-1)] 81 | } 82 | -------------------------------------------------------------------------------- /progress/progress_test.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestProgress_Display(t *testing.T) { 12 | is := assert.New(t) 13 | ss := widgetMatch.FindAllString(TxtFormat, -1) 14 | is.Len(ss, 4) 15 | 16 | is.Contains(ss, "{@message}") 17 | } 18 | 19 | func TestSpinner(t *testing.T) { 20 | chars := []rune(`你\|/`) 21 | str := `你\|/` 22 | 23 | fmt.Println(chars, string(chars[0]), string(str[0])) 24 | } 25 | 26 | func TestLoading(t *testing.T) { 27 | chars := []rune("◐◑◒◓") 28 | str := "◐◑◒◓" 29 | 30 | fmt.Println(chars, string(chars[0]), str, string(str[0])) 31 | } 32 | 33 | func ExampleBar() { 34 | maxStep := 105 35 | p := CustomBar(60, BarStyles[0], maxStep) 36 | p.MaxSteps = uint(maxStep) 37 | p.Format = FullBarFormat 38 | 39 | p.Start() 40 | for i := 0; i < maxStep; i++ { 41 | time.Sleep(80 * time.Millisecond) 42 | p.Advance() 43 | } 44 | p.Finish() 45 | } 46 | 47 | func ExampleDynamicText() { 48 | messages := map[int]string{ 49 | // key is percent, range is 0 - 100. 50 | 20: " Prepare ...", 51 | 40: " Request ...", 52 | 65: " Transport ...", 53 | 95: " Saving ...", 54 | 100: " Handle Complete.", 55 | } 56 | 57 | maxStep := 105 58 | p := DynamicText(messages, maxStep) 59 | 60 | p.Start() 61 | 62 | for i := 0; i < maxStep; i++ { 63 | time.Sleep(80 * time.Millisecond) 64 | p.Advance() 65 | } 66 | 67 | p.Finish() 68 | } 69 | -------------------------------------------------------------------------------- /progress/quickstart.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | // some built in chars 9 | const ( 10 | CharStar rune = '*' 11 | CharPlus rune = '+' 12 | CharWell rune = '#' 13 | CharEqual rune = '=' 14 | CharEqual1 rune = '═' 15 | CharSpace rune = ' ' 16 | CharCenter rune = '●' 17 | CharSquare rune = '■' 18 | CharSquare1 rune = '▇' 19 | CharSquare2 rune = '▉' 20 | CharSquare3 rune = '░' 21 | CharSquare4 rune = '▒' 22 | CharSquare5 rune = '▢' 23 | // Hyphen Minus 24 | CharHyphen rune = '-' 25 | CharCNHyphen rune = '—' 26 | CharUnderline rune = '_' 27 | CharLeftArrow rune = '<' 28 | CharRightArrow rune = '>' 29 | CharRightArrow1 rune = '▶' 30 | ) 31 | 32 | // internal format for text progress 33 | const ( 34 | MinFormat = "{@message}{@current}" 35 | TxtFormat = "{@message}{@percent:4s}%({@current}/{@max})" 36 | DefFormat = "{@message}{@percent:4s}%({@current}/{@max})" 37 | FullFormat = "{@percent:4s}%({@current}/{@max}) {@elapsed:7s}/{@estimated:-7s} {@memory:6s}" 38 | ) 39 | 40 | // Txt progress bar create. 41 | func Txt(maxSteps ...int) *Progress { 42 | return New(maxSteps...).Config(func(p *Progress) { 43 | p.Format = TxtFormat 44 | }) 45 | } 46 | 47 | // Full text progress bar create. 48 | func Full(maxSteps ...int) *Progress { 49 | return NewWithConfig(func(p *Progress) { 50 | p.Format = FullFormat 51 | }, maxSteps...) 52 | } 53 | 54 | // Counter progress bar create 55 | func Counter(maxSteps ...int) *Progress { 56 | return NewWithConfig(func(p *Progress) { 57 | p.Format = MinFormat 58 | }, maxSteps...) 59 | } 60 | 61 | // DynamicText progress bar create 62 | func DynamicText(messages map[int]string, maxSteps ...int) *Progress { 63 | return NewWithConfig(func(p *Progress) { 64 | p.Format = "{@percent:4s}%({@current}/{@max}){@message}" 65 | p.AddWidget("message", DynamicTextWidget(messages)) 66 | }, maxSteps...) 67 | } 68 | 69 | /************************************************************* 70 | * Generic progress bar 71 | *************************************************************/ 72 | 73 | // internal format for ProgressBar 74 | const ( 75 | // BarWidth default bar width 76 | BarWidth = 40 77 | BarFormat = "{@bar} {@percent:4s}%({@current}/{@max}){@message}" 78 | 79 | // MdlBarFormat more format 80 | MdlBarFormat = "{@bar} {@percent:4s}%({@current}/{@max}) {@elapsed:7s}/{@estimated:-7s}" 81 | FullBarFormat = "{@bar} {@percent:4s}%({@current}/{@max}) {@elapsed:7s}/{@estimated:-7s} {@memory:6s}" 82 | ) 83 | 84 | // BarChars setting for a progress bar. default {'#', '>', ' '} 85 | type BarChars struct { 86 | Completed, Processing, Remaining rune 87 | } 88 | 89 | // BarStyles some built in BarChars style collection 90 | var BarStyles = []BarChars{ 91 | {'=', '>', ' '}, 92 | {'=', '>', '-'}, 93 | {'#', '>', ' '}, 94 | {'#', '>', '-'}, 95 | {'*', '>', '-'}, 96 | {'▉', '▉', '░'}, 97 | {'■', '■', ' '}, 98 | {'■', '■', '▢'}, 99 | {'■', '▶', ' '}, 100 | } 101 | 102 | // Bar create a default progress bar. 103 | // Preview: 104 | // 1 [->--------------------------] 105 | // 3 [■■■>------------------------] 106 | // 25/50 [==============>-------------] 50% 107 | func Bar(maxSteps ...int) *Progress { 108 | return CustomBar(BarWidth, BarStyles[0], maxSteps...) 109 | } 110 | 111 | // Tape create new tape progress bar. is alias of Bar() 112 | func Tape(maxSteps ...int) *Progress { 113 | return Bar(maxSteps...) 114 | } 115 | 116 | // CustomBar create a custom progress bar. 117 | func CustomBar(width int, cs BarChars, maxSteps ...int) *Progress { 118 | return New(maxSteps...). 119 | Config(func(p *Progress) { 120 | p.Format = BarFormat 121 | }). 122 | AddWidget("bar", BarWidget(width, cs)) 123 | } 124 | 125 | // RandomBarStyle get random bar style 126 | func RandomBarStyle() BarChars { 127 | rand.Seed(time.Now().UnixNano()) 128 | return BarStyles[rand.Intn(len(BarStyles)-1)] 129 | } 130 | 131 | /************************************************************* 132 | * RoundTrip progress bar: `[ ==== ] Pending ...` 133 | *************************************************************/ 134 | 135 | // RoundTripBar alias of RoundTrip() 136 | func RoundTripBar(char rune, charNumAndBoxWidth ...int) *Progress { 137 | return RoundTrip(char, charNumAndBoxWidth...) 138 | } 139 | 140 | // RoundTrip create a RoundTrip progress bar. 141 | // 142 | // Usage: 143 | // p := RoundTrip(CharEqual) 144 | // // p := RoundTrip('*') // custom char 145 | // p.Start() 146 | // .... 147 | // p.Finish() 148 | func RoundTrip(char rune, charNumAndBoxWidth ...int) *Progress { 149 | charNum := 4 150 | boxWidth := 12 151 | if ln := len(charNumAndBoxWidth); ln > 0 { 152 | charNum = charNumAndBoxWidth[0] 153 | if ln > 1 { 154 | boxWidth = charNumAndBoxWidth[1] 155 | } 156 | } 157 | 158 | return New(). 159 | AddWidget("rtBar", RoundTripWidget(char, charNum, boxWidth)). 160 | Config(func(p *Progress) { 161 | p.Format = "[{@rtBar}] {@percent:4s}% ({@current}/{@max}){@message}" 162 | }) 163 | } 164 | 165 | /************************************************************* 166 | * Loading bar 167 | *************************************************************/ 168 | 169 | // LoadingBar alias of load bar LoadBar() 170 | func LoadingBar(chars []rune, maxSteps ...int) *Progress { 171 | return LoadBar(chars, maxSteps...) 172 | } 173 | 174 | // SpinnerBar alias of load bar LoadBar() 175 | func SpinnerBar(chars []rune, maxSteps ...int) *Progress { 176 | return LoadBar(chars, maxSteps...) 177 | } 178 | 179 | // LoadBar create loading progress bar 180 | func LoadBar(chars []rune, maxSteps ...int) *Progress { 181 | return New(maxSteps...).Config(func(p *Progress) { 182 | p.Format = "{@loading} ({@current}/{@max}){@message}" 183 | p.AddWidget("loading", LoadingWidget(chars)) 184 | }) 185 | } 186 | -------------------------------------------------------------------------------- /progress/spinner.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gookit/color" 10 | ) 11 | 12 | // BuilderFunc build char string 13 | type BuilderFunc func() string 14 | 15 | // SpinnerFactory definition. ref https://github.com/briandowns/spinner 16 | type SpinnerFactory struct { 17 | // Speed is the running speed 18 | Speed time.Duration 19 | // Format setting display format 20 | Format string 21 | // Builder build custom spinner text 22 | Builder BuilderFunc 23 | // locker 24 | lock *sync.RWMutex 25 | // mark spinner status 26 | active bool 27 | // control the spinner running. 28 | stopCh chan struct{} 29 | } 30 | 31 | // Spinner instance 32 | func Spinner(speed time.Duration) *SpinnerFactory { 33 | return &SpinnerFactory{ 34 | Speed: speed, 35 | Format: "%s", 36 | // color: color.Normal.Sprint, 37 | lock: &sync.RWMutex{}, 38 | // writer: os.Stdout, 39 | stopCh: make(chan struct{}, 1), 40 | } 41 | } 42 | 43 | // RoundTripLoading create 44 | func RoundTripLoading(char rune, speed time.Duration, charNumAndBoxWidth ...int) *SpinnerFactory { 45 | return RoundTripSpinner(char, speed, charNumAndBoxWidth...) 46 | } 47 | 48 | // RoundTripSpinner instance create 49 | func RoundTripSpinner(char rune, speed time.Duration, charNumAndBoxWidth ...int) *SpinnerFactory { 50 | charNum := 4 51 | boxWidth := 12 52 | if ln := len(charNumAndBoxWidth); ln > 0 { 53 | charNum = charNumAndBoxWidth[0] 54 | if ln > 1 { 55 | boxWidth = charNumAndBoxWidth[1] 56 | } 57 | } 58 | 59 | return Spinner(speed).WithBuilder(roundTripTextBuilder(char, charNum, boxWidth)) 60 | } 61 | 62 | // LoadingSpinner instance create 63 | func LoadingSpinner(chars []rune, speed time.Duration) *SpinnerFactory { 64 | return Spinner(speed).WithBuilder(loadingCharBuilder(chars)) 65 | } 66 | 67 | /************************************************************* 68 | * spinner running 69 | *************************************************************/ 70 | 71 | // WithBuilder set spinner text builder 72 | func (s *SpinnerFactory) WithBuilder(builder BuilderFunc) *SpinnerFactory { 73 | s.Builder = builder 74 | return s 75 | } 76 | 77 | func (s *SpinnerFactory) prepare(format []string) { 78 | if s.Builder == nil { 79 | panic("spinner: field SpinnerFactory.Builder must be setting") 80 | } 81 | 82 | if len(format) > 0 { 83 | s.Format = format[0] 84 | } 85 | 86 | if s.Format != "" && !strings.Contains(s.Format, "%s") { 87 | s.Format = "%s " + s.Format 88 | } 89 | 90 | if s.Speed == 0 { 91 | s.Speed = 100 * time.Millisecond 92 | } 93 | } 94 | 95 | // Start run spinner 96 | func (s *SpinnerFactory) Start(format ...string) { 97 | if s.active { 98 | return 99 | } 100 | 101 | s.active = true 102 | s.prepare(format) 103 | 104 | go func() { 105 | for { 106 | select { 107 | case <-s.stopCh: 108 | return 109 | default: 110 | s.lock.Lock() 111 | 112 | // \x0D - Move the cursor to the beginning of the line 113 | // \x1B[2K - Erase(Delete) the line 114 | fmt.Print("\x0D\x1B[2K") 115 | color.Printf(s.Format, s.Builder()) 116 | s.lock.Unlock() 117 | 118 | time.Sleep(s.Speed) 119 | } 120 | } 121 | }() 122 | } 123 | 124 | // Stop run spinner 125 | func (s *SpinnerFactory) Stop(finalMsg ...string) { 126 | if !s.active { 127 | return 128 | } 129 | 130 | s.lock.Lock() 131 | s.active = false 132 | fmt.Print("\x0D\x1B[2K") 133 | 134 | if len(finalMsg) > 0 { 135 | fmt.Println(finalMsg[0]) 136 | } 137 | 138 | s.stopCh <- struct{}{} 139 | s.lock.Unlock() 140 | } 141 | 142 | // Restart will stop and start the spinner 143 | func (s *SpinnerFactory) Restart() { 144 | s.Stop() 145 | s.Start() 146 | } 147 | 148 | // Active status 149 | func (s *SpinnerFactory) Active() bool { 150 | return s.active 151 | } 152 | -------------------------------------------------------------------------------- /progress/spinner_test.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import "time" 4 | 5 | func ExampleLoadingSpinner() { 6 | s := LoadingSpinner(RandomCharsTheme(), 100*time.Millisecond) 7 | 8 | s.Start("%s work handling ... ...") 9 | // Run for some time to simulate work 10 | time.Sleep(4 * time.Second) 11 | s.Stop("work handle complete") 12 | } 13 | 14 | func ExampleRoundTripSpinner() { 15 | s := RoundTripSpinner(RandomCharTheme(), 100*time.Millisecond) 16 | 17 | s.Start("%s work handling ... ...") 18 | // Run for some time to simulate work 19 | time.Sleep(4 * time.Second) 20 | s.Stop("work handle complete") 21 | } 22 | -------------------------------------------------------------------------------- /progress/widgets.go: -------------------------------------------------------------------------------- 1 | package progress 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "sort" 7 | "strings" 8 | "time" 9 | 10 | "github.com/gookit/goutil/fmtutil" 11 | ) 12 | 13 | var builtinWidgets = map[string]WidgetFunc{ 14 | "elapsed": func(p *Progress) string { // 消耗时间 15 | // fmt.Sprintf("%.3f", time.Since(startTime).Seconds()*1000) 16 | elapsed := time.Since(p.StartedAt()).Seconds() 17 | return fmtutil.HowLongAgo(int64(elapsed)) 18 | }, 19 | "remaining": func(p *Progress) string { // 剩余时间 20 | step := p.Progress() // current progress 21 | 22 | // not set max steps OR current progress is 0 23 | if p.MaxSteps == 0 || step == 0 { 24 | return "unknown" 25 | } 26 | 27 | // get elapsed time 28 | elapsed := int64(time.Since(p.StartedAt()).Seconds()) 29 | // calc remaining time 30 | remaining := uint(elapsed) / step * (p.MaxSteps - step) 31 | return fmtutil.HowLongAgo(int64(remaining)) 32 | }, 33 | "estimated": func(p *Progress) string { // 计算总的预计时间 34 | step := p.Progress() // current progress 35 | 36 | // not set max steps OR current progress is 0 37 | if p.MaxSteps == 0 || step == 0 { 38 | return "unknown" 39 | } 40 | 41 | // get elapsed time 42 | elapsed := int64(time.Since(p.StartedAt()).Seconds()) 43 | // calc estimated time 44 | estimated := float32(elapsed) / float32(step) * float32(p.MaxSteps) 45 | 46 | return fmtutil.HowLongAgo(int64(estimated)) 47 | }, 48 | "memory": func(p *Progress) string { // Memory consumption 49 | mem := new(runtime.MemStats) 50 | runtime.ReadMemStats(mem) 51 | return fmtutil.DataSize(mem.Sys) 52 | }, 53 | "max": func(p *Progress) string { 54 | return fmt.Sprint(p.MaxSteps) 55 | }, 56 | "current": func(p *Progress) string { 57 | step := fmt.Sprint(p.Progress()) 58 | width := fmt.Sprint(p.StepWidth) 59 | diff := len(width) - len(step) 60 | if diff <= 0 { 61 | return step 62 | } 63 | 64 | return strings.Repeat(" ", diff) + step 65 | }, 66 | "percent": func(p *Progress) string { 67 | return fmt.Sprintf("%.1f", p.Percent()*100) 68 | }, 69 | } 70 | 71 | // DynamicTextWidget dynamic text message widget for progress bar. 72 | // for param messages: int is percent, range is 0 - 100. value is message string. 73 | // Usage please example. 74 | func DynamicTextWidget(messages map[int]string) WidgetFunc { 75 | var numbers []int 76 | for val := range messages { 77 | numbers = append(numbers, val) 78 | } 79 | 80 | // sort 81 | sort.Ints(numbers) 82 | 83 | return func(p *Progress) string { 84 | percent := int(p.Percent() * 100) 85 | for _, val := range numbers { 86 | if percent <= val { 87 | return messages[val] 88 | } 89 | } 90 | 91 | return " Handling ..." // Should never happen 92 | } 93 | } 94 | 95 | // LoadingWidget create loading progress widget 96 | func LoadingWidget(chars []rune) WidgetFunc { 97 | builder := loadingCharBuilder(chars) 98 | 99 | return func(_ *Progress) string { 100 | return builder() 101 | } 102 | } 103 | 104 | // RoundTripWidget create a round-trip widget for progress bar. 105 | // 106 | // Output like `[ ==== ]` 107 | func RoundTripWidget(char rune, charNum, boxWidth int) WidgetFunc { 108 | builder := roundTripTextBuilder(char, charNum, boxWidth) 109 | 110 | return func(_ *Progress) string { 111 | return builder() 112 | } 113 | } 114 | 115 | // BarWidget create a progress bar widget. 116 | // 117 | // Output like `[==============>-------------]` 118 | func BarWidget(width int, cs BarChars) WidgetFunc { 119 | if width < 1 { 120 | width = BarWidth 121 | } 122 | 123 | if cs.Completed == 0 { 124 | cs.Completed = CharWell 125 | } 126 | 127 | return func(p *Progress) string { 128 | var completeLen float32 129 | 130 | if p.MaxSteps > 0 { // MaxSteps is valid 131 | completeLen = p.percent * float32(width) 132 | } else { // not set MaxSteps 133 | completeLen = float32(p.step % uint(width)) 134 | } 135 | 136 | bar := string(repeatRune(cs.Completed, int(completeLen))) 137 | 138 | if diff := width - int(completeLen); diff > 0 { 139 | bar += string(cs.Processing) + string(repeatRune(cs.Remaining, diff-1)) 140 | } 141 | 142 | return bar 143 | } 144 | } 145 | 146 | func loadingCharBuilder(chars []rune) func() string { 147 | if len(chars) == 0 { 148 | chars = RandomCharsTheme() 149 | } 150 | 151 | index := 0 152 | length := len(chars) 153 | 154 | return func() string { 155 | char := string(chars[index]) 156 | if index+1 == length { // reset 157 | index = 0 158 | } else { 159 | index++ 160 | } 161 | 162 | return char 163 | } 164 | } 165 | 166 | func roundTripTextBuilder(char rune, charNum, boxWidth int) func() string { 167 | if char == 0 { 168 | char = CharEqual 169 | } 170 | 171 | if charNum < 1 { 172 | charNum = 4 173 | } 174 | 175 | if boxWidth < 1 { 176 | boxWidth = 12 177 | } 178 | 179 | cursor := string(repeatRune(char, charNum)) 180 | // control direction. False: -> True: <-> 181 | direction := false 182 | // record cursor position 183 | position := 0 184 | 185 | return func() string { 186 | var bar string 187 | if position > 0 { 188 | bar += strings.Repeat(" ", position) 189 | } 190 | 191 | bar += cursor + strings.Repeat(" ", boxWidth-position-charNum) 192 | 193 | if direction { // left <- 194 | if position <= 0 { // begin -> 195 | direction = false 196 | } else { 197 | position-- 198 | } 199 | } else { // -> right 200 | if position+charNum >= boxWidth { // begin <- 201 | direction = true 202 | } else { 203 | position++ 204 | } 205 | } 206 | 207 | return bar 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /resource/Changelog-TODO.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## TODO 4 | 5 | - [ ] hook on set flag value 6 | - [x] option support multi shorts names 7 | - [ ] cmd support flag option category 8 | - [ ] app support command category by `c.Category` 9 | - [ ] print parent's options on subcommand help panel 10 | - [ ] prompt completion by readline 11 | - [ ] keyboard and cursor control on terminal 12 | - [ ] refactor gflag.Flags, remove dep the `flag.FlagSet` 13 | - [ ] collect option/argument value by interactive `Option.Question` 14 | - [ ] support all command docs to markdown 15 | 16 | readline refers: 17 | 18 | - https://github.com/chzyer/readline/tree/master/example 19 | - https://github.com/abiosoft/ishell/blob/master/completer.go 20 | 21 | ## v3.0.1 22 | 23 | **new** 24 | 25 | - [x] add some special flag type vars 26 | - [x] support hidden command on render help by `c.Hidden=true` 27 | 28 | **fixed** 29 | 30 | - [x] alias not works on command ID 31 | - [x] render color on command/option/argument description 32 | 33 | ## v3.0.0 34 | 35 | **new** 36 | 37 | - [x] support multi level sub commands 38 | - [x] support parse flags from struct tags 39 | - [x] support flag/argument validate 40 | - [ ] support controller on application `app.controllers []Controller` 41 | - 独立于commands之外的。Independent of commands. 42 | - 支持组选项,全部子命令都拥有这些选项 `Config/GroupOptions()` 里绑定组选项。 43 | - [x] 支持单个command、controller独立运行 44 | -------------------------------------------------------------------------------- /resource/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # @build-example build . -f Dockerfile -t gcli:test 3 | # 4 | 5 | ################################################################################ 6 | ### builder image 7 | ################################################################################ 8 | FROM golang:1.16-alpine as Builder 9 | 10 | # Recompile the standard library without CGO 11 | #RUN CGO_ENABLED=0 go install -a std 12 | 13 | ENV APP_DIR $GOPATH/src/github.com/gookit/gcli 14 | RUN mkdir -p $APP_DIR 15 | 16 | ADD . $APP_DIR 17 | 18 | # Compile the binary and statically link 19 | # -ldflags '-w -s' 20 | # -s: 去掉符号表 21 | # -w: 去掉调试信息,不能gdb调试了 22 | # RUN cd $APP_DIR && CGO_ENABLED=0 go build -ldflags '-d -w -s' -o /tmp/app-server 23 | RUN go version && cd $APP_DIR && go build -ldflags '-w -s' -o /tmp/app-server 24 | # RUN cd $APP_DIR && go build -o /tmp/app-server 25 | 26 | ################################################################################ 27 | ### target image 28 | ################################################################################ 29 | FROM alpine:3.10 30 | LABEL maintainer="inhere " version="1.0" 31 | 32 | ## 33 | # ---------- env settings ---------- 34 | ## 35 | 36 | ARG timezone 37 | # prod audit test dev. --build-arg app_env=dev 38 | ARG app_env=dev 39 | ARG app_port 40 | 41 | ENV APP_ENV=${app_env:-"dev"} \ 42 | APP_PORT=${app_port:-59430} \ 43 | TIMEZONE=${timezone:-"Asia/Shanghai"} 44 | 45 | ## 46 | # ---------- some config, clear work ---------- 47 | ## 48 | RUN set -ex \ 49 | && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/' /etc/apk/repositories \ 50 | # install some tools 51 | && apk update && apk add --no-cache tzdata ca-certificates \ 52 | 53 | # clear caches 54 | && rm -rf /var/cache/apk/* \ 55 | 56 | # - config timezone 57 | && ln -sf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime \ 58 | && echo "${TIMEZONE}" > /etc/timezone \ 59 | && date -R \ 60 | 61 | # - create logs, caches dir 62 | && mkdir -p /data/logs /var/www \ 63 | # && chown -R www:www /data/logs \ 64 | 65 | && echo -e "\033[42;97m Build Completed :).\033[0m\n" 66 | 67 | EXPOSE ${APP_PORT} 68 | WORKDIR "/var/www" 69 | 70 | COPY --from=Builder /tmp/app-server app-server 71 | #COPY conf conf 72 | #COPY static static 73 | #COPY resources resources 74 | 75 | ENTRYPOINT ["./app-server"] 76 | -------------------------------------------------------------------------------- /resource/auto-completion/about-auto-complete.md: -------------------------------------------------------------------------------- 1 | # auto-completion 脚本编写 2 | 3 | ## bash 环境 4 | 5 | bash 环境下的命令自动补全脚本 6 | 7 | - 示例文件: [auto-completion.bash](auto-completion.bash) 8 | 9 | ### 说明 10 | 11 | `complete -F` 后面接一个函数,该函数将输入三个参数: 12 | 13 | 1. 要补全的命令名 14 | 2. 当前光标所在的词 15 | 3. 当前光标所在的词的前一个词 16 | 17 | 生成的补全结果需要存储到COMPREPLY变量中,以待bash获取。 18 | 19 | `complete` 选项参数: 20 | 21 | - `-F function` 指定补全函数名 22 | - `-A file` 表示默认的动作是补全文件名,也即是如果bash找不到补全的内容,就会默认以文件名进行补全 23 | 24 | 参数接收: 25 | 26 | ```bash 27 | local cur prev 28 | 29 | // 方式 1 30 | 31 | _get_comp_words_by_ref -n = cur prev 32 | 33 | // 方式 2 34 | 35 | pre="$3" 36 | cur="$2" 37 | 38 | // 方式 3 39 | 40 | pre=${COMP_WORDS[COMP_CWORD-1]} # COMP_WORDS变量是一个数组,存储着当前输入所有的词 41 | cur=${COMP_WORDS[COMP_CWORD]} 42 | ``` 43 | 44 | ### 参考链接 45 | 46 | - https://segmentfault.com/a/1190000002968878 47 | 48 | ## zsh 环境 49 | 50 | zsh 环境下的命令自动补全脚本 51 | 52 | 提示: 53 | 54 | - `echo $fpath` zsh在启动时会加载 `$fpath` 路径下的脚本文件,可以在这些文件夹下找文件参考 55 | 56 | ### 参考链接 57 | 58 | - https://segmentfault.com/a/1190000002994217 59 | - https://github.com/zsh-users/zsh-completions/blob/master/zsh-completions-howto.org 60 | -------------------------------------------------------------------------------- /resource/auto-completion/auto-completion.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # ------------------------------------------------------------------------------ 4 | # FILE: auto-completion.bash 5 | # AUTHOR: inhere (https://github.com/inhere) 6 | # VERSION: 1.0.0 7 | # DESCRIPTION: zsh shell complete for cli app: cliapp 8 | # ------------------------------------------------------------------------------ 9 | # usage: source auto-completion.bash 10 | # run 'complete' to see registered complete function. 11 | 12 | 13 | _complete_for_cliapp () { 14 | local cur prev 15 | _get_comp_words_by_ref -n = cur prev 16 | 17 | COMPREPLY=() 18 | commands="exp ex example env-info ei env git-info git clr colors color" 19 | 20 | case "$prev" in 21 | clr|colors|color) 22 | COMPREPLY=($(compgen -W "--id -c --dir" -- "$cur")) 23 | return 0 24 | ;; 25 | env-info|ei|env) 26 | COMPREPLY=($(compgen -W "--id -c -d --dir" -- "$cur")) 27 | return 0 28 | ;; 29 | exp|ex|example) 30 | COMPREPLY=($(compgen -W "-d --dir -o --opt -n --names" -- "$cur")) 31 | return 0 32 | ;; 33 | git-info|git) 34 | COMPREPLY=($(compgen -W "--id -c -d --dir" -- "$cur")) 35 | return 0 36 | ;; 37 | help) 38 | COMPREPLY=($(compgen -W "$commands" -- "$cur")) 39 | return 0 40 | ;; 41 | esac 42 | 43 | COMPREPLY=($(compgen -W "$commands" -- "$cur")) 44 | 45 | } && 46 | # complete -F {auto_complete_func} {bin_filename} 47 | # complete -F _complete_for_cliapp -A file cliapp cliapp.exe 48 | complete -F _complete_for_cliapp cliapp cliapp.exe 49 | -------------------------------------------------------------------------------- /resource/auto-completion/auto-completion.zsh: -------------------------------------------------------------------------------- 1 | #compdef cliapp 2 | # ------------------------------------------------------------------------------ 3 | # FILE: auto-completion.zsh 4 | # AUTHOR: inhere (https://github.com/inhere) 5 | # VERSION: 1.0.0 6 | # DESCRIPTION: zsh shell complete for cli app: cliapp 7 | # ------------------------------------------------------------------------------ 8 | # usage: source auto-completion.zsh 9 | 10 | _complete_for_cliapp () { 11 | typeset -a commands 12 | commands+=( 13 | 'color[This is a example for cli color usage(alias clr,colors)]' 14 | 'env[Collect project info by git info(alias env-info,ei)]' 15 | 'example[This is a description message(alias exp,ex)]' 16 | 'git[Collect project info by git info(alias git-info)]' 17 | 'test[This is a description message for command test(alias ts)]' 18 | 'help[Display help information]' 19 | ) 20 | 21 | if (( CURRENT == 2 )); then 22 | # explain commands 23 | _values 'cliapp commands' ${commands[@]} 24 | return 25 | fi 26 | 27 | case ${words[2]} in 28 | clr|colors|color) 29 | _values 'command options' \ 30 | '--id[the id option]' \ 31 | '-c[the config option]' \ 32 | '--dir[the dir option]' 33 | ;; 34 | env-info|ei|env) 35 | _values 'command options' \ 36 | '--id[the id option]' \ 37 | '-c[the config option]' \ 38 | {-d,--dir}'[the dir option]' 39 | ;; 40 | exp|ex|example) 41 | _values 'command options' \ 42 | {-n,--names}'[the option message]' \ 43 | {-d,--dir}'[the DIRECTORY option]' \ 44 | {-o,--opt}'[the option message]' 45 | ;; 46 | git-info|git) 47 | _values 'command options' \ 48 | {-d,--dir}'[the dir option]' \ 49 | '--id[the id option]' \ 50 | '-c[the config option]' 51 | ;; 52 | help) 53 | _values "${commands[@]}" 54 | ;; 55 | *) 56 | # use files by default 57 | _files 58 | ;; 59 | esac 60 | } 61 | 62 | compdef _complete_for_cliapp cliapp 63 | compdef _complete_for_cliapp cliapp.exe 64 | -------------------------------------------------------------------------------- /resource/auto-completion/composer.plg.zsh: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # FILE: composer.plugin.zsh 3 | # DESCRIPTION: oh-my-zsh composer plugin file. 4 | # AUTHOR: Daniel Gomes (me@danielcsgomes.com) 5 | # VERSION: 1.0.0 6 | # ------------------------------------------------------------------------------ 7 | 8 | # Composer basic command completion 9 | _composer_get_command_list () { 10 | $_comp_command1 --no-ansi 2>/dev/null | sed "1,/Available commands/d" | awk '/^[ \t]*[a-z]+/ { print $1 }' 11 | } 12 | 13 | _composer_get_required_list () { 14 | $_comp_command1 show -s --no-ansi 2>/dev/null | sed '1,/requires/d' | awk 'NF > 0 && !/^requires \(dev\)/{ print $1 }' 15 | } 16 | 17 | _composer () { 18 | local curcontext="$curcontext" state line 19 | typeset -A opt_args 20 | _arguments \ 21 | '1: :->command'\ 22 | '*: :->args' 23 | 24 | case $state in 25 | command) 26 | compadd $(_composer_get_command_list) 27 | ;; 28 | *) 29 | compadd $(_composer_get_required_list) 30 | ;; 31 | esac 32 | } 33 | 34 | compdef _composer composer 35 | compdef _composer composer.phar 36 | -------------------------------------------------------------------------------- /resource/auto-completion/sf-console.zsh: -------------------------------------------------------------------------------- 1 | #compdef console 2 | # from https://github.com/zsh-users/zsh-completions/blob/master/src/_console 3 | # ------------------------------------------------------------------------------ 4 | # Copyright (c) 2011 Github zsh-users - http://github.com/zsh-users 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # * Neither the name of the zsh-users nor the 15 | # names of its contributors may be used to endorse or promote products 16 | # derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL ZSH-USERS BE LIABLE FOR ANY 22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | # ------------------------------------------------------------------------------ 29 | # Description 30 | # ----------- 31 | # 32 | # Completion script for symfony console (https://github.com/symfony/Console). 33 | # 34 | # ------------------------------------------------------------------------------ 35 | # Authors 36 | # ------- 37 | # 38 | # * loranger (https://github.com/loranger) 39 | # * Yohan Tambè (https://github.com/Cronos87) 40 | # 41 | # ------------------------------------------------------------------------------ 42 | 43 | _find_console () { 44 | echo "php $(find . -maxdepth 2 -mindepth 1 -name 'console' -type f | head -n 1)" 45 | } 46 | 47 | _console_get_command_list () { 48 | IFS=" " 49 | `_find_console` --no-ansi | \ 50 | sed "1,/Available commands/d" | \ 51 | awk '/ [a-z]+/ { print $0 }' | \ 52 | sed -E 's/^[ ]+//g' | \ 53 | sed -E 's/[:]+/\\:/g' | \ 54 | sed -E 's/[ ]{2,}/\:/g' 55 | } 56 | 57 | _console () { 58 | local -a commands 59 | IFS=$'\n' 60 | commands=(`_console_get_command_list`) 61 | _describe 'commands' commands 62 | } 63 | 64 | compdef _console php console 65 | compdef _console console 66 | -------------------------------------------------------------------------------- /resource/dev.md: -------------------------------------------------------------------------------- 1 | # Dev 2 | 3 | 4 | ## Tests 5 | 6 | ```shell 7 | go test -v -test.run ^TestCommand_Run_X ./ 8 | ``` 9 | -------------------------------------------------------------------------------- /resource/gcli-cmd-code.tpl: -------------------------------------------------------------------------------- 1 | 2 | var {{.Name}}Cmd = &gcli.Command { 3 | Name: "{{ .Name }}", 4 | Name: "{{ .Desc }}", 5 | } 6 | -------------------------------------------------------------------------------- /resource/resource.md: -------------------------------------------------------------------------------- 1 | # resource 2 | 3 | ## Related 4 | 5 | - https://github.com/spf13/cobra 6 | - https://github.com/mitchellh/cli 7 | 8 | ## Completion 9 | 10 | - https://github.com/posener/complete bash completion written in go + bash completion for go command 11 | 12 | -------------------------------------------------------------------------------- /show/README.md: -------------------------------------------------------------------------------- 1 | # Show data 2 | 3 | contains `section, panel, padding, helpPanel, table, tree, title, list, multiList` 4 | 5 | - title 6 | - table 7 | - panel 8 | - section 9 | - padding 10 | - list 11 | - multi list 12 | - alert(block) 13 | 14 | - markdown 15 | - json 16 | 17 | ## GoDoc 18 | 19 | Please see https://pkg.go.dev/github.com/gookit/gcli/v3/show 20 | 21 | ## Install 22 | 23 | ```shell 24 | go get github.com/gookit/gcli/v3/show 25 | ``` 26 | 27 | ## Related 28 | 29 | - https://github.com/jedib0t/go-pretty 30 | - https://github.com/alexeyco/simpletable 31 | - https://github.com/InVisionApp/tabular 32 | - https://github.com/gosuri/uitable 33 | - https://github.com/rodaine/table 34 | - https://github.com/tomlazar/table 35 | - https://github.com/nwidger/jsoncolor 36 | -------------------------------------------------------------------------------- /show/alert.go: -------------------------------------------------------------------------------- 1 | package show 2 | -------------------------------------------------------------------------------- /show/banner.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | /* 4 | eg: 5 | ╭──────────────────────────────────────────────────────────────────╮ 6 | │ │ 7 | │ Update available! 3.21.0 → 3.27.0. │ 8 | │ Changelog: https://github.com/gookit/gcli/releases/tag/v3.2.0 │ 9 | │ Run "x y z" to update. │ 10 | │ │ 11 | ╰──────────────────────────────────────────────────────────────────╯ 12 | */ 13 | -------------------------------------------------------------------------------- /show/emoji/emoji.go: -------------------------------------------------------------------------------- 1 | package emoji 2 | 3 | import ( 4 | "regexp" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | var codeMatch = regexp.MustCompile(`(:\w+:)`) 10 | 11 | // Emoji is alias of the GetByName() 12 | func Emoji(name string) string { 13 | return GetByName(name) 14 | } 15 | 16 | // GetByName returns the unicode value for the given emoji name. If the 17 | // specified emoji does not exist, will return the input string. 18 | func GetByName(name string) string { 19 | if val, ok := emojiMap[name]; ok { 20 | return val 21 | } 22 | return name 23 | } 24 | 25 | // Search emoji by name 26 | func Search(kw string, limits ...int) (ret map[string]string) { 27 | kw = strings.TrimSpace(kw) 28 | if kw == "" || len(kw) > 12 { 29 | return 30 | } 31 | 32 | limit := 8 33 | if len(limits) > 0 { 34 | limit = limits[0] 35 | } 36 | 37 | ret = make(map[string]string, limit) 38 | for name, code := range emojiMap { 39 | if len(ret) == limit { 40 | return 41 | } 42 | 43 | if strings.Contains(name, kw) { 44 | ret[name] = code 45 | } 46 | } 47 | 48 | return 49 | } 50 | 51 | // Render a string, parse emoji name, returns rendered string. 52 | // 53 | // Usage: 54 | // 55 | // msg := Render("a :smile: message") 56 | // fmt.Println(msg) 57 | func Render(str string) string { 58 | // not contains emoji name. 59 | if strings.IndexByte(str, ':') == -1 { 60 | return str 61 | } 62 | 63 | return codeMatch.ReplaceAllStringFunc(str, func(name string) string { 64 | return GetByName(name) // + " " 65 | }) 66 | } 67 | 68 | // FromUnicode unicode string to emoji string 69 | // 70 | // Usage: 71 | // 72 | // emoji := FromUnicode("\U0001f496") 73 | func FromUnicode(s string) string { 74 | // emoji表情的数据表达式 75 | re := regexp.MustCompile("\\[[\\\\u0-9a-zA-Z]+\\]") 76 | // 提取emoji数据表达式 77 | reg := regexp.MustCompile("\\[\\\\u|]") 78 | src := re.FindAllString(s, -1) 79 | for i := 0; i < len(src); i++ { 80 | e := reg.ReplaceAllString(src[i], "") 81 | p, err := strconv.ParseInt(e, 16, 32) 82 | if err == nil { 83 | s = strings.Replace(s, src[i], string(rune(p)), -1) 84 | } 85 | } 86 | 87 | return s 88 | } 89 | 90 | // ToUnicode convert emoji to unicode string 91 | // Usage: 92 | // 93 | // unicode := ToUnicode("💖") 94 | // fmt.Print(unicode) // "1f496" 95 | // 96 | // // with prefix 97 | // unicode := ToUnicode("💖", "\U000") // "\U0001f496" 98 | // fmt.Print(unicode) // "💖" 99 | func ToUnicode(emoji string, prefix ...string) string { 100 | code := strconv.FormatInt(int64(emoji[0]), 16) 101 | 102 | if len(prefix) > 0 { 103 | return prefix[0] + code 104 | } 105 | return code 106 | } 107 | 108 | // Decode a string, convert unicode to emoji chat 109 | // 110 | // Usage: 111 | // 112 | // str := Decode("a msg [\u1f496]") 113 | func Decode(s string) string { 114 | // emoji表情的数据表达式 115 | re := regexp.MustCompile("\\[[\\\\u0-9a-zA-Z]+\\]") 116 | // 提取emoji数据表达式 117 | reg := regexp.MustCompile("\\[\\\\u|]") 118 | src := re.FindAllString(s, -1) 119 | 120 | for i := 0; i < len(src); i++ { 121 | e := reg.ReplaceAllString(src[i], "") 122 | p, err := strconv.ParseInt(e, 16, 32) 123 | if err == nil { 124 | s = strings.Replace(s, src[i], string(rune(p)), -1) 125 | } 126 | } 127 | 128 | return s 129 | } 130 | 131 | // Encode a string, convert emoji chat to unicode string 132 | func Encode(s string) string { 133 | var sb strings.Builder 134 | for _, r := range []rune(s) { 135 | if len(string(r)) == 4 { // is unicode emoji char 136 | code := strconv.FormatInt(int64(r), 16) 137 | sb.WriteString(`[\u` + code + `]`) 138 | } else { 139 | sb.WriteRune(r) 140 | } 141 | } 142 | 143 | return sb.String() 144 | } 145 | -------------------------------------------------------------------------------- /show/emoji/simple_emoji.go: -------------------------------------------------------------------------------- 1 | package emoji 2 | 3 | // some simple emoji chars 4 | const ( 5 | ID = "🆔" 6 | Key = "🔑" 7 | Box = "📦" 8 | Gift = "🎁" 9 | Flag = "🚩" 10 | Tool = "🔧" 11 | GUN = "🔫" 12 | Ding = "📌" 13 | Stop = "🚫" 14 | 15 | DOC = "📄" 16 | DIR = "📂" 17 | BOOK = "📔" 18 | RECYCLE = "♻" 19 | 20 | EDIT = "✍" 21 | SMILE = "😊" 22 | LAUGH = "😆" 23 | LIKE = "😍" 24 | ANGER = "😡" 25 | HAPPY = "😀" 26 | DOZE = "😴" 27 | 28 | OK = "👌" 29 | YES = "✌" 30 | NO = "✋" 31 | PRAISE = "👍" 32 | TREAD = "👎" 33 | STEP = "🐾" 34 | 35 | UP = "👆" 36 | DOWN = "👇" 37 | LEFT = "👈" 38 | RIGHT = "👉" 39 | 40 | TopArrow = "🔝" 41 | BackArrow = "🔙" 42 | SoonArrow = "🔜" 43 | 44 | FIRE = "🔥" 45 | SNOW = "❄" 46 | WATER = "💧" 47 | FLASH = "⚡" 48 | 49 | Eye = "👀" 50 | 51 | HeartStar = "💖" 52 | HeartBreak = "💔" 53 | HeartRed = "❤️" 54 | HeartOrange = "🧡" 55 | HeartYellow = "💛" 56 | HeartGreen = "💚" 57 | 58 | // SUC 🔝➕➖🎶✖️💲✔️☑️🔘🟢🟡🔵🟣🟠⚪️🟩🔲🔳 59 | SUC = "✅" 60 | FAIL = "❌" 61 | 62 | // TickSGreen square green tick 方框绿色勾 63 | TickSGreen = "✅" 64 | // TickBlack black tick 65 | TickBlack = "✔️" 66 | // TickSBlack square black tick 67 | TickSBlack = "☑️" 68 | // BtnSingle 单选按钮 69 | BtnSingle = "🔘" 70 | BtnSquare = "🔲" 71 | 72 | // CircleSRed red solid circle 红色实心圆圈 73 | CircleSRed = "🔴" 74 | // CircleSGreen green solid circle 绿色实心圆圈 75 | CircleSGreen = "🟢" 76 | // CircleSYellow yellow solid circle 绿色实心圆圈 77 | CircleSYellow = "🟡" 78 | 79 | Warning = "⚠️" 80 | QUESTION = "❓" 81 | // RedExcMark 红色感叹号 82 | RedExcMark = "❗" 83 | // RedExcMarkD red double exclamation mark 红色双感叹号 84 | RedExcMarkD = "‼️" 85 | 86 | // Points100 100分符号 87 | Points100 = "💯" 88 | // Recycling 回收标志 89 | Recycling = "♻️" 90 | 91 | Music1 = "🎵" 92 | Music2 = "🎶" 93 | 94 | Clock = "⏰" 95 | Clock4 = "🕓" 96 | 97 | Car = "🚕" 98 | 99 | Tree = "🌲" 100 | Flower = "🌺" 101 | 102 | Pear = "🍐" 103 | Apple = "🍎" 104 | 105 | Elephant = "🐘" 106 | Whale = "🐳" 107 | 108 | Sun = "🌞" 109 | Star = "⭐" 110 | Moon = "🌜" 111 | Earth = "🌏" 112 | ) 113 | -------------------------------------------------------------------------------- /show/emoji/some-record.md: -------------------------------------------------------------------------------- 1 | # record 2 | 3 | - UTF-8字符集的编码范围 `\u0000 - \uFFFF` 4 | 5 | ## emoji表情 6 | 7 | emoji表情采用的是 Unicode编码,Emoji就是一种在Unicode位于 `\u1F601-\u1F64F`区段的字符。 8 | 9 | 显然超过了目前常用的UTF-8字符集的编码范围`\u0000-\uFFFF`。 10 | 11 | ```bash 12 | #First, tan the following command to generate emojis.txt 13 | wget -qO- https://unicode.org/Public/emoji/11.0/emoji-test.txt | cut -f 1 -d ' ' | sort -u | sed '/^[#0]/ d' | sed '/^\s*$/d' > /tmp/emojis.txt 14 | ``` 15 | 16 | ## data links: 17 | 18 | - https://unicode.org/Public/emoji/15.0/emoji-sequences.txt 19 | - https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json 20 | - https://github.com/muan/emojilib/blob/main/dist/emoji-en-US.json 21 | - https://github.com/muan/unicode-emoji-json/blob/main/data-by-emoji.json 22 | - https://www.unicode.org/Public/emoji/11.0/ 23 | 24 | ## sites 25 | 26 | - http://emoji.muan.co/ 27 | - http://unicode-table.com/ 28 | - https://unicode-table.com/cn/sets/arrows-symbols/ 29 | 30 | ## refer 31 | 32 | - https://github.com/unicode-table/unicode-table-data 33 | - https://github.com/muan/emoji 34 | - https://github.com/kyokomi/generateEmojiCodeMap 35 | -------------------------------------------------------------------------------- /show/json.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | // PrettyJSON struct 4 | type PrettyJSON struct { 5 | Base 6 | } 7 | 8 | // NewPrettyJSON instance 9 | func NewPrettyJSON() *PrettyJSON { 10 | return &PrettyJSON{} 11 | } 12 | -------------------------------------------------------------------------------- /show/show.go: -------------------------------------------------------------------------------- 1 | // Package show provides some formatter tools for display data. 2 | package show 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "os" 9 | "text/tabwriter" 10 | 11 | "github.com/gookit/color" 12 | ) 13 | 14 | // Output the global input out stream 15 | var Output io.Writer = os.Stdout 16 | 17 | // SetOutput stream 18 | func SetOutput(out io.Writer) { Output = out } 19 | 20 | // ResetOutput stream 21 | func ResetOutput() { Output = os.Stdout } 22 | 23 | // Error tips message print 24 | func Error(format string, v ...any) int { 25 | prefix := color.Red.Sprint("ERROR: ") 26 | _, _ = fmt.Fprintf(Output, prefix+format+"\n", v...) 27 | return ERR 28 | } 29 | 30 | // Success tips message print 31 | func Success(format string, v ...any) int { 32 | prefix := color.Green.Sprint("SUCCESS: ") 33 | _, _ = fmt.Fprintf(Output, prefix+format+"\n", v...) 34 | return OK 35 | } 36 | 37 | // JSON print pretty JSON data 38 | func JSON(v any, prefixAndIndent ...string) int { 39 | prefix := "" 40 | indent := " " 41 | 42 | l := len(prefixAndIndent) 43 | if l > 0 { 44 | prefix = prefixAndIndent[0] 45 | if l > 1 { 46 | indent = prefixAndIndent[1] 47 | } 48 | } 49 | 50 | bs, err := json.MarshalIndent(v, prefix, indent) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | _, _ = fmt.Fprintln(Output, string(bs)) 56 | return OK 57 | } 58 | 59 | // AList create a List instance and print. 60 | // 61 | // Usage: 62 | // 63 | // show.AList("some info", map[string]string{"name": "tom"}) 64 | func AList(title string, data any, fns ...ListOpFunc) { 65 | NewList(title, data).WithOptionFns(fns).Println() 66 | } 67 | 68 | // MList show multi list data. 69 | // 70 | // Usage: 71 | // 72 | // show.MList(data) 73 | // show.MList(data, func(opts *ListOption) { 74 | // opts.LeftIndent = " " 75 | // }) 76 | func MList(listMap any, fns ...ListOpFunc) { 77 | NewLists(listMap).WithOptionFns(fns).Println() 78 | } 79 | 80 | // TabWriter create. 81 | // more please see: package text/tabwriter/example_test.go 82 | // 83 | // Usage: 84 | // 85 | // w := TabWriter([]string{ 86 | // "a\tb\tc\td\t.", 87 | // "123\t12345\t1234567\t123456789\t." 88 | // }) 89 | // w.Flush() 90 | func TabWriter(rows []string) *tabwriter.Writer { 91 | w := tabwriter.NewWriter(Output, 0, 4, 2, ' ', tabwriter.Debug) 92 | 93 | for _, row := range rows { 94 | if _, err := fmt.Fprintln(w, row); err != nil { 95 | panic(err) 96 | } 97 | } 98 | 99 | return w 100 | } 101 | -------------------------------------------------------------------------------- /show/show_test.go: -------------------------------------------------------------------------------- 1 | package show_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gookit/gcli/v3/show" 8 | "github.com/gookit/goutil/testutil/assert" 9 | ) 10 | 11 | func TestList(t *testing.T) { 12 | // is := assert.New(t) 13 | l := show.NewList("test list", []string{ 14 | "list item 0", 15 | "list item 1", 16 | "list item 2", 17 | }) 18 | l.Println() 19 | 20 | l = show.NewList("test list1", map[string]string{ 21 | "key0": "list item 0", 22 | "the key1": "list item 1", 23 | "key2": "list item 2", 24 | "key3": "", // empty value 25 | }) 26 | l.Opts.SepChar = " | " 27 | l.Println() 28 | } 29 | 30 | func TestList_mlevel(t *testing.T) { 31 | d := map[string]any{ 32 | "key0": "list item 0", 33 | "key2": []string{"abc", "def"}, 34 | "key4": map[string]int{"abc": 23, "def": 45}, 35 | "the key1": "list item 1", 36 | "key3": "", // empty value 37 | } 38 | 39 | l := show.NewList("test list", d) 40 | l.Println() 41 | 42 | l = show.NewList("test list2", d).WithOptions(func(opts *show.ListOption) { 43 | opts.SepChar = " | " 44 | }) 45 | l.Println() 46 | } 47 | 48 | func TestLists(t *testing.T) { 49 | ls := show.NewLists(map[string]any{ 50 | "test list": []string{ 51 | "list item 0", 52 | "list item 1", 53 | "list item 2", 54 | }, 55 | "test list1": map[string]string{ 56 | "key0": "list item 0", 57 | "the key1": "list item 1", 58 | "key2": "list item 2", 59 | "key3": "", // empty value 60 | }, 61 | }) 62 | ls.Opts.SepChar = " : " 63 | ls.Println() 64 | } 65 | 66 | func TestTabWriter(t *testing.T) { 67 | is := assert.New(t) 68 | ss := []string{ 69 | "a\tb\taligned\t", 70 | "aa\tbb\taligned\t", 71 | "aaa\tbbb\tunaligned", 72 | "aaaa\tbbbb\taligned\t", 73 | } 74 | 75 | err := show.TabWriter(ss).Flush() 76 | is.NoErr(err) 77 | } 78 | 79 | func TestSome(t *testing.T) { 80 | fmt.Printf("|%8s|\n", "text") 81 | fmt.Printf("|%-8s|\n", "text") 82 | fmt.Printf("|%8s|\n", "text") 83 | } 84 | -------------------------------------------------------------------------------- /show/symbols/chars.go: -------------------------------------------------------------------------------- 1 | package symbols 2 | 3 | // links: 4 | // 5 | // http://cn.piliapp.com/symbol/ 6 | // 7 | // 卍 卐 ■ ▶ ☐☑☒ ❖ 8 | const ( 9 | OK = '✔' 10 | NO = '✘' 11 | PEN = '✎' 12 | 13 | Center rune = '●' 14 | Square rune = '■' 15 | Square1 rune = '▇' 16 | Square2 rune = '▉' 17 | Square3 rune = '░' 18 | Square4 rune = '▒' 19 | Square5 rune = '▢' 20 | 21 | HEART = '❤' 22 | HEART1 = '♥' 23 | SMILE = '☺' 24 | 25 | FLOWER = '✿' 26 | MUSIC = '♬' 27 | 28 | // UP ☚ ☜ ☛ ☞ 29 | UP = '⇧' 30 | DOWN = '⇩' 31 | LEFT = '⇦' 32 | RIGHT = '⇨' 33 | SEARCH = '' 34 | 35 | // ❝❞❛❜ 36 | // ⌜⌝⌞⌟ 37 | // ▶➔➙➛➜➞➟➠➡➢➣➥➦➧➨➩➪➫➬➭➮➯➱➵ 38 | 39 | MALE = '♂' 40 | FEMALE = '♀' 41 | 42 | SUN = '☀' 43 | STAR = '★' 44 | SNOW = '❈' 45 | CLOUD = '☁' 46 | 47 | ENTER = '⌥' 48 | 49 | Star rune = '*' 50 | Plus rune = '+' 51 | Well rune = '#' 52 | Equal rune = '=' 53 | Equal1 rune = '═' 54 | Space rune = ' ' 55 | // Hyphen Minus 56 | Hyphen rune = '-' // eg: ------- 57 | CNHyphen rune = '—' // eg: ————— 58 | Hyphen2 rune = '─' // eg: ──── 59 | 60 | Underline rune = '_' 61 | LeftArrow rune = '<' 62 | RightArrow rune = '>' 63 | 64 | VLine rune = '|' 65 | VLineFull rune = '│' 66 | 67 | // TChar eg TChar + Hyphen2: ──┬── 68 | TChar rune = '┬' 69 | // CCChar criss-cross 70 | CCChar rune = '┼' 71 | ) 72 | -------------------------------------------------------------------------------- /show/table/style.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | // Style for table 4 | /* 5 | ━━━┯━━━━━━━┯━━━━━━━━━━━━━━━━━┯━━━━━━━━━━┯━━━━━━━━━━ 6 | # │ pid │ name │ status │ cpu 7 | ───┼───────┼─────────────────┼──────────┼────────── 8 | 0 │ 992 │ chrome │ Sleeping │ 6.988768 9 | 2 │ 13973 │ qemu-system-x86 │ Sleeping │ 4.996551 10 | ━━━┷━━━━━━━┷━━━━━━━━━━━━━━━━━┷━━━━━━━━━━┷━━━━━━━━━━ 11 | 12 | +-----+------------+-----------+--------+-----------------------------+ 13 | | # | FIRST NAME | LAST NAME | SALARY | | 14 | +-----+------------+-----------+--------+-----------------------------+ 15 | | 1 | Arya | Stark | 3000 | | 16 | | 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | 17 | | 300 | Tyrion | Lannister | 5000 | | 18 | +-----+------------+-----------+--------+-----------------------------+ 19 | | | | TOTAL | 10000 | | 20 | +-----+------------+-----------+--------+-----------------------------+ 21 | */ 22 | type Style struct { 23 | Border BorderStyle 24 | Divider DividerStyle 25 | 26 | HeadColor string 27 | RowColor string 28 | } 29 | 30 | // BorderStyle for table 31 | type BorderStyle struct { 32 | TopLeft, Top, TopIntersect, TopRight rune 33 | 34 | Right, Center, Cell, Left rune 35 | 36 | BottomRight, Bottom, BottomIntersect, BottomLeft rune 37 | } 38 | 39 | // DividerStyle defines table divider style 40 | type DividerStyle struct { 41 | Left rune 42 | Right rune 43 | Intersect rune 44 | } 45 | 46 | var ( 47 | // StyleDefault - MySql-like table style: 48 | StyleDefault = Style{ 49 | HeadColor: "", 50 | RowColor: "", 51 | Border: BorderStyle{ 52 | // Top 53 | TopLeft: '+', 54 | Top: '-', 55 | TopIntersect: '+', 56 | TopRight: '+', 57 | // Body 58 | Right: '|', 59 | Cell: '|', 60 | // Bottom 61 | BottomRight: '+', 62 | Bottom: '-', 63 | BottomLeft: '+', 64 | BottomIntersect: '+', 65 | }, 66 | Divider: DividerStyle{}, 67 | } 68 | 69 | StyleSimple = Style{} 70 | StyleMarkdown = Style{} 71 | ) 72 | -------------------------------------------------------------------------------- /show/table/table.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/gookit/gcli/v3/show" 9 | "github.com/gookit/goutil/comdef" 10 | "github.com/gookit/goutil/strutil" 11 | ) 12 | 13 | // Options struct 14 | type Options struct { 15 | TitleStyle string 16 | HeaderStyle string 17 | 18 | BottomBorder []rune 19 | 20 | Alignment strutil.PosFlag 21 | ColMaxWidth int 22 | LineNumber bool 23 | WrapContent bool 24 | 25 | // HasBorder show border line 26 | HasBorder bool 27 | // RowBorder show row border 28 | RowBorder bool 29 | // HeadBorder show head border 30 | HeadBorder bool 31 | // WrapBorder wrap border for table 32 | WrapBorder bool 33 | } 34 | 35 | // OpFunc define 36 | type OpFunc func(opts *Options) 37 | 38 | // Table a cli Table show 39 | type Table struct { 40 | show.Base // use for internal 41 | out comdef.ByteStringWriter 42 | 43 | // Title for the table 44 | Title string 45 | // Heads the table head data 46 | Heads []string 47 | // Rows table data rows 48 | Rows []*Row 49 | // options ... 50 | opts *Options 51 | 52 | // column value align type. 53 | // key is col index. start from 0. 54 | colAlign map[int]strutil.PosFlag 55 | } 56 | 57 | // New create table 58 | func New(title string, fns ...OpFunc) *Table { 59 | t := &Table{ 60 | Title: title, 61 | opts: &Options{ 62 | BottomBorder: []rune{'━', '┷'}, 63 | }, 64 | } 65 | 66 | return t.WithOptions(fns...) 67 | } 68 | 69 | // WithOptions for table 70 | func (t *Table) WithOptions(fns ...OpFunc) *Table { 71 | for _, fn := range fns { 72 | fn(t.opts) 73 | } 74 | return t 75 | } 76 | 77 | // AddHead column names to table 78 | func (t *Table) AddHead(names ...string) *Table { 79 | t.Heads = names 80 | return t 81 | } 82 | 83 | // AddRow data to table 84 | func (t *Table) AddRow(cols ...any) { 85 | tr := &Row{ 86 | Cells: make([]*Cell, 0, len(cols)), 87 | Separator: '|', 88 | } 89 | 90 | for _, colVal := range cols { 91 | cell := &Cell{ 92 | Width: 0, 93 | Wrap: false, 94 | Align: 0, 95 | Val: colVal, 96 | } 97 | 98 | tr.Cells = append(tr.Cells, cell) 99 | } 100 | 101 | t.Rows = append(t.Rows, tr) 102 | } 103 | 104 | // SetRows to table 105 | func (t *Table) SetRows(rs any) *Table { 106 | 107 | return t 108 | } 109 | 110 | // String format as string 111 | func (t *Table) String() string { 112 | t.Format() 113 | return t.Buffer().String() 114 | } 115 | 116 | // Print formatted message 117 | func (t *Table) Print() { 118 | t.Format() 119 | t.Base.Print() 120 | } 121 | 122 | // Println formatted message with newline 123 | func (t *Table) Println() { 124 | t.Format() 125 | t.Base.Println() 126 | } 127 | 128 | // Render formatted message with newline 129 | func (t *Table) Render() { 130 | t.Format() 131 | t.Base.Println() 132 | } 133 | 134 | // Format as string 135 | func (t *Table) Format() { 136 | t.prepare() 137 | 138 | t.formatHeader() 139 | 140 | t.formatBody() 141 | 142 | t.formatFooter() 143 | 144 | panic("implement me") 145 | } 146 | 147 | func (t *Table) prepare() { 148 | 149 | // determine the width for each column (cell in a row) 150 | var colWidths []int 151 | for _, row := range t.Rows { 152 | for i, cell := range row.Cells { 153 | // resize colwidth array 154 | if i+1 > len(colWidths) { 155 | colWidths = append(colWidths, 0) 156 | } 157 | 158 | cellWidth := cell.MaxWidth() 159 | if t.opts.ColMaxWidth != 0 && cellWidth > t.opts.ColMaxWidth { 160 | cellWidth = t.opts.ColMaxWidth 161 | } 162 | 163 | if cellWidth > colWidths[i] { 164 | colWidths[i] = cellWidth 165 | } 166 | } 167 | } 168 | } 169 | 170 | // Format as string 171 | func (t *Table) formatHeader() { 172 | panic("implement me") 173 | } 174 | 175 | // Format as string 176 | func (t *Table) formatBody() { 177 | for _, row := range t.Rows { 178 | fmt.Println(row) 179 | } 180 | 181 | panic("implement me") 182 | } 183 | 184 | // Format as string 185 | func (t *Table) formatFooter() { 186 | panic("implement me") 187 | } 188 | 189 | // WriteTo format table to string and write to w. 190 | func (t *Table) WriteTo(w io.Writer) (int64, error) { 191 | t.Format() 192 | return t.Buffer().WriteTo(w) 193 | } 194 | 195 | // Row represents a row in a table 196 | type Row struct { 197 | // Cells is the group of cell for the row 198 | Cells []*Cell 199 | 200 | // Separator for table columns 201 | Separator rune 202 | } 203 | 204 | // Cell represents a column in a row 205 | type Cell struct { 206 | // Width is the width of the cell 207 | Width int 208 | // Wrap when true wraps the contents of the cell when the length exceeds the width 209 | Wrap bool 210 | // Align when true aligns contents to the right 211 | Align strutil.PosFlag 212 | 213 | // Val is the cell data 214 | Val any 215 | str string // string cache of Val 216 | } 217 | 218 | // MaxWidth returns the max width of all the lines in a cell 219 | func (c *Cell) MaxWidth() int { 220 | width := 0 221 | for _, s := range strings.Split(c.String(), "\n") { 222 | w := strutil.Utf8Width(s) 223 | if w > width { 224 | width = w 225 | } 226 | } 227 | 228 | return width 229 | } 230 | 231 | // String returns the string formatted representation of the cell 232 | func (c *Cell) String() string { 233 | if c.Val == nil { 234 | return strutil.PadLeft(" ", " ", c.Width) 235 | // return c.str 236 | } 237 | 238 | s := strutil.QuietString(c.Val) 239 | if c.Width == 0 { 240 | return s 241 | } 242 | 243 | if c.Wrap && len(s) > c.Width { 244 | return strutil.WordWrap(s, c.Width) 245 | } 246 | return strutil.Resize(s, c.Width, c.Align) 247 | } 248 | -------------------------------------------------------------------------------- /show/table/table_test.go: -------------------------------------------------------------------------------- 1 | package table_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/gcli/v3/show/table" 7 | ) 8 | 9 | func TestNewTable(t *testing.T) { 10 | tb := table.New("Table example1") 11 | tb.SetRows([]any{ 12 | // TODO ... 13 | }) 14 | 15 | // tb.Println() 16 | } 17 | -------------------------------------------------------------------------------- /show/title.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import "github.com/gookit/gcli/v3/show/symbols" 4 | 5 | // Title definition 6 | type Title struct { 7 | Title string 8 | Style string 9 | Formatter func(t *Title) string 10 | // Formatter IFormatter 11 | Char rune 12 | Width int 13 | Indent int 14 | Align PosFlag 15 | ShowBorder bool 16 | } 17 | 18 | // NewTitle instance 19 | func NewTitle(title string) *Title { 20 | return &Title{ 21 | Title: title, 22 | Width: 80, 23 | Char: symbols.Equal, 24 | Indent: 2, 25 | Align: PosLeft, 26 | Style: "comment", 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /show/writer.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import "io" 4 | 5 | // Writer definition 6 | type Writer struct { 7 | // buf bytes.Buffer 8 | out io.Writer 9 | } 10 | 11 | // NewWriter create a new writer 12 | func NewWriter(output io.Writer) *Writer { 13 | if output == nil { 14 | output = Output 15 | } 16 | 17 | return &Writer{ 18 | out: output, 19 | } 20 | } 21 | 22 | // Write bytes message 23 | func (w *Writer) Write(p []byte) (n int, err error) { 24 | return w.out.Write(p) 25 | } 26 | 27 | // Print data to io.Writer 28 | func (w *Writer) Print() { 29 | 30 | } 31 | 32 | // Flush data to io.Writer 33 | func (w *Writer) Flush() error { 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package gcli 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/gookit/color" 9 | "github.com/gookit/gcli/v3/helper" 10 | "github.com/gookit/goutil/goinfo" 11 | ) 12 | 13 | /************************************************************* 14 | * console log 15 | *************************************************************/ 16 | 17 | var level2color = map[VerbLevel]color.Color{ 18 | VerbError: color.FgRed, 19 | VerbWarn: color.FgYellow, 20 | VerbInfo: color.FgGreen, 21 | VerbDebug: color.FgCyan, 22 | VerbCrazy: color.FgMagenta, 23 | } 24 | 25 | // Debugf print log message 26 | func Debugf(format string, v ...any) { 27 | logf(VerbDebug, format, v...) 28 | } 29 | 30 | // Logf print log message 31 | func Logf(level VerbLevel, format string, v ...any) { 32 | logf(level, format, v...) 33 | } 34 | 35 | // print log message 36 | func logf(level VerbLevel, format string, v ...any) { 37 | if gOpts.Verbose < level { 38 | return 39 | } 40 | 41 | name := level2color[level].Render(level.Upper()) 42 | logAt := goinfo.GetCallerInfo(3) 43 | 44 | color.Printf("GCli: [%s] [%s] %s \n", name, logAt, fmt.Sprintf(format, v...)) 45 | } 46 | 47 | func defaultErrHandler(ctx *HookCtx) (stop bool) { 48 | if errV := ctx.Get("err"); errV != nil { 49 | if err, ok := errV.(error); ok { 50 | color.Error.Tips(err.Error()) 51 | // fmt.Println(color.Red.Render("ERROR:"), err.Error()) 52 | } 53 | } 54 | 55 | return 56 | } 57 | 58 | func name2verbLevel(name string) VerbLevel { 59 | switch strings.ToLower(name) { 60 | case "quiet": 61 | return VerbQuiet 62 | case "error": 63 | return VerbError 64 | case "warn": 65 | return VerbWarn 66 | case "info": 67 | return VerbInfo 68 | case "debug": 69 | return VerbDebug 70 | case "crazy": 71 | return VerbCrazy 72 | } 73 | 74 | // default level 75 | return defaultVerb 76 | } 77 | 78 | /************************************************************* 79 | * some helper methods 80 | *************************************************************/ 81 | 82 | // Print messages 83 | func Print(args ...any) { 84 | color.Print(args...) 85 | } 86 | 87 | // Println messages 88 | func Println(args ...any) { 89 | color.Println(args...) 90 | } 91 | 92 | // Printf messages 93 | func Printf(format string, args ...any) { 94 | color.Printf(format, args...) 95 | } 96 | 97 | func panicf(format string, v ...any) { 98 | panic(fmt.Sprintf("GCli: "+format, v...)) 99 | } 100 | 101 | func aliasNameCheck(name string) { 102 | if helper.IsGoodCmdName(name) { 103 | return 104 | } 105 | panicf("alias name '%s' is invalid, must match: %s", name, helper.RegGoodCmdName) 106 | } 107 | 108 | // strictFormatArgs 109 | // TODO mode: 110 | // 111 | // POSIX '-ab' will split to '-a -b', '--o' -> '-o' 112 | // UNIX '-ab' will split to '-a b' 113 | func strictFormatArgs(args []string) (fmtArgs []string) { 114 | if len(args) == 0 { 115 | return args 116 | } 117 | 118 | for _, arg := range args { 119 | // if contains '=' append self 120 | // TODO mode: 121 | // '--test=x', '-t=x' , '-test=x', '-test' 122 | if strings.ContainsRune(arg, '=') { 123 | fmtArgs = append(fmtArgs, arg) 124 | continue 125 | } 126 | 127 | // eg: --a ---name 128 | if strings.HasPrefix(arg, "--") { 129 | farg := strings.TrimLeft(arg, "-") 130 | if rl := len(farg); rl == 1 { // fix: "--a" -> "-a" 131 | arg = "-" + farg 132 | } else if rl > 1 { // fix: "---name" -> "--name" 133 | arg = "--" + farg 134 | } 135 | 136 | // TODO No change remain OR remove like "--" "---" 137 | // maybe ... 138 | 139 | } else if strings.HasPrefix(arg, "-") { 140 | ln := len(arg) 141 | // fix: "-abc" -> "-a -b -c" 142 | if ln > 2 { 143 | for _, s := range arg[1:] { 144 | fmtArgs = append(fmtArgs, "-"+string(s)) 145 | } 146 | continue 147 | } 148 | } 149 | 150 | fmtArgs = append(fmtArgs, arg) 151 | } 152 | 153 | return fmtArgs 154 | } 155 | 156 | // flags parser is flag#FlagSet.Parse(), so: 157 | // - if args like: "arg0 arg1 --opt", will parse fail 158 | // - if args convert to: "--opt arg0 arg1", can correctly parse 159 | func moveArgumentsToEnd(args []string) []string { 160 | if len(args) < 2 { 161 | return args 162 | } 163 | 164 | var argEnd int 165 | for i, arg := range args { 166 | // strop on the first option 167 | if strings.IndexByte(arg, '-') == 0 { 168 | argEnd = i 169 | break 170 | } 171 | } 172 | 173 | // the first is an option 174 | if argEnd == -1 { 175 | return args 176 | } 177 | 178 | return append(args[argEnd:], args[0:argEnd]...) 179 | } 180 | 181 | func splitPath2names(path string) []string { 182 | var names []string 183 | path = strings.TrimSpace(path) 184 | if path != "" { 185 | if strings.ContainsRune(path, ':') { // command ID 186 | names = strings.Split(path, CommandSep) 187 | } else if strings.ContainsRune(path, ' ') { // command path 188 | names = strings.Split(path, " ") 189 | } else { 190 | names = []string{path} 191 | } 192 | } 193 | 194 | return names 195 | } 196 | 197 | // regex: "`[\w ]+`" 198 | // regex: "`.+`" 199 | var codeReg = regexp.MustCompile("`" + `.+` + "`") 200 | 201 | // convert "`keywords`" to "keywords" 202 | func wrapColor2string(s string) string { 203 | if strings.ContainsRune(s, '`') { 204 | s = codeReg.ReplaceAllStringFunc(s, func(code string) string { 205 | code = strings.Trim(code, "`") 206 | return color.WrapTag(code, "mga") 207 | }) 208 | } 209 | return s 210 | } 211 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package gcli_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gookit/gcli/v3" 7 | "github.com/gookit/goutil/testutil/assert" 8 | ) 9 | 10 | func Test_strictFormatArgs(t *testing.T) { 11 | str1 := "" 12 | t1 := false 13 | t2 := false 14 | t3 := false 15 | // t4 := false 16 | is := assert.New(t) 17 | cmd := gcli.NewCommand("init", "test bool pare", func(c *gcli.Command) { 18 | c.StrOpt(&str1, "name", "n", "", "test string parse") 19 | c.BoolOpt(&t1, "test1", "t", false, "test bool arse") 20 | c.BoolOpt(&t2, "test2", "s", false, "test bool arse") 21 | c.BoolOpt(&t3, "test3", "c", true, "test bool arse") 22 | // c.BoolOpt(&t4, "test4", "d", false, "test bool arse") 23 | }) 24 | 25 | err := cmd.Run([]string{"-n", "ccc", "-test1=true", "-s", "--test3=false"}) 26 | is.NoErr(err) 27 | is.Eq("ccc", str1) 28 | is.Eq(true, t1) 29 | is.Eq(true, t2) 30 | is.Eq(false, t3) 31 | } 32 | --------------------------------------------------------------------------------