diff --git a/.github/.codecov.yml b/.github/.codecov.yml deleted file mode 100644 index 69cb760..0000000 --- a/.github/.codecov.yml +++ /dev/null @@ -1 +0,0 @@ -comment: false diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..256cd10 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,9 @@ +comment: false +coverage: + status: + project: + default: + threshold: 5% + patch: + default: + threshold: 5% diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 6577764..800bbeb 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -1,5 +1,4 @@ name: Run Tests - on: push: branches: @@ -12,87 +11,68 @@ on: branches: - main - v3-dev-main - +permissions: + contents: read jobs: test: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: [1.18.x] + go: [1.18.x, 1.19.x] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: - - name: Set up Go ${{ matrix.go }} - uses: actions/setup-go@v3 + - uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - - name: Set PATH run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}" - - - name: Checkout Code - uses: actions/checkout@v3 - - - name: GOFMT Check - if: matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest' - run: test -z $(gofmt -l .) - - - name: vet - run: go run internal/build/build.go vet - - - name: test with urfave_cli_no_docs tag - run: go run internal/build/build.go -tags urfave_cli_no_docs test - - - name: test - run: go run internal/build/build.go test - - - name: check-binary-size - run: go run internal/build/build.go check-binary-size - - - name: check-binary-size with tags (informational only) - run: go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size - - - name: Upload coverage to Codecov - if: success() && matrix.go == '1.18.x' && matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@v2 + - uses: actions/checkout@v3 + - if: matrix.go == '1.19.x' && matrix.os == 'ubuntu-latest' + run: make ensure-goimports + - if: matrix.go == '1.19.x' && matrix.os == 'ubuntu-latest' + run: make lint + - run: make vet + - run: make test + env: + GFLAGS: -tags urfave_cli_no_docs + - run: make test + - run: make -C cmd/urfave-cli-genflags + - run: make check-binary-size + env: + GFLAGS: -tags urfave_cli_no_docs + - run: make check-binary-size + - run: make yamlfmt + - run: make diffcheck + - if: matrix.go == '1.19.x' && matrix.os == 'ubuntu-latest' + run: make v2diff + - if: success() && matrix.go == '1.19.x' && matrix.os == 'ubuntu-latest' + uses: codecov/codecov-action@v3 with: + token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true test-docs: name: test-docs runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v3 + - uses: actions/setup-go@v3 with: - go-version: 1.18.x - - - name: Use Node.js 16 - uses: actions/setup-node@v3 + go-version: 1.19.x + - uses: actions/setup-node@v3 with: node-version: '16' - - name: Set PATH run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}" - - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Install Dependencies - run: | - mkdir -p "${GITHUB_WORKSPACE}/.local/bin" - curl -fsSL -o "${GITHUB_WORKSPACE}/.local/bin/gfmrun" "https://github.com/urfave/gfmrun/releases/download/v1.3.0/gfmrun-$(go env GOOS)-$(go env GOARCH)-v1.3.0" - chmod +x "${GITHUB_WORKSPACE}/.local/bin/gfmrun" - - - name: gfmrun - run: go run internal/build/build.go gfmrun docs/v3/index.md - - - name: diff check - run: | - git diff --exit-code - git diff --cached --exit-code - + - uses: actions/checkout@v3 + - run: make ensure-gfmrun + - run: make gfmrun + env: + FLAGS: --walk docs/v3/ + - run: make diffcheck publish: + permissions: + contents: write # TODO: switch once v3 is released {{ # if: startswith(github.ref, 'refs/tags/') if: 'false' @@ -101,18 +81,13 @@ jobs: needs: [test-docs] runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@v3 + - uses: actions/checkout@v3 with: fetch-depth: 0 - - - name: Setup mkdocs - run: | - pip install -U pip - pip install -r mkdocs-requirements.txt - git remote rm origin - git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/urfave/cli.git - - - name: Publish Docs - run: | - mkdocs gh-deploy --force + - run: make ensure-mkdocs + env: + FLAGS: --upgrade-pip + - run: make set-mkdocs-remote + env: + MKDOCS_REMOTE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: make deploy-mkdocs diff --git a/.gitignore b/.gitignore index c04fcc5..3c6768f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,12 @@ *.coverprofile +*.exe *.orig -vendor +.*envrc +.envrc .idea -internal/*/built-example -coverage.txt /.local/ +/cmd/urfave-cli-genflags/urfave-cli-genflags /site/ - -*.exe +coverage.txt +internal/*/built-example +vendor diff --git a/Makefile b/Makefile index 3b0e5e0..f0d4190 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,10 @@ # are very important so that maintainers and contributors can focus their # attention on files that are primarily Go. +GO_RUN_BUILD := go run internal/build/build.go + .PHONY: all -all: generate vet tag-test test check-binary-size tag-check-binary-size gfmrun v2diff +all: generate vet test check-binary-size gfmrun yamlfmt v2diff # NOTE: this is a special catch-all rule to run any of the commands # defined in internal/build/build.go with optional arguments passed @@ -13,28 +15,12 @@ all: generate vet tag-test test check-binary-size tag-check-binary-size gfmrun v # # $ make test GFLAGS='--packages cli' %: - go run internal/build/build.go $(GFLAGS) $* $(FLAGS) - -.PHONY: tag-test -tag-test: - go run internal/build/build.go -tags urfave_cli_no_docs test - -.PHONY: tag-check-binary-size -tag-check-binary-size: - go run internal/build/build.go -tags urfave_cli_no_docs check-binary-size - -.PHONY: gfmrun -gfmrun: - go run internal/build/build.go gfmrun docs/v2/manual.md + $(GO_RUN_BUILD) $(GFLAGS) $* $(FLAGS) .PHONY: docs docs: mkdocs build -.PHONY: docs-deps -docs-deps: - pip install -r mkdocs-requirements.txt - .PHONY: serve-docs serve-docs: mkdocs serve diff --git a/altsrc/flag.go b/altsrc/flag.go index cf85072..a12262b 100644 --- a/altsrc/flag.go +++ b/altsrc/flag.go @@ -64,15 +64,22 @@ func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(cCtx *c // ApplyInputSourceValue applies a generic value to the flagSet if required func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.GenericFlag.Name) { - value, err := isc.Generic(f.GenericFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.GenericFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.Generic(name) if err != nil { return err } - if value != nil { - for _, name := range f.Names() { - _ = f.set.Set(name, value.String()) - } + if value == nil { + continue + } + for _, n := range f.Names() { + _ = f.set.Set(n, value.String()) } } @@ -81,19 +88,30 @@ func (f *GenericFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceCo // ApplyInputSourceValue applies a StringSlice value to the flagSet if required func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.StringSliceFlag.Name) { - value, err := isc.StringSlice(f.StringSliceFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.StringSliceFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.StringSlice(name) if err != nil { return err } - if value != nil { - var sliceValue cli.StringSlice = *(cli.NewStringSlice(value...)) - for _, name := range f.Names() { - underlyingFlag := f.set.Lookup(name) - if underlyingFlag != nil { - underlyingFlag.Value = &sliceValue - } + if value == nil { + continue + } + var sliceValue = *(cli.NewStringSlice(value...)) + for _, n := range f.Names() { + underlyingFlag := f.set.Lookup(n) + if underlyingFlag == nil { + continue } + underlyingFlag.Value = &sliceValue + } + if f.Destination != nil { + f.Destination.Set(sliceValue.Serialize()) } } return nil @@ -101,19 +119,30 @@ func (f *StringSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSour // ApplyInputSourceValue applies a IntSlice value if required func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.IntSliceFlag.Name) { - value, err := isc.IntSlice(f.IntSliceFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.IntSliceFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.IntSlice(name) if err != nil { return err } - if value != nil { - var sliceValue cli.IntSlice = *(cli.NewIntSlice(value...)) - for _, name := range f.Names() { - underlyingFlag := f.set.Lookup(name) - if underlyingFlag != nil { - underlyingFlag.Value = &sliceValue - } + if value == nil { + continue + } + var sliceValue = *(cli.NewIntSlice(value...)) + for _, n := range f.Names() { + underlyingFlag := f.set.Lookup(n) + if underlyingFlag == nil { + continue } + underlyingFlag.Value = &sliceValue + } + if f.Destination != nil { + f.Destination.Set(sliceValue.Serialize()) } } return nil @@ -121,15 +150,19 @@ func (f *IntSliceFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceC // ApplyInputSourceValue applies a Bool value to the flagSet if required func (f *BoolFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !cCtx.IsSet(f.Name) && !isEnvVarSet(f.EnvVars) && isc.isSet(f.BoolFlag.Name) { - value, err := isc.Bool(f.BoolFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.BoolFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.Bool(name) if err != nil { return err } - if value { - for _, name := range f.Names() { - _ = f.set.Set(name, strconv.FormatBool(value)) - } + for _, n := range f.Names() { + _ = f.set.Set(n, strconv.FormatBool(value)) } } return nil @@ -137,15 +170,19 @@ func (f *BoolFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceConte // ApplyInputSourceValue applies a String value to the flagSet if required func (f *StringFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.StringFlag.Name) { - value, err := isc.String(f.StringFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.StringFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.String(name) if err != nil { return err } - if value != "" { - for _, name := range f.Names() { - _ = f.set.Set(name, value) - } + for _, n := range f.Names() { + _ = f.set.Set(n, value) } } return nil @@ -153,25 +190,29 @@ func (f *StringFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceCon // ApplyInputSourceValue applies a Path value to the flagSet if required func (f *PathFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.PathFlag.Name) { - value, err := isc.String(f.PathFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.PathFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.String(name) if err != nil { return err } - if value != "" { - for _, name := range f.Names() { - - if !filepath.IsAbs(value) && isc.Source() != "" { - basePathAbs, err := filepath.Abs(isc.Source()) - if err != nil { - return err - } - - value = filepath.Join(filepath.Dir(basePathAbs), value) + if value == "" { + continue + } + for _, n := range f.Names() { + if !filepath.IsAbs(value) && isc.Source() != "" { + basePathAbs, err := filepath.Abs(isc.Source()) + if err != nil { + return err } - - _ = f.set.Set(name, value) + value = filepath.Join(filepath.Dir(basePathAbs), value) } + _ = f.set.Set(n, value) } } return nil @@ -179,13 +220,19 @@ func (f *PathFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceConte // ApplyInputSourceValue applies a int value to the flagSet if required func (f *IntFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.IntFlag.Name) { - value, err := isc.Int(f.IntFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.IntFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.Int(name) if err != nil { return err } - for _, name := range f.Names() { - _ = f.set.Set(name, strconv.FormatInt(int64(value), 10)) + for _, n := range f.Names() { + _ = f.set.Set(n, strconv.FormatInt(int64(value), 10)) } } return nil @@ -193,13 +240,19 @@ func (f *IntFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContex // ApplyInputSourceValue applies a Duration value to the flagSet if required func (f *DurationFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.DurationFlag.Name) { - value, err := isc.Duration(f.DurationFlag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.DurationFlag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.Duration(name) if err != nil { return err } - for _, name := range f.Names() { - _ = f.set.Set(name, value.String()) + for _, n := range f.Names() { + _ = f.set.Set(n, value.String()) } } return nil @@ -207,14 +260,20 @@ func (f *DurationFlag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceC // ApplyInputSourceValue applies a Float64 value to the flagSet if required func (f *Float64Flag) ApplyInputSourceValue(cCtx *cli.Context, isc InputSourceContext) error { - if f.set != nil && !(cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars)) && isc.isSet(f.Float64Flag.Name) { - value, err := isc.Float64(f.Float64Flag.Name) + if f.set == nil || cCtx.IsSet(f.Name) || isEnvVarSet(f.EnvVars) { + return nil + } + for _, name := range f.Float64Flag.Names() { + if !isc.isSet(name) { + continue + } + value, err := isc.Float64(name) if err != nil { return err } floatStr := float64ToString(value) - for _, name := range f.Names() { - _ = f.set.Set(name, floatStr) + for _, n := range f.Names() { + _ = f.set.Set(n, floatStr) } } return nil diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go index 3c496e5..1a5da15 100644 --- a/altsrc/flag_test.go +++ b/altsrc/flag_test.go @@ -37,6 +37,20 @@ func (ris *racyInputSource) isSet(name string) bool { return true } +func TestGenericApplyInputSourceValue_Alias(t *testing.T) { + v := &Parser{"abc", "def"} + tis := testApplyInputSource{ + Flag: NewGenericFlag(&cli.GenericFlag{Name: "test", Aliases: []string{"test_alias"}, Value: &Parser{}}), + FlagName: "test_alias", + MapValue: v, + } + c := runTest(t, tis) + expect(t, v, c.Generic("test_alias")) + + c = runRacyTest(t, tis) + refute(t, v, c.Generic("test_alias")) +} + func TestGenericApplyInputSourceValue(t *testing.T) { v := &Parser{"abc", "def"} tis := testApplyInputSource{ @@ -85,27 +99,62 @@ func TestGenericApplyInputSourceMethodEnvVarSet(t *testing.T) { refute(t, &Parser{"abc", "def"}, c.Generic("test")) } +func TestStringSliceApplyInputSourceValue_Alias(t *testing.T) { + dest := cli.NewStringSlice() + tis := testApplyInputSource{ + Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), + FlagName: "test_alias", + MapValue: []interface{}{"hello", "world"}, + } + c := runTest(t, tis) + expect(t, c.StringSlice("test_alias"), []string{"hello", "world"}) + expect(t, dest.Value(), []string{"hello", "world"}) + + // reset dest + dest = cli.NewStringSlice() + tis = testApplyInputSource{ + Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), + FlagName: "test_alias", + MapValue: []interface{}{"hello", "world"}, + } + c = runRacyTest(t, tis) + refute(t, c.StringSlice("test_alias"), []string{"hello", "world"}) + refute(t, dest.Value(), []string{"hello", "world"}) +} + func TestStringSliceApplyInputSourceValue(t *testing.T) { + dest := cli.NewStringSlice() tis := testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}), + Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Destination: dest}), FlagName: "test", MapValue: []interface{}{"hello", "world"}, } c := runTest(t, tis) expect(t, c.StringSlice("test"), []string{"hello", "world"}) + expect(t, dest.Value(), []string{"hello", "world"}) + // reset dest + dest = cli.NewStringSlice() + tis = testApplyInputSource{ + Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Destination: dest}), + FlagName: "test", + MapValue: []interface{}{"hello", "world"}, + } c = runRacyTest(t, tis) refute(t, c.StringSlice("test"), []string{"hello", "world"}) + refute(t, dest.Value(), []string{"hello", "world"}) } func TestStringSliceApplyInputSourceMethodContextSet(t *testing.T) { + dest := cli.NewStringSlice() c := runTest(t, testApplyInputSource{ - Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test"}), + Flag: NewStringSliceFlag(&cli.StringSliceFlag{Name: "test", Destination: dest}), FlagName: "test", MapValue: []interface{}{"hello", "world"}, ContextValueString: "ohno", }) expect(t, c.StringSlice("test"), []string{"ohno"}) + expect(t, dest.Value(), []string{"ohno"}) } func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { @@ -123,31 +172,74 @@ func TestStringSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { refute(t, c.StringSlice("test"), []string{"oh", "no"}) } +func TestIntSliceApplyInputSourceValue_Alias(t *testing.T) { + dest := cli.NewIntSlice() + tis := testApplyInputSource{ + Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), + FlagName: "test_alias", + MapValue: []interface{}{1, 2}, + } + c := runTest(t, tis) + expect(t, c.IntSlice("test_alias"), []int{1, 2}) + expect(t, dest.Value(), []int{1, 2}) + + dest = cli.NewIntSlice() + tis = testApplyInputSource{ + Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Aliases: []string{"test_alias"}, Destination: dest}), + FlagName: "test_alias", + MapValue: []interface{}{1, 2}, + } + c = runRacyTest(t, tis) + refute(t, c.IntSlice("test_alias"), []int{1, 2}) + refute(t, dest.Value(), []int{1, 2}) +} + func TestIntSliceApplyInputSourceValue(t *testing.T) { + dest := cli.NewIntSlice() tis := testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}), + Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), FlagName: "test", MapValue: []interface{}{1, 2}, } c := runTest(t, tis) expect(t, c.IntSlice("test"), []int{1, 2}) + expect(t, dest.Value(), []int{1, 2}) + // reset dest + dest = cli.NewIntSlice() + tis = testApplyInputSource{ + Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), + FlagName: "test", + MapValue: []interface{}{1, 2}, + } c = runRacyTest(t, tis) refute(t, c.IntSlice("test"), []int{1, 2}) + refute(t, dest.Value(), []int{1, 2}) } func TestIntSliceApplyInputSourceMethodContextSet(t *testing.T) { + dest := cli.NewIntSlice() tis := testApplyInputSource{ - Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test"}), + Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), FlagName: "test", MapValue: []interface{}{1, 2}, ContextValueString: "3", } c := runTest(t, tis) expect(t, c.IntSlice("test"), []int{3}) + expect(t, dest.Value(), []int{3}) + // reset dest + dest = cli.NewIntSlice() + tis = testApplyInputSource{ + Flag: NewIntSliceFlag(&cli.IntSliceFlag{Name: "test", Destination: dest}), + FlagName: "test", + MapValue: []interface{}{1, 2}, + ContextValueString: "3", + } c = runRacyTest(t, tis) refute(t, c.IntSlice("test"), []int{3}) + refute(t, dest.Value(), []int{3}) } func TestIntSliceApplyInputSourceMethodEnvVarSet(t *testing.T) { @@ -178,6 +270,19 @@ func TestBoolApplyInputSourceMethodSet(t *testing.T) { refute(t, true, c.Bool("test")) } +func TestBoolApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewBoolFlag(&cli.BoolFlag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: true, + } + c := runTest(t, tis) + expect(t, true, c.Bool("test_alias")) + + c = runRacyTest(t, tis) + refute(t, true, c.Bool("test_alias")) +} + func TestBoolApplyInputSourceMethodContextSet(t *testing.T) { tis := testApplyInputSource{ Flag: NewBoolFlag(&cli.BoolFlag{Name: "test"}), @@ -207,6 +312,19 @@ func TestBoolApplyInputSourceMethodEnvVarSet(t *testing.T) { refute(t, true, c.Bool("test")) } +func TestStringApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewStringFlag(&cli.StringFlag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: "hello", + } + c := runTest(t, tis) + expect(t, "hello", c.String("test_alias")) + + c = runRacyTest(t, tis) + refute(t, "hello", c.String("test_alias")) +} + func TestStringApplyInputSourceMethodSet(t *testing.T) { tis := testApplyInputSource{ Flag: NewStringFlag(&cli.StringFlag{Name: "test"}), @@ -249,6 +367,31 @@ func TestStringApplyInputSourceMethodEnvVarSet(t *testing.T) { refute(t, "goodbye", c.String("test")) } +func TestPathApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewPathFlag(&cli.PathFlag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: "hello", + SourcePath: "/path/to/source/file", + } + c := runTest(t, tis) + + expected := "/path/to/source/hello" + if runtime.GOOS == "windows" { + var err error + // Prepend the corresponding drive letter (or UNC path?), and change + // to windows-style path: + expected, err = filepath.Abs(expected) + if err != nil { + t.Fatal(err) + } + } + expect(t, expected, c.String("test_alias")) + + c = runRacyTest(t, tis) + refute(t, expected, c.String("test_alias")) +} + func TestPathApplyInputSourceMethodSet(t *testing.T) { tis := testApplyInputSource{ Flag: NewPathFlag(&cli.PathFlag{Name: "test"}), @@ -305,6 +448,19 @@ func TestPathApplyInputSourceMethodEnvVarSet(t *testing.T) { refute(t, "goodbye", c.String("test")) } +func TestIntApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewIntFlag(&cli.IntFlag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: 15, + } + c := runTest(t, tis) + expect(t, 15, c.Int("test_alias")) + + c = runRacyTest(t, tis) + refute(t, 15, c.Int("test_alias")) +} + func TestIntApplyInputSourceMethodSet(t *testing.T) { tis := testApplyInputSource{ Flag: NewIntFlag(&cli.IntFlag{Name: "test"}), @@ -360,6 +516,19 @@ func TestIntApplyInputSourceMethodEnvVarSet(t *testing.T) { refute(t, 12, c.Int("test")) } +func TestDurationApplyInputSourceMethodSet_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: 30 * time.Second, + } + c := runTest(t, tis) + expect(t, 30*time.Second, c.Duration("test_alias")) + + c = runRacyTest(t, tis) + refute(t, 30*time.Second, c.Duration("test_alias")) +} + func TestDurationApplyInputSourceMethodSet(t *testing.T) { tis := testApplyInputSource{ Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), @@ -428,6 +597,19 @@ func TestFloat64ApplyInputSourceMethodSet(t *testing.T) { refute(t, 1.3, c.Float64("test")) } +func TestFloat64ApplyInputSourceMethodSetNegativeValue_Alias(t *testing.T) { + tis := testApplyInputSource{ + Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test", Aliases: []string{"test_alias"}}), + FlagName: "test_alias", + MapValue: -1.3, + } + c := runTest(t, tis) + expect(t, -1.3, c.Float64("test_alias")) + + c = runRacyTest(t, tis) + refute(t, -1.3, c.Float64("test_alias")) +} + func TestFloat64ApplyInputSourceMethodSetNegativeValue(t *testing.T) { tis := testApplyInputSource{ Flag: NewFloat64Flag(&cli.Float64Flag{Name: "test"}), diff --git a/altsrc/json_command_test.go b/altsrc/json_command_test.go index 22c5958..82ac922 100644 --- a/altsrc/json_command_test.go +++ b/altsrc/json_command_test.go @@ -11,7 +11,7 @@ import ( const ( fileName = "current.json" - simpleJSON = `{"test": 15}` + simpleJSON = `{"test": 15, "testb": false}` nestedJSON = `{"top": {"test": 15}}` ) @@ -34,11 +34,16 @@ func TestCommandJSONFileTest(t *testing.T) { Action: func(c *cli.Context) error { val := c.Int("test") expect(t, val, 15) + + valb := c.Bool("testb") + expect(t, valb, false) return nil }, Flags: []cli.Flag{ NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}}, + &cli.StringFlag{Name: "load"}, + NewBoolFlag(&cli.BoolFlag{Name: "testb", Value: true}), + }, } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) err := command.Run(c) diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index 66afe58..b1f6c84 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -33,11 +33,9 @@ func NewYamlSourceFromFile(file string) (InputSourceContext, error) { // NewYamlSourceFromFlagFunc creates a new Yaml InputSourceContext from a provided flag name and source context. func NewYamlSourceFromFlagFunc(flagFileName string) func(cCtx *cli.Context) (InputSourceContext, error) { return func(cCtx *cli.Context) (InputSourceContext, error) { - if cCtx.IsSet(flagFileName) { - filePath := cCtx.String(flagFileName) + if filePath := cCtx.String(flagFileName); filePath != "" { return NewYamlSourceFromFile(filePath) } - return defaultInputSource() } } diff --git a/app.go b/app.go index 834873a..7be6f76 100644 --- a/app.go +++ b/app.go @@ -78,6 +78,8 @@ type App struct { CommandNotFound CommandNotFoundFunc // Execute this function if a usage error occurs OnUsageError OnUsageErrorFunc + // Execute this function when an invalid flag is accessed from the context + InvalidFlagAccessHandler InvalidFlagAccessFunc // Compilation date Compiled time.Time // List of all authors who contributed @@ -131,7 +133,6 @@ func compileTime() time.Time { func NewApp() *App { return &App{ Name: filepath.Base(os.Args[0]), - HelpName: filepath.Base(os.Args[0]), Usage: "A new cli application", UsageText: "", BashComplete: DefaultAppComplete, @@ -273,7 +274,9 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { cCtx := NewContext(a, set, &Context{Context: ctx}) if nerr != nil { _, _ = fmt.Fprintln(a.Writer, nerr) - _ = ShowAppHelp(cCtx) + if !a.HideHelp { + _ = ShowAppHelp(cCtx) + } return nerr } cCtx.shellComplete = shellComplete @@ -294,10 +297,24 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { fmt.Fprintf(a.Writer, suggestion) } } - _ = ShowAppHelp(cCtx) + if !a.HideHelp { + _ = ShowAppHelp(cCtx) + } return err } + if a.After != nil && !cCtx.shellComplete { + defer func() { + if afterErr := a.After(cCtx); afterErr != nil { + if err != nil { + err = newMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + if !a.HideHelp && checkHelp(cCtx) { _ = ShowAppHelp(cCtx) return nil @@ -314,19 +331,7 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { return cerr } - if a.After != nil { - defer func() { - if afterErr := a.After(cCtx); afterErr != nil { - if err != nil { - err = newMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { + if a.Before != nil && !cCtx.shellComplete { beforeErr := a.Before(cCtx) if beforeErr != nil { a.handleExitCoder(cCtx, beforeErr) @@ -335,6 +340,10 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { } } + if err = runFlagActions(cCtx, a.Flags); err != nil { + return err + } + var c *Command args := cCtx.Args() if args.Present() { @@ -493,7 +502,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { return cerr } - if a.After != nil { + if a.After != nil && !cCtx.shellComplete { defer func() { afterErr := a.After(cCtx) if afterErr != nil { @@ -507,7 +516,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { }() } - if a.Before != nil { + if a.Before != nil && !cCtx.shellComplete { beforeErr := a.Before(cCtx) if beforeErr != nil { a.handleExitCoder(cCtx, beforeErr) @@ -516,6 +525,10 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } } + if err = runFlagActions(cCtx, a.Flags); err != nil { + return err + } + args := cCtx.Args() if args.Present() { name := args.First() @@ -639,6 +652,24 @@ func (a *App) argsWithDefaultCommand(oldArgs Args) Args { return oldArgs } +func runFlagActions(c *Context, fs []Flag) error { + for _, f := range fs { + isSet := false + for _, name := range f.Names() { + if c.IsSet(name) { + isSet = true + break + } + } + if isSet { + if err := f.RunAction(c); err != nil { + return err + } + } + } + return nil +} + // Author represents someone who has contributed to a cli project. type Author struct { Name string // The Authors name diff --git a/app_test.go b/app_test.go index da3981c..1428c39 100644 --- a/app_test.go +++ b/app_test.go @@ -9,8 +9,10 @@ import ( "io/ioutil" "os" "reflect" + "strconv" "strings" "testing" + "time" ) var ( @@ -175,10 +177,13 @@ func ExampleApp_Run_commandHelp() { // greet describeit - use it to see a description // // USAGE: - // greet describeit [arguments...] + // greet describeit [command options] [arguments...] // // DESCRIPTION: // This is how we describe describeit the function + // + // OPTIONS: + // --help, -h show help (default: false) } func ExampleApp_Run_noAction() { @@ -1245,6 +1250,58 @@ func TestApp_BeforeFunc(t *testing.T) { } } +func TestApp_BeforeAfterFuncShellCompletion(t *testing.T) { + counts := &opCounts{} + var err error + + app := &App{ + EnableBashCompletion: true, + Before: func(c *Context) error { + counts.Total++ + counts.Before = counts.Total + return nil + }, + After: func(c *Context) error { + counts.Total++ + counts.After = counts.Total + return nil + }, + Commands: []*Command{ + { + Name: "sub", + Action: func(c *Context) error { + counts.Total++ + counts.SubCommand = counts.Total + return nil + }, + }, + }, + Flags: []Flag{ + &StringFlag{Name: "opt"}, + }, + Writer: ioutil.Discard, + } + + // run with the Before() func succeeding + err = app.Run([]string{"command", "--opt", "succeed", "sub", "--generate-bash-completion"}) + + if err != nil { + t.Fatalf("Run error: %s", err) + } + + if counts.Before != 0 { + t.Errorf("Before() executed when not expected") + } + + if counts.After != 0 { + t.Errorf("After() executed when not expected") + } + + if counts.SubCommand != 0 { + t.Errorf("Subcommand executed more than expected") + } +} + func TestApp_AfterFunc(t *testing.T) { counts := &opCounts{} afterError := fmt.Errorf("fail") @@ -1309,6 +1366,27 @@ func TestApp_AfterFunc(t *testing.T) { if counts.SubCommand != 1 { t.Errorf("Subcommand not executed when expected") } + + /* + reset + */ + counts = &opCounts{} + + // run with none args + err = app.Run([]string{"command"}) + + // should be the same error produced by the Before func + if err != nil { + t.Fatalf("Run error: %s", err) + } + + if counts.After != 1 { + t.Errorf("After() not executed when expected") + } + + if counts.SubCommand != 0 { + t.Errorf("Subcommand not executed when expected") + } } func TestAppNoHelpFlag(t *testing.T) { @@ -1873,31 +1951,67 @@ func TestApp_Run_CommandSubcommandHelpName(t *testing.T) { } func TestApp_Run_Help(t *testing.T) { - var helpArguments = [][]string{{"boom", "--help"}, {"boom", "-h"}, {"boom", "help"}} - - for _, args := range helpArguments { - t.Run(fmt.Sprintf("checking with arguments %v", args), func(t *testing.T) { + var tests = []struct { + helpArguments []string + hideHelp bool + wantContains string + wantErr error + }{ + { + helpArguments: []string{"boom", "--help"}, + hideHelp: false, + wantContains: "boom - make an explosive entrance", + }, + { + helpArguments: []string{"boom", "-h"}, + hideHelp: false, + wantContains: "boom - make an explosive entrance", + }, + { + helpArguments: []string{"boom", "help"}, + hideHelp: false, + wantContains: "boom - make an explosive entrance", + }, + { + helpArguments: []string{"boom", "--help"}, + hideHelp: true, + wantErr: fmt.Errorf("flag: help requested"), + }, + { + helpArguments: []string{"boom", "-h"}, + hideHelp: true, + wantErr: fmt.Errorf("flag: help requested"), + }, + { + helpArguments: []string{"boom", "help"}, + hideHelp: true, + wantContains: "boom I say!", + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("checking with arguments %v", tt.helpArguments), func(t *testing.T) { buf := new(bytes.Buffer) app := &App{ - Name: "boom", - Usage: "make an explosive entrance", - Writer: buf, + Name: "boom", + Usage: "make an explosive entrance", + Writer: buf, + HideHelp: tt.hideHelp, Action: func(c *Context) error { buf.WriteString("boom I say!") return nil }, } - err := app.Run(args) - if err != nil { - t.Error(err) + err := app.Run(tt.helpArguments) + if err != nil && err.Error() != tt.wantErr.Error() { + t.Errorf("want err: %s, did note %s\n", tt.wantErr, err) } output := buf.String() - if !strings.Contains(output, "boom - make an explosive entrance") { + if !strings.Contains(output, tt.wantContains) { t.Errorf("want help to contain %q, did not: \n%q", "boom - make an explosive entrance", output) } }) @@ -2248,6 +2362,10 @@ func (c *customBoolFlag) Apply(set *flag.FlagSet) error { return nil } +func (c *customBoolFlag) RunAction(*Context) error { + return nil +} + func (c *customBoolFlag) IsSet() bool { return false } @@ -2487,3 +2605,392 @@ func TestSetupInitializesOnlyNilWriters(t *testing.T) { t.Errorf("expected a.Writer to be os.Stdout") } } + +type stringGeneric struct { + value string +} + +func (s *stringGeneric) Set(value string) error { + s.value = value + return nil +} + +func (s *stringGeneric) String() string { + return s.value +} + +func TestFlagAction(t *testing.T) { + stringFlag := &StringFlag{ + Name: "f_string", + Action: func(c *Context, v string) error { + if v == "" { + return fmt.Errorf("empty string") + } + c.App.Writer.Write([]byte(v + " ")) + return nil + }, + } + app := &App{ + Name: "app", + Commands: []*Command{ + { + Name: "c1", + Flags: []Flag{stringFlag}, + Action: func(ctx *Context) error { return nil }, + Subcommands: []*Command{ + { + Name: "sub1", + Action: func(ctx *Context) error { return nil }, + Flags: []Flag{stringFlag}, + }, + }, + }, + }, + Flags: []Flag{ + stringFlag, + &StringFlag{ + Name: "f_no_action", + }, + &StringSliceFlag{ + Name: "f_string_slice", + Action: func(c *Context, v []string) error { + if v[0] == "err" { + return fmt.Errorf("error string slice") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &BoolFlag{ + Name: "f_bool", + Action: func(c *Context, v bool) error { + if !v { + return fmt.Errorf("value is false") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%t ", v))) + return nil + }, + }, + &DurationFlag{ + Name: "f_duration", + Action: func(c *Context, v time.Duration) error { + if v == 0 { + return fmt.Errorf("empty duration") + } + c.App.Writer.Write([]byte(v.String() + " ")) + return nil + }, + }, + &Float64Flag{ + Name: "f_float64", + Action: func(c *Context, v float64) error { + if v < 0 { + return fmt.Errorf("negative float64") + } + c.App.Writer.Write([]byte(strconv.FormatFloat(v, 'f', -1, 64) + " ")) + return nil + }, + }, + &Float64SliceFlag{ + Name: "f_float64_slice", + Action: func(c *Context, v []float64) error { + if len(v) > 0 && v[0] < 0 { + return fmt.Errorf("invalid float64 slice") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &GenericFlag{ + Name: "f_generic", + Value: new(stringGeneric), + Action: func(c *Context, v interface{}) error { + fmt.Printf("%T %v\n", v, v) + switch vv := v.(type) { + case *stringGeneric: + if vv.value == "" { + return fmt.Errorf("generic value not set") + } + } + + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &IntFlag{ + Name: "f_int", + Action: func(c *Context, v int) error { + if v < 0 { + return fmt.Errorf("negative int") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &IntSliceFlag{ + Name: "f_int_slice", + Action: func(c *Context, v []int) error { + if len(v) > 0 && v[0] < 0 { + return fmt.Errorf("invalid int slice") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &Int64Flag{ + Name: "f_int64", + Action: func(c *Context, v int64) error { + if v < 0 { + return fmt.Errorf("negative int64") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &Int64SliceFlag{ + Name: "f_int64_slice", + Action: func(c *Context, v []int64) error { + if len(v) > 0 && v[0] < 0 { + return fmt.Errorf("invalid int64 slice") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &PathFlag{ + Name: "f_path", + Action: func(c *Context, v string) error { + if v == "" { + return fmt.Errorf("empty path") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &TimestampFlag{ + Name: "f_timestamp", + Layout: "2006-01-02 15:04:05", + Action: func(c *Context, v *time.Time) error { + if v.IsZero() { + return fmt.Errorf("zero timestamp") + } + c.App.Writer.Write([]byte(v.Format(time.RFC3339) + " ")) + return nil + }, + }, + &UintFlag{ + Name: "f_uint", + Action: func(c *Context, v uint) error { + if v == 0 { + return fmt.Errorf("zero uint") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + &Uint64Flag{ + Name: "f_uint64", + Action: func(c *Context, v uint64) error { + if v == 0 { + return fmt.Errorf("zero uint64") + } + c.App.Writer.Write([]byte(fmt.Sprintf("%v ", v))) + return nil + }, + }, + }, + Action: func(ctx *Context) error { return nil }, + } + + tests := []struct { + name string + args []string + err error + exp string + }{ + { + name: "flag_string", + args: []string{"app", "--f_string=string"}, + exp: "string ", + }, + { + name: "flag_string_error", + args: []string{"app", "--f_string="}, + err: fmt.Errorf("empty string"), + }, + { + name: "flag_string_slice", + args: []string{"app", "--f_string_slice=s1,s2,s3"}, + exp: "[s1 s2 s3] ", + }, + { + name: "flag_string_slice_error", + args: []string{"app", "--f_string_slice=err"}, + err: fmt.Errorf("error string slice"), + }, + { + name: "flag_bool", + args: []string{"app", "--f_bool"}, + exp: "true ", + }, + { + name: "flag_bool_error", + args: []string{"app", "--f_bool=false"}, + err: fmt.Errorf("value is false"), + }, + { + name: "flag_duration", + args: []string{"app", "--f_duration=1h30m20s"}, + exp: "1h30m20s ", + }, + { + name: "flag_duration_error", + args: []string{"app", "--f_duration=0"}, + err: fmt.Errorf("empty duration"), + }, + { + name: "flag_float64", + args: []string{"app", "--f_float64=3.14159"}, + exp: "3.14159 ", + }, + { + name: "flag_float64_error", + args: []string{"app", "--f_float64=-1"}, + err: fmt.Errorf("negative float64"), + }, + { + name: "flag_float64_slice", + args: []string{"app", "--f_float64_slice=1.1,2.2,3.3"}, + exp: "[1.1 2.2 3.3] ", + }, + { + name: "flag_float64_slice_error", + args: []string{"app", "--f_float64_slice=-1"}, + err: fmt.Errorf("invalid float64 slice"), + }, + { + name: "flag_generic", + args: []string{"app", "--f_generic=1"}, + exp: "1 ", + }, + { + name: "flag_generic_error", + args: []string{"app", "--f_generic="}, + err: fmt.Errorf("generic value not set"), + }, + { + name: "flag_int", + args: []string{"app", "--f_int=1"}, + exp: "1 ", + }, + { + name: "flag_int_error", + args: []string{"app", "--f_int=-1"}, + err: fmt.Errorf("negative int"), + }, + { + name: "flag_int_slice", + args: []string{"app", "--f_int_slice=1,2,3"}, + exp: "[1 2 3] ", + }, + { + name: "flag_int_slice_error", + args: []string{"app", "--f_int_slice=-1"}, + err: fmt.Errorf("invalid int slice"), + }, + { + name: "flag_int64", + args: []string{"app", "--f_int64=1"}, + exp: "1 ", + }, + { + name: "flag_int64_error", + args: []string{"app", "--f_int64=-1"}, + err: fmt.Errorf("negative int64"), + }, + { + name: "flag_int64_slice", + args: []string{"app", "--f_int64_slice=1,2,3"}, + exp: "[1 2 3] ", + }, + { + name: "flag_int64_slice", + args: []string{"app", "--f_int64_slice=-1"}, + err: fmt.Errorf("invalid int64 slice"), + }, + { + name: "flag_path", + args: []string{"app", "--f_path=/root"}, + exp: "/root ", + }, + { + name: "flag_path_error", + args: []string{"app", "--f_path="}, + err: fmt.Errorf("empty path"), + }, + { + name: "flag_timestamp", + args: []string{"app", "--f_timestamp", "2022-05-01 02:26:20"}, + exp: "2022-05-01T02:26:20Z ", + }, + { + name: "flag_timestamp_error", + args: []string{"app", "--f_timestamp", "0001-01-01 00:00:00"}, + err: fmt.Errorf("zero timestamp"), + }, + { + name: "flag_uint", + args: []string{"app", "--f_uint=1"}, + exp: "1 ", + }, + { + name: "flag_uint_error", + args: []string{"app", "--f_uint=0"}, + err: fmt.Errorf("zero uint"), + }, + { + name: "flag_uint64", + args: []string{"app", "--f_uint64=1"}, + exp: "1 ", + }, + { + name: "flag_uint64_error", + args: []string{"app", "--f_uint64=0"}, + err: fmt.Errorf("zero uint64"), + }, + { + name: "flag_no_action", + args: []string{"app", "--f_no_action="}, + exp: "", + }, + { + name: "command_flag", + args: []string{"app", "c1", "--f_string=c1"}, + exp: "c1 ", + }, + { + name: "subCommand_flag", + args: []string{"app", "c1", "sub1", "--f_string=sub1"}, + exp: "sub1 ", + }, + { + name: "mixture", + args: []string{"app", "--f_string=app", "--f_uint=1", "--f_int_slice=1,2,3", "--f_duration=1h30m20s", "c1", "--f_string=c1", "sub1", "--f_string=sub1"}, + exp: "app 1h30m20s [1 2 3] 1 c1 sub1 ", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + buf := new(bytes.Buffer) + app.Writer = buf + err := app.Run(test.args) + if test.err != nil { + expect(t, err, test.err) + } else { + expect(t, err, nil) + expect(t, buf.String(), test.exp) + } + }) + } +} diff --git a/cli.go b/cli.go index 2a11c5a..b3b864c 100644 --- a/cli.go +++ b/cli.go @@ -1,23 +1,25 @@ // Package cli provides a minimal framework for creating and organizing command line // Go applications. cli is designed to be easy to understand and write, the most simple // cli application can be written as follows: -// func main() { -// (&cli.App{}).Run(os.Args) -// } +// +// func main() { +// (&cli.App{}).Run(os.Args) +// } // // Of course this application does not do much, so let's make this an actual application: -// func main() { -// app := &cli.App{ -// Name: "greet", -// Usage: "say a greeting", -// Action: func(c *cli.Context) error { -// fmt.Println("Greetings") -// return nil -// }, -// } // -// app.Run(os.Args) -// } +// func main() { +// app := &cli.App{ +// Name: "greet", +// Usage: "say a greeting", +// Action: func(c *cli.Context) error { +// fmt.Println("Greetings") +// return nil +// }, +// } +// +// app.Run(os.Args) +// } package cli -//go:generate go run internal/genflags/cmd/genflags/main.go +//go:generate go run cmd/urfave-cli-genflags/main.go diff --git a/cmd/urfave-cli-genflags/Makefile b/cmd/urfave-cli-genflags/Makefile new file mode 100644 index 0000000..3b11415 --- /dev/null +++ b/cmd/urfave-cli-genflags/Makefile @@ -0,0 +1,21 @@ +GOTEST_FLAGS ?= -v --coverprofile main.coverprofile --covermode count --cover github.com/urfave/cli/v2/cmd/urfave-cli-genflags +GOBUILD_FLAGS ?= -x + +.PHONY: all +all: test build smoke-test + +.PHONY: test +test: + go test $(GOTEST_FLAGS) ./... + +.PHONY: build +build: + go build $(GOBUILD_FLAGS) ./... + +.PHONY: smoke-test +smoke-test: build + ./urfave-cli-genflags --help + +.PHONY: show-cover +show-cover: + go tool cover -func main.coverprofile diff --git a/cmd/urfave-cli-genflags/README.md b/cmd/urfave-cli-genflags/README.md new file mode 100644 index 0000000..9424234 --- /dev/null +++ b/cmd/urfave-cli-genflags/README.md @@ -0,0 +1,15 @@ +# urfave-cli-genflags + +This is a tool that is used internally by [urfave/cli] to generate +flag types and methods from a YAML input. It intentionally pins +usage of `github.com/urfave/cli/v2` to a *release* rather than +using the adjacent code so that changes don't result in *this* tool +refusing to compile. It's almost like dogfooding? + +## support warning + +This tool is maintained as a sub-project and is not covered by the +API and backward compatibility guaranteed by releases of +[urfave/cli]. + +[urfave/cli]: https://github.com/urfave/cli diff --git a/internal/genflags/generated.gotmpl b/cmd/urfave-cli-genflags/generated.gotmpl similarity index 94% rename from internal/genflags/generated.gotmpl rename to cmd/urfave-cli-genflags/generated.gotmpl index 964658e..85bd66f 100644 --- a/internal/genflags/generated.gotmpl +++ b/cmd/urfave-cli-genflags/generated.gotmpl @@ -17,13 +17,13 @@ type {{.TypeName}} struct { HasBeenSet bool Value {{if .ValuePointer}}*{{end}}{{.GoType}} - Destination *{{.GoType}} + Destination {{if .NoDestinationPointer}}{{else}}*{{end}}{{.GoType}} Aliases []string EnvVars []string {{range .StructFields}} - {{.Name}} {{.Type}} + {{.Name}} {{if .Pointer}}*{{end}}{{.Type}} {{end}} } diff --git a/internal/genflags/generated_test.gotmpl b/cmd/urfave-cli-genflags/generated_test.gotmpl similarity index 100% rename from internal/genflags/generated_test.gotmpl rename to cmd/urfave-cli-genflags/generated_test.gotmpl diff --git a/cmd/urfave-cli-genflags/go.mod b/cmd/urfave-cli-genflags/go.mod new file mode 100644 index 0000000..af40aaa --- /dev/null +++ b/cmd/urfave-cli-genflags/go.mod @@ -0,0 +1,15 @@ +module github.com/urfave/cli/v2/cmd/urfave-cli-genflags + +go 1.18 + +require ( + github.com/urfave/cli/v2 v2.11.2 + golang.org/x/text v0.3.7 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect +) diff --git a/cmd/urfave-cli-genflags/go.sum b/cmd/urfave-cli-genflags/go.sum new file mode 100644 index 0000000..e59916d --- /dev/null +++ b/cmd/urfave-cli-genflags/go.sum @@ -0,0 +1,14 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.11.2 h1:FVfNg4m3vbjbBpLYxW//WjxUoHvJ9TlppXcqY9Q9ZfA= +github.com/urfave/cli/v2 v2.11.2/go.mod h1:f8iq5LtQ/bLxafbdBSLPPNsgaW0l/2fYYEHhAyPlwvo= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/genflags/cmd/genflags/main.go b/cmd/urfave-cli-genflags/main.go similarity index 51% rename from internal/genflags/cmd/genflags/main.go rename to cmd/urfave-cli-genflags/main.go index 54759c0..1b0fe53 100644 --- a/internal/genflags/cmd/genflags/main.go +++ b/cmd/urfave-cli-genflags/main.go @@ -9,12 +9,14 @@ import ( "os/exec" "os/signal" "path/filepath" + "sort" "strings" "syscall" "text/template" - "github.com/urfave/cli/v3" - "github.com/urfave/cli/v3/internal/genflags" + "github.com/urfave/cli/v2" + "golang.org/x/text/cases" + "golang.org/x/text/language" "gopkg.in/yaml.v3" ) @@ -22,6 +24,16 @@ const ( defaultPackageName = "cli" ) +var ( + //go:embed generated.gotmpl + TemplateString string + + //go:embed generated_test.gotmpl + TestTemplateString string + + titler = cases.Title(language.Und, cases.NoLower) +) + func sh(ctx context.Context, exe string, args ...string) (string, error) { cmd := exec.CommandContext(ctx, exe, args...) cmd.Stderr = os.Stderr @@ -92,7 +104,7 @@ func runGenFlags(cCtx *cli.Context) error { return err } - spec := &genflags.Spec{} + spec := &Spec{} if err := yaml.Unmarshal(specBytes, spec); err != nil { return err } @@ -123,12 +135,12 @@ func runGenFlags(cCtx *cli.Context) error { spec.UrfaveCLITestNamespace = "cli." } - genTmpl, err := template.New("gen").Parse(genflags.TemplateString) + genTmpl, err := template.New("gen").Parse(TemplateString) if err != nil { return err } - genTestTmpl, err := template.New("gen_test").Parse(genflags.TestTemplateString) + genTestTmpl, err := template.New("gen_test").Parse(TestTemplateString) if err != nil { return err } @@ -161,3 +173,130 @@ func runGenFlags(cCtx *cli.Context) error { return nil } + +func TypeName(goType string, fc *FlagTypeConfig) string { + if fc != nil && strings.TrimSpace(fc.TypeName) != "" { + return strings.TrimSpace(fc.TypeName) + } + + dotSplit := strings.Split(goType, ".") + goType = dotSplit[len(dotSplit)-1] + + if strings.HasPrefix(goType, "[]") { + return titler.String(strings.TrimPrefix(goType, "[]")) + "SliceFlag" + } + + return titler.String(goType) + "Flag" +} + +type Spec struct { + FlagTypes map[string]*FlagTypeConfig `yaml:"flag_types"` + PackageName string `yaml:"package_name"` + TestPackageName string `yaml:"test_package_name"` + UrfaveCLINamespace string `yaml:"urfave_cli_namespace"` + UrfaveCLITestNamespace string `yaml:"urfave_cli_test_namespace"` +} + +func (gfs *Spec) SortedFlagTypes() []*FlagType { + typeNames := []string{} + + for name := range gfs.FlagTypes { + if strings.HasPrefix(name, "[]") { + name = strings.TrimPrefix(name, "[]") + "Slice" + } + + typeNames = append(typeNames, name) + } + + sort.Strings(typeNames) + + ret := make([]*FlagType, len(typeNames)) + + for i, typeName := range typeNames { + ret[i] = &FlagType{ + GoType: typeName, + Config: gfs.FlagTypes[typeName], + } + } + + return ret +} + +type FlagTypeConfig struct { + SkipInterfaces []string `yaml:"skip_interfaces"` + StructFields []*FlagStructField `yaml:"struct_fields"` + TypeName string `yaml:"type_name"` + ValuePointer bool `yaml:"value_pointer"` + NoDestinationPointer bool `yaml:"no_destination_pointer"` +} + +type FlagStructField struct { + Name string + Type string + Pointer bool +} + +type FlagType struct { + GoType string + Config *FlagTypeConfig +} + +func (ft *FlagType) StructFields() []*FlagStructField { + if ft.Config == nil || ft.Config.StructFields == nil { + return []*FlagStructField{} + } + + return ft.Config.StructFields +} + +func (ft *FlagType) ValuePointer() bool { + if ft.Config == nil { + return false + } + + return ft.Config.ValuePointer +} + +func (ft *FlagType) NoDestinationPointer() bool { + if ft.Config == nil { + return false + } + + return ft.Config.NoDestinationPointer +} + +func (ft *FlagType) TypeName() string { + return TypeName(ft.GoType, ft.Config) +} + +func (ft *FlagType) GenerateFmtStringerInterface() bool { + return ft.skipInterfaceNamed("fmt.Stringer") +} + +func (ft *FlagType) GenerateFlagInterface() bool { + return ft.skipInterfaceNamed("Flag") +} + +func (ft *FlagType) GenerateRequiredFlagInterface() bool { + return ft.skipInterfaceNamed("RequiredFlag") +} + +func (ft *FlagType) GenerateVisibleFlagInterface() bool { + return ft.skipInterfaceNamed("VisibleFlag") +} + +func (ft *FlagType) skipInterfaceNamed(name string) bool { + if ft.Config == nil { + return true + } + + lowName := strings.ToLower(name) + + for _, interfaceName := range ft.Config.SkipInterfaces { + if strings.ToLower(interfaceName) == lowName { + return false + } + } + + return true +} diff --git a/internal/genflags/spec_test.go b/cmd/urfave-cli-genflags/main_test.go similarity index 52% rename from internal/genflags/spec_test.go rename to cmd/urfave-cli-genflags/main_test.go index 4a0cd4f..b5c9fee 100644 --- a/internal/genflags/spec_test.go +++ b/cmd/urfave-cli-genflags/main_test.go @@ -1,29 +1,63 @@ -package genflags_test +package main_test import ( + "fmt" "reflect" "testing" - "github.com/urfave/cli/v3/internal/genflags" + main "github.com/urfave/cli/v2/cmd/urfave-cli-genflags" ) +func TestTypeName(t *testing.T) { + for _, tc := range []struct { + gt string + fc *main.FlagTypeConfig + expected string + }{ + {gt: "int", fc: nil, expected: "IntFlag"}, + {gt: "int", fc: &main.FlagTypeConfig{}, expected: "IntFlag"}, + {gt: "int", fc: &main.FlagTypeConfig{TypeName: "VeryIntyFlag"}, expected: "VeryIntyFlag"}, + {gt: "[]bool", fc: nil, expected: "BoolSliceFlag"}, + {gt: "[]bool", fc: &main.FlagTypeConfig{}, expected: "BoolSliceFlag"}, + {gt: "[]bool", fc: &main.FlagTypeConfig{TypeName: "ManyTruthsFlag"}, expected: "ManyTruthsFlag"}, + {gt: "time.Rumination", fc: nil, expected: "RuminationFlag"}, + {gt: "time.Rumination", fc: &main.FlagTypeConfig{}, expected: "RuminationFlag"}, + {gt: "time.Rumination", fc: &main.FlagTypeConfig{TypeName: "PonderFlag"}, expected: "PonderFlag"}, + } { + t.Run( + fmt.Sprintf("type=%s,cfg=%v", tc.gt, func() string { + if tc.fc != nil { + return tc.fc.TypeName + } + return "nil" + }()), + func(ct *testing.T) { + actual := main.TypeName(tc.gt, tc.fc) + if tc.expected != actual { + ct.Errorf("expected %q, got %q", tc.expected, actual) + } + }, + ) + } +} + func TestSpec_SortedFlagTypes(t *testing.T) { - spec := &genflags.Spec{ - FlagTypes: map[string]*genflags.FlagTypeConfig{ - "nerf": &genflags.FlagTypeConfig{}, + spec := &main.Spec{ + FlagTypes: map[string]*main.FlagTypeConfig{ + "nerf": &main.FlagTypeConfig{}, "gerf": nil, }, } actual := spec.SortedFlagTypes() - expected := []*genflags.FlagType{ + expected := []*main.FlagType{ { GoType: "gerf", Config: nil, }, { GoType: "nerf", - Config: &genflags.FlagTypeConfig{}, + Config: &main.FlagTypeConfig{}, }, } if !reflect.DeepEqual(expected, actual) { @@ -31,12 +65,12 @@ func TestSpec_SortedFlagTypes(t *testing.T) { } } -func genFlagType() *genflags.FlagType { - return &genflags.FlagType{ +func genFlagType() *main.FlagType { + return &main.FlagType{ GoType: "blerf", - Config: &genflags.FlagTypeConfig{ + Config: &main.FlagTypeConfig{ SkipInterfaces: []string{"fmt.Stringer"}, - StructFields: []*genflags.FlagStructField{ + StructFields: []*main.FlagStructField{ { Name: "Foibles", Type: "int", diff --git a/command.go b/command.go index 2cafd8e..decfb73 100644 --- a/command.go +++ b/command.go @@ -125,7 +125,9 @@ func (c *Command) Run(ctx *Context) (err error) { fmt.Fprintf(cCtx.App.Writer, suggestion) } } - _ = ShowCommandHelp(cCtx, c.Name) + if !c.HideHelp { + _ = ShowCommandHelp(cCtx, c.Name) + } return err } @@ -135,7 +137,9 @@ func (c *Command) Run(ctx *Context) (err error) { cerr := cCtx.checkRequiredFlags(c.Flags) if cerr != nil { - _ = ShowCommandHelp(cCtx, c.Name) + if !c.HideHelp { + _ = ShowCommandHelp(cCtx, c.Name) + } return cerr } @@ -161,8 +165,12 @@ func (c *Command) Run(ctx *Context) (err error) { } } + if err = runFlagActions(cCtx, c.Flags); err != nil { + return err + } + if c.Action == nil { - c.Action = helpSubcommand.Action + c.Action = helpCommand.Action } cCtx.Command = c @@ -276,7 +284,7 @@ func (c *Command) startApp(ctx *Context) error { if c.Action != nil { app.Action = c.Action } else { - app.Action = helpSubcommand.Action + app.Action = helpCommand.Action } app.OnUsageError = c.OnUsageError @@ -290,7 +298,10 @@ func (c *Command) startApp(ctx *Context) error { // VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain func (c *Command) VisibleFlagCategories() []VisibleFlagCategory { if c.flagCategories == nil { - return []VisibleFlagCategory{} + c.flagCategories = newFlagCategories() + for _, fl := range c.Flags { + c.flagCategories.AddFlag(fl.GetCategory(), fl) + } } return c.flagCategories.VisibleCategories() } diff --git a/context.go b/context.go index 216e958..81416da 100644 --- a/context.go +++ b/context.go @@ -3,6 +3,7 @@ package cli import ( "context" "flag" + "fmt" "strings" ) @@ -46,7 +47,11 @@ func (cCtx *Context) NumFlags() int { // Set sets a context flag to a value. func (cCtx *Context) Set(name, value string) error { - return cCtx.flagSet.Set(name, value) + if fs := cCtx.lookupFlagSet(name); fs != nil { + return fs.Set(name, value) + } + + return fmt.Errorf("no such flag -%s", name) } // IsSet determines if the flag was actually set @@ -102,6 +107,16 @@ func (cCtx *Context) Lineage() []*Context { return lineage } +// Count returns the num of occurences of this flag +func (cCtx *Context) Count(name string) int { + if fs := cCtx.lookupFlagSet(name); fs != nil { + if cf, ok := fs.Lookup(name).Value.(Countable); ok { + return cf.Count() + } + } + return 0 +} + // Value returns the value of the flag corresponding to `name` func (cCtx *Context) Value(name string) interface{} { if fs := cCtx.lookupFlagSet(name); fs != nil { @@ -158,7 +173,7 @@ func (cCtx *Context) lookupFlagSet(name string) *flag.FlagSet { return c.flagSet } } - + cCtx.onInvalidFlag(name) return nil } @@ -170,9 +185,7 @@ func (cCtx *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { var flagName string for _, key := range f.Names() { - if len(key) > 1 { - flagName = key - } + flagName = key if cCtx.IsSet(strings.TrimSpace(key)) { flagPresent = true @@ -192,6 +205,16 @@ func (cCtx *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { return nil } +func (cCtx *Context) onInvalidFlag(name string) { + for cCtx != nil { + if cCtx.App != nil && cCtx.App.InvalidFlagAccessHandler != nil { + cCtx.App.InvalidFlagAccessHandler(cCtx, name) + break + } + cCtx = cCtx.parentContext + } +} + func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { return func(f *flag.Flag) { nameParts := strings.Split(f.Name, ",") diff --git a/context_test.go b/context_test.go index 8475706..246590d 100644 --- a/context_test.go +++ b/context_test.go @@ -150,6 +150,31 @@ func TestContext_Value(t *testing.T) { expect(t, c.Value("unknown-flag"), nil) } +func TestContext_Value_InvalidFlagAccessHandler(t *testing.T) { + var flagName string + app := &App{ + InvalidFlagAccessHandler: func(_ *Context, name string) { + flagName = name + }, + Commands: []*Command{ + { + Name: "command", + Subcommands: []*Command{ + { + Name: "subcommand", + Action: func(ctx *Context) error { + ctx.Value("missing") + return nil + }, + }, + }, + }, + }, + } + expect(t, app.Run([]string{"run", "command", "subcommand"}), nil) + expect(t, flagName, "missing") +} + func TestContext_Args(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") @@ -258,6 +283,19 @@ func TestContext_Set(t *testing.T) { expect(t, c.IsSet("int"), true) } +func TestContext_Set_InvalidFlagAccessHandler(t *testing.T) { + set := flag.NewFlagSet("test", 0) + var flagName string + app := &App{ + InvalidFlagAccessHandler: func(_ *Context, name string) { + flagName = name + }, + } + c := NewContext(app, set, nil) + c.Set("missing", "") + expect(t, flagName, "missing") +} + func TestContext_LocalFlagNames(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("one-flag", false, "doc") @@ -556,6 +594,14 @@ func TestCheckRequiredFlags(t *testing.T) { &StringSliceFlag{Name: "names, n", Required: true}, }, }, + { + testCase: "required_flag_with_one_character", + expectedAnError: true, + expectedErrorContents: []string{"Required flag \"n\" not set"}, + flags: []Flag{ + &StringFlag{Name: "n", Required: true}, + }, + }, } for _, test := range tdata { @@ -597,3 +643,19 @@ func TestCheckRequiredFlags(t *testing.T) { }) } } + +func TestContext_ParentContext_Set(t *testing.T) { + parentSet := flag.NewFlagSet("parent", flag.ContinueOnError) + parentSet.String("Name", "", "") + + context := NewContext( + nil, + flag.NewFlagSet("child", flag.ContinueOnError), + NewContext(nil, parentSet, nil), + ) + + err := context.Set("Name", "aaa") + if err != nil { + t.Errorf("expect nil. set parent context flag return err: %s", err) + } +} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b0d9bfc..d657477 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -95,7 +95,7 @@ The built-in `go generate` command is used to run the commands specified in line help system which may be consulted for further information, e.g.: ```sh -go run internal/genflags/cmd/genflags/main.go --help +go run cmd/urfave-cli-genflags/main.go --help ``` #### docs output @@ -107,7 +107,7 @@ following `make` targets may be used if desired: ```sh # install documentation dependencies with `pip` -make docs-deps +make ensure-mkdocs ``` ```sh diff --git a/docs/RELEASING.md b/docs/RELEASING.md index b10e4e0..079791b 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -41,9 +41,11 @@ git tag -a -s -m 'Release 2.4.8' v2.4.8 git push origin v2.4.8 ``` -The tag push will trigger a GitHub Actions workflow. The remaining -steps require human intervention through the GitHub web view -although [automated solutions +The tag push will trigger a GitHub Actions workflow and will be +**immediately available** to the [Go module mirror, index, and +checksum database](https://proxy.golang.org/). The remaining steps +require human intervention through the GitHub web view although +[automated solutions exist](https://github.com/softprops/action-gh-release) that may be adopted in the future. @@ -58,4 +60,6 @@ adopted in the future. [^2]: This was not always true. The [`docs/CHANGELOG.md`](./CHANGELOG.md) document used to be - manually maintained. + manually maintained. Relying on the automatic release notes + generation requires the use of **merge commits** as opposed to + squash merging or rebase merging. diff --git a/docs/index.md b/docs/index.md index 5f02fed..60a1102 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,8 +11,8 @@ an expressive way. These are the guides for each major supported version: -- [`v2`](./v2/) -- [`v1`](./v1/) +- [`v2`](./v2/getting-started) +- [`v1`](./v1/getting-started) In addition to the version-specific guides, these other documents are available: diff --git a/docs/v1/examples/arguments.md b/docs/v1/examples/arguments.md new file mode 100644 index 0000000..4416d32 --- /dev/null +++ b/docs/v1/examples/arguments.md @@ -0,0 +1,35 @@ +--- +tags: + - v1 +--- + +You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Action = func(c *cli.Context) error { + fmt.Printf("Hello %q", c.Args().Get(0)) + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v1/examples/bash-completions.md b/docs/v1/examples/bash-completions.md new file mode 100644 index 0000000..73af176 --- /dev/null +++ b/docs/v1/examples/bash-completions.md @@ -0,0 +1,119 @@ +--- +tags: + - v1 +--- + +You can enable completion commands by setting the `EnableBashCompletion` +flag on the `App` object. By default, this setting will only auto-complete to +show an app's subcommands, but you can write your own completion methods for +the App or its subcommands. + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if c.NArg() > 0 { + return + } + for _, t := range tasks { + fmt.Println(t) + } + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Enabling + +Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while +setting the `PROG` variable to the name of your program: + +`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` + +#### Distribution + +Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename +it to the name of the program you wish to add autocomplete support for (or +automatically install it there if you are distributing a package). Don't forget +to source the file to make it active in the current shell. + +``` +sudo cp src/bash_autocomplete /etc/bash_completion.d/ +source /etc/bash_completion.d/ +``` + +Alternatively, you can just document that users should source the generic +`autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set +to the name of their program (as above). + +#### Customization + +The default bash completion flag (`--generate-bash-completion`) is defined as +`cli.BashCompletionFlag`, and may be redefined if desired, e.g.: + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.BashCompletionFlag = cli.BoolFlag{ + Name: "compgen", + Hidden: true, + } + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "wat", + }, + } + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v1/examples/combining-short-options.md b/docs/v1/examples/combining-short-options.md new file mode 100644 index 0000000..0496300 --- /dev/null +++ b/docs/v1/examples/combining-short-options.md @@ -0,0 +1,72 @@ +--- +tags: + - v1 +--- + +Traditional use of options using their shortnames look like this: + +``` +$ cmd -s -o -m "Some message" +``` + +Suppose you want users to be able to combine options with their shortnames. This +can be done using the `UseShortOptionHandling` bool in your app configuration, +or for individual commands by attaching it to the command configuration. For +example: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.UseShortOptionHandling = true + app.Commands = []cli.Command{ + { + Name: "short", + Usage: "complete a task on the list", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "serve, s"}, + cli.BoolFlag{Name: "option, o"}, + cli.StringFlag{Name: "message, m"}, + }, + Action: func(c *cli.Context) error { + fmt.Println("serve:", c.Bool("serve")) + fmt.Println("option:", c.Bool("option")) + fmt.Println("message:", c.String("message")) + return nil + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +If your program has any number of bool flags such as `serve` and `option`, and +optionally one non-bool flag `message`, with the short options of `-s`, `-o`, +and `-m` respectively, setting `UseShortOptionHandling` will also support the +following syntax: + +``` +$ cmd -som "Some message" +``` + +If you enable `UseShortOptionHandling`, then you must not use any flags that +have a single leading `-` or this will result in failures. For example, +`-option` can no longer be used. Flags with two leading dashes (such as +`--options`) are still valid. diff --git a/docs/v1/examples/exit-codes.md b/docs/v1/examples/exit-codes.md new file mode 100644 index 0000000..1d6627d --- /dev/null +++ b/docs/v1/examples/exit-codes.md @@ -0,0 +1,43 @@ +--- +tags: + - v1 +--- + +Calling `App.Run` will not automatically call `os.Exit`, which means that by +default the exit code will "fall through" to being `0`. An explicit exit code +may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a +`cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Flags = []cli.Flag{ + cli.BoolFlag{ + Name: "ginger-crouton", + Usage: "Add ginger croutons to the soup", + }, + } + app.Action = func(ctx *cli.Context) error { + if !ctx.Bool("ginger-crouton") { + return cli.NewExitError("Ginger croutons are not in the soup", 86) + } + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v1/examples/flags.md b/docs/v1/examples/flags.md new file mode 100644 index 0000000..2f9300b --- /dev/null +++ b/docs/v1/examples/flags.md @@ -0,0 +1,462 @@ +--- +tags: + - v1 +--- + +Setting and querying flags is simple. + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, + } + + app.Action = func(c *cli.Context) error { + name := "Nefertiti" + if c.NArg() > 0 { + name = c.Args().Get(0) + } + if c.String("lang") == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +You can also set a destination variable for a flag, to which the content will be +scanned. + + +``` go +package main + +import ( + "log" + "os" + "fmt" + + "github.com/urfave/cli" +) + +func main() { + var language string + + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Destination: &language, + }, + } + + app.Action = func(c *cli.Context) error { + name := "someone" + if c.NArg() > 0 { + name = c.Args()[0] + } + if language == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +See full list of flags at http://godoc.org/github.com/urfave/cli + +#### Placeholder Values + +Sometimes it's useful to specify a flag's value within the usage string itself. +Such placeholders are indicated with back quotes. + +For example this: + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +``` + +Note that only the first placeholder is used. Subsequent back-quoted words will +be left as-is. + +#### Alternate Names + +You can set alternate (or short) names for flags by providing a comma-delimited +list for the `Name`. e.g. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +That flag can then be set with `--lang spanish` or `-l spanish`. Note that +giving two different forms of the same flag in the same command invocation is an +error. + +#### Ordering + +Flags for the application and commands are shown in the order they are defined. +However, it's possible to sort them from outside this library by using `FlagsByName` +or `CommandsByName` with `sort`. + +For example this: + + +``` go +package main + +import ( + "log" + "os" + "sort" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "Language for the greeting", + }, + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +--lang value, -l value Language for the greeting (default: "english") +``` + +#### Values from the Environment + +You can also have the default value set from the environment via `EnvVar`. e.g. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "APP_LANG", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +The `EnvVar` may also be given as a comma-delimited "cascade", where the first +environment variable that resolves is used as the default. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Values from files + +You can also have the default value set from file via `FilePath`. e.g. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "password, p", + Usage: "password for the mysql database", + FilePath: "/etc/mysql/password", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Note that default values set from file (e.g. `FilePath`) take precedence over +default values set from the environment (e.g. `EnvVar`). + +#### Values from alternate input sources (YAML, TOML, and others) + +There is a separate package altsrc that adds support for getting flag values +from other file input sources. + +Currently supported input source formats: +* YAML +* JSON +* TOML + +In order to get values for a flag from an alternate input source the following +code would be added to wrap an existing cli.Flag like below: + +``` go + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) +``` + +Initialization must also occur for these flags. Below is an example initializing +getting data from a yaml file below. + +``` go + command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) +``` + +The code above will use the "load" string as a flag name to get the file name of +a yaml file from the cli.Context. It will then use that file name to initialize +the yaml input source for any flags that are defined on that command. As a note +the "load" flag used would also have to be defined on the command flags in order +for this code snippet to work. + +Currently only YAML, JSON, and TOML files are supported but developers can add support +for other input sources by implementing the altsrc.InputSourceContext for their +given sources. + +Here is a more complete sample of a command using YAML support: + + +``` go +package notmain + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" + "github.com/urfave/cli/altsrc" +) + +func main() { + app := cli.NewApp() + + flags := []cli.Flag{ + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}, + } + + app.Action = func(c *cli.Context) error { + fmt.Println("yaml ist rad") + return nil + } + + app.Before = altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")) + app.Flags = flags + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Precedence + +The precedence for flag value sources is as follows (highest to lowest): + +0. Command line flag value from user +0. Environment variable (if specified) +0. Configuration file (if specified) +0. Default defined on the flag diff --git a/docs/v1/examples/generated-help-text.md b/docs/v1/examples/generated-help-text.md new file mode 100644 index 0000000..84ec272 --- /dev/null +++ b/docs/v1/examples/generated-help-text.md @@ -0,0 +1,106 @@ +--- +tags: + - v1 +--- + +The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked +by the cli internals in order to print generated help text for the app, command, +or subcommand, and break execution. + +#### Customization + +All of the help text generation may be customized, and at multiple levels. The +templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and +`SubcommandHelpTemplate` which may be reassigned or augmented, and full override +is possible by assigning a compatible func to the `cli.HelpPrinter` variable, +e.g.: + + +``` go +package main + +import ( + "fmt" + "log" + "io" + "os" + + "github.com/urfave/cli" +) + +func main() { + // EXAMPLE: Append to an existing template + cli.AppHelpTemplate = fmt.Sprintf(`%s + +WEBSITE: http://awesometown.example.com + +SUPPORT: support@awesometown.example.com + +`, cli.AppHelpTemplate) + + // EXAMPLE: Override a template + cli.AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} +USAGE: + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if len .Authors}} +AUTHOR: + {{range .Authors}}{{ . }}{{end}} + {{end}}{{if .Commands}} +COMMANDS: +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +GLOBAL OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}}{{if .Copyright }} +COPYRIGHT: + {{.Copyright}} + {{end}}{{if .Version}} +VERSION: + {{.Version}} + {{end}} +` + + // EXAMPLE: Replace the `HelpPrinter` func + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Println("Ha HA. I pwnd the help!!1") + } + + err := cli.NewApp().Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +The default flag may be customized to something other than `-h/--help` by +setting `cli.HelpFlag`, e.g.: + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.HelpFlag = cli.BoolFlag{ + Name: "halp, haaaaalp", + Usage: "HALP", + EnvVar: "SHOW_HALP,HALPPLZ", + } + + err := cli.NewApp().Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v1/examples/greet.md b/docs/v1/examples/greet.md new file mode 100644 index 0000000..7314893 --- /dev/null +++ b/docs/v1/examples/greet.md @@ -0,0 +1,74 @@ +--- +tags: + - v1 +--- + +Being a programmer can be a lonely job. Thankfully by the power of automation +that is not the case! Let's create a greeter app to fend off our demons of +loneliness! + +Start by creating a directory named `greet`, and within it, add a file, +`greet.go` with the following code in it: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "greet" + app.Usage = "fight the loneliness!" + app.Action = func(c *cli.Context) error { + fmt.Println("Hello friend!") + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Install our command to the `$GOPATH/bin` directory: + +``` +$ go install +``` + +Finally run our new command: + +``` +$ greet +Hello friend! +``` + +cli also generates neat help text: + +``` +$ greet help +NAME: + greet - fight the loneliness! + +USAGE: + greet [global options] command [command options] [arguments...] + +VERSION: + 0.0.0 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS + --version Shows version information +``` diff --git a/docs/v1/examples/subcommands-categories.md b/docs/v1/examples/subcommands-categories.md new file mode 100644 index 0000000..27c2bd1 --- /dev/null +++ b/docs/v1/examples/subcommands-categories.md @@ -0,0 +1,55 @@ +--- +tags: + - v1 +--- + +For additional organization in apps that have many subcommands, you can +associate a category for each command to group them together in the help +output. + +E.g. + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Commands = []cli.Command{ + { + Name: "noop", + }, + { + Name: "add", + Category: "Template actions", + }, + { + Name: "remove", + Category: "Template actions", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Will include: + +``` +COMMANDS: + noop + + Template actions: + add + remove +``` diff --git a/docs/v1/examples/subcommands.md b/docs/v1/examples/subcommands.md new file mode 100644 index 0000000..e1be306 --- /dev/null +++ b/docs/v1/examples/subcommands.md @@ -0,0 +1,75 @@ +--- +tags: + - v1 +--- + +Subcommands can be defined for a more git-like command line app. + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Commands = []cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + fmt.Println("added task: ", c.Args().First()) + return nil + }, + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + }, + { + Name: "template", + Aliases: []string{"t"}, + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) error { + fmt.Println("new task template: ", c.Args().First()) + return nil + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) error { + fmt.Println("removed task template: ", c.Args().First()) + return nil + }, + }, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v1/examples/version-flag.md b/docs/v1/examples/version-flag.md new file mode 100644 index 0000000..dc089a1 --- /dev/null +++ b/docs/v1/examples/version-flag.md @@ -0,0 +1,363 @@ +--- +tags: + - v1 +--- + +The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which +is checked by the cli internals in order to print the `App.Version` via +`cli.VersionPrinter` and break execution. + +#### Customization + +The default flag may be customized to something other than `-v/--version` by +setting `cli.VersionFlag`, e.g.: + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.VersionFlag = cli.BoolFlag{ + Name: "print-version, V", + Usage: "print only the version", + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +var ( + Revision = "fafafaf" +) + +func main() { + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Full API Example + +**Notice**: This is a contrived (functioning) example meant strictly for API +demonstration purposes. Use of one's imagination is encouraged. + + +``` go +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/urfave/cli" +) + +func init() { + cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" + cli.CommandHelpTemplate += "\nYMMV\n" + cli.SubcommandHelpTemplate += "\nor something\n" + + cli.HelpFlag = cli.BoolFlag{Name: "halp"} + cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} + cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} + + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Fprintf(w, "best of luck to you\n") + } + cli.VersionPrinter = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) + } + cli.OsExiter = func(c int) { + fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) + } + cli.ErrWriter = ioutil.Discard + cli.FlagStringer = func(fl cli.Flag) string { + return fmt.Sprintf("\t\t%s", fl.GetName()) + } +} + +type hexWriter struct{} + +func (w *hexWriter) Write(p []byte) (int, error) { + for _, b := range p { + fmt.Printf("%x", b) + } + fmt.Printf("\n") + + return len(p), nil +} + +type genericType struct{ + s string +} + +func (g *genericType) Set(value string) error { + g.s = value + return nil +} + +func (g *genericType) String() string { + return g.s +} + +func main() { + app := cli.NewApp() + app.Name = "kənˈtrīv" + app.Version = "19.99.0" + app.Compiled = time.Now() + app.Authors = []cli.Author{ + cli.Author{ + Name: "Example Human", + Email: "human@example.com", + }, + } + app.Copyright = "(c) 1999 Serious Enterprise" + app.HelpName = "contrive" + app.Usage = "demonstrate available API" + app.UsageText = "contrive - demonstrating the available API" + app.ArgsUsage = "[args and such]" + app.Commands = []cli.Command{ + cli.Command{ + Name: "doo", + Aliases: []string{"do"}, + Category: "motion", + Usage: "do the doo", + UsageText: "doo - does the dooing", + Description: "no really, there is a lot of dooing to be done", + ArgsUsage: "[arrgh]", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "forever, forevvarr"}, + }, + Subcommands: cli.Commands{ + cli.Command{ + Name: "wop", + Action: wopAction, + }, + }, + SkipFlagParsing: false, + HideHelp: false, + Hidden: false, + HelpName: "doo!", + BashComplete: func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "--better\n") + }, + Before: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "brace for impact\n") + return nil + }, + After: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") + return nil + }, + Action: func(c *cli.Context) error { + c.Command.FullName() + c.Command.HasName("wop") + c.Command.Names() + c.Command.VisibleFlags() + fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") + if c.Bool("forever") { + c.Command.Run(c) + } + return nil + }, + OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { + fmt.Fprintf(c.App.Writer, "for shame\n") + return err + }, + }, + } + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "fancy"}, + cli.BoolTFlag{Name: "fancier"}, + cli.DurationFlag{Name: "howlong, H", Value: time.Second * 3}, + cli.Float64Flag{Name: "howmuch"}, + cli.GenericFlag{Name: "wat", Value: &genericType{}}, + cli.Int64Flag{Name: "longdistance"}, + cli.Int64SliceFlag{Name: "intervals"}, + cli.IntFlag{Name: "distance"}, + cli.IntSliceFlag{Name: "times"}, + cli.StringFlag{Name: "dance-move, d"}, + cli.StringSliceFlag{Name: "names, N"}, + cli.UintFlag{Name: "age"}, + cli.Uint64Flag{Name: "bigage"}, + } + app.EnableBashCompletion = true + app.UseShortOptionHandling = true + app.HideHelp = false + app.HideVersion = false + app.BashComplete = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") + } + app.Before = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") + return nil + } + app.After = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "Phew!\n") + return nil + } + app.CommandNotFound = func(c *cli.Context, command string) { + fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) + } + app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { + if isSubcommand { + return err + } + + fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) + return nil + } + app.Action = func(c *cli.Context) error { + cli.DefaultAppComplete(c) + cli.HandleExitCoder(errors.New("not an exit coder, though")) + cli.ShowAppHelp(c) + cli.ShowCommandCompletions(c, "nope") + cli.ShowCommandHelp(c, "also-nope") + cli.ShowCompletions(c) + cli.ShowSubcommandHelp(c) + cli.ShowVersion(c) + + categories := c.App.Categories() + categories.AddCommand("sounds", cli.Command{ + Name: "bloop", + }) + + for _, category := range c.App.Categories() { + fmt.Fprintf(c.App.Writer, "%s\n", category.Name) + fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) + fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) + } + + fmt.Printf("%#v\n", c.App.Command("doo")) + if c.Bool("infinite") { + c.App.Run([]string{"app", "doo", "wop"}) + } + + if c.Bool("forevar") { + c.App.RunAsSubcommand(c) + } + c.App.Setup() + fmt.Printf("%#v\n", c.App.VisibleCategories()) + fmt.Printf("%#v\n", c.App.VisibleCommands()) + fmt.Printf("%#v\n", c.App.VisibleFlags()) + + fmt.Printf("%#v\n", c.Args().First()) + if len(c.Args()) > 0 { + fmt.Printf("%#v\n", c.Args()[1]) + } + fmt.Printf("%#v\n", c.Args().Present()) + fmt.Printf("%#v\n", c.Args().Tail()) + + set := flag.NewFlagSet("contrive", 0) + nc := cli.NewContext(c.App, set, c) + + fmt.Printf("%#v\n", nc.Args()) + fmt.Printf("%#v\n", nc.Bool("nope")) + fmt.Printf("%#v\n", nc.BoolT("nerp")) + fmt.Printf("%#v\n", nc.Duration("howlong")) + fmt.Printf("%#v\n", nc.Float64("hay")) + fmt.Printf("%#v\n", nc.Generic("bloop")) + fmt.Printf("%#v\n", nc.Int64("bonk")) + fmt.Printf("%#v\n", nc.Int64Slice("burnks")) + fmt.Printf("%#v\n", nc.Int("bips")) + fmt.Printf("%#v\n", nc.IntSlice("blups")) + fmt.Printf("%#v\n", nc.String("snurt")) + fmt.Printf("%#v\n", nc.StringSlice("snurkles")) + fmt.Printf("%#v\n", nc.Uint("flub")) + fmt.Printf("%#v\n", nc.Uint64("florb")) + fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) + fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) + fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) + fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) + fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) + fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) + fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) + fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) + fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) + + fmt.Printf("%#v\n", nc.FlagNames()) + fmt.Printf("%#v\n", nc.GlobalFlagNames()) + fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) + fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) + fmt.Printf("%#v\n", nc.NArg()) + fmt.Printf("%#v\n", nc.NumFlags()) + fmt.Printf("%#v\n", nc.Parent()) + + nc.Set("wat", "also-nope") + + ec := cli.NewExitError("ohwell", 86) + fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) + fmt.Printf("made it!\n") + return nil + } + + if os.Getenv("HEXY") != "" { + app.Writer = &hexWriter{} + app.ErrWriter = &hexWriter{} + } + + app.Metadata = map[string]interface{}{ + "layers": "many", + "explicable": false, + "whatever-values": 19.99, + } + + + // ignore error so we don't exit non-zero and break gfmrun README example tests + _ = app.Run(os.Args) +} + +func wopAction(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") + return nil +} +``` diff --git a/docs/v1/getting-started.md b/docs/v1/getting-started.md new file mode 100644 index 0000000..b98f011 --- /dev/null +++ b/docs/v1/getting-started.md @@ -0,0 +1,65 @@ +--- +tags: + - v1 +--- + +One of the philosophies behind cli is that an API should be playful and full of +discovery. So a cli app can be as little as one line of code in `main()`. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + err := cli.NewApp().Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +This app will run and show help text, but is not very useful. Let's give an +action to execute and some help documentation: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "boom" + app.Usage = "make an explosive entrance" + app.Action = func(c *cli.Context) error { + fmt.Println("boom! I say!") + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Running this already gives you a ton of functionality, plus support for things +like subcommands and flags, which are covered below. diff --git a/docs/v1/index.md b/docs/v1/index.md deleted file mode 120000 index 9d0493a..0000000 --- a/docs/v1/index.md +++ /dev/null @@ -1 +0,0 @@ -manual.md \ No newline at end of file diff --git a/docs/v1/manual.md b/docs/v1/manual.md deleted file mode 100644 index 6f568b7..0000000 --- a/docs/v1/manual.md +++ /dev/null @@ -1,1455 +0,0 @@ -# v1 guide - -## Getting Started - -One of the philosophies behind cli is that an API should be playful and full of -discovery. So a cli app can be as little as one line of code in `main()`. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - err := cli.NewApp().Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -This app will run and show help text, but is not very useful. Let's give an -action to execute and some help documentation: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.Name = "boom" - app.Usage = "make an explosive entrance" - app.Action = func(c *cli.Context) error { - fmt.Println("boom! I say!") - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Running this already gives you a ton of functionality, plus support for things -like subcommands and flags, which are covered below. - -## Examples - -Being a programmer can be a lonely job. Thankfully by the power of automation -that is not the case! Let's create a greeter app to fend off our demons of -loneliness! - -Start by creating a directory named `greet`, and within it, add a file, -`greet.go` with the following code in it: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.Name = "greet" - app.Usage = "fight the loneliness!" - app.Action = func(c *cli.Context) error { - fmt.Println("Hello friend!") - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Install our command to the `$GOPATH/bin` directory: - -``` -$ go install -``` - -Finally run our new command: - -``` -$ greet -Hello friend! -``` - -cli also generates neat help text: - -``` -$ greet help -NAME: - greet - fight the loneliness! - -USAGE: - greet [global options] command [command options] [arguments...] - -VERSION: - 0.0.0 - -COMMANDS: - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS - --version Shows version information -``` - -### Arguments - -You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Action = func(c *cli.Context) error { - fmt.Printf("Hello %q", c.Args().Get(0)) - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Flags - -Setting and querying flags is simple. - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - }, - } - - app.Action = func(c *cli.Context) error { - name := "Nefertiti" - if c.NArg() > 0 { - name = c.Args().Get(0) - } - if c.String("lang") == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) - } - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -You can also set a destination variable for a flag, to which the content will be -scanned. - - -``` go -package main - -import ( - "log" - "os" - "fmt" - - "github.com/urfave/cli" -) - -func main() { - var language string - - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - Destination: &language, - }, - } - - app.Action = func(c *cli.Context) error { - name := "someone" - if c.NArg() > 0 { - name = c.Args()[0] - } - if language == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) - } - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -See full list of flags at http://godoc.org/github.com/urfave/cli - -#### Placeholder Values - -Sometimes it's useful to specify a flag's value within the usage string itself. -Such placeholders are indicated with back quotes. - -For example this: - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "config, c", - Usage: "Load configuration from `FILE`", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Will result in help output like: - -``` ---config FILE, -c FILE Load configuration from FILE -``` - -Note that only the first placeholder is used. Subsequent back-quoted words will -be left as-is. - -#### Alternate Names - -You can set alternate (or short) names for flags by providing a comma-delimited -list for the `Name`. e.g. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -That flag can then be set with `--lang spanish` or `-l spanish`. Note that -giving two different forms of the same flag in the same command invocation is an -error. - -#### Ordering - -Flags for the application and commands are shown in the order they are defined. -However, it's possible to sort them from outside this library by using `FlagsByName` -or `CommandsByName` with `sort`. - -For example this: - - -``` go -package main - -import ( - "log" - "os" - "sort" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "Language for the greeting", - }, - cli.StringFlag{ - Name: "config, c", - Usage: "Load configuration from `FILE`", - }, - } - - app.Commands = []cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - return nil - }, - }, - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(c *cli.Context) error { - return nil - }, - }, - } - - sort.Sort(cli.FlagsByName(app.Flags)) - sort.Sort(cli.CommandsByName(app.Commands)) - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Will result in help output like: - -``` ---config FILE, -c FILE Load configuration from FILE ---lang value, -l value Language for the greeting (default: "english") -``` - -#### Values from the Environment - -You can also have the default value set from the environment via `EnvVar`. e.g. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "APP_LANG", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -The `EnvVar` may also be given as a comma-delimited "cascade", where the first -environment variable that resolves is used as the default. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Values from files - -You can also have the default value set from file via `FilePath`. e.g. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "password, p", - Usage: "password for the mysql database", - FilePath: "/etc/mysql/password", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Note that default values set from file (e.g. `FilePath`) take precedence over -default values set from the environment (e.g. `EnvVar`). - -#### Values from alternate input sources (YAML, TOML, and others) - -There is a separate package altsrc that adds support for getting flag values -from other file input sources. - -Currently supported input source formats: -* YAML -* JSON -* TOML - -In order to get values for a flag from an alternate input source the following -code would be added to wrap an existing cli.Flag like below: - -``` go - altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) -``` - -Initialization must also occur for these flags. Below is an example initializing -getting data from a yaml file below. - -``` go - command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) -``` - -The code above will use the "load" string as a flag name to get the file name of -a yaml file from the cli.Context. It will then use that file name to initialize -the yaml input source for any flags that are defined on that command. As a note -the "load" flag used would also have to be defined on the command flags in order -for this code snippet to work. - -Currently only YAML, JSON, and TOML files are supported but developers can add support -for other input sources by implementing the altsrc.InputSourceContext for their -given sources. - -Here is a more complete sample of a command using YAML support: - - -``` go -package notmain - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" - "github.com/urfave/cli/altsrc" -) - -func main() { - app := cli.NewApp() - - flags := []cli.Flag{ - altsrc.NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}, - } - - app.Action = func(c *cli.Context) error { - fmt.Println("yaml ist rad") - return nil - } - - app.Before = altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")) - app.Flags = flags - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Precedence - -The precedence for flag value sources is as follows (highest to lowest): - -0. Command line flag value from user -0. Environment variable (if specified) -0. Configuration file (if specified) -0. Default defined on the flag - -### Subcommands - -Subcommands can be defined for a more git-like command line app. - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Commands = []cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(c *cli.Context) error { - fmt.Println("added task: ", c.Args().First()) - return nil - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - }, - { - Name: "template", - Aliases: []string{"t"}, - Usage: "options for task templates", - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(c *cli.Context) error { - fmt.Println("new task template: ", c.Args().First()) - return nil - }, - }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(c *cli.Context) error { - fmt.Println("removed task template: ", c.Args().First()) - return nil - }, - }, - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Subcommands categories - -For additional organization in apps that have many subcommands, you can -associate a category for each command to group them together in the help -output. - -E.g. - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Commands = []cli.Command{ - { - Name: "noop", - }, - { - Name: "add", - Category: "Template actions", - }, - { - Name: "remove", - Category: "Template actions", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Will include: - -``` -COMMANDS: - noop - - Template actions: - add - remove -``` - -### Exit code - -Calling `App.Run` will not automatically call `os.Exit`, which means that by -default the exit code will "fall through" to being `0`. An explicit exit code -may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a -`cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.Flags = []cli.Flag{ - cli.BoolFlag{ - Name: "ginger-crouton", - Usage: "Add ginger croutons to the soup", - }, - } - app.Action = func(ctx *cli.Context) error { - if !ctx.Bool("ginger-crouton") { - return cli.NewExitError("Ginger croutons are not in the soup", 86) - } - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Combining short options - -Traditional use of options using their shortnames look like this: - -``` -$ cmd -s -o -m "Some message" -``` - -Suppose you want users to be able to combine options with their shortnames. This -can be done using the `UseShortOptionHandling` bool in your app configuration, -or for individual commands by attaching it to the command configuration. For -example: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.UseShortOptionHandling = true - app.Commands = []cli.Command{ - { - Name: "short", - Usage: "complete a task on the list", - Flags: []cli.Flag{ - cli.BoolFlag{Name: "serve, s"}, - cli.BoolFlag{Name: "option, o"}, - cli.StringFlag{Name: "message, m"}, - }, - Action: func(c *cli.Context) error { - fmt.Println("serve:", c.Bool("serve")) - fmt.Println("option:", c.Bool("option")) - fmt.Println("message:", c.String("message")) - return nil - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -If your program has any number of bool flags such as `serve` and `option`, and -optionally one non-bool flag `message`, with the short options of `-s`, `-o`, -and `-m` respectively, setting `UseShortOptionHandling` will also support the -following syntax: - -``` -$ cmd -som "Some message" -``` - -If you enable `UseShortOptionHandling`, then you must not use any flags that -have a single leading `-` or this will result in failures. For example, -`-option` can no longer be used. Flags with two leading dashes (such as -`--options`) are still valid. - -### Bash Completion - -You can enable completion commands by setting the `EnableBashCompletion` -flag on the `App` object. By default, this setting will only auto-complete to -show an app's subcommands, but you can write your own completion methods for -the App or its subcommands. - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} - - app := cli.NewApp() - app.EnableBashCompletion = true - app.Commands = []cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - BashComplete: func(c *cli.Context) { - // This will complete if no args are passed - if c.NArg() > 0 { - return - } - for _, t := range tasks { - fmt.Println(t) - } - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Enabling - -Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while -setting the `PROG` variable to the name of your program: - -`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` - -#### Distribution - -Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename -it to the name of the program you wish to add autocomplete support for (or -automatically install it there if you are distributing a package). Don't forget -to source the file to make it active in the current shell. - -``` -sudo cp src/bash_autocomplete /etc/bash_completion.d/ -source /etc/bash_completion.d/ -``` - -Alternatively, you can just document that users should source the generic -`autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set -to the name of their program (as above). - -#### Customization - -The default bash completion flag (`--generate-bash-completion`) is defined as -`cli.BashCompletionFlag`, and may be redefined if desired, e.g.: - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - cli.BashCompletionFlag = cli.BoolFlag{ - Name: "compgen", - Hidden: true, - } - - app := cli.NewApp() - app.EnableBashCompletion = true - app.Commands = []cli.Command{ - { - Name: "wat", - }, - } - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Generated Help Text - -The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked -by the cli internals in order to print generated help text for the app, command, -or subcommand, and break execution. - -#### Customization - -All of the help text generation may be customized, and at multiple levels. The -templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and -`SubcommandHelpTemplate` which may be reassigned or augmented, and full override -is possible by assigning a compatible func to the `cli.HelpPrinter` variable, -e.g.: - - -``` go -package main - -import ( - "fmt" - "log" - "io" - "os" - - "github.com/urfave/cli" -) - -func main() { - // EXAMPLE: Append to an existing template - cli.AppHelpTemplate = fmt.Sprintf(`%s - -WEBSITE: http://awesometown.example.com - -SUPPORT: support@awesometown.example.com - -`, cli.AppHelpTemplate) - - // EXAMPLE: Override a template - cli.AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} -USAGE: - {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} - {{if len .Authors}} -AUTHOR: - {{range .Authors}}{{ . }}{{end}} - {{end}}{{if .Commands}} -COMMANDS: -{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} -GLOBAL OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{if .Copyright }} -COPYRIGHT: - {{.Copyright}} - {{end}}{{if .Version}} -VERSION: - {{.Version}} - {{end}} -` - - // EXAMPLE: Replace the `HelpPrinter` func - cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { - fmt.Println("Ha HA. I pwnd the help!!1") - } - - err := cli.NewApp().Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -The default flag may be customized to something other than `-h/--help` by -setting `cli.HelpFlag`, e.g.: - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - cli.HelpFlag = cli.BoolFlag{ - Name: "halp, haaaaalp", - Usage: "HALP", - EnvVar: "SHOW_HALP,HALPPLZ", - } - - err := cli.NewApp().Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Version Flag - -The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which -is checked by the cli internals in order to print the `App.Version` via -`cli.VersionPrinter` and break execution. - -#### Customization - -The default flag may be customized to something other than `-v/--version` by -setting `cli.VersionFlag`, e.g.: - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - cli.VersionFlag = cli.BoolFlag{ - Name: "print-version, V", - Usage: "print only the version", - } - - app := cli.NewApp() - app.Name = "partay" - app.Version = "19.99.0" - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -var ( - Revision = "fafafaf" -) - -func main() { - cli.VersionPrinter = func(c *cli.Context) { - fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) - } - - app := cli.NewApp() - app.Name = "partay" - app.Version = "19.99.0" - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Full API Example - -**Notice**: This is a contrived (functioning) example meant strictly for API -demonstration purposes. Use of one's imagination is encouraged. - - -``` go -package main - -import ( - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "os" - "time" - - "github.com/urfave/cli" -) - -func init() { - cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" - cli.CommandHelpTemplate += "\nYMMV\n" - cli.SubcommandHelpTemplate += "\nor something\n" - - cli.HelpFlag = cli.BoolFlag{Name: "halp"} - cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} - cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} - - cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { - fmt.Fprintf(w, "best of luck to you\n") - } - cli.VersionPrinter = func(c *cli.Context) { - fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) - } - cli.OsExiter = func(c int) { - fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) - } - cli.ErrWriter = ioutil.Discard - cli.FlagStringer = func(fl cli.Flag) string { - return fmt.Sprintf("\t\t%s", fl.GetName()) - } -} - -type hexWriter struct{} - -func (w *hexWriter) Write(p []byte) (int, error) { - for _, b := range p { - fmt.Printf("%x", b) - } - fmt.Printf("\n") - - return len(p), nil -} - -type genericType struct{ - s string -} - -func (g *genericType) Set(value string) error { - g.s = value - return nil -} - -func (g *genericType) String() string { - return g.s -} - -func main() { - app := cli.NewApp() - app.Name = "kənˈtrīv" - app.Version = "19.99.0" - app.Compiled = time.Now() - app.Authors = []cli.Author{ - cli.Author{ - Name: "Example Human", - Email: "human@example.com", - }, - } - app.Copyright = "(c) 1999 Serious Enterprise" - app.HelpName = "contrive" - app.Usage = "demonstrate available API" - app.UsageText = "contrive - demonstrating the available API" - app.ArgsUsage = "[args and such]" - app.Commands = []cli.Command{ - cli.Command{ - Name: "doo", - Aliases: []string{"do"}, - Category: "motion", - Usage: "do the doo", - UsageText: "doo - does the dooing", - Description: "no really, there is a lot of dooing to be done", - ArgsUsage: "[arrgh]", - Flags: []cli.Flag{ - cli.BoolFlag{Name: "forever, forevvarr"}, - }, - Subcommands: cli.Commands{ - cli.Command{ - Name: "wop", - Action: wopAction, - }, - }, - SkipFlagParsing: false, - HideHelp: false, - Hidden: false, - HelpName: "doo!", - BashComplete: func(c *cli.Context) { - fmt.Fprintf(c.App.Writer, "--better\n") - }, - Before: func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "brace for impact\n") - return nil - }, - After: func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") - return nil - }, - Action: func(c *cli.Context) error { - c.Command.FullName() - c.Command.HasName("wop") - c.Command.Names() - c.Command.VisibleFlags() - fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") - if c.Bool("forever") { - c.Command.Run(c) - } - return nil - }, - OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { - fmt.Fprintf(c.App.Writer, "for shame\n") - return err - }, - }, - } - app.Flags = []cli.Flag{ - cli.BoolFlag{Name: "fancy"}, - cli.BoolTFlag{Name: "fancier"}, - cli.DurationFlag{Name: "howlong, H", Value: time.Second * 3}, - cli.Float64Flag{Name: "howmuch"}, - cli.GenericFlag{Name: "wat", Value: &genericType{}}, - cli.Int64Flag{Name: "longdistance"}, - cli.Int64SliceFlag{Name: "intervals"}, - cli.IntFlag{Name: "distance"}, - cli.IntSliceFlag{Name: "times"}, - cli.StringFlag{Name: "dance-move, d"}, - cli.StringSliceFlag{Name: "names, N"}, - cli.UintFlag{Name: "age"}, - cli.Uint64Flag{Name: "bigage"}, - } - app.EnableBashCompletion = true - app.UseShortOptionHandling = true - app.HideHelp = false - app.HideVersion = false - app.BashComplete = func(c *cli.Context) { - fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") - } - app.Before = func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") - return nil - } - app.After = func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "Phew!\n") - return nil - } - app.CommandNotFound = func(c *cli.Context, command string) { - fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) - } - app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { - if isSubcommand { - return err - } - - fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) - return nil - } - app.Action = func(c *cli.Context) error { - cli.DefaultAppComplete(c) - cli.HandleExitCoder(errors.New("not an exit coder, though")) - cli.ShowAppHelp(c) - cli.ShowCommandCompletions(c, "nope") - cli.ShowCommandHelp(c, "also-nope") - cli.ShowCompletions(c) - cli.ShowSubcommandHelp(c) - cli.ShowVersion(c) - - categories := c.App.Categories() - categories.AddCommand("sounds", cli.Command{ - Name: "bloop", - }) - - for _, category := range c.App.Categories() { - fmt.Fprintf(c.App.Writer, "%s\n", category.Name) - fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) - fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) - } - - fmt.Printf("%#v\n", c.App.Command("doo")) - if c.Bool("infinite") { - c.App.Run([]string{"app", "doo", "wop"}) - } - - if c.Bool("forevar") { - c.App.RunAsSubcommand(c) - } - c.App.Setup() - fmt.Printf("%#v\n", c.App.VisibleCategories()) - fmt.Printf("%#v\n", c.App.VisibleCommands()) - fmt.Printf("%#v\n", c.App.VisibleFlags()) - - fmt.Printf("%#v\n", c.Args().First()) - if len(c.Args()) > 0 { - fmt.Printf("%#v\n", c.Args()[1]) - } - fmt.Printf("%#v\n", c.Args().Present()) - fmt.Printf("%#v\n", c.Args().Tail()) - - set := flag.NewFlagSet("contrive", 0) - nc := cli.NewContext(c.App, set, c) - - fmt.Printf("%#v\n", nc.Args()) - fmt.Printf("%#v\n", nc.Bool("nope")) - fmt.Printf("%#v\n", nc.BoolT("nerp")) - fmt.Printf("%#v\n", nc.Duration("howlong")) - fmt.Printf("%#v\n", nc.Float64("hay")) - fmt.Printf("%#v\n", nc.Generic("bloop")) - fmt.Printf("%#v\n", nc.Int64("bonk")) - fmt.Printf("%#v\n", nc.Int64Slice("burnks")) - fmt.Printf("%#v\n", nc.Int("bips")) - fmt.Printf("%#v\n", nc.IntSlice("blups")) - fmt.Printf("%#v\n", nc.String("snurt")) - fmt.Printf("%#v\n", nc.StringSlice("snurkles")) - fmt.Printf("%#v\n", nc.Uint("flub")) - fmt.Printf("%#v\n", nc.Uint64("florb")) - fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) - fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) - fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) - fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) - fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) - fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) - fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) - fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) - fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) - - fmt.Printf("%#v\n", nc.FlagNames()) - fmt.Printf("%#v\n", nc.GlobalFlagNames()) - fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) - fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) - fmt.Printf("%#v\n", nc.NArg()) - fmt.Printf("%#v\n", nc.NumFlags()) - fmt.Printf("%#v\n", nc.Parent()) - - nc.Set("wat", "also-nope") - - ec := cli.NewExitError("ohwell", 86) - fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) - fmt.Printf("made it!\n") - return nil - } - - if os.Getenv("HEXY") != "" { - app.Writer = &hexWriter{} - app.ErrWriter = &hexWriter{} - } - - app.Metadata = map[string]interface{}{ - "layers": "many", - "explicable": false, - "whatever-values": 19.99, - } - - - // ignore error so we don't exit non-zero and break gfmrun README example tests - _ = app.Run(os.Args) -} - -func wopAction(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") - return nil -} -``` - -## Migrating to V2 - -There are a small set of breaking changes between v1 and v2. -Converting is relatively straightforward and typically takes less than -an hour. Specific steps are included in -[Migration Guide: v1 to v2](../migrate-v1-to-v2.md). diff --git a/docs/v1/migrating-to-v2.md b/docs/v1/migrating-to-v2.md new file mode 100644 index 0000000..059b7db --- /dev/null +++ b/docs/v1/migrating-to-v2.md @@ -0,0 +1,9 @@ +--- +tags: + - v1 +--- + +There are a small set of breaking changes between v1 and v2. +Converting is relatively straightforward and typically takes less than +an hour. Specific steps are included in +[Migration Guide: v1 to v2](../migrate-v1-to-v2.md). diff --git a/docs/v2/examples/arguments.md b/docs/v2/examples/arguments.md new file mode 100644 index 0000000..88dd460 --- /dev/null +++ b/docs/v2/examples/arguments.md @@ -0,0 +1,36 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Action: func(cCtx *cli.Context) error { + fmt.Printf("Hello %q", cCtx.Args().Get(0)) + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v2/examples/bash-completions.md b/docs/v2/examples/bash-completions.md new file mode 100644 index 0000000..e9265f4 --- /dev/null +++ b/docs/v2/examples/bash-completions.md @@ -0,0 +1,257 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +You can enable completion commands by setting the `EnableBashCompletion` flag on +the `App` object to `true`. By default, this setting will allow auto-completion +for an app's subcommands, but you can write your own completion methods for the +App or its subcommands as well. + +#### Default auto-completion + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + EnableBashCompletion: true, + Commands: []*cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(cCtx *cli.Context) error { + fmt.Println("added task: ", cCtx.Args().First()) + return nil + }, + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(cCtx *cli.Context) error { + fmt.Println("completed task: ", cCtx.Args().First()) + return nil + }, + }, + { + Name: "template", + Aliases: []string{"t"}, + Usage: "options for task templates", + Subcommands: []*cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(cCtx *cli.Context) error { + fmt.Println("new task template: ", cCtx.Args().First()) + return nil + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(cCtx *cli.Context) error { + fmt.Println("removed task template: ", cCtx.Args().First()) + return nil + }, + }, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` +![](/docs/v2/images/default-bash-autocomplete.gif) + +#### Custom auto-completion + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} + + app := &cli.App{ + EnableBashCompletion: true, + Commands: []*cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(cCtx *cli.Context) error { + fmt.Println("completed task: ", cCtx.Args().First()) + return nil + }, + BashComplete: func(cCtx *cli.Context) { + // This will complete if no args are passed + if cCtx.NArg() > 0 { + return + } + for _, t := range tasks { + fmt.Println(t) + } + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` +![](/docs/v2/images/custom-bash-autocomplete.gif) + +#### Enabling + +To enable auto-completion for the current shell session, a bash script, +`autocomplete/bash_autocomplete` is included in this repo. + +To use `autocomplete/bash_autocomplete` set an environment variable named `PROG` +to the name of your program and then `source` the +`autocomplete/bash_autocomplete` file. + +For example, if your cli program is called `myprogram`: + +```sh-session +$ PROG=myprogram source path/to/cli/autocomplete/bash_autocomplete +``` + +Auto-completion is now enabled for the current shell, but will not persist into +a new shell. + +#### Distribution and Persistent Autocompletion + +Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename +it to the name of the program you wish to add autocomplete support for (or +automatically install it there if you are distributing a package). Don't forget +to source the file or restart your shell to activate the auto-completion. + +```sh-session +$ sudo cp path/to/autocomplete/bash_autocomplete /etc/bash_completion.d/ +$ source /etc/bash_completion.d/ +``` + +Alternatively, you can just document that users should `source` the generic +`autocomplete/bash_autocomplete` and set `$PROG` within their bash configuration +file, adding these lines: + +```sh-session +$ PROG= +$ source path/to/cli/autocomplete/bash_autocomplete +``` + +Keep in mind that if they are enabling auto-completion for more than one +program, they will need to set `PROG` and source +`autocomplete/bash_autocomplete` for each program, like so: + +```sh-session +$ PROG= +$ source path/to/cli/autocomplete/bash_autocomplete + +$ PROG= +$ source path/to/cli/autocomplete/bash_autocomplete +``` + +#### Customization + +The default shell completion flag (`--generate-bash-completion`) is defined as +`cli.EnableBashCompletion`, and may be redefined if desired, e.g.: + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + EnableBashCompletion: true, + Commands: []*cli.Command{ + { + Name: "wat", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +#### ZSH Support + +Auto-completion for ZSH is also supported using the +`autocomplete/zsh_autocomplete` file included in this repo. One environment +variable is used, `PROG`. Set `PROG` to the program name as before, and then +`source path/to/autocomplete/zsh_autocomplete`. Adding the following lines to +your ZSH configuration file (usually `.zshrc`) will allow the auto-completion to +persist across new shells: + +```sh-session +$ PROG= +$ source path/to/autocomplete/zsh_autocomplete +``` + +#### ZSH default auto-complete example +![](/docs/v2/images/default-zsh-autocomplete.gif) + +#### ZSH custom auto-complete example +![](/docs/v2/images/custom-zsh-autocomplete.gif) + +#### PowerShell Support + +Auto-completion for PowerShell is also supported using the +`autocomplete/powershell_autocomplete.ps1` file included in this repo. + +Rename the script to `.ps1` and move it anywhere in your file +system. The location of script does not matter, only the file name of the +script has to match the your program's binary name. + +To activate it, enter: + +```powershell +& path/to/autocomplete/.ps1 +``` + +To persist across new shells, open the PowerShell profile (with `code $profile` +or `notepad $profile`) and add the line: + +```powershell +& path/to/autocomplete/.ps1 +``` diff --git a/docs/v2/examples/combining-short-options.md b/docs/v2/examples/combining-short-options.md new file mode 100644 index 0000000..f502877 --- /dev/null +++ b/docs/v2/examples/combining-short-options.md @@ -0,0 +1,74 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +Traditional use of options using their shortnames look like this: + +```sh-session +$ cmd -s -o -m "Some message" +``` + +Suppose you want users to be able to combine options with their shortnames. This +can be done using the `UseShortOptionHandling` bool in your app configuration, +or for individual commands by attaching it to the command configuration. For +example: + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + UseShortOptionHandling: true, + Commands: []*cli.Command{ + { + Name: "short", + Usage: "complete a task on the list", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}}, + &cli.BoolFlag{Name: "option", Aliases: []string{"o"}}, + &cli.StringFlag{Name: "message", Aliases: []string{"m"}}, + }, + Action: func(cCtx *cli.Context) error { + fmt.Println("serve:", cCtx.Bool("serve")) + fmt.Println("option:", cCtx.Bool("option")) + fmt.Println("message:", cCtx.String("message")) + return nil + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +If your program has any number of bool flags such as `serve` and `option`, and +optionally one non-bool flag `message`, with the short options of `-s`, `-o`, +and `-m` respectively, setting `UseShortOptionHandling` will also support the +following syntax: + +```sh-session +$ cmd -som "Some message" +``` + +If you enable `UseShortOptionHandling`, then you must not use any flags that +have a single leading `-` or this will result in failures. For example, +`-option` can no longer be used. Flags with two leading dashes (such as +`--options`) are still valid. diff --git a/docs/v2/examples/exit-codes.md b/docs/v2/examples/exit-codes.md new file mode 100644 index 0000000..75db781 --- /dev/null +++ b/docs/v2/examples/exit-codes.md @@ -0,0 +1,45 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +Calling `App.Run` will not automatically call `os.Exit`, which means that by +default the exit code will "fall through" to being `0`. An explicit exit code +may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a +`cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "ginger-crouton", + Usage: "is it in the soup?", + }, + }, + Action: func(ctx *cli.Context) error { + if !ctx.Bool("ginger-crouton") { + return cli.Exit("Ginger croutons are not in the soup", 86) + } + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v2/examples/flags.md b/docs/v2/examples/flags.md new file mode 100644 index 0000000..dbb41ea --- /dev/null +++ b/docs/v2/examples/flags.md @@ -0,0 +1,597 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +Setting and querying flags is simple. + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, + }, + Action: func(cCtx *cli.Context) error { + name := "Nefertiti" + if cCtx.NArg() > 0 { + name = cCtx.Args().Get(0) + } + if cCtx.String("lang") == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +You can also set a destination variable for a flag, to which the content will be +scanned. + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + var language string + + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Destination: &language, + }, + }, + Action: func(cCtx *cli.Context) error { + name := "someone" + if cCtx.NArg() > 0 { + name = cCtx.Args().Get(0) + } + if language == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +See full list of flags at https://pkg.go.dev/github.com/urfave/cli/v2 + +For bool flags you can specify the flag multiple times to get a count(e.g -v -v -v or -vvv) + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + var count int + + app := &cli.App{ + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "foo", + Usage: "foo greeting", + Count: &count, + }, + }, + Action: func(cCtx *cli.Context) error { + fmt.Println("count", count) + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +#### Placeholder Values + +Sometimes it's useful to specify a flag's value within the usage string itself. +Such placeholders are indicated with back quotes. + +For example this: + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "Load configuration from `FILE`", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +``` + +Note that only the first placeholder is used. Subsequent back-quoted words will +be left as-is. + +#### Alternate Names + +You can set alternate (or short) names for flags by providing a comma-delimited +list for the `Name`. e.g. + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "lang", + Aliases: []string{"l"}, + Value: "english", + Usage: "language for the greeting", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +That flag can then be set with `--lang spanish` or `-l spanish`. Note that +giving two different forms of the same flag in the same command invocation is an +error. + +#### Ordering + +Flags for the application and commands are shown in the order they are defined. +However, it's possible to sort them from outside this library by using `FlagsByName` +or `CommandsByName` with `sort`. + +For example this: + + +```go +package main + +import ( + "log" + "os" + "sort" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "lang", + Aliases: []string{"l"}, + Value: "english", + Usage: "Language for the greeting", + }, + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "Load configuration from `FILE`", + }, + }, + Commands: []*cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(*cli.Context) error { + return nil + }, + }, + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(*cli.Context) error { + return nil + }, + }, + }, + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +--lang value, -l value Language for the greeting (default: "english") +``` + +#### Values from the Environment + +You can also have the default value set from the environment via `EnvVars`. e.g. + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "lang", + Aliases: []string{"l"}, + Value: "english", + Usage: "language for the greeting", + EnvVars: []string{"APP_LANG"}, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +If `EnvVars` contains more than one string, the first environment variable that +resolves is used. + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "lang", + Aliases: []string{"l"}, + Value: "english", + Usage: "language for the greeting", + EnvVars: []string{"LEGACY_COMPAT_LANG", "APP_LANG", "LANG"}, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +#### Values from files + +You can also have the default value set from file via `FilePath`. e.g. + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "password", + Aliases: []string{"p"}, + Usage: "password for the mysql database", + FilePath: "/etc/mysql/password", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +Note that default values set from file (e.g. `FilePath`) take precedence over +default values set from the environment (e.g. `EnvVar`). + +#### Values from alternate input sources (YAML, TOML, and others) + +There is a separate package altsrc that adds support for getting flag values +from other file input sources. + +Currently supported input source formats: + +- YAML +- JSON +- TOML + +In order to get values for a flag from an alternate input source the following +code would be added to wrap an existing cli.Flag like below: + +```go + // --- >8 --- + altsrc.NewIntFlag(&cli.IntFlag{Name: "test"}) +``` + +Initialization must also occur for these flags. Below is an example initializing +getting data from a yaml file below. + +```go + // --- >8 --- + command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) +``` + +The code above will use the "load" string as a flag name to get the file name of +a yaml file from the cli.Context. It will then use that file name to initialize +the yaml input source for any flags that are defined on that command. As a note +the "load" flag used would also have to be defined on the command flags in order +for this code snippet to work. + +Currently only YAML, JSON, and TOML files are supported but developers can add +support for other input sources by implementing the altsrc.InputSourceContext +for their given sources. + +Here is a more complete sample of a command using YAML support: + + +```go +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" +) + +func main() { + flags := []cli.Flag{ + altsrc.NewIntFlag(&cli.IntFlag{Name: "test"}), + &cli.StringFlag{Name: "load"}, + } + + app := &cli.App{ + Action: func(*cli.Context) error { + fmt.Println("--test value.*default: 0") + return nil + }, + Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), + Flags: flags, + } + + app.Run(os.Args) +} +``` + +#### Required Flags + +You can make a flag required by setting the `Required` field to `true`. If a user +does not provide a required flag, they will be shown an error message. + +Take for example this app that requires the `lang` flag: + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Required: true, + }, + }, + Action: func(cCtx *cli.Context) error { + output := "Hello" + if cCtx.String("lang") == "spanish" { + output = "Hola" + } + fmt.Println(output) + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +If the app is run without the `lang` flag, the user will see the following message + +``` +Required flag "lang" not set +``` + +#### Default Values for help output + +Sometimes it's useful to specify a flag's default help-text value within the +flag declaration. This can be useful if the default value for a flag is a +computed value. The default value can be set via the `DefaultText` struct field. + +For example this: + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "port", + Usage: "Use a randomized port", + Value: 0, + DefaultText: "random", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +Will result in help output like: + +``` +--port value Use a randomized port (default: random) +``` + +#### Precedence + +The precedence for flag value sources is as follows (highest to lowest): + +0. Command line flag value from user +0. Environment variable (if specified) +0. Configuration file (if specified) +0. Default defined on the flag diff --git a/docs/v2/examples/full-api-example.md b/docs/v2/examples/full-api-example.md new file mode 100644 index 0000000..289cffe --- /dev/null +++ b/docs/v2/examples/full-api-example.md @@ -0,0 +1,262 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +**Notice**: This is a contrived (functioning) example meant strictly for API +demonstration purposes. Use of one's imagination is encouraged. + + +```go +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/urfave/cli/v2" +) + +func init() { + cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" + cli.CommandHelpTemplate += "\nYMMV\n" + cli.SubcommandHelpTemplate += "\nor something\n" + + cli.HelpFlag = &cli.BoolFlag{Name: "halp"} + cli.VersionFlag = &cli.BoolFlag{Name: "print-version", Aliases: []string{"V"}} + + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Fprintf(w, "best of luck to you\n") + } + cli.VersionPrinter = func(cCtx *cli.Context) { + fmt.Fprintf(cCtx.App.Writer, "version=%s\n", cCtx.App.Version) + } + cli.OsExiter = func(cCtx int) { + fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", cCtx) + } + cli.ErrWriter = ioutil.Discard + cli.FlagStringer = func(fl cli.Flag) string { + return fmt.Sprintf("\t\t%s", fl.Names()[0]) + } +} + +type hexWriter struct{} + +func (w *hexWriter) Write(p []byte) (int, error) { + for _, b := range p { + fmt.Printf("%x", b) + } + fmt.Printf("\n") + + return len(p), nil +} + +type genericType struct { + s string +} + +func (g *genericType) Set(value string) error { + g.s = value + return nil +} + +func (g *genericType) String() string { + return g.s +} + +func main() { + app := &cli.App{ + Name: "kənˈtrīv", + Version: "v19.99.0", + Compiled: time.Now(), + Authors: []*cli.Author{ + &cli.Author{ + Name: "Example Human", + Email: "human@example.com", + }, + }, + Copyright: "(c) 1999 Serious Enterprise", + HelpName: "contrive", + Usage: "demonstrate available API", + UsageText: "contrive - demonstrating the available API", + ArgsUsage: "[args and such]", + Commands: []*cli.Command{ + &cli.Command{ + Name: "doo", + Aliases: []string{"do"}, + Category: "motion", + Usage: "do the doo", + UsageText: "doo - does the dooing", + Description: "no really, there is a lot of dooing to be done", + ArgsUsage: "[arrgh]", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "forever", Aliases: []string{"forevvarr"}}, + }, + Subcommands: []*cli.Command{ + &cli.Command{ + Name: "wop", + Action: wopAction, + }, + }, + SkipFlagParsing: false, + HideHelp: false, + Hidden: false, + HelpName: "doo!", + BashComplete: func(cCtx *cli.Context) { + fmt.Fprintf(cCtx.App.Writer, "--better\n") + }, + Before: func(cCtx *cli.Context) error { + fmt.Fprintf(cCtx.App.Writer, "brace for impact\n") + return nil + }, + After: func(cCtx *cli.Context) error { + fmt.Fprintf(cCtx.App.Writer, "did we lose anyone?\n") + return nil + }, + Action: func(cCtx *cli.Context) error { + cCtx.Command.FullName() + cCtx.Command.HasName("wop") + cCtx.Command.Names() + cCtx.Command.VisibleFlags() + fmt.Fprintf(cCtx.App.Writer, "dodododododoodododddooooododododooo\n") + if cCtx.Bool("forever") { + cCtx.Command.Run(cCtx) + } + return nil + }, + OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { + fmt.Fprintf(cCtx.App.Writer, "for shame\n") + return err + }, + }, + }, + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "fancy"}, + &cli.BoolFlag{Value: true, Name: "fancier"}, + &cli.DurationFlag{Name: "howlong", Aliases: []string{"H"}, Value: time.Second * 3}, + &cli.Float64Flag{Name: "howmuch"}, + &cli.GenericFlag{Name: "wat", Value: &genericType{}}, + &cli.Int64Flag{Name: "longdistance"}, + &cli.Int64SliceFlag{Name: "intervals"}, + &cli.IntFlag{Name: "distance"}, + &cli.IntSliceFlag{Name: "times"}, + &cli.StringFlag{Name: "dance-move", Aliases: []string{"d"}}, + &cli.StringSliceFlag{Name: "names", Aliases: []string{"N"}}, + &cli.UintFlag{Name: "age"}, + &cli.Uint64Flag{Name: "bigage"}, + }, + EnableBashCompletion: true, + HideHelp: false, + HideVersion: false, + BashComplete: func(cCtx *cli.Context) { + fmt.Fprintf(cCtx.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") + }, + Before: func(cCtx *cli.Context) error { + fmt.Fprintf(cCtx.App.Writer, "HEEEERE GOES\n") + return nil + }, + After: func(cCtx *cli.Context) error { + fmt.Fprintf(cCtx.App.Writer, "Phew!\n") + return nil + }, + CommandNotFound: func(cCtx *cli.Context, command string) { + fmt.Fprintf(cCtx.App.Writer, "Thar be no %q here.\n", command) + }, + OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { + if isSubcommand { + return err + } + + fmt.Fprintf(cCtx.App.Writer, "WRONG: %#v\n", err) + return nil + }, + Action: func(cCtx *cli.Context) error { + cli.DefaultAppComplete(cCtx) + cli.HandleExitCoder(errors.New("not an exit coder, though")) + cli.ShowAppHelp(cCtx) + cli.ShowCommandCompletions(cCtx, "nope") + cli.ShowCommandHelp(cCtx, "also-nope") + cli.ShowCompletions(cCtx) + cli.ShowSubcommandHelp(cCtx) + cli.ShowVersion(cCtx) + + fmt.Printf("%#v\n", cCtx.App.Command("doo")) + if cCtx.Bool("infinite") { + cCtx.App.Run([]string{"app", "doo", "wop"}) + } + + if cCtx.Bool("forevar") { + cCtx.App.RunAsSubcommand(cCtx) + } + cCtx.App.Setup() + fmt.Printf("%#v\n", cCtx.App.VisibleCategories()) + fmt.Printf("%#v\n", cCtx.App.VisibleCommands()) + fmt.Printf("%#v\n", cCtx.App.VisibleFlags()) + + fmt.Printf("%#v\n", cCtx.Args().First()) + if cCtx.Args().Len() > 0 { + fmt.Printf("%#v\n", cCtx.Args().Get(1)) + } + fmt.Printf("%#v\n", cCtx.Args().Present()) + fmt.Printf("%#v\n", cCtx.Args().Tail()) + + set := flag.NewFlagSet("contrive", 0) + nc := cli.NewContext(cCtx.App, set, cCtx) + + fmt.Printf("%#v\n", nc.Args()) + fmt.Printf("%#v\n", nc.Bool("nope")) + fmt.Printf("%#v\n", !nc.Bool("nerp")) + fmt.Printf("%#v\n", nc.Duration("howlong")) + fmt.Printf("%#v\n", nc.Float64("hay")) + fmt.Printf("%#v\n", nc.Generic("bloop")) + fmt.Printf("%#v\n", nc.Int64("bonk")) + fmt.Printf("%#v\n", nc.Int64Slice("burnks")) + fmt.Printf("%#v\n", nc.Int("bips")) + fmt.Printf("%#v\n", nc.IntSlice("blups")) + fmt.Printf("%#v\n", nc.String("snurt")) + fmt.Printf("%#v\n", nc.StringSlice("snurkles")) + fmt.Printf("%#v\n", nc.Uint("flub")) + fmt.Printf("%#v\n", nc.Uint64("florb")) + + fmt.Printf("%#v\n", nc.FlagNames()) + fmt.Printf("%#v\n", nc.IsSet("wat")) + fmt.Printf("%#v\n", nc.Set("wat", "nope")) + fmt.Printf("%#v\n", nc.NArg()) + fmt.Printf("%#v\n", nc.NumFlags()) + fmt.Printf("%#v\n", nc.Lineage()[1]) + nc.Set("wat", "also-nope") + + ec := cli.Exit("ohwell", 86) + fmt.Fprintf(cCtx.App.Writer, "%d", ec.ExitCode()) + fmt.Printf("made it!\n") + return ec + }, + Metadata: map[string]interface{}{ + "layers": "many", + "explicable": false, + "whatever-values": 19.99, + }, + } + + if os.Getenv("HEXY") != "" { + app.Writer = &hexWriter{} + app.ErrWriter = &hexWriter{} + } + + app.Run(os.Args) +} + +func wopAction(cCtx *cli.Context) error { + fmt.Fprintf(cCtx.App.Writer, ":wave: over here, eh\n") + return nil +} +``` diff --git a/docs/v2/examples/generated-help-text.md b/docs/v2/examples/generated-help-text.md new file mode 100644 index 0000000..f733b96 --- /dev/null +++ b/docs/v2/examples/generated-help-text.md @@ -0,0 +1,101 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked +by the cli internals in order to print generated help text for the app, command, +or subcommand, and break execution. + +#### Customization + +All of the help text generation may be customized, and at multiple levels. The +templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and +`SubcommandHelpTemplate` which may be reassigned or augmented, and full override +is possible by assigning a compatible func to the `cli.HelpPrinter` variable, +e.g.: + + +```go +package main + +import ( + "fmt" + "io" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + // EXAMPLE: Append to an existing template + cli.AppHelpTemplate = fmt.Sprintf(`%s + +WEBSITE: http://awesometown.example.com + +SUPPORT: support@awesometown.example.com + +`, cli.AppHelpTemplate) + + // EXAMPLE: Override a template + cli.AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} +USAGE: + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if len .Authors}} +AUTHOR: + {{range .Authors}}{{ . }}{{end}} + {{end}}{{if .Commands}} +COMMANDS: +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +GLOBAL OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}}{{if .Copyright }} +COPYRIGHT: + {{.Copyright}} + {{end}}{{if .Version}} +VERSION: + {{.Version}} + {{end}} +` + + // EXAMPLE: Replace the `HelpPrinter` func + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Println("Ha HA. I pwnd the help!!1") + } + + (&cli.App{}).Run(os.Args) +} +``` + +The default flag may be customized to something other than `-h/--help` by +setting `cli.HelpFlag`, e.g.: + + +```go +package main + +import ( + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + cli.HelpFlag = &cli.BoolFlag{ + Name: "haaaaalp", + Aliases: []string{"halp"}, + Usage: "HALP", + EnvVars: []string{"SHOW_HALP", "HALPPLZ"}, + } + + (&cli.App{}).Run(os.Args) +} +``` diff --git a/docs/v2/examples/greet.md b/docs/v2/examples/greet.md new file mode 100644 index 0000000..aee4b0c --- /dev/null +++ b/docs/v2/examples/greet.md @@ -0,0 +1,73 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +Being a programmer can be a lonely job. Thankfully by the power of automation +that is not the case! Let's create a greeter app to fend off our demons of +loneliness! + +Start by creating a directory named `greet`, and within it, add a file, +`greet.go` with the following code in it: + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "greet", + Usage: "fight the loneliness!", + Action: func(*cli.Context) error { + fmt.Println("Hello friend!") + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +Install our command to the `$GOPATH/bin` directory: + +```sh-session +$ go install +``` + +Finally run our new command: + +```sh-session +$ greet +Hello friend! +``` + +cli also generates neat help text: + +```sh-session +$ greet help +NAME: + greet - fight the loneliness! + +USAGE: + greet [global options] command [command options] [arguments...] + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS + --help, -h show help (default: false) +``` diff --git a/docs/v2/examples/subcommands-categories.md b/docs/v2/examples/subcommands-categories.md new file mode 100644 index 0000000..5f0b8d2 --- /dev/null +++ b/docs/v2/examples/subcommands-categories.md @@ -0,0 +1,54 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +For additional organization in apps that have many subcommands, you can +associate a category for each command to group them together in the help +output, e.g.: + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Commands: []*cli.Command{ + { + Name: "noop", + }, + { + Name: "add", + Category: "template", + }, + { + Name: "remove", + Category: "template", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +Will include: + +``` +COMMANDS: + noop + + Template actions: + add + remove +``` diff --git a/docs/v2/examples/subcommands.md b/docs/v2/examples/subcommands.md new file mode 100644 index 0000000..b054622 --- /dev/null +++ b/docs/v2/examples/subcommands.md @@ -0,0 +1,76 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +Subcommands can be defined for a more git-like command line app. + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Commands: []*cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(cCtx *cli.Context) error { + fmt.Println("added task: ", cCtx.Args().First()) + return nil + }, + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(cCtx *cli.Context) error { + fmt.Println("completed task: ", cCtx.Args().First()) + return nil + }, + }, + { + Name: "template", + Aliases: []string{"t"}, + Usage: "options for task templates", + Subcommands: []*cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(cCtx *cli.Context) error { + fmt.Println("new task template: ", cCtx.Args().First()) + return nil + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(cCtx *cli.Context) error { + fmt.Println("removed task template: ", cCtx.Args().First()) + return nil + }, + }, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` diff --git a/docs/v2/examples/suggestions.md b/docs/v2/examples/suggestions.md new file mode 100644 index 0000000..9a0cd3f --- /dev/null +++ b/docs/v2/examples/suggestions.md @@ -0,0 +1,11 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +To enable flag and command suggestions, set `app.Suggest = true`. If the suggest +feature is enabled, then the help output of the corresponding command will +provide an appropriate suggestion for the provided flag or subcommand if +available. diff --git a/docs/v2/examples/timestamp-flag.md b/docs/v2/examples/timestamp-flag.md new file mode 100644 index 0000000..4e19adf --- /dev/null +++ b/docs/v2/examples/timestamp-flag.md @@ -0,0 +1,64 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +Using the timestamp flag is simple. Please refer to +[`time.Parse`](https://golang.org/pkg/time/#example_Parse) to get possible +formats. + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.TimestampFlag{Name: "meeting", Layout: "2006-01-02T15:04:05"}, + }, + Action: func(cCtx *cli.Context) error { + fmt.Printf("%s", cCtx.Timestamp("meeting").String()) + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +In this example the flag could be used like this: + +```sh-session +$ myapp --meeting 2019-08-12T15:04:05 +``` + +When the layout doesn't contain timezones, timestamp will render with UTC. To +change behavior, a default timezone can be provided with flag definition: + +```go +app := &cli.App{ + Flags: []cli.Flag{ + &cli.TimestampFlag{Name: "meeting", Layout: "2006-01-02T15:04:05", Timezone: time.Local}, + }, +} +``` + +(time.Local contains the system's local time zone.) + +Side note: quotes may be necessary around the date depending on your layout (if +you have spaces for instance) diff --git a/docs/v2/examples/version-flag.md b/docs/v2/examples/version-flag.md new file mode 100644 index 0000000..af0508c --- /dev/null +++ b/docs/v2/examples/version-flag.md @@ -0,0 +1,77 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which +is checked by the cli internals in order to print the `App.Version` via +`cli.VersionPrinter` and break execution. + +#### Customization + +The default flag may be customized to something other than `-v/--version` by +setting `cli.VersionFlag`, e.g.: + + +```go +package main + +import ( + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + cli.VersionFlag = &cli.BoolFlag{ + Name: "print-version", + Aliases: []string{"V"}, + Usage: "print only the version", + } + + app := &cli.App{ + Name: "partay", + Version: "v19.99.0", + } + app.Run(os.Args) +} +``` + +Alternatively, the version printer at `cli.VersionPrinter` may be overridden, +e.g.: + + +```go +package main + +import ( + "fmt" + "os" + + "github.com/urfave/cli/v2" +) + +var ( + Revision = "fafafaf" +) + +func main() { + cli.VersionPrinter = func(cCtx *cli.Context) { + fmt.Printf("version=%s revision=%s\n", cCtx.App.Version, Revision) + } + + app := &cli.App{ + Name: "partay", + Version: "v19.99.0", + } + app.Run(os.Args) +} +``` diff --git a/docs/v2/getting-started.md b/docs/v2/getting-started.md new file mode 100644 index 0000000..1d631cf --- /dev/null +++ b/docs/v2/getting-started.md @@ -0,0 +1,63 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +One of the philosophies behind cli is that an API should be playful and full of +discovery. So a cli app can be as little as one line of code in `main()`. + + +```go +package main + +import ( + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + (&cli.App{}).Run(os.Args) +} +``` + +This app will run and show help text, but is not very useful. Let's give an +action to execute and some help documentation: + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "boom", + Usage: "make an explosive entrance", + Action: func(*cli.Context) error { + fmt.Println("boom! I say!") + return nil + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatal(err) + } +} +``` + +Running this already gives you a ton of functionality, plus support for things +like subcommands and flags, which are covered below. diff --git a/docs/v2/index.md b/docs/v2/index.md deleted file mode 120000 index 9d0493a..0000000 --- a/docs/v2/index.md +++ /dev/null @@ -1 +0,0 @@ -manual.md \ No newline at end of file diff --git a/docs/v2/manual.md b/docs/v2/manual.md deleted file mode 100644 index 475e047..0000000 --- a/docs/v2/manual.md +++ /dev/null @@ -1,1703 +0,0 @@ -# v2 guide - -## Getting Started - -One of the philosophies behind cli is that an API should be playful and full of -discovery. So a cli app can be as little as one line of code in `main()`. - - -```go -package main - -import ( - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - (&cli.App{}).Run(os.Args) -} -``` - -This app will run and show help text, but is not very useful. Let's give an -action to execute and some help documentation: - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Name: "boom", - Usage: "make an explosive entrance", - Action: func(*cli.Context) error { - fmt.Println("boom! I say!") - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -Running this already gives you a ton of functionality, plus support for things -like subcommands and flags, which are covered below. - -## Examples - -Being a programmer can be a lonely job. Thankfully by the power of automation -that is not the case! Let's create a greeter app to fend off our demons of -loneliness! - -Start by creating a directory named `greet`, and within it, add a file, -`greet.go` with the following code in it: - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Name: "greet", - Usage: "fight the loneliness!", - Action: func(*cli.Context) error { - fmt.Println("Hello friend!") - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -Install our command to the `$GOPATH/bin` directory: - -```sh-session -$ go install -``` - -Finally run our new command: - -```sh-session -$ greet -Hello friend! -``` - -cli also generates neat help text: - -```sh-session -$ greet help -NAME: - greet - fight the loneliness! - -USAGE: - greet [global options] command [command options] [arguments...] - -COMMANDS: - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS - --help, -h show help (default: false) -``` - -### Arguments - -You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Action: func(cCtx *cli.Context) error { - fmt.Printf("Hello %q", cCtx.Args().Get(0)) - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -### Flags - -Setting and querying flags is simple. - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - }, - }, - Action: func(cCtx *cli.Context) error { - name := "Nefertiti" - if cCtx.NArg() > 0 { - name = cCtx.Args().Get(0) - } - if cCtx.String("lang") == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) - } - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -You can also set a destination variable for a flag, to which the content will be -scanned. - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - var language string - - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - Destination: &language, - }, - }, - Action: func(cCtx *cli.Context) error { - name := "someone" - if cCtx.NArg() > 0 { - name = cCtx.Args().Get(0) - } - if language == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) - } - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -See full list of flags at https://pkg.go.dev/github.com/urfave/cli/v2 - -#### Placeholder Values - -Sometimes it's useful to specify a flag's value within the usage string itself. -Such placeholders are indicated with back quotes. - -For example this: - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Usage: "Load configuration from `FILE`", - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -Will result in help output like: - -``` ---config FILE, -c FILE Load configuration from FILE -``` - -Note that only the first placeholder is used. Subsequent back-quoted words will -be left as-is. - -#### Alternate Names - -You can set alternate (or short) names for flags by providing a comma-delimited -list for the `Name`. e.g. - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Aliases: []string{"l"}, - Value: "english", - Usage: "language for the greeting", - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -That flag can then be set with `--lang spanish` or `-l spanish`. Note that -giving two different forms of the same flag in the same command invocation is an -error. - -#### Ordering - -Flags for the application and commands are shown in the order they are defined. -However, it's possible to sort them from outside this library by using `FlagsByName` -or `CommandsByName` with `sort`. - -For example this: - - -```go -package main - -import ( - "log" - "os" - "sort" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Aliases: []string{"l"}, - Value: "english", - Usage: "Language for the greeting", - }, - &cli.StringFlag{ - Name: "config", - Aliases: []string{"c"}, - Usage: "Load configuration from `FILE`", - }, - }, - Commands: []*cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(*cli.Context) error { - return nil - }, - }, - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(*cli.Context) error { - return nil - }, - }, - }, - } - - sort.Sort(cli.FlagsByName(app.Flags)) - sort.Sort(cli.CommandsByName(app.Commands)) - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -Will result in help output like: - -``` ---config FILE, -c FILE Load configuration from FILE ---lang value, -l value Language for the greeting (default: "english") -``` - -#### Values from the Environment - -You can also have the default value set from the environment via `EnvVars`. e.g. - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Aliases: []string{"l"}, - Value: "english", - Usage: "language for the greeting", - EnvVars: []string{"APP_LANG"}, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -If `EnvVars` contains more than one string, the first environment variable that -resolves is used. - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Aliases: []string{"l"}, - Value: "english", - Usage: "language for the greeting", - EnvVars: []string{"LEGACY_COMPAT_LANG", "APP_LANG", "LANG"}, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -#### Values from files - -You can also have the default value set from file via `FilePath`. e.g. - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "password", - Aliases: []string{"p"}, - Usage: "password for the mysql database", - FilePath: "/etc/mysql/password", - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -Note that default values set from file (e.g. `FilePath`) take precedence over -default values set from the environment (e.g. `EnvVar`). - -#### Values from alternate input sources (YAML, TOML, and others) - -There is a separate package altsrc that adds support for getting flag values -from other file input sources. - -Currently supported input source formats: - -- YAML -- JSON -- TOML - -In order to get values for a flag from an alternate input source the following -code would be added to wrap an existing cli.Flag like below: - -```go - // --- >8 --- - altsrc.NewIntFlag(&cli.IntFlag{Name: "test"}) -``` - -Initialization must also occur for these flags. Below is an example initializing -getting data from a yaml file below. - -```go - // --- >8 --- - command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) -``` - -The code above will use the "load" string as a flag name to get the file name of -a yaml file from the cli.Context. It will then use that file name to initialize -the yaml input source for any flags that are defined on that command. As a note -the "load" flag used would also have to be defined on the command flags in order -for this code snippet to work. - -Currently only YAML, JSON, and TOML files are supported but developers can add -support for other input sources by implementing the altsrc.InputSourceContext -for their given sources. - -Here is a more complete sample of a command using YAML support: - - -```go -package main - -import ( - "fmt" - "os" - - "github.com/urfave/cli/v2" - "github.com/urfave/cli/v2/altsrc" -) - -func main() { - flags := []cli.Flag{ - altsrc.NewIntFlag(&cli.IntFlag{Name: "test"}), - &cli.StringFlag{Name: "load"}, - } - - app := &cli.App{ - Action: func(*cli.Context) error { - fmt.Println("--test value.*default: 0") - return nil - }, - Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), - Flags: flags, - } - - app.Run(os.Args) -} -``` - -#### Required Flags - -You can make a flag required by setting the `Required` field to `true`. If a user -does not provide a required flag, they will be shown an error message. - -Take for example this app that requires the `lang` flag: - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - Required: true, - }, - }, - Action: func(cCtx *cli.Context) error { - output := "Hello" - if cCtx.String("lang") == "spanish" { - output = "Hola" - } - fmt.Println(output) - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -If the app is run without the `lang` flag, the user will see the following message - -``` -Required flag "lang" not set -``` - -#### Default Values for help output - -Sometimes it's useful to specify a flag's default help-text value within the -flag declaration. This can be useful if the default value for a flag is a -computed value. The default value can be set via the `DefaultText` struct field. - -For example this: - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.IntFlag{ - Name: "port", - Usage: "Use a randomized port", - Value: 0, - DefaultText: "random", - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -Will result in help output like: - -``` ---port value Use a randomized port (default: random) -``` - -#### Precedence - -The precedence for flag value sources is as follows (highest to lowest): - -0. Command line flag value from user -0. Environment variable (if specified) -0. Configuration file (if specified) -0. Default defined on the flag - -### Subcommands - -Subcommands can be defined for a more git-like command line app. - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Commands: []*cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(cCtx *cli.Context) error { - fmt.Println("added task: ", cCtx.Args().First()) - return nil - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(cCtx *cli.Context) error { - fmt.Println("completed task: ", cCtx.Args().First()) - return nil - }, - }, - { - Name: "template", - Aliases: []string{"t"}, - Usage: "options for task templates", - Subcommands: []*cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(cCtx *cli.Context) error { - fmt.Println("new task template: ", cCtx.Args().First()) - return nil - }, - }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(cCtx *cli.Context) error { - fmt.Println("removed task template: ", cCtx.Args().First()) - return nil - }, - }, - }, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -### Subcommands categories - -For additional organization in apps that have many subcommands, you can -associate a category for each command to group them together in the help -output, e.g.: - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Commands: []*cli.Command{ - { - Name: "noop", - }, - { - Name: "add", - Category: "template", - }, - { - Name: "remove", - Category: "template", - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -Will include: - -``` -COMMANDS: - noop - - Template actions: - add - remove -``` - -### Exit code - -Calling `App.Run` will not automatically call `os.Exit`, which means that by -default the exit code will "fall through" to being `0`. An explicit exit code -may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a -`cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.BoolFlag{ - Name: "ginger-crouton", - Usage: "is it in the soup?", - }, - }, - Action: func(ctx *cli.Context) error { - if !ctx.Bool("ginger-crouton") { - return cli.Exit("Ginger croutons are not in the soup", 86) - } - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -### Combining short options - -Traditional use of options using their shortnames look like this: - -```sh-session -$ cmd -s -o -m "Some message" -``` - -Suppose you want users to be able to combine options with their shortnames. This -can be done using the `UseShortOptionHandling` bool in your app configuration, -or for individual commands by attaching it to the command configuration. For -example: - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - UseShortOptionHandling: true, - Commands: []*cli.Command{ - { - Name: "short", - Usage: "complete a task on the list", - Flags: []cli.Flag{ - &cli.BoolFlag{Name: "serve", Aliases: []string{"s"}}, - &cli.BoolFlag{Name: "option", Aliases: []string{"o"}}, - &cli.StringFlag{Name: "message", Aliases: []string{"m"}}, - }, - Action: func(cCtx *cli.Context) error { - fmt.Println("serve:", cCtx.Bool("serve")) - fmt.Println("option:", cCtx.Bool("option")) - fmt.Println("message:", cCtx.String("message")) - return nil - }, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -If your program has any number of bool flags such as `serve` and `option`, and -optionally one non-bool flag `message`, with the short options of `-s`, `-o`, -and `-m` respectively, setting `UseShortOptionHandling` will also support the -following syntax: - -```sh-session -$ cmd -som "Some message" -``` - -If you enable `UseShortOptionHandling`, then you must not use any flags that -have a single leading `-` or this will result in failures. For example, -`-option` can no longer be used. Flags with two leading dashes (such as -`--options`) are still valid. - -### Bash Completion - -You can enable completion commands by setting the `EnableBashCompletion` flag on -the `App` object to `true`. By default, this setting will allow auto-completion -for an app's subcommands, but you can write your own completion methods for the -App or its subcommands as well. - -#### Default auto-completion - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - EnableBashCompletion: true, - Commands: []*cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(cCtx *cli.Context) error { - fmt.Println("added task: ", cCtx.Args().First()) - return nil - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(cCtx *cli.Context) error { - fmt.Println("completed task: ", cCtx.Args().First()) - return nil - }, - }, - { - Name: "template", - Aliases: []string{"t"}, - Usage: "options for task templates", - Subcommands: []*cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(cCtx *cli.Context) error { - fmt.Println("new task template: ", cCtx.Args().First()) - return nil - }, - }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(cCtx *cli.Context) error { - fmt.Println("removed task template: ", cCtx.Args().First()) - return nil - }, - }, - }, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` -![](/docs/v2/images/default-bash-autocomplete.gif) - -#### Custom auto-completion - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} - - app := &cli.App{ - EnableBashCompletion: true, - Commands: []*cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(cCtx *cli.Context) error { - fmt.Println("completed task: ", cCtx.Args().First()) - return nil - }, - BashComplete: func(cCtx *cli.Context) { - // This will complete if no args are passed - if cCtx.NArg() > 0 { - return - } - for _, t := range tasks { - fmt.Println(t) - } - }, - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` -![](/docs/v2/images/custom-bash-autocomplete.gif) - -#### Enabling - -To enable auto-completion for the current shell session, a bash script, -`autocomplete/bash_autocomplete` is included in this repo. - -To use `autocomplete/bash_autocomplete` set an environment variable named `PROG` -to the name of your program and then `source` the -`autocomplete/bash_autocomplete` file. - -For example, if your cli program is called `myprogram`: - -```sh-session -$ PROG=myprogram source path/to/cli/autocomplete/bash_autocomplete -``` - -Auto-completion is now enabled for the current shell, but will not persist into -a new shell. - -#### Distribution and Persistent Autocompletion - -Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename -it to the name of the program you wish to add autocomplete support for (or -automatically install it there if you are distributing a package). Don't forget -to source the file or restart your shell to activate the auto-completion. - -```sh-session -$ sudo cp path/to/autocomplete/bash_autocomplete /etc/bash_completion.d/ -$ source /etc/bash_completion.d/ -``` - -Alternatively, you can just document that users should `source` the generic -`autocomplete/bash_autocomplete` and set `$PROG` within their bash configuration -file, adding these lines: - -```sh-session -$ PROG= -$ source path/to/cli/autocomplete/bash_autocomplete -``` - -Keep in mind that if they are enabling auto-completion for more than one -program, they will need to set `PROG` and source -`autocomplete/bash_autocomplete` for each program, like so: - -```sh-session -$ PROG= -$ source path/to/cli/autocomplete/bash_autocomplete - -$ PROG= -$ source path/to/cli/autocomplete/bash_autocomplete -``` - -#### Customization - -The default shell completion flag (`--generate-bash-completion`) is defined as -`cli.EnableBashCompletion`, and may be redefined if desired, e.g.: - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - EnableBashCompletion: true, - Commands: []*cli.Command{ - { - Name: "wat", - }, - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -#### ZSH Support - -Auto-completion for ZSH is also supported using the -`autocomplete/zsh_autocomplete` file included in this repo. One environment -variable is used, `PROG`. Set `PROG` to the program name as before, and then -`source path/to/autocomplete/zsh_autocomplete`. Adding the following lines to -your ZSH configuration file (usually `.zshrc`) will allow the auto-completion to -persist across new shells: - -```sh-session -$ PROG= -$ source path/to/autocomplete/zsh_autocomplete -``` - -#### ZSH default auto-complete example -![](/docs/v2/images/default-zsh-autocomplete.gif) - -#### ZSH custom auto-complete example -![](/docs/v2/images/custom-zsh-autocomplete.gif) - -#### PowerShell Support - -Auto-completion for PowerShell is also supported using the -`autocomplete/powershell_autocomplete.ps1` file included in this repo. - -Rename the script to `.ps1` and move it anywhere in your file -system. The location of script does not matter, only the file name of the -script has to match the your program's binary name. - -To activate it, enter: - -```powershell -& path/to/autocomplete/.ps1 -``` - -To persist across new shells, open the PowerShell profile (with `code $profile` -or `notepad $profile`) and add the line: - -```powershell -& path/to/autocomplete/.ps1 -``` - -### Generated Help Text - -The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked -by the cli internals in order to print generated help text for the app, command, -or subcommand, and break execution. - -#### Customization - -All of the help text generation may be customized, and at multiple levels. The -templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and -`SubcommandHelpTemplate` which may be reassigned or augmented, and full override -is possible by assigning a compatible func to the `cli.HelpPrinter` variable, -e.g.: - - -```go -package main - -import ( - "fmt" - "io" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - // EXAMPLE: Append to an existing template - cli.AppHelpTemplate = fmt.Sprintf(`%s - -WEBSITE: http://awesometown.example.com - -SUPPORT: support@awesometown.example.com - -`, cli.AppHelpTemplate) - - // EXAMPLE: Override a template - cli.AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} -USAGE: - {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} - {{if len .Authors}} -AUTHOR: - {{range .Authors}}{{ . }}{{end}} - {{end}}{{if .Commands}} -COMMANDS: -{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} -GLOBAL OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{if .Copyright }} -COPYRIGHT: - {{.Copyright}} - {{end}}{{if .Version}} -VERSION: - {{.Version}} - {{end}} -` - - // EXAMPLE: Replace the `HelpPrinter` func - cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { - fmt.Println("Ha HA. I pwnd the help!!1") - } - - (&cli.App{}).Run(os.Args) -} -``` - -The default flag may be customized to something other than `-h/--help` by -setting `cli.HelpFlag`, e.g.: - - -```go -package main - -import ( - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - cli.HelpFlag = &cli.BoolFlag{ - Name: "haaaaalp", - Aliases: []string{"halp"}, - Usage: "HALP", - EnvVars: []string{"SHOW_HALP", "HALPPLZ"}, - } - - (&cli.App{}).Run(os.Args) -} -``` - -### Version Flag - -The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which -is checked by the cli internals in order to print the `App.Version` via -`cli.VersionPrinter` and break execution. - -#### Customization - -The default flag may be customized to something other than `-v/--version` by -setting `cli.VersionFlag`, e.g.: - - -```go -package main - -import ( - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - cli.VersionFlag = &cli.BoolFlag{ - Name: "print-version", - Aliases: []string{"V"}, - Usage: "print only the version", - } - - app := &cli.App{ - Name: "partay", - Version: "v19.99.0", - } - app.Run(os.Args) -} -``` - -Alternatively, the version printer at `cli.VersionPrinter` may be overridden, -e.g.: - - -```go -package main - -import ( - "fmt" - "os" - - "github.com/urfave/cli/v2" -) - -var ( - Revision = "fafafaf" -) - -func main() { - cli.VersionPrinter = func(cCtx *cli.Context) { - fmt.Printf("version=%s revision=%s\n", cCtx.App.Version, Revision) - } - - app := &cli.App{ - Name: "partay", - Version: "v19.99.0", - } - app.Run(os.Args) -} -``` - -### Timestamp Flag - -Using the timestamp flag is simple. Please refer to -[`time.Parse`](https://golang.org/pkg/time/#example_Parse) to get possible -formats. - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli/v2" -) - -func main() { - app := &cli.App{ - Flags: []cli.Flag{ - &cli.TimestampFlag{Name: "meeting", Layout: "2006-01-02T15:04:05"}, - }, - Action: func(cCtx *cli.Context) error { - fmt.Printf("%s", cCtx.Timestamp("meeting").String()) - return nil - }, - } - - if err := app.Run(os.Args); err != nil { - log.Fatal(err) - } -} -``` - -In this example the flag could be used like this: - -```sh-session -$ myapp --meeting 2019-08-12T15:04:05 -``` - -When the layout doesn't contain timezones, timestamp will render with UTC. To -change behavior, a default timezone can be provided with flag definition: - -```go -app := &cli.App{ - Flags: []cli.Flag{ - &cli.TimestampFlag{Name: "meeting", Layout: "2006-01-02T15:04:05", Timezone: time.Local}, - }, -} -``` - -(time.Local contains the system's local time zone.) - -Side note: quotes may be necessary around the date depending on your layout (if -you have spaces for instance) - -### Suggestions - -To enable flag and command suggestions, set `app.Suggest = true`. If the suggest -feature is enabled, then the help output of the corresponding command will -provide an appropriate suggestion for the provided flag or subcommand if -available. - -### Full API Example - -**Notice**: This is a contrived (functioning) example meant strictly for API -demonstration purposes. Use of one's imagination is encouraged. - - -```go -package main - -import ( - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "os" - "time" - - "github.com/urfave/cli/v2" -) - -func init() { - cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" - cli.CommandHelpTemplate += "\nYMMV\n" - cli.SubcommandHelpTemplate += "\nor something\n" - - cli.HelpFlag = &cli.BoolFlag{Name: "halp"} - cli.VersionFlag = &cli.BoolFlag{Name: "print-version", Aliases: []string{"V"}} - - cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { - fmt.Fprintf(w, "best of luck to you\n") - } - cli.VersionPrinter = func(cCtx *cli.Context) { - fmt.Fprintf(cCtx.App.Writer, "version=%s\n", cCtx.App.Version) - } - cli.OsExiter = func(cCtx int) { - fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", cCtx) - } - cli.ErrWriter = ioutil.Discard - cli.FlagStringer = func(fl cli.Flag) string { - return fmt.Sprintf("\t\t%s", fl.Names()[0]) - } -} - -type hexWriter struct{} - -func (w *hexWriter) Write(p []byte) (int, error) { - for _, b := range p { - fmt.Printf("%x", b) - } - fmt.Printf("\n") - - return len(p), nil -} - -type genericType struct { - s string -} - -func (g *genericType) Set(value string) error { - g.s = value - return nil -} - -func (g *genericType) String() string { - return g.s -} - -func main() { - app := &cli.App{ - Name: "kənˈtrīv", - Version: "v19.99.0", - Compiled: time.Now(), - Authors: []*cli.Author{ - &cli.Author{ - Name: "Example Human", - Email: "human@example.com", - }, - }, - Copyright: "(c) 1999 Serious Enterprise", - HelpName: "contrive", - Usage: "demonstrate available API", - UsageText: "contrive - demonstrating the available API", - ArgsUsage: "[args and such]", - Commands: []*cli.Command{ - &cli.Command{ - Name: "doo", - Aliases: []string{"do"}, - Category: "motion", - Usage: "do the doo", - UsageText: "doo - does the dooing", - Description: "no really, there is a lot of dooing to be done", - ArgsUsage: "[arrgh]", - Flags: []cli.Flag{ - &cli.BoolFlag{Name: "forever", Aliases: []string{"forevvarr"}}, - }, - Subcommands: []*cli.Command{ - &cli.Command{ - Name: "wop", - Action: wopAction, - }, - }, - SkipFlagParsing: false, - HideHelp: false, - Hidden: false, - HelpName: "doo!", - BashComplete: func(cCtx *cli.Context) { - fmt.Fprintf(cCtx.App.Writer, "--better\n") - }, - Before: func(cCtx *cli.Context) error { - fmt.Fprintf(cCtx.App.Writer, "brace for impact\n") - return nil - }, - After: func(cCtx *cli.Context) error { - fmt.Fprintf(cCtx.App.Writer, "did we lose anyone?\n") - return nil - }, - Action: func(cCtx *cli.Context) error { - cCtx.Command.FullName() - cCtx.Command.HasName("wop") - cCtx.Command.Names() - cCtx.Command.VisibleFlags() - fmt.Fprintf(cCtx.App.Writer, "dodododododoodododddooooododododooo\n") - if cCtx.Bool("forever") { - cCtx.Command.Run(cCtx) - } - return nil - }, - OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { - fmt.Fprintf(cCtx.App.Writer, "for shame\n") - return err - }, - }, - }, - Flags: []cli.Flag{ - &cli.BoolFlag{Name: "fancy"}, - &cli.BoolFlag{Value: true, Name: "fancier"}, - &cli.DurationFlag{Name: "howlong", Aliases: []string{"H"}, Value: time.Second * 3}, - &cli.Float64Flag{Name: "howmuch"}, - &cli.GenericFlag{Name: "wat", Value: &genericType{}}, - &cli.Int64Flag{Name: "longdistance"}, - &cli.Int64SliceFlag{Name: "intervals"}, - &cli.IntFlag{Name: "distance"}, - &cli.IntSliceFlag{Name: "times"}, - &cli.StringFlag{Name: "dance-move", Aliases: []string{"d"}}, - &cli.StringSliceFlag{Name: "names", Aliases: []string{"N"}}, - &cli.UintFlag{Name: "age"}, - &cli.Uint64Flag{Name: "bigage"}, - }, - EnableBashCompletion: true, - HideHelp: false, - HideVersion: false, - BashComplete: func(cCtx *cli.Context) { - fmt.Fprintf(cCtx.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") - }, - Before: func(cCtx *cli.Context) error { - fmt.Fprintf(cCtx.App.Writer, "HEEEERE GOES\n") - return nil - }, - After: func(cCtx *cli.Context) error { - fmt.Fprintf(cCtx.App.Writer, "Phew!\n") - return nil - }, - CommandNotFound: func(cCtx *cli.Context, command string) { - fmt.Fprintf(cCtx.App.Writer, "Thar be no %q here.\n", command) - }, - OnUsageError: func(cCtx *cli.Context, err error, isSubcommand bool) error { - if isSubcommand { - return err - } - - fmt.Fprintf(cCtx.App.Writer, "WRONG: %#v\n", err) - return nil - }, - Action: func(cCtx *cli.Context) error { - cli.DefaultAppComplete(cCtx) - cli.HandleExitCoder(errors.New("not an exit coder, though")) - cli.ShowAppHelp(cCtx) - cli.ShowCommandCompletions(cCtx, "nope") - cli.ShowCommandHelp(cCtx, "also-nope") - cli.ShowCompletions(cCtx) - cli.ShowSubcommandHelp(cCtx) - cli.ShowVersion(cCtx) - - fmt.Printf("%#v\n", cCtx.App.Command("doo")) - if cCtx.Bool("infinite") { - cCtx.App.Run([]string{"app", "doo", "wop"}) - } - - if cCtx.Bool("forevar") { - cCtx.App.RunAsSubcommand(cCtx) - } - cCtx.App.Setup() - fmt.Printf("%#v\n", cCtx.App.VisibleCategories()) - fmt.Printf("%#v\n", cCtx.App.VisibleCommands()) - fmt.Printf("%#v\n", cCtx.App.VisibleFlags()) - - fmt.Printf("%#v\n", cCtx.Args().First()) - if cCtx.Args().Len() > 0 { - fmt.Printf("%#v\n", cCtx.Args().Get(1)) - } - fmt.Printf("%#v\n", cCtx.Args().Present()) - fmt.Printf("%#v\n", cCtx.Args().Tail()) - - set := flag.NewFlagSet("contrive", 0) - nc := cli.NewContext(cCtx.App, set, cCtx) - - fmt.Printf("%#v\n", nc.Args()) - fmt.Printf("%#v\n", nc.Bool("nope")) - fmt.Printf("%#v\n", !nc.Bool("nerp")) - fmt.Printf("%#v\n", nc.Duration("howlong")) - fmt.Printf("%#v\n", nc.Float64("hay")) - fmt.Printf("%#v\n", nc.Generic("bloop")) - fmt.Printf("%#v\n", nc.Int64("bonk")) - fmt.Printf("%#v\n", nc.Int64Slice("burnks")) - fmt.Printf("%#v\n", nc.Int("bips")) - fmt.Printf("%#v\n", nc.IntSlice("blups")) - fmt.Printf("%#v\n", nc.String("snurt")) - fmt.Printf("%#v\n", nc.StringSlice("snurkles")) - fmt.Printf("%#v\n", nc.Uint("flub")) - fmt.Printf("%#v\n", nc.Uint64("florb")) - - fmt.Printf("%#v\n", nc.FlagNames()) - fmt.Printf("%#v\n", nc.IsSet("wat")) - fmt.Printf("%#v\n", nc.Set("wat", "nope")) - fmt.Printf("%#v\n", nc.NArg()) - fmt.Printf("%#v\n", nc.NumFlags()) - fmt.Printf("%#v\n", nc.Lineage()[1]) - nc.Set("wat", "also-nope") - - ec := cli.Exit("ohwell", 86) - fmt.Fprintf(cCtx.App.Writer, "%d", ec.ExitCode()) - fmt.Printf("made it!\n") - return ec - }, - Metadata: map[string]interface{}{ - "layers": "many", - "explicable": false, - "whatever-values": 19.99, - }, - } - - if os.Getenv("HEXY") != "" { - app.Writer = &hexWriter{} - app.ErrWriter = &hexWriter{} - } - - app.Run(os.Args) -} - -func wopAction(cCtx *cli.Context) error { - fmt.Fprintf(cCtx.App.Writer, ":wave: over here, eh\n") - return nil -} -``` - -## Migrating From Older Releases - -There are a small set of breaking changes between v1 and v2. Converting is -relatively straightforward and typically takes less than an hour. Specific steps -are included in [Migration Guide: v1 to v2](../migrate-v1-to-v2.md). Also see -the [pkg.go.dev docs](https://pkg.go.dev/github.com/urfave/cli/v2) for v2 API -documentation. diff --git a/docs/v2/migrating-from-older-releases.md b/docs/v2/migrating-from-older-releases.md new file mode 100644 index 0000000..7bb33af --- /dev/null +++ b/docs/v2/migrating-from-older-releases.md @@ -0,0 +1,12 @@ +--- +tags: + - v2 +search: + boost: 2 +--- + +There are a small set of breaking changes between v1 and v2. Converting is +relatively straightforward and typically takes less than an hour. Specific steps +are included in [Migration Guide: v1 to v2](../migrate-v1-to-v2.md). Also see +the [pkg.go.dev docs](https://pkg.go.dev/github.com/urfave/cli/v2) for v2 API +documentation. diff --git a/flag-spec.yaml b/flag-spec.yaml index 3fa2d10..a38b933 100644 --- a/flag-spec.yaml +++ b/flag-spec.yaml @@ -1,54 +1,116 @@ # NOTE: this file is used by the tool defined in -# ./internal/genflags/cmd/genflags/main.go which uses the -# `genflags.Spec` type that maps to this file structure. - +# ./cmd/urfave-cli-genflags/main.go which uses the +# `Spec` type that maps to this file structure. flag_types: - bool: + bool: no_default_text: true - float64: {} - int64: {} - int: {} - time.Duration: {} - uint64: {} - uint: {} - - string: - no_default_text: true - struct_fields: - - { name: TakesFile, type: bool } - Generic: struct_fields: - - { name: TakesFile, type: bool } - Path: - no_default_text: true + - name: Count + type: int + pointer: true + - name: Action + type: "func(*Context, bool) error" + float64: struct_fields: - - { name: TakesFile, type: bool } - + - name: Action + type: "func(*Context, float64) error" Float64Slice: value_pointer: true skip_interfaces: - fmt.Stringer + struct_fields: + - name: Action + type: "func(*Context, []float64) error" + int: + struct_fields: + - name: Base + type: int + - name: Action + type: "func(*Context, int) error" + IntSlice: + value_pointer: true + skip_interfaces: + - fmt.Stringer + struct_fields: + - name: Action + type: "func(*Context, []int) error" + int64: + struct_fields: + - name: Base + type: int + - name: Action + type: "func(*Context, int64) error" Int64Slice: value_pointer: true skip_interfaces: - fmt.Stringer - IntSlice: + struct_fields: + - name: Action + type: "func(*Context, []int64) error" + uint: + struct_fields: + - name: Base + type: int + - name: Action + type: "func(*Context, uint) error" + UintSlice: + value_pointer: true + skip_interfaces: + - fmt.Stringer + struct_fields: + - name: Action + type: "func(*Context, []uint) error" + uint64: + struct_fields: + - name: Base + type: int + - name: Action + type: "func(*Context, uint64) error" + Uint64Slice: value_pointer: true skip_interfaces: - fmt.Stringer + struct_fields: + - name: Action + type: "func(*Context, []uint64) error" + string: + struct_fields: + - name: TakesFile + type: bool + - name: Action + type: "func(*Context, string) error" StringSlice: value_pointer: true skip_interfaces: - fmt.Stringer struct_fields: - - { name: TakesFile, type: bool } + - name: TakesFile + type: bool + - name: Action + type: "func(*Context, []string) error" + time.Duration: + struct_fields: + - name: Action + type: "func(*Context, time.Duration) error" Timestamp: value_pointer: true struct_fields: - - { name: Layout, type: string } - - { name: Timezone, type: "*time.Location" } - - # TODO: enable UintSlice - # UintSlice: {} - # TODO: enable Uint64Slice once #1334 lands - # Uint64Slice: {} + - name: Layout + type: string + - name: Timezone + type: "*time.Location" + - name: Action + type: "func(*Context, *time.Time) error" + Generic: + no_destination_pointer: true + struct_fields: + - name: TakesFile + type: bool + - name: Action + type: "func(*Context, interface{}) error" + Path: + struct_fields: + - name: TakesFile + type: bool + - name: Action + type: "func(*Context, Path) error" diff --git a/flag.go b/flag.go index a0b9834..aade1de 100644 --- a/flag.go +++ b/flag.go @@ -7,7 +7,6 @@ import ( "io/ioutil" "regexp" "runtime" - "strconv" "strings" "syscall" "time" @@ -123,6 +122,14 @@ type Flag interface { // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. GetValue() string + + RunAction(*Context) error +} + +// Countable is an interface to enable detection of flag values which support +// repetitive flags +type Countable interface { + Count() int } func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { @@ -297,53 +304,6 @@ func stringifyFlag(f Flag) string { fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault)) } -func stringifyIntSliceFlag(f *IntSliceFlag) string { - var defaultVals []string - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strconv.Itoa(i)) - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - -func stringifyInt64SliceFlag(f *Int64SliceFlag) string { - var defaultVals []string - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - -func stringifyFloat64SliceFlag(f *Float64SliceFlag) string { - var defaultVals []string - - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), ".")) - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - -func stringifyStringSliceFlag(f *StringSliceFlag) string { - var defaultVals []string - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, s := range f.Value.Value() { - if len(s) > 0 { - defaultVals = append(defaultVals, strconv.Quote(s)) - } - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - func stringifySliceFlag(usage string, names, defaultVals []string) string { placeholder, usage := unquoteUsage(usage) if placeholder == "" { @@ -356,11 +316,8 @@ func stringifySliceFlag(usage string, names, defaultVals []string) string { } usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) - multiInputString := "(accepts multiple inputs)" - if usageWithDefault != "" { - multiInputString = "\t" + multiInputString - } - return fmt.Sprintf("%s\t%s%s", prefixedNames(names, placeholder), usageWithDefault, multiInputString) + pn := prefixedNames(names, placeholder) + return fmt.Sprintf("%s [ %s ]\t%s", pn, pn, usageWithDefault) } func hasFlag(flags []Flag, fl Flag) bool { diff --git a/flag_bool.go b/flag_bool.go index c8a01bb..55b719a 100644 --- a/flag_bool.go +++ b/flag_bool.go @@ -1,11 +1,63 @@ package cli import ( + "errors" "flag" "fmt" "strconv" ) +// boolValue needs to implement the boolFlag internal interface in flag +// to be able to capture bool fields and values +// +// type boolFlag interface { +// Value +// IsBoolFlag() bool +// } +type boolValue struct { + destination *bool + count *int +} + +func newBoolValue(val bool, p *bool, count *int) *boolValue { + *p = val + return &boolValue{ + destination: p, + count: count, + } +} + +func (b *boolValue) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + err = errors.New("parse error") + return err + } + *b.destination = v + if b.count != nil { + *b.count = *b.count + 1 + } + return err +} + +func (b *boolValue) Get() interface{} { return *b.destination } + +func (b *boolValue) String() string { + if b.destination != nil { + return strconv.FormatBool(*b.destination) + } + return strconv.FormatBool(false) +} + +func (b *boolValue) IsBoolFlag() bool { return true } + +func (b *boolValue) Count() int { + if b.count != nil { + return *b.count + } + return 0 +} + // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *BoolFlag) GetValue() string { @@ -20,6 +72,15 @@ func (f *BoolFlag) GetDefaultText() string { return fmt.Sprintf("%v", f.Value) } +// RunAction executes flag action if set +func (f *BoolFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Bool(f.Name)) + } + + return nil +} + // Apply populates the flag given the flag set and environment func (f *BoolFlag) Apply(set *flag.FlagSet) error { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { @@ -31,16 +92,28 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error { } f.Value = valBool - f.HasBeenSet = true + } else { + // empty value implies that the env is defined but set to empty string, we have to assume that this is + // what the user wants. If user doesnt want this then the env needs to be deleted or the flag removed from + // file + f.Value = false } + f.HasBeenSet = true + } + + count := f.Count + dest := f.Destination + + if count == nil { + count = new(int) + } + if dest == nil { + dest = new(bool) } for _, name := range f.Names() { - if f.Destination != nil { - set.BoolVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Bool(name, f.Value, f.Usage) + value := newBoolValue(f.Value, dest, count) + set.Var(value, name, f.Usage) } return nil diff --git a/flag_duration.go b/flag_duration.go index 35b376d..b6adce3 100644 --- a/flag_duration.go +++ b/flag_duration.go @@ -42,6 +42,15 @@ func (f *DurationFlag) Get(ctx *Context) time.Duration { return ctx.Duration(f.Name) } +// RunAction executes flag action if set +func (f *DurationFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Duration(f.Name)) + } + + return nil +} + // Duration looks up the value of a local DurationFlag, returns // 0 if not found func (cCtx *Context) Duration(name string) time.Duration { diff --git a/flag_float64.go b/flag_float64.go index b7b8044..3e21a01 100644 --- a/flag_float64.go +++ b/flag_float64.go @@ -42,6 +42,15 @@ func (f *Float64Flag) Get(ctx *Context) float64 { return ctx.Float64(f.Name) } +// RunAction executes flag action if set +func (f *Float64Flag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Float64(f.Name)) + } + + return nil +} + // Float64 looks up the value of a local Float64Flag, returns // 0 if not found func (cCtx *Context) Float64(name string) float64 { diff --git a/flag_float64_slice.go b/flag_float64_slice.go index 56745c1..5bb2693 100644 --- a/flag_float64_slice.go +++ b/flag_float64_slice.go @@ -83,7 +83,7 @@ func (f *Float64Slice) Get() interface{} { // String returns a readable representation of this value // (for usage defaults) func (f *Float64SliceFlag) String() string { - return withEnvHint(f.GetEnvVars(), stringifyFloat64SliceFlag(f)) + return withEnvHint(f.GetEnvVars(), f.stringify()) } // GetValue returns the flags value as string representation and an empty @@ -141,6 +141,27 @@ func (f *Float64SliceFlag) Get(ctx *Context) []float64 { return ctx.Float64Slice(f.Name) } +func (f *Float64SliceFlag) stringify() string { + var defaultVals []string + + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), ".")) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +// RunAction executes flag action if set +func (f *Float64SliceFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Float64Slice(f.Name)) + } + + return nil +} + // Float64Slice looks up the value of a local Float64SliceFlag, returns // nil if not found func (cCtx *Context) Float64Slice(name string) []float64 { diff --git a/flag_generic.go b/flag_generic.go index cb20486..9f06338 100644 --- a/flag_generic.go +++ b/flag_generic.go @@ -22,7 +22,7 @@ func (f *GenericFlag) GetValue() string { // Apply takes the flagset and calls Set on the generic flag with the value // provided by the user for parsing by the flag -func (f GenericFlag) Apply(set *flag.FlagSet) error { +func (f *GenericFlag) Apply(set *flag.FlagSet) error { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { if err := f.Value.Set(val); err != nil { @@ -45,6 +45,15 @@ func (f *GenericFlag) Get(ctx *Context) interface{} { return ctx.Generic(f.Name) } +// RunAction executes flag action if set +func (f *GenericFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Generic(f.Name)) + } + + return nil +} + // Generic looks up the value of a local GenericFlag, returns // nil if not found func (cCtx *Context) Generic(name string) interface{} { diff --git a/flag_int.go b/flag_int.go index 908e076..bcaab7f 100644 --- a/flag_int.go +++ b/flag_int.go @@ -16,7 +16,7 @@ func (f *IntFlag) GetValue() string { func (f *IntFlag) Apply(set *flag.FlagSet) error { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { - valInt, err := strconv.ParseInt(val, 0, 64) + valInt, err := strconv.ParseInt(val, f.Base, 64) if err != nil { return fmt.Errorf("could not parse %q as int value from %s for flag %s: %s", val, source, f.Name, err) @@ -43,6 +43,15 @@ func (f *IntFlag) Get(ctx *Context) int { return ctx.Int(f.Name) } +// RunAction executes flag action if set +func (f *IntFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Int(f.Name)) + } + + return nil +} + // Int looks up the value of a local IntFlag, returns // 0 if not found func (cCtx *Context) Int(name string) int { diff --git a/flag_int64.go b/flag_int64.go index 9f07d79..d8e591d 100644 --- a/flag_int64.go +++ b/flag_int64.go @@ -16,7 +16,7 @@ func (f *Int64Flag) GetValue() string { func (f *Int64Flag) Apply(set *flag.FlagSet) error { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { - valInt, err := strconv.ParseInt(val, 0, 64) + valInt, err := strconv.ParseInt(val, f.Base, 64) if err != nil { return fmt.Errorf("could not parse %q as int value from %s for flag %s: %s", val, source, f.Name, err) @@ -42,6 +42,15 @@ func (f *Int64Flag) Get(ctx *Context) int64 { return ctx.Int64(f.Name) } +// RunAction executes flag action if set +func (f *Int64Flag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Int64(f.Name)) + } + + return nil +} + // Int64 looks up the value of a local Int64Flag, returns // 0 if not found func (cCtx *Context) Int64(name string) int64 { diff --git a/flag_int64_slice.go b/flag_int64_slice.go index 8316709..89fd445 100644 --- a/flag_int64_slice.go +++ b/flag_int64_slice.go @@ -84,7 +84,7 @@ func (i *Int64Slice) Get() interface{} { // String returns a readable representation of this value // (for usage defaults) func (f *Int64SliceFlag) String() string { - return withEnvHint(f.GetEnvVars(), stringifyInt64SliceFlag(f)) + return withEnvHint(f.GetEnvVars(), f.stringify()) } // GetValue returns the flags value as string representation and an empty @@ -140,6 +140,26 @@ func (f *Int64SliceFlag) Get(ctx *Context) []int64 { return ctx.Int64Slice(f.Name) } +func (f *Int64SliceFlag) stringify() string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +// RunAction executes flag action if set +func (f *Int64SliceFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Int64Slice(f.Name)) + } + + return nil +} + // Int64Slice looks up the value of a local Int64SliceFlag, returns // nil if not found func (cCtx *Context) Int64Slice(name string) []int64 { diff --git a/flag_int_slice.go b/flag_int_slice.go index 7865dab..f7a6b06 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -95,7 +95,7 @@ func (i *IntSlice) Get() interface{} { // String returns a readable representation of this value // (for usage defaults) func (f *IntSliceFlag) String() string { - return withEnvHint(f.GetEnvVars(), stringifyIntSliceFlag(f)) + return withEnvHint(f.GetEnvVars(), f.stringify()) } // GetValue returns the flags value as string representation and an empty @@ -151,6 +151,26 @@ func (f *IntSliceFlag) Get(ctx *Context) []int { return ctx.IntSlice(f.Name) } +func (f *IntSliceFlag) stringify() string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.Itoa(i)) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +// RunAction executes flag action if set +func (f *IntSliceFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.IntSlice(f.Name)) + } + + return nil +} + // IntSlice looks up the value of a local IntSliceFlag, returns // nil if not found func (cCtx *Context) IntSlice(name string) []int { diff --git a/flag_path.go b/flag_path.go index e622c40..f8bb9c3 100644 --- a/flag_path.go +++ b/flag_path.go @@ -47,6 +47,15 @@ func (f *PathFlag) Get(ctx *Context) string { return ctx.Path(f.Name) } +// RunAction executes flag action if set +func (f *PathFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Path(f.Name)) + } + + return nil +} + // Path looks up the value of a local PathFlag, returns // "" if not found func (cCtx *Context) Path(name string) string { diff --git a/flag_string.go b/flag_string.go index 31cd89d..669f254 100644 --- a/flag_string.go +++ b/flag_string.go @@ -45,6 +45,15 @@ func (f *StringFlag) Get(ctx *Context) string { return ctx.String(f.Name) } +// RunAction executes flag action if set +func (f *StringFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.String(f.Name)) + } + + return nil +} + // String looks up the value of a local StringFlag, returns // "" if not found func (cCtx *Context) String(name string) string { diff --git a/flag_string_slice.go b/flag_string_slice.go index f50323b..2a24ec5 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -4,6 +4,7 @@ import ( "encoding/json" "flag" "fmt" + "strconv" "strings" ) @@ -73,7 +74,7 @@ func (s *StringSlice) Get() interface{} { // String returns a readable representation of this value // (for usage defaults) func (f *StringSliceFlag) String() string { - return withEnvHint(f.GetEnvVars(), stringifyStringSliceFlag(f)) + return withEnvHint(f.GetEnvVars(), f.stringify()) } // GetValue returns the flags value as string representation and an empty @@ -129,6 +130,28 @@ func (f *StringSliceFlag) Get(ctx *Context) []string { return ctx.StringSlice(f.Name) } +func (f *StringSliceFlag) stringify() string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, s := range f.Value.Value() { + if len(s) > 0 { + defaultVals = append(defaultVals, strconv.Quote(s)) + } + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +// RunAction executes flag action if set +func (f *StringSliceFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.StringSlice(f.Name)) + } + + return nil +} + // StringSlice looks up the value of a local StringSliceFlag, returns // nil if not found func (cCtx *Context) StringSlice(name string) []string { diff --git a/flag_test.go b/flag_test.go index deac525..e7fa309 100644 --- a/flag_test.go +++ b/flag_test.go @@ -62,6 +62,53 @@ func TestBoolFlagValueFromContext(t *testing.T) { expect(t, ff.Get(ctx), false) } +func TestBoolFlagApply_SetsCount(t *testing.T) { + v := false + count := 0 + fl := BoolFlag{Name: "wat", Aliases: []string{"W", "huh"}, Destination: &v, Count: &count} + set := flag.NewFlagSet("test", 0) + err := fl.Apply(set) + expect(t, err, nil) + + err = set.Parse([]string{"--wat", "-W", "--huh"}) + expect(t, err, nil) + expect(t, v, true) + expect(t, count, 3) +} + +func TestBoolFlagCountFromContext(t *testing.T) { + + boolCountTests := []struct { + input []string + expectedVal bool + expectedCount int + }{ + { + input: []string{"-tf", "-w", "-huh"}, + expectedVal: true, + expectedCount: 3, + }, + { + input: []string{}, + expectedVal: false, + expectedCount: 0, + }, + } + + for _, bct := range boolCountTests { + set := flag.NewFlagSet("test", 0) + ctx := NewContext(nil, set, nil) + tf := &BoolFlag{Name: "tf", Aliases: []string{"w", "huh"}} + err := tf.Apply(set) + expect(t, err, nil) + + err = set.Parse(bct.input) + expect(t, err, nil) + expect(t, tf.Get(ctx), bct.expectedVal) + expect(t, ctx.Count("tf"), bct.expectedCount) + } +} + func TestFlagsFromEnv(t *testing.T) { newSetFloat64Slice := func(defaults ...float64) Float64Slice { s := NewFloat64Slice(defaults...) @@ -75,12 +122,24 @@ func TestFlagsFromEnv(t *testing.T) { return *s } + newSetUintSlice := func(defaults ...uint) UintSlice { + s := NewUintSlice(defaults...) + s.hasBeenSet = false + return *s + } + newSetInt64Slice := func(defaults ...int64) Int64Slice { s := NewInt64Slice(defaults...) s.hasBeenSet = false return *s } + newSetUint64Slice := func(defaults ...uint64) Uint64Slice { + s := NewUint64Slice(defaults...) + s.hasBeenSet = false + return *s + } + newSetStringSlice := func(defaults ...string) StringSlice { s := NewStringSlice(defaults...) s.hasBeenSet = false @@ -110,6 +169,10 @@ func TestFlagsFromEnv(t *testing.T) { {"foobar", 0, &Int64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value from environment variable "SECONDS" for flag seconds: .*`}, {"1", 1, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"08", 8, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 10}, ""}, + {"755", 493, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 8}, ""}, + {"deadBEEF", 3735928559, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 16}, ""}, + {"08", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 0}, `could not parse "08" as int value from environment variable "SECONDS" for flag seconds: .*`}, {"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value from environment variable "SECONDS" for flag seconds: .*`}, {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value from environment variable "SECONDS" for flag seconds: .*`}, @@ -120,20 +183,36 @@ func TestFlagsFromEnv(t *testing.T) { {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value from environment variable "SECONDS" for flag seconds: .*`}, {"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"1,2", newSetUintSlice(1, 2), &UintSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2,2", newSetUintSlice(), &UintSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as uint slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"foobar", newSetUintSlice(), &UintSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"1,2", newSetInt64Slice(1, 2), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1.2,2", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int64 slice value from environment variable "SECONDS" for flag seconds: .*`}, {"foobar", newSetInt64Slice(), &Int64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int64 slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"1,2", newSetUint64Slice(1, 2), &Uint64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"1.2,2", newSetUint64Slice(), &Uint64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as uint64 slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"foobar", newSetUint64Slice(), &Uint64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint64 slice value from environment variable "SECONDS" for flag seconds: .*`}, + {"foo", "foo", &StringFlag{Name: "name", EnvVars: []string{"NAME"}}, ""}, {"path", "path", &PathFlag{Name: "path", EnvVars: []string{"PATH"}}, ""}, {"foo,bar", newSetStringSlice("foo", "bar"), &StringSliceFlag{Name: "names", EnvVars: []string{"NAMES"}}, ""}, {"1", uint(1), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"08", uint(8), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 10}, ""}, + {"755", uint(493), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 8}, ""}, + {"deadBEEF", uint(3735928559), &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 16}, ""}, + {"08", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 0}, `could not parse "08" as uint value from environment variable "SECONDS" for flag seconds: .*`}, {"1.2", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint value from environment variable "SECONDS" for flag seconds: .*`}, {"foobar", 0, &UintFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint value from environment variable "SECONDS" for flag seconds: .*`}, {"1", uint64(1), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"08", uint64(8), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 10}, ""}, + {"755", uint64(493), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 8}, ""}, + {"deadBEEF", uint64(3735928559), &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 16}, ""}, + {"08", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}, Base: 0}, `could not parse "08" as uint64 value from environment variable "SECONDS" for flag seconds: .*`}, {"1.2", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as uint64 value from environment variable "SECONDS" for flag seconds: .*`}, {"foobar", 0, &Uint64Flag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as uint64 value from environment variable "SECONDS" for flag seconds: .*`}, @@ -154,6 +233,10 @@ func TestFlagsFromEnv(t *testing.T) { if !reflect.DeepEqual(ctx.Value(test.flag.Names()[0]), test.output) { t.Errorf("ex:%01d expected %q to be parsed as %#v, instead was %#v", i, test.input, test.output, ctx.Value(test.flag.Names()[0])) } + if !f.IsSet() { + t.Errorf("Flag %s not set", f.Names()[0]) + } + return nil }, } @@ -252,6 +335,16 @@ func TestFlagStringifying(t *testing.T) { fl: &IntFlag{Name: "pens", DefaultText: "-19"}, expected: "--pens value\t(default: -19)", }, + { + name: "uint-slice-flag", + fl: &UintSliceFlag{Name: "pencils"}, + expected: "--pencils value\t", + }, + { + name: "uint-slice-flag-with-default-text", + fl: &UintFlag{Name: "pens", DefaultText: "29"}, + expected: "--pens value\t(default: 29)", + }, { name: "int64-flag", fl: &Int64Flag{Name: "flume"}, @@ -272,6 +365,16 @@ func TestFlagStringifying(t *testing.T) { fl: &Int64SliceFlag{Name: "handles", DefaultText: "-2"}, expected: "--handles value\t(default: -2)", }, + { + name: "uint64-slice-flag", + fl: &Uint64SliceFlag{Name: "drawers"}, + expected: "--drawers value\t", + }, + { + name: "uint64-slice-flag-with-default-text", + fl: &Uint64SliceFlag{Name: "handles", DefaultText: "-2"}, + expected: "--handles value\t(default: -2)", + }, { name: "path-flag", fl: &PathFlag{Name: "soup"}, @@ -544,11 +647,11 @@ var stringSliceFlagTests = []struct { value *StringSlice expected string }{ - {"foo", nil, NewStringSlice(""), "--foo value\t(accepts multiple inputs)"}, - {"f", nil, NewStringSlice(""), "-f value\t(accepts multiple inputs)"}, - {"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")\t(accepts multiple inputs)"}, - {"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")\t(accepts multiple inputs)"}, - {"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")\t(accepts multiple inputs)"}, + {"foo", nil, NewStringSlice(""), "--foo value [ --foo value ]\t"}, + {"f", nil, NewStringSlice(""), "-f value [ -f value ]\t"}, + {"f", nil, NewStringSlice("Lipstick"), "-f value [ -f value ]\t(default: \"Lipstick\")"}, + {"test", nil, NewStringSlice("Something"), "--test value [ --test value ]\t(default: \"Something\")"}, + {"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value [ --dee value, -d value ]\t(default: \"Inka\", \"Dinka\", \"dooo\")"}, } func TestStringSliceFlagHelpOutput(t *testing.T) { @@ -897,9 +1000,9 @@ var intSliceFlagTests = []struct { value *IntSlice expected string }{ - {"heads", nil, NewIntSlice(), "--heads value\t(accepts multiple inputs)"}, - {"H", nil, NewIntSlice(), "-H value\t(accepts multiple inputs)"}, - {"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)\t(accepts multiple inputs)"}, + {"heads", nil, NewIntSlice(), "--heads value [ --heads value ]\t"}, + {"H", nil, NewIntSlice(), "-H value [ -H value ]\t"}, + {"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value [ -H value, --heads value ]\t(default: 9, 3)"}, } func TestIntSliceFlagHelpOutput(t *testing.T) { @@ -941,6 +1044,47 @@ func TestIntSliceFlagApply_SetsAllNames(t *testing.T) { expect(t, err, nil) } +func TestIntSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + var val IntSlice + fl := IntSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []int(nil)) + expect(t, set.Lookup("goat").Value.(*IntSlice).Value(), []int{1, 2}) +} + +func TestIntSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + val := NewIntSlice(3, 4) + fl := IntSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []int{3, 4}) + expect(t, set.Lookup("goat").Value.(*IntSlice).Value(), []int{1, 2}) +} + +func TestIntSliceFlagApply_DefaultValueWithDestination(t *testing.T) { + defValue := []int{1, 2} + + fl := IntSliceFlag{Name: "country", Value: NewIntSlice(defValue...), Destination: NewIntSlice(3)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{}) + expect(t, err, nil) + expect(t, defValue, fl.Destination.Value()) +} + func TestIntSliceFlagApply_ParentContext(t *testing.T) { _ = (&App{ Flags: []Flag{ @@ -994,10 +1138,10 @@ var int64SliceFlagTests = []struct { value *Int64Slice expected string }{ - {"heads", nil, NewInt64Slice(), "--heads value\t(accepts multiple inputs)"}, - {"H", nil, NewInt64Slice(), "-H value\t(accepts multiple inputs)"}, + {"heads", nil, NewInt64Slice(), "--heads value [ --heads value ]\t"}, + {"H", nil, NewInt64Slice(), "-H value [ -H value ]\t"}, {"heads", []string{"H"}, NewInt64Slice(int64(2), int64(17179869184)), - "--heads value, -H value\t(default: 2, 17179869184)\t(accepts multiple inputs)"}, + "--heads value, -H value [ --heads value, -H value ]\t(default: 2, 17179869184)"}, } func TestInt64SliceFlagHelpOutput(t *testing.T) { @@ -1030,6 +1174,56 @@ func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) { } } +func TestInt64SliceFlagApply_SetsAllNames(t *testing.T) { + fl := Int64SliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{"--bits", "23", "-B", "3", "--bips", "99"}) + expect(t, err, nil) +} + +func TestInt64SliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + var val Int64Slice + fl := Int64SliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []int64(nil)) + expect(t, set.Lookup("goat").Value.(*Int64Slice).Value(), []int64{1, 2}) +} + +func TestInt64SliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + val := NewInt64Slice(3, 4) + fl := Int64SliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []int64{3, 4}) + expect(t, set.Lookup("goat").Value.(*Int64Slice).Value(), []int64{1, 2}) +} + +func TestInt64SliceFlagApply_DefaultValueWithDestination(t *testing.T) { + defValue := []int64{1, 2} + + fl := Int64SliceFlag{Name: "country", Value: NewInt64Slice(defValue...), Destination: NewInt64Slice(3)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{}) + expect(t, err, nil) + expect(t, defValue, fl.Destination.Value()) +} + func TestInt64SliceFlagApply_ParentContext(t *testing.T) { _ = (&App{ Flags: []Flag{ @@ -1092,6 +1286,298 @@ func TestInt64SliceFlagValueFromContext(t *testing.T) { expect(t, f.Get(ctx), []int64{1, 2, 3}) } +var uintSliceFlagTests = []struct { + name string + aliases []string + value *UintSlice + expected string +}{ + {"heads", nil, NewUintSlice(), "--heads value [ --heads value ]\t"}, + {"H", nil, NewUintSlice(), "-H value [ -H value ]\t"}, + {"heads", []string{"H"}, NewUintSlice(uint(2), uint(17179869184)), + "--heads value, -H value [ --heads value, -H value ]\t(default: 2, 17179869184)"}, +} + +func TestUintSliceFlagHelpOutput(t *testing.T) { + for _, test := range uintSliceFlagTests { + fl := UintSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} + output := fl.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestUintSliceFlagWithEnvVarHelpOutput(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_SMURF", "42,17179869184") + + for _, test := range uintSliceFlagTests { + fl := UintSliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}} + output := fl.String() + + expectedSuffix := " [$APP_SMURF]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_SMURF%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%q does not end with"+expectedSuffix, output) + } + } +} + +func TestUintSliceFlagApply_SetsAllNames(t *testing.T) { + fl := UintSliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{"--bits", "23", "-B", "3", "--bips", "99"}) + expect(t, err, nil) +} + +func TestUintSliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + var val UintSlice + fl := UintSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []uint(nil)) + expect(t, set.Lookup("goat").Value.(*UintSlice).Value(), []uint{1, 2}) +} + +func TestUintSliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + val := NewUintSlice(3, 4) + fl := UintSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []uint{3, 4}) + expect(t, set.Lookup("goat").Value.(*UintSlice).Value(), []uint{1, 2}) +} + +func TestUintSliceFlagApply_DefaultValueWithDestination(t *testing.T) { + defValue := []uint{1, 2} + + fl := UintSliceFlag{Name: "country", Value: NewUintSlice(defValue...), Destination: NewUintSlice(3)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{}) + expect(t, err, nil) + expect(t, defValue, fl.Destination.Value()) +} + +func TestUintSliceFlagApply_ParentContext(t *testing.T) { + _ = (&App{ + Flags: []Flag{ + &UintSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewUintSlice(1, 2, 3)}, + }, + Commands: []*Command{ + { + Name: "child", + Action: func(ctx *Context) error { + expected := []uint{1, 2, 3} + if !reflect.DeepEqual(ctx.UintSlice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.UintSlice("numbers")) + } + if !reflect.DeepEqual(ctx.UintSlice("n"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.UintSlice("n")) + } + return nil + }, + }, + }, + }).Run([]string{"run", "child"}) +} + +func TestUintSliceFlag_SetFromParentContext(t *testing.T) { + fl := &UintSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewUintSlice(1, 2, 3, 4)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + ctx := &Context{ + parentContext: &Context{ + flagSet: set, + }, + flagSet: flag.NewFlagSet("empty", 0), + } + expected := []uint{1, 2, 3, 4} + if !reflect.DeepEqual(ctx.UintSlice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.UintSlice("numbers")) + } +} +func TestUintSliceFlag_ReturnNil(t *testing.T) { + fl := &UintSliceFlag{} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + ctx := &Context{ + parentContext: &Context{ + flagSet: set, + }, + flagSet: flag.NewFlagSet("empty", 0), + } + expected := []uint(nil) + if !reflect.DeepEqual(ctx.UintSlice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.UintSlice("numbers")) + } +} + +var uint64SliceFlagTests = []struct { + name string + aliases []string + value *Uint64Slice + expected string +}{ + {"heads", nil, NewUint64Slice(), "--heads value [ --heads value ]\t"}, + {"H", nil, NewUint64Slice(), "-H value [ -H value ]\t"}, + {"heads", []string{"H"}, NewUint64Slice(uint64(2), uint64(17179869184)), + "--heads value, -H value [ --heads value, -H value ]\t(default: 2, 17179869184)"}, +} + +func TestUint64SliceFlagHelpOutput(t *testing.T) { + for _, test := range uint64SliceFlagTests { + fl := Uint64SliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value} + output := fl.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestUint64SliceFlagWithEnvVarHelpOutput(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("APP_SMURF", "42,17179869184") + + for _, test := range uint64SliceFlagTests { + fl := Uint64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}} + output := fl.String() + + expectedSuffix := " [$APP_SMURF]" + if runtime.GOOS == "windows" { + expectedSuffix = " [%APP_SMURF%]" + } + if !strings.HasSuffix(output, expectedSuffix) { + t.Errorf("%q does not end with"+expectedSuffix, output) + } + } +} + +func TestUint64SliceFlagApply_SetsAllNames(t *testing.T) { + fl := Uint64SliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{"--bits", "23", "-B", "3", "--bips", "99"}) + expect(t, err, nil) +} + +func TestUint64SliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + var val Uint64Slice + fl := Uint64SliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []uint64(nil)) + expect(t, set.Lookup("goat").Value.(*Uint64Slice).Value(), []uint64{1, 2}) +} + +func TestUint64SliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1 , 2") + val := NewUint64Slice(3, 4) + fl := Uint64SliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []uint64{3, 4}) + expect(t, set.Lookup("goat").Value.(*Uint64Slice).Value(), []uint64{1, 2}) +} + +func TestUint64SliceFlagApply_DefaultValueWithDestination(t *testing.T) { + defValue := []uint64{1, 2} + + fl := Uint64SliceFlag{Name: "country", Value: NewUint64Slice(defValue...), Destination: NewUint64Slice(3)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{}) + expect(t, err, nil) + expect(t, defValue, fl.Destination.Value()) +} + +func TestUint64SliceFlagApply_ParentContext(t *testing.T) { + _ = (&App{ + Flags: []Flag{ + &Uint64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewUint64Slice(1, 2, 3)}, + }, + Commands: []*Command{ + { + Name: "child", + Action: func(ctx *Context) error { + expected := []uint64{1, 2, 3} + if !reflect.DeepEqual(ctx.Uint64Slice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Uint64Slice("numbers")) + } + if !reflect.DeepEqual(ctx.Uint64Slice("n"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Uint64Slice("n")) + } + return nil + }, + }, + }, + }).Run([]string{"run", "child"}) +} + +func TestUint64SliceFlag_SetFromParentContext(t *testing.T) { + fl := &Uint64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewUint64Slice(1, 2, 3, 4)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + ctx := &Context{ + parentContext: &Context{ + flagSet: set, + }, + flagSet: flag.NewFlagSet("empty", 0), + } + expected := []uint64{1, 2, 3, 4} + if !reflect.DeepEqual(ctx.Uint64Slice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Uint64Slice("numbers")) + } +} +func TestUint64SliceFlag_ReturnNil(t *testing.T) { + fl := &Uint64SliceFlag{} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + ctx := &Context{ + parentContext: &Context{ + flagSet: set, + }, + flagSet: flag.NewFlagSet("empty", 0), + } + expected := []uint64(nil) + if !reflect.DeepEqual(ctx.Uint64Slice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Uint64Slice("numbers")) + } +} + var float64FlagTests = []struct { name string expected string @@ -1155,10 +1641,10 @@ var float64SliceFlagTests = []struct { value *Float64Slice expected string }{ - {"heads", nil, NewFloat64Slice(), "--heads value\t(accepts multiple inputs)"}, - {"H", nil, NewFloat64Slice(), "-H value\t(accepts multiple inputs)"}, + {"heads", nil, NewFloat64Slice(), "--heads value [ --heads value ]\t"}, + {"H", nil, NewFloat64Slice(), "-H value [ -H value ]\t"}, {"heads", []string{"H"}, NewFloat64Slice(0.1234, -10.5), - "--heads value, -H value\t(default: 0.1234, -10.5)\t(accepts multiple inputs)"}, + "--heads value, -H value [ --heads value, -H value ]\t(default: 0.1234, -10.5)"}, } func TestFloat64SliceFlagHelpOutput(t *testing.T) { @@ -1190,6 +1676,56 @@ func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) { } } +func TestFloat64SliceFlagApply_SetsAllNames(t *testing.T) { + fl := Float64SliceFlag{Name: "bits", Aliases: []string{"B", "bips"}} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{"--bits", "23", "-B", "3", "--bips", "99"}) + expect(t, err, nil) +} + +func TestFloat64SliceFlagApply_UsesEnvValues_noDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1.0 , 2.0") + var val Float64Slice + fl := Float64SliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []float64(nil)) + expect(t, set.Lookup("goat").Value.(*Float64Slice).Value(), []float64{1, 2}) +} + +func TestFloat64SliceFlagApply_UsesEnvValues_withDefault(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "1.0 , 2.0") + val := NewFloat64Slice(3.0, 4.0) + fl := Float64SliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), []float64{3, 4}) + expect(t, set.Lookup("goat").Value.(*Float64Slice).Value(), []float64{1, 2}) +} + +func TestFloat64SliceFlagApply_DefaultValueWithDestination(t *testing.T) { + defValue := []float64{1.0, 2.0} + + fl := Float64SliceFlag{Name: "country", Value: NewFloat64Slice(defValue...), Destination: NewFloat64Slice(3)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{}) + expect(t, err, nil) + expect(t, defValue, fl.Destination.Value()) +} + func TestFloat64SliceFlagValueFromContext(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Var(NewFloat64Slice(1.23, 4.56), "myflag", "doc") @@ -1198,6 +1734,29 @@ func TestFloat64SliceFlagValueFromContext(t *testing.T) { expect(t, f.Get(ctx), []float64{1.23, 4.56}) } +func TestFloat64SliceFlagApply_ParentContext(t *testing.T) { + _ = (&App{ + Flags: []Flag{ + &Float64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewFloat64Slice(1.0, 2.0, 3.0)}, + }, + Commands: []*Command{ + { + Name: "child", + Action: func(ctx *Context) error { + expected := []float64{1.0, 2.0, 3.0} + if !reflect.DeepEqual(ctx.Float64Slice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Float64Slice("numbers")) + } + if !reflect.DeepEqual(ctx.Float64Slice("n"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Float64Slice("n")) + } + return nil + }, + }, + }, + }).Run([]string{"run", "child"}) +} + var genericFlagTests = []struct { name string value Generic @@ -2282,6 +2841,38 @@ func TestInt64Slice_Serialized_Set(t *testing.T) { } } +func TestUintSlice_Serialized_Set(t *testing.T) { + sl0 := NewUintSlice(1, 2) + ser0 := sl0.Serialize() + + if len(ser0) < len(slPfx) { + t.Fatalf("serialized shorter than expected: %q", ser0) + } + + sl1 := NewUintSlice(3, 4) + _ = sl1.Set(ser0) + + if sl0.String() != sl1.String() { + t.Fatalf("pre and post serialization do not match: %v != %v", sl0, sl1) + } +} + +func TestUint64Slice_Serialized_Set(t *testing.T) { + sl0 := NewUint64Slice(1, 2) + ser0 := sl0.Serialize() + + if len(ser0) < len(slPfx) { + t.Fatalf("serialized shorter than expected: %q", ser0) + } + + sl1 := NewUint64Slice(3, 4) + _ = sl1.Set(ser0) + + if sl0.String() != sl1.String() { + t.Fatalf("pre and post serialization do not match: %v != %v", sl0, sl1) + } +} + func TestTimestamp_set(t *testing.T) { ts := Timestamp{ timestamp: nil, @@ -2381,46 +2972,58 @@ type flagDefaultTestCase struct { func TestFlagDefaultValue(t *testing.T) { cases := []*flagDefaultTestCase{ { - name: "stringSclice", + name: "stringSlice", flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")}, toParse: []string{"--flag", "parsed"}, - expect: `--flag value (default: "default1", "default2") (accepts multiple inputs)`, + expect: `--flag value [ --flag value ] (default: "default1", "default2")`, }, { - name: "float64Sclice", + name: "float64Slice", flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)}, toParse: []string{"--flag", "13.3"}, - expect: `--flag value (default: 1.1, 2.2) (accepts multiple inputs)`, + expect: `--flag value [ --flag value ] (default: 1.1, 2.2)`, }, { - name: "int64Sclice", + name: "int64Slice", flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)}, toParse: []string{"--flag", "13"}, - expect: `--flag value (default: 1, 2) (accepts multiple inputs)`, + expect: `--flag value [ --flag value ] (default: 1, 2)`, }, { - name: "intSclice", + name: "intSlice", flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)}, toParse: []string{"--flag", "13"}, - expect: `--flag value (default: 1, 2) (accepts multiple inputs)`, + expect: `--flag value [ --flag value ] (default: 1, 2)`, + }, + { + name: "uint64Slice", + flag: &Uint64SliceFlag{Name: "flag", Value: NewUint64Slice(1, 2)}, + toParse: []string{"--flag", "13"}, + expect: `--flag value [ --flag value ] (default: 1, 2)`, + }, + { + name: "uintSlice", + flag: &UintSliceFlag{Name: "flag", Value: NewUintSlice(1, 2)}, + toParse: []string{"--flag", "13"}, + expect: `--flag value [ --flag value ] (default: 1, 2)`, }, { name: "string", flag: &StringFlag{Name: "flag", Value: "default"}, toParse: []string{"--flag", "parsed"}, - expect: `--flag value (default: "default")`, + expect: `--flag value (default: "default")`, }, { name: "bool", flag: &BoolFlag{Name: "flag", Value: true}, toParse: []string{"--flag", "false"}, - expect: `--flag (default: true)`, + expect: `--flag (default: true)`, }, { name: "uint64", flag: &Uint64Flag{Name: "flag", Value: 1}, toParse: []string{"--flag", "13"}, - expect: `--flag value (default: 1)`, + expect: `--flag value (default: 1)`, }, } for i, v := range cases { @@ -2446,29 +3049,41 @@ type flagValueTestCase struct { func TestFlagValue(t *testing.T) { cases := []*flagValueTestCase{ &flagValueTestCase{ - name: "stringSclice", + name: "stringSlice", flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")}, toParse: []string{"--flag", "parsed,parsed2", "--flag", "parsed3,parsed4"}, expect: `[parsed parsed2 parsed3 parsed4]`, }, &flagValueTestCase{ - name: "float64Sclice", + name: "float64Slice", flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)}, toParse: []string{"--flag", "13.3,14.4", "--flag", "15.5,16.6"}, expect: `[]float64{13.3, 14.4, 15.5, 16.6}`, }, &flagValueTestCase{ - name: "int64Sclice", + name: "int64Slice", flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)}, toParse: []string{"--flag", "13,14", "--flag", "15,16"}, expect: `[]int64{13, 14, 15, 16}`, }, &flagValueTestCase{ - name: "intSclice", + name: "intSlice", flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)}, toParse: []string{"--flag", "13,14", "--flag", "15,16"}, expect: `[]int{13, 14, 15, 16}`, }, + &flagValueTestCase{ + name: "uint64Slice", + flag: &Uint64SliceFlag{Name: "flag", Value: NewUint64Slice(1, 2)}, + toParse: []string{"--flag", "13,14", "--flag", "15,16"}, + expect: `[]uint64{13, 14, 15, 16}`, + }, + &flagValueTestCase{ + name: "uintSlice", + flag: &UintSliceFlag{Name: "flag", Value: NewUintSlice(1, 2)}, + toParse: []string{"--flag", "13,14", "--flag", "15,16"}, + expect: `[]uint{13, 14, 15, 16}`, + }, } for i, v := range cases { set := flag.NewFlagSet("test", 0) diff --git a/flag_timestamp.go b/flag_timestamp.go index 8759b85..671be8b 100644 --- a/flag_timestamp.go +++ b/flag_timestamp.go @@ -75,7 +75,7 @@ func (t *Timestamp) Get() interface{} { // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. func (f *TimestampFlag) GetValue() string { - if f.Value != nil { + if f.Value != nil && f.Value.timestamp != nil { return f.Value.timestamp.String() } return "" @@ -120,6 +120,15 @@ func (f *TimestampFlag) Get(ctx *Context) *time.Time { return ctx.Timestamp(f.Name) } +// RunAction executes flag action if set +func (f *TimestampFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Timestamp(f.Name)) + } + + return nil +} + // Timestamp gets the timestamp from a flag name func (cCtx *Context) Timestamp(name string) *time.Time { if fs := cCtx.lookupFlagSet(name); fs != nil { diff --git a/flag_uint.go b/flag_uint.go index d03fa7c..e1b0450 100644 --- a/flag_uint.go +++ b/flag_uint.go @@ -10,7 +10,7 @@ import ( func (f *UintFlag) Apply(set *flag.FlagSet) error { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { - valInt, err := strconv.ParseUint(val, 0, 64) + valInt, err := strconv.ParseUint(val, f.Base, 64) if err != nil { return fmt.Errorf("could not parse %q as uint value from %s for flag %s: %s", val, source, f.Name, err) } @@ -42,6 +42,15 @@ func (f *UintFlag) Get(ctx *Context) uint { return ctx.Uint(f.Name) } +// RunAction executes flag action if set +func (f *UintFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Uint(f.Name)) + } + + return nil +} + // Uint looks up the value of a local UintFlag, returns // 0 if not found func (cCtx *Context) Uint(name string) uint { diff --git a/flag_uint64.go b/flag_uint64.go index 5c06ab4..593b260 100644 --- a/flag_uint64.go +++ b/flag_uint64.go @@ -10,7 +10,7 @@ import ( func (f *Uint64Flag) Apply(set *flag.FlagSet) error { if val, source, found := flagFromEnvOrFile(f.EnvVars, f.FilePath); found { if val != "" { - valInt, err := strconv.ParseUint(val, 0, 64) + valInt, err := strconv.ParseUint(val, f.Base, 64) if err != nil { return fmt.Errorf("could not parse %q as uint64 value from %s for flag %s: %s", val, source, f.Name, err) } @@ -42,6 +42,15 @@ func (f *Uint64Flag) Get(ctx *Context) uint64 { return ctx.Uint64(f.Name) } +// RunAction executes flag action if set +func (f *Uint64Flag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Uint64(f.Name)) + } + + return nil +} + // Uint64 looks up the value of a local Uint64Flag, returns // 0 if not found func (cCtx *Context) Uint64(name string) uint64 { diff --git a/flag_uint64_slice.go b/flag_uint64_slice.go new file mode 100644 index 0000000..662a03e --- /dev/null +++ b/flag_uint64_slice.go @@ -0,0 +1,212 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// Uint64Slice wraps []int64 to satisfy flag.Value +type Uint64Slice struct { + slice []uint64 + hasBeenSet bool +} + +// NewUint64Slice makes an *Uint64Slice with default values +func NewUint64Slice(defaults ...uint64) *Uint64Slice { + return &Uint64Slice{slice: append([]uint64{}, defaults...)} +} + +// clone allocate a copy of self object +func (i *Uint64Slice) clone() *Uint64Slice { + n := &Uint64Slice{ + slice: make([]uint64, len(i.slice)), + hasBeenSet: i.hasBeenSet, + } + copy(n.slice, i.slice) + return n +} + +// Set parses the value into an integer and appends it to the list of values +func (i *Uint64Slice) Set(value string) error { + if !i.hasBeenSet { + i.slice = []uint64{} + i.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) + i.hasBeenSet = true + return nil + } + + for _, s := range flagSplitMultiValues(value) { + tmp, err := strconv.ParseUint(strings.TrimSpace(s), 0, 64) + if err != nil { + return err + } + + i.slice = append(i.slice, tmp) + } + + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (i *Uint64Slice) String() string { + v := i.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]uint64, 0) + } + str := fmt.Sprintf("%d", v) + str = strings.Replace(str, " ", ", ", -1) + str = strings.Replace(str, "[", "{", -1) + str = strings.Replace(str, "]", "}", -1) + return fmt.Sprintf("[]uint64%s", str) +} + +// Serialize allows Uint64Slice to fulfill Serializer +func (i *Uint64Slice) Serialize() string { + jsonBytes, _ := json.Marshal(i.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of ints set by this flag +func (i *Uint64Slice) Value() []uint64 { + return i.slice +} + +// Get returns the slice of ints set by this flag +func (i *Uint64Slice) Get() interface{} { + return *i +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Uint64SliceFlag) String() string { + return withEnvHint(f.GetEnvVars(), f.stringify()) +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Uint64SliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Uint64SliceFlag) GetUsage() string { + return f.Usage +} + +// GetCategory returns the category for the flag +func (f *Uint64SliceFlag) GetCategory() string { + return f.Category +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Uint64SliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// GetDefaultText returns the default text for this flag +func (f *Uint64SliceFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *Uint64SliceFlag) GetEnvVars() []string { + return f.EnvVars +} + +// Apply populates the flag given the flag set and environment +func (f *Uint64SliceFlag) Apply(set *flag.FlagSet) error { + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]uint64, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + + // resolve setValue (what we will assign to the set) + var setValue *Uint64Slice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(Uint64Slice) + } + + if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" { + for _, s := range flagSplitMultiValues(val) { + if err := setValue.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %q as uint64 slice value from %s for flag %s: %s", val, source, f.Name, err) + } + } + + // Set this to false so that we reset the slice if we then set values from + // flags that have already been set by the environment. + setValue.hasBeenSet = false + f.HasBeenSet = true + } + + for _, name := range f.Names() { + set.Var(setValue, name, f.Usage) + } + + return nil +} + +// Get returns the flag’s value in the given Context. +func (f *Uint64SliceFlag) Get(ctx *Context) []uint64 { + return ctx.Uint64Slice(f.Name) +} + +func (f *Uint64SliceFlag) stringify() string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.FormatUint(i, 10)) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +// RunAction executes flag action if set +func (f *Uint64SliceFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.Uint64Slice(f.Name)) + } + + return nil +} + +// Uint64Slice looks up the value of a local Uint64SliceFlag, returns +// nil if not found +func (cCtx *Context) Uint64Slice(name string) []uint64 { + if fs := cCtx.lookupFlagSet(name); fs != nil { + return lookupUint64Slice(name, fs) + } + return nil +} + +func lookupUint64Slice(name string, set *flag.FlagSet) []uint64 { + f := set.Lookup(name) + if f != nil { + if slice, ok := unwrapFlagValue(f.Value).(*Uint64Slice); ok { + return slice.Value() + } + } + return nil +} diff --git a/flag_uint_slice.go b/flag_uint_slice.go new file mode 100644 index 0000000..3689eaa --- /dev/null +++ b/flag_uint_slice.go @@ -0,0 +1,223 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// UintSlice wraps []int to satisfy flag.Value +type UintSlice struct { + slice []uint + hasBeenSet bool +} + +// NewUintSlice makes an *UintSlice with default values +func NewUintSlice(defaults ...uint) *UintSlice { + return &UintSlice{slice: append([]uint{}, defaults...)} +} + +// clone allocate a copy of self object +func (i *UintSlice) clone() *UintSlice { + n := &UintSlice{ + slice: make([]uint, len(i.slice)), + hasBeenSet: i.hasBeenSet, + } + copy(n.slice, i.slice) + return n +} + +// TODO: Consistently have specific Set function for Int64 and Float64 ? +// SetInt directly adds an integer to the list of values +func (i *UintSlice) SetUint(value uint) { + if !i.hasBeenSet { + i.slice = []uint{} + i.hasBeenSet = true + } + + i.slice = append(i.slice, value) +} + +// Set parses the value into an integer and appends it to the list of values +func (i *UintSlice) Set(value string) error { + if !i.hasBeenSet { + i.slice = []uint{} + i.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) + i.hasBeenSet = true + return nil + } + + for _, s := range flagSplitMultiValues(value) { + tmp, err := strconv.ParseUint(strings.TrimSpace(s), 0, 32) + if err != nil { + return err + } + + i.slice = append(i.slice, uint(tmp)) + } + + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (i *UintSlice) String() string { + v := i.slice + if v == nil { + // treat nil the same as zero length non-nil + v = make([]uint, 0) + } + str := fmt.Sprintf("%d", v) + str = strings.Replace(str, " ", ", ", -1) + str = strings.Replace(str, "[", "{", -1) + str = strings.Replace(str, "]", "}", -1) + return fmt.Sprintf("[]uint%s", str) +} + +// Serialize allows UintSlice to fulfill Serializer +func (i *UintSlice) Serialize() string { + jsonBytes, _ := json.Marshal(i.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of ints set by this flag +func (i *UintSlice) Value() []uint { + return i.slice +} + +// Get returns the slice of ints set by this flag +func (i *UintSlice) Get() interface{} { + return *i +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *UintSliceFlag) String() string { + return withEnvHint(f.GetEnvVars(), f.stringify()) +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *UintSliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *UintSliceFlag) GetUsage() string { + return f.Usage +} + +// GetCategory returns the category for the flag +func (f *UintSliceFlag) GetCategory() string { + return f.Category +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *UintSliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// GetDefaultText returns the default text for this flag +func (f *UintSliceFlag) GetDefaultText() string { + if f.DefaultText != "" { + return f.DefaultText + } + return f.GetValue() +} + +// GetEnvVars returns the env vars for this flag +func (f *UintSliceFlag) GetEnvVars() []string { + return f.EnvVars +} + +// Apply populates the flag given the flag set and environment +func (f *UintSliceFlag) Apply(set *flag.FlagSet) error { + // apply any default + if f.Destination != nil && f.Value != nil { + f.Destination.slice = make([]uint, len(f.Value.slice)) + copy(f.Destination.slice, f.Value.slice) + } + + // resolve setValue (what we will assign to the set) + var setValue *UintSlice + switch { + case f.Destination != nil: + setValue = f.Destination + case f.Value != nil: + setValue = f.Value.clone() + default: + setValue = new(UintSlice) + } + + if val, source, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok && val != "" { + for _, s := range flagSplitMultiValues(val) { + if err := setValue.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %q as uint slice value from %s for flag %s: %s", val, source, f.Name, err) + } + } + + // Set this to false so that we reset the slice if we then set values from + // flags that have already been set by the environment. + setValue.hasBeenSet = false + f.HasBeenSet = true + } + + for _, name := range f.Names() { + set.Var(setValue, name, f.Usage) + } + + return nil +} + +// Get returns the flag’s value in the given Context. +func (f *UintSliceFlag) Get(ctx *Context) []uint { + return ctx.UintSlice(f.Name) +} + +func (f *UintSliceFlag) stringify() string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.FormatUint(uint64(i), 10)) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +// RunAction executes flag action if set +func (f *UintSliceFlag) RunAction(c *Context) error { + if f.Action != nil { + return f.Action(c, c.UintSlice(f.Name)) + } + + return nil +} + +// UintSlice looks up the value of a local UintSliceFlag, returns +// nil if not found +func (cCtx *Context) UintSlice(name string) []uint { + if fs := cCtx.lookupFlagSet(name); fs != nil { + return lookupUintSlice(name, fs) + } + return nil +} + +func lookupUintSlice(name string, set *flag.FlagSet) []uint { + f := set.Lookup(name) + if f != nil { + if slice, ok := unwrapFlagValue(f.Value).(*UintSlice); ok { + return slice.Value() + } + } + return nil +} diff --git a/funcs.go b/funcs.go index 0a9b22c..e77b0d0 100644 --- a/funcs.go +++ b/funcs.go @@ -23,6 +23,9 @@ type CommandNotFoundFunc func(*Context, string) // is displayed and the execution is interrupted. type OnUsageErrorFunc func(cCtx *Context, err error, isSubcommand bool) error +// InvalidFlagAccessFunc is executed when an invalid flag is accessed from the context. +type InvalidFlagAccessFunc func(*Context, string) + // ExitErrHandlerFunc is executed if provided in order to handle exitError values // returned by Actions and Before/After functions. type ExitErrHandlerFunc func(cCtx *Context, err error) diff --git a/go.mod b/go.mod index 62910e1..d277426 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,10 @@ require ( github.com/BurntSushi/toml v1.1.0 github.com/cpuguy83/go-md2man/v2 v2.0.2 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 - golang.org/x/text v0.3.7 gopkg.in/yaml.v3 v3.0.1 ) -require github.com/russross/blackfriday/v2 v2.1.0 // indirect +require ( + github.com/russross/blackfriday/v2 v2.1.0 // indirect + golang.org/x/text v0.3.7 // indirect +) diff --git a/godoc-current.txt b/godoc-current.txt index 697c130..2c8b5e0 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -5,24 +5,24 @@ line Go applications. cli is designed to be easy to understand and write, the most simple cli application can be written as follows: func main() { - (&cli.App{}).Run(os.Args) + (&cli.App{}).Run(os.Args) } Of course this application does not do much, so let's make this an actual application: - func main() { - app := &cli.App{ - Name: "greet", - Usage: "say a greeting", - Action: func(c *cli.Context) error { - fmt.Println("Greetings") - return nil - }, - } - - app.Run(os.Args) - } + func main() { + app := &cli.App{ + Name: "greet", + Usage: "say a greeting", + Action: func(c *cli.Context) error { + fmt.Println("Greetings") + return nil + }, + } + + app.Run(os.Args) + } VARIABLES @@ -49,8 +49,8 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} GLOBAL OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} @@ -82,12 +82,10 @@ DESCRIPTION: OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} - {{end}}{{range .Flags}}{{.}} - {{end}}{{end}}{{else}}{{if .VisibleFlags}} + {{end}}{{range .Flags}}{{.}}{{end}}{{end}}{{else}}{{if .VisibleFlags}} OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{end}} + {{range .VisibleFlags}}{{.}}{{end}}{{end}}{{end}} ` CommandHelpTemplate is the text template for the command help topic. cli.go uses text/template to render templates. You can render custom help text by @@ -157,12 +155,11 @@ DESCRIPTION: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} + {{range .VisibleFlags}}{{.}}{{end}}{{end}} ` SubcommandHelpTemplate is the text template for the subcommand help topic. cli.go uses text/template to render templates. You can render custom help @@ -300,6 +297,8 @@ type App struct { CommandNotFound CommandNotFoundFunc // Execute this function if a usage error occurs OnUsageError OnUsageErrorFunc + // Execute this function when an invalid flag is accessed from the context + InvalidFlagAccessHandler InvalidFlagAccessFunc // Compilation date Compiled time.Time // List of all authors who contributed @@ -450,6 +449,10 @@ type BoolFlag struct { Aliases []string EnvVars []string + + Count *int + + Action func(*Context, bool) error } BoolFlag is a flag with type bool @@ -487,6 +490,9 @@ func (f *BoolFlag) IsVisible() bool func (f *BoolFlag) Names() []string Names returns the names of the flag +func (f *BoolFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *BoolFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -621,6 +627,9 @@ func (cCtx *Context) Args() Args func (cCtx *Context) Bool(name string) bool Bool looks up the value of a local BoolFlag, returns false if not found +func (cCtx *Context) Count(name string) int + Count returns the num of occurences of this flag + func (cCtx *Context) Duration(name string) time.Duration Duration looks up the value of a local DurationFlag, returns 0 if not found @@ -690,9 +699,23 @@ func (cCtx *Context) Uint(name string) uint func (cCtx *Context) Uint64(name string) uint64 Uint64 looks up the value of a local Uint64Flag, returns 0 if not found +func (cCtx *Context) Uint64Slice(name string) []uint64 + Uint64Slice looks up the value of a local Uint64SliceFlag, returns nil if + not found + +func (cCtx *Context) UintSlice(name string) []uint + UintSlice looks up the value of a local UintSliceFlag, returns nil if not + found + func (cCtx *Context) Value(name string) interface{} Value returns the value of the flag corresponding to `name` +type Countable interface { + Count() int +} + Countable is an interface to enable detection of flag values which support + repetitive flags + type DurationFlag struct { Name string @@ -710,6 +733,8 @@ type DurationFlag struct { Aliases []string EnvVars []string + + Action func(*Context, time.Duration) error } DurationFlag is a flag with type time.Duration @@ -747,6 +772,9 @@ func (f *DurationFlag) IsVisible() bool func (f *DurationFlag) Names() []string Names returns the names of the flag +func (f *DurationFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *DurationFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -820,6 +848,8 @@ type Flag interface { // GetValue returns the flags value as string representation and an empty // string if the flag takes no value at all. GetValue() string + + RunAction(*Context) error } Flag is a common interface related to parsing flags in cli. For more advanced flag parsing techniques, it is recommended that this interface be @@ -913,6 +943,8 @@ type Float64Flag struct { Aliases []string EnvVars []string + + Action func(*Context, float64) error } Float64Flag is a flag with type float64 @@ -950,6 +982,9 @@ func (f *Float64Flag) IsVisible() bool func (f *Float64Flag) Names() []string Names returns the names of the flag +func (f *Float64Flag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Float64Flag) String() string String returns a readable representation of this value (for usage defaults) @@ -996,6 +1031,8 @@ type Float64SliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []float64) error } Float64SliceFlag is a flag with type *Float64Slice @@ -1035,6 +1072,9 @@ func (f *Float64SliceFlag) IsVisible() bool func (f *Float64SliceFlag) Names() []string Names returns the names of the flag +func (f *Float64SliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Float64SliceFlag) SetDestination(slice []float64) func (f *Float64SliceFlag) SetValue(slice []float64) @@ -1064,16 +1104,18 @@ type GenericFlag struct { HasBeenSet bool Value Generic - Destination *Generic + Destination Generic Aliases []string EnvVars []string TakesFile bool + + Action func(*Context, interface{}) error } GenericFlag is a flag with type Generic -func (f GenericFlag) Apply(set *flag.FlagSet) error +func (f *GenericFlag) Apply(set *flag.FlagSet) error Apply takes the flagset and calls Set on the generic flag with the value provided by the user for parsing by the flag @@ -1108,6 +1150,9 @@ func (f *GenericFlag) IsVisible() bool func (f *GenericFlag) Names() []string Names returns the names of the flag +func (f *GenericFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *GenericFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1131,6 +1176,10 @@ type Int64Flag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, int64) error } Int64Flag is a flag with type int64 @@ -1168,6 +1217,9 @@ func (f *Int64Flag) IsVisible() bool func (f *Int64Flag) Names() []string Names returns the names of the flag +func (f *Int64Flag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Int64Flag) String() string String returns a readable representation of this value (for usage defaults) @@ -1214,6 +1266,8 @@ type Int64SliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []int64) error } Int64SliceFlag is a flag with type *Int64Slice @@ -1253,6 +1307,9 @@ func (f *Int64SliceFlag) IsVisible() bool func (f *Int64SliceFlag) Names() []string Names returns the names of the flag +func (f *Int64SliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Int64SliceFlag) SetDestination(slice []int64) func (f *Int64SliceFlag) SetValue(slice []int64) @@ -1280,6 +1337,10 @@ type IntFlag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, int) error } IntFlag is a flag with type int @@ -1317,6 +1378,9 @@ func (f *IntFlag) IsVisible() bool func (f *IntFlag) Names() []string Names returns the names of the flag +func (f *IntFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *IntFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1367,6 +1431,8 @@ type IntSliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []int) error } IntSliceFlag is a flag with type *IntSlice @@ -1406,6 +1472,9 @@ func (f *IntSliceFlag) IsVisible() bool func (f *IntSliceFlag) Names() []string Names returns the names of the flag +func (f *IntSliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *IntSliceFlag) SetDestination(slice []int) func (f *IntSliceFlag) SetValue(slice []int) @@ -1416,6 +1485,10 @@ func (f *IntSliceFlag) String() string func (f *IntSliceFlag) TakesValue() bool TakesValue returns true if the flag takes a value, otherwise false +type InvalidFlagAccessFunc func(*Context, string) + InvalidFlagAccessFunc is executed when an invalid flag is accessed from the + context. + type MultiError interface { error Errors() []error @@ -1465,6 +1538,8 @@ type PathFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, Path) error } PathFlag is a flag with type Path @@ -1502,6 +1577,9 @@ func (f *PathFlag) IsVisible() bool func (f *PathFlag) Names() []string Names returns the names of the flag +func (f *PathFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *PathFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1545,6 +1623,8 @@ func (x *SliceFlag[T, S, E]) IsVisible() bool func (x *SliceFlag[T, S, E]) Names() []string +func (x *SliceFlag[T, S, E]) RunAction(c *Context) error + func (x *SliceFlag[T, S, E]) SetDestination(slice S) func (x *SliceFlag[T, S, E]) SetValue(slice S) @@ -1589,6 +1669,8 @@ type StringFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, string) error } StringFlag is a flag with type string @@ -1626,6 +1708,9 @@ func (f *StringFlag) IsVisible() bool func (f *StringFlag) Names() []string Names returns the names of the flag +func (f *StringFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *StringFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1674,6 +1759,8 @@ type StringSliceFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, []string) error } StringSliceFlag is a flag with type *StringSlice @@ -1713,6 +1800,9 @@ func (f *StringSliceFlag) IsVisible() bool func (f *StringSliceFlag) Names() []string Names returns the names of the flag +func (f *StringSliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *StringSliceFlag) SetDestination(slice []string) func (f *StringSliceFlag) SetValue(slice []string) @@ -1777,6 +1867,8 @@ type TimestampFlag struct { Layout string Timezone *time.Location + + Action func(*Context, *time.Time) error } TimestampFlag is a flag with type *Timestamp @@ -1814,6 +1906,9 @@ func (f *TimestampFlag) IsVisible() bool func (f *TimestampFlag) Names() []string Names returns the names of the flag +func (f *TimestampFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *TimestampFlag) String() string String returns a readable representation of this value (for usage defaults) @@ -1837,6 +1932,10 @@ type Uint64Flag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, uint64) error } Uint64Flag is a flag with type uint64 @@ -1874,12 +1973,103 @@ func (f *Uint64Flag) IsVisible() bool func (f *Uint64Flag) Names() []string Names returns the names of the flag +func (f *Uint64Flag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Uint64Flag) String() string String returns a readable representation of this value (for usage defaults) func (f *Uint64Flag) TakesValue() bool TakesValue returns true if the flag takes a value, otherwise false +type Uint64Slice struct { + // Has unexported fields. +} + Uint64Slice wraps []int64 to satisfy flag.Value + +func NewUint64Slice(defaults ...uint64) *Uint64Slice + NewUint64Slice makes an *Uint64Slice with default values + +func (i *Uint64Slice) Get() interface{} + Get returns the slice of ints set by this flag + +func (i *Uint64Slice) Serialize() string + Serialize allows Uint64Slice to fulfill Serializer + +func (i *Uint64Slice) Set(value string) error + Set parses the value into an integer and appends it to the list of values + +func (i *Uint64Slice) String() string + String returns a readable representation of this value (for usage defaults) + +func (i *Uint64Slice) Value() []uint64 + Value returns the slice of ints set by this flag + +type Uint64SliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Uint64Slice + Destination *Uint64Slice + + Aliases []string + EnvVars []string + + Action func(*Context, []uint64) error +} + Uint64SliceFlag is a flag with type *Uint64Slice + +func (f *Uint64SliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *Uint64SliceFlag) Get(ctx *Context) []uint64 + Get returns the flag’s value in the given Context. + +func (f *Uint64SliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *Uint64SliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *Uint64SliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *Uint64SliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *Uint64SliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *Uint64SliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *Uint64SliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *Uint64SliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *Uint64SliceFlag) Names() []string + Names returns the names of the flag + +func (f *Uint64SliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + +func (f *Uint64SliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Uint64SliceFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + type UintFlag struct { Name string @@ -1897,6 +2087,10 @@ type UintFlag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, uint) error } UintFlag is a flag with type uint @@ -1934,12 +2128,107 @@ func (f *UintFlag) IsVisible() bool func (f *UintFlag) Names() []string Names returns the names of the flag +func (f *UintFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *UintFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *UintFlag) TakesValue() bool TakesValue returns true if the flag takes a value, otherwise false +type UintSlice struct { + // Has unexported fields. +} + UintSlice wraps []int to satisfy flag.Value + +func NewUintSlice(defaults ...uint) *UintSlice + NewUintSlice makes an *UintSlice with default values + +func (i *UintSlice) Get() interface{} + Get returns the slice of ints set by this flag + +func (i *UintSlice) Serialize() string + Serialize allows UintSlice to fulfill Serializer + +func (i *UintSlice) Set(value string) error + Set parses the value into an integer and appends it to the list of values + +func (i *UintSlice) SetUint(value uint) + TODO: Consistently have specific Set function for Int64 and Float64 ? SetInt + directly adds an integer to the list of values + +func (i *UintSlice) String() string + String returns a readable representation of this value (for usage defaults) + +func (i *UintSlice) Value() []uint + Value returns the slice of ints set by this flag + +type UintSliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *UintSlice + Destination *UintSlice + + Aliases []string + EnvVars []string + + Action func(*Context, []uint) error +} + UintSliceFlag is a flag with type *UintSlice + +func (f *UintSliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *UintSliceFlag) Get(ctx *Context) []uint + Get returns the flag’s value in the given Context. + +func (f *UintSliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *UintSliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *UintSliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *UintSliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *UintSliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *UintSliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *UintSliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *UintSliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *UintSliceFlag) Names() []string + Names returns the names of the flag + +func (f *UintSliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + +func (f *UintSliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *UintSliceFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false + type VisibleFlagCategory interface { // Name returns the category name string Name() string diff --git a/help.go b/help.go index 9a8d243..ba5f803 100644 --- a/help.go +++ b/help.go @@ -15,33 +15,59 @@ const ( helpAlias = "h" ) -var helpCommand = &Command{ +// this instance is to avoid recursion in the ShowCommandHelp which can +// add a help command again +var helpCommandDontUse = &Command{ Name: helpName, Aliases: []string{helpAlias}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", - Action: func(cCtx *Context) error { - args := cCtx.Args() - if args.Present() { - return ShowCommandHelp(cCtx, args.First()) - } - - _ = ShowAppHelp(cCtx) - return nil - }, } -var helpSubcommand = &Command{ +var helpCommand = &Command{ Name: helpName, Aliases: []string{helpAlias}, Usage: "Shows a list of commands or help for one command", ArgsUsage: "[command]", Action: func(cCtx *Context) error { args := cCtx.Args() - if args.Present() { - return ShowCommandHelp(cCtx, args.First()) + argsPresent := args.First() != "" + firstArg := args.First() + + // This action can be triggered by a "default" action of a command + // or via cmd.Run when cmd == helpCmd. So we have following possibilities + // + // 1 $ app + // 2 $ app help + // 3 $ app foo + // 4 $ app help foo + // 5 $ app foo help + + // Case 4. when executing a help command set the context to parent + // to allow resolution of subsequent args. This will transform + // $ app help foo + // to + // $ app foo + // which will then be handled as case 3 + if cCtx.Command.Name == helpName || cCtx.Command.Name == helpAlias { + cCtx = cCtx.parentContext + } + + // Case 4. $ app hello foo + // foo is the command for which help needs to be shown + if argsPresent { + return ShowCommandHelp(cCtx, firstArg) + } + + // Case 1 & 2 + // Special case when running help on main app itself as opposed to indivdual + // commands/subcommands + if cCtx.parentContext.App == nil { + _ = ShowAppHelp(cCtx) + return nil } + // Case 3, 5 return ShowSubcommandHelp(cCtx) }, } @@ -212,9 +238,19 @@ func ShowCommandHelp(ctx *Context, command string) error { for _, c := range ctx.App.Commands { if c.HasName(command) { + if !ctx.App.HideHelpCommand && !c.HasName(helpName) && len(c.Subcommands) != 0 { + c.Subcommands = append(c.Subcommands, helpCommandDontUse) + } + if !ctx.App.HideHelp && HelpFlag != nil { + c.appendFlag(HelpFlag) + } templ := c.CustomHelpTemplate if templ == "" { - templ = CommandHelpTemplate + if len(c.Subcommands) == 0 { + templ = CommandHelpTemplate + } else { + templ = SubcommandHelpTemplate + } } HelpPrinter(ctx.App.Writer, templ, c) @@ -295,12 +331,14 @@ func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs const maxLineLength = 10000 funcMap := template.FuncMap{ - "join": strings.Join, - "indent": indent, - "nindent": nindent, - "trim": strings.TrimSpace, - "wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) }, - "offset": offset, + "join": strings.Join, + "subtract": subtract, + "indent": indent, + "nindent": nindent, + "trim": strings.TrimSpace, + "wrap": func(input string, offset int) string { return wrap(input, offset, maxLineLength) }, + "offset": offset, + "offsetCommands": offsetCommands, } if customFuncs["wrapAt"] != nil { @@ -416,6 +454,10 @@ func checkCommandCompletions(c *Context, name string) bool { return true } +func subtract(a, b int) int { + return a - b +} + func indent(spaces int, v string) string { pad := strings.Repeat(" ", spaces) return pad + strings.Replace(v, "\n", "\n"+pad, -1) @@ -476,3 +518,28 @@ func wrapLine(input string, offset int, wrapAt int, padding string) string { func offset(input string, fixed int) int { return len(input) + fixed } + +// this function tries to find the max width of the names column +// so say we have the following rows for help +// +// foo1, foo2, foo3 some string here +// bar1, b2 some other string here +// +// We want to offset the 2nd row usage by some amount so that everything +// is aligned +// +// foo1, foo2, foo3 some string here +// bar1, b2 some other string here +// +// to find that offset we find the length of all the rows and use the max +// to calculate the offset +func offsetCommands(cmds []*Command, fixed int) int { + var max int = 0 + for _, cmd := range cmds { + s := strings.Join(cmd.Names(), ", ") + if len(s) > max { + max = len(s) + } + } + return max + fixed +} diff --git a/help_test.go b/help_test.go index 4feb7f0..5a50586 100644 --- a/help_test.go +++ b/help_test.go @@ -186,7 +186,7 @@ func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) { c := NewContext(app, set, nil) - err := helpSubcommand.Action(c) + err := helpCommand.Action(c) if err == nil { t.Fatalf("expected error from helpCommand.Action(), but got nil") @@ -248,7 +248,7 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { fmt.Fprint(w, "yo") }, command: "", - wantTemplate: SubcommandHelpTemplate, + wantTemplate: AppHelpTemplate, wantOutput: "yo", }, { @@ -333,7 +333,7 @@ func TestShowCommandHelp_HelpPrinterCustom(t *testing.T) { fmt.Fprint(w, "yo") }, command: "", - wantTemplate: SubcommandHelpTemplate, + wantTemplate: AppHelpTemplate, wantOutput: "yo", }, { @@ -428,6 +428,58 @@ func TestShowCommandHelp_CommandAliases(t *testing.T) { } } +func TestHelpNameConsistency(t *testing.T) { + // Setup some very basic templates based on actual AppHelp, CommandHelp + // and SubcommandHelp templates to display the help name + // The inconsistency shows up when users use NewApp() as opposed to + // using App{...} directly + SubcommandHelpTemplate = `{{.HelpName}}` + app := NewApp() + app.Name = "bar" + app.CustomAppHelpTemplate = `{{.HelpName}}` + app.Commands = []*Command{ + { + Name: "command1", + CustomHelpTemplate: `{{.HelpName}}`, + Subcommands: []*Command{ + { + Name: "subcommand1", + CustomHelpTemplate: `{{.HelpName}}`, + }, + }, + }, + } + + tests := []struct { + name string + args []string + }{ + { + name: "App help", + args: []string{"foo"}, + }, + { + name: "Command help", + args: []string{"foo", "command1"}, + }, + { + name: "Subcommand help", + args: []string{"foo", "command1", "subcommand1"}, + }, + } + + for _, tt := range tests { + output := &bytes.Buffer{} + app.Writer = output + if err := app.Run(tt.args); err != nil { + t.Error(err) + } + if !strings.Contains(output.String(), "bar") { + t.Errorf("expected output to contain bar; got: %q", output.String()) + } + } +} + func TestShowSubcommandHelp_CommandAliases(t *testing.T) { app := &App{ Commands: []*Command{ @@ -895,6 +947,42 @@ App UsageText`, } } +func TestShowAppHelp_CommandMultiLine_UsageText(t *testing.T) { + app := &App{ + UsageText: `This is a +multi +line +App UsageText`, + Commands: []*Command{ + { + Name: "frobbly", + Aliases: []string{"frb1", "frbb2", "frl2"}, + Usage: "this is a long help output for the run command, long usage \noutput, long usage output, long usage output, long usage output\noutput, long usage output, long usage output", + }, + { + Name: "grobbly", + Aliases: []string{"grb1", "grbb2"}, + Usage: "this is another long help output for the run command, long usage \noutput, long usage output", + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + + _ = app.Run([]string{"foo"}) + + expected := "COMMANDS:\n" + + " frobbly, frb1, frbb2, frl2 this is a long help output for the run command, long usage \n" + + " output, long usage output, long usage output, long usage output\n" + + " output, long usage output, long usage output\n" + + " grobbly, grb1, grbb2 this is another long help output for the run command, long usage \n" + + " output, long usage output" + if !strings.Contains(output.String(), expected) { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + func TestHideHelpCommand(t *testing.T) { app := &App{ HideHelpCommand: true, @@ -1269,10 +1357,13 @@ DESCRIPTION: and a description long enough to wrap in this test case + +OPTIONS: + --help, -h show help (default: false) ` if output.String() != expected { - t.Errorf("Unexpected wrapping, got:\n%s\nexpected: %s", + t.Errorf("Unexpected wrapping, got:\n%s\nexpected:\n%s", output.String(), expected) } } @@ -1338,7 +1429,6 @@ USAGE: OPTIONS: --help, -h show help (default: false) - ` if output.String() != expected { diff --git a/internal/build/build.go b/internal/build/build.go index 777a630..dcef50b 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -5,12 +5,17 @@ package main import ( "bufio" "bytes" + "errors" "fmt" + "io" "log" "math" + "net/http" + "net/url" "os" "os/exec" "path/filepath" + "runtime" "strings" "github.com/urfave/cli/v3" @@ -21,6 +26,8 @@ const ( goodNewsEmoji = "✨" checksPassedEmoji = "✅" + gfmrunVersion = "v1.3.0" + v2diffWarning = ` # The unified diff above indicates that the public API surface area # has changed. If you feel that the changes are acceptable and adhere @@ -45,56 +52,112 @@ func main() { log.Fatal(err) } - app := cli.NewApp() - - app.Name = "builder" - app.Usage = "Generates a new urfave/cli build!" - - app.Commands = cli.Commands{ - { - Name: "vet", - Action: VetActionFunc, - }, - { - Name: "test", - Action: TestActionFunc, - }, - { - Name: "gfmrun", - Action: GfmrunActionFunc, - }, - { - Name: "check-binary-size", - Action: checkBinarySizeActionFunc, - }, - { - Name: "generate", - Action: GenerateActionFunc, - }, - { - Name: "v2diff", - Flags: []cli.Flag{ - &cli.BoolFlag{Name: "color", Value: false}, + app := &cli.App{ + Name: "builder", + Usage: "Do a thing for urfave/cli! (maybe build?)", + Commands: cli.Commands{ + { + Name: "vet", + Action: topRunAction("go", "vet", "./..."), + }, + { + Name: "test", + Action: TestActionFunc, + }, + { + Name: "gfmrun", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "walk", + Value: false, + Usage: "Walk the specified directory and perform validation on all markdown files", + }, + }, + Action: GfmrunActionFunc, + }, + { + Name: "check-binary-size", + Action: checkBinarySizeActionFunc, + }, + { + Name: "generate", + Action: GenerateActionFunc, + }, + { + Name: "yamlfmt", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "strict", Value: false, Usage: "require presence of yq"}, + }, + Action: YAMLFmtActionFunc, + }, + { + Name: "diffcheck", + Action: DiffCheckActionFunc, + }, + { + Name: "ensure-goimports", + Action: EnsureGoimportsActionFunc, + }, + { + Name: "ensure-gfmrun", + Action: EnsureGfmrunActionFunc, + }, + { + Name: "ensure-mkdocs", + Action: EnsureMkdocsActionFunc, + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "upgrade-pip"}, + }, + }, + { + Name: "set-mkdocs-remote", + Action: SetMkdocsRemoteActionFunc, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "github-token", + EnvVars: []string{"MKDOCS_REMOTE_GITHUB_TOKEN"}, + Required: true, + }, + }, + }, + { + Name: "deploy-mkdocs", + Action: topRunAction("mkdocs", "gh-deploy", "--force"), + }, + { + Name: "lint", + Action: LintActionFunc, + }, + { + Name: "v2diff", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "color", Value: false}, + }, + Action: V2Diff, + }, + { + Name: "v2approve", + Action: topRunAction( + "cp", + "-v", + "godoc-current.txt", + filepath.Join("testdata", "godoc-v2.x.txt"), + ), }, - Action: V2Diff, - }, - { - Name: "v2approve", - Action: V2Approve, - }, - } - app.Flags = []cli.Flag{ - &cli.StringFlag{ - Name: "tags", - Usage: "set build tags", - }, - &cli.PathFlag{ - Name: "top", - Value: top, }, - &cli.StringSliceFlag{ - Name: "packages", - Value: cli.NewStringSlice("cli", "altsrc", "internal/build", "internal/genflags"), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "tags", + Usage: "set build tags", + }, + &cli.PathFlag{ + Name: "top", + Value: top, + }, + &cli.StringSliceFlag{ + Name: "packages", + Value: cli.NewStringSlice("cli", "altsrc", "internal/build"), + }, }, } @@ -113,6 +176,14 @@ func sh(exe string, args ...string) (string, error) { return string(outBytes), err } +func topRunAction(arg string, args ...string) cli.ActionFunc { + return func(cCtx *cli.Context) error { + os.Chdir(cCtx.Path("top")) + + return runCmd(arg, args...) + } +} + func runCmd(arg string, args ...string) error { cmd := exec.Command(arg, args...) @@ -124,6 +195,43 @@ func runCmd(arg string, args ...string) error { return cmd.Run() } +func downloadFile(src, dest string, dirPerm, perm os.FileMode) error { + req, err := http.NewRequest(http.MethodGet, src, nil) + if err != nil { + return err + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode >= 300 { + return fmt.Errorf("download response %[1]v", resp.StatusCode) + } + + if err := os.MkdirAll(filepath.Dir(dest), dirPerm); err != nil { + return err + } + + out, err := os.Create(dest) + if err != nil { + return err + } + + if _, err := io.Copy(out, resp.Body); err != nil { + return err + } + + if err := out.Close(); err != nil { + return err + } + + return os.Chmod(dest, perm) +} + func VetActionFunc(cCtx *cli.Context) error { return runCmd("go", "vet", cCtx.Path("top")+"/...") } @@ -138,15 +246,20 @@ func TestActionFunc(c *cli.Context) error { packageName = fmt.Sprintf("github.com/urfave/cli/v3/%s", pkg) } - if err := runCmd( - "go", "test", - "-tags", tags, + args := []string{"test"} + if tags != "" { + args = append(args, []string{"-tags", tags}...) + } + + args = append(args, []string{ "-v", - "--coverprofile", pkg+".coverprofile", + "--coverprofile", pkg + ".coverprofile", "--covermode", "count", "--cover", packageName, packageName, - ); err != nil { + }...) + + if err := runCmd("go", args...); err != nil { return err } } @@ -215,36 +328,75 @@ func GfmrunActionFunc(cCtx *cli.Context) error { return err } - filename := cCtx.Args().Get(0) - if filename == "" { - filename = "README.md" + dirPath := cCtx.Args().Get(0) + if dirPath == "" { + dirPath = "README.md" } - file, err := os.Open(filename) - if err != nil { - return err + walk := cCtx.Bool("walk") + sources := []string{} + + if walk { + // Walk the directory and find all markdown files. + err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + if filepath.Ext(path) != ".md" { + return nil + } + + sources = append(sources, path) + return nil + }) + if err != nil { + return err + } + } else { + sources = append(sources, dirPath) } - defer file.Close() var counter int - scanner := bufio.NewScanner(file) - for scanner.Scan() { - if strings.Contains(scanner.Text(), "package main") { - counter++ + + for _, src := range sources { + file, err := os.Open(src) + if err != nil { + return err } - } + defer file.Close() - err = file.Close() - if err != nil { - return err + scanner := bufio.NewScanner(file) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "package main") { + counter++ + } + } + + err = file.Close() + if err != nil { + return err + } + + err = scanner.Err() + if err != nil { + return err + } } - err = scanner.Err() - if err != nil { - return err + gfmArgs := []string{ + "--count", + fmt.Sprint(counter), + } + for _, src := range sources { + gfmArgs = append(gfmArgs, "--sources", src) } - if err := runCmd("gfmrun", "-c", fmt.Sprint(counter), "-s", filename); err != nil { + if err := runCmd("gfmrun", gfmArgs...); err != nil { return err } @@ -365,6 +517,125 @@ func GenerateActionFunc(cCtx *cli.Context) error { return runCmd("go", "generate", cCtx.Path("top")+"/...") } +func YAMLFmtActionFunc(cCtx *cli.Context) error { + yqBin, err := exec.LookPath("yq") + if err != nil { + if !cCtx.Bool("strict") { + fmt.Fprintln(cCtx.App.ErrWriter, "# ---> no yq found; skipping") + return nil + } + + return err + } + + os.Chdir(cCtx.Path("top")) + + return runCmd(yqBin, "eval", "--inplace", "flag-spec.yaml") +} + +func DiffCheckActionFunc(cCtx *cli.Context) error { + os.Chdir(cCtx.Path("top")) + + if err := runCmd("git", "diff", "--exit-code"); err != nil { + return err + } + + return runCmd("git", "diff", "--cached", "--exit-code") +} + +func EnsureGoimportsActionFunc(cCtx *cli.Context) error { + top := cCtx.Path("top") + os.Chdir(top) + + if err := runCmd( + "goimports", + "-d", + filepath.Join(top, "internal/build/build.go"), + ); err == nil { + return nil + } + + os.Setenv("GOBIN", filepath.Join(top, ".local/bin")) + + return runCmd("go", "install", "golang.org/x/tools/cmd/goimports@latest") +} + +func EnsureGfmrunActionFunc(cCtx *cli.Context) error { + top := cCtx.Path("top") + gfmrunExe := filepath.Join(top, ".local/bin/gfmrun") + + os.Chdir(top) + + if v, err := sh(gfmrunExe, "--version"); err == nil && strings.TrimSpace(v) == gfmrunVersion { + return nil + } + + gfmrunURL, err := url.Parse( + fmt.Sprintf( + "https://github.com/urfave/gfmrun/releases/download/%[1]s/gfmrun-%[2]s-%[3]s-%[1]s", + gfmrunVersion, runtime.GOOS, runtime.GOARCH, + ), + ) + if err != nil { + return err + } + + return downloadFile(gfmrunURL.String(), gfmrunExe, 0755, 0755) +} + +func EnsureMkdocsActionFunc(cCtx *cli.Context) error { + os.Chdir(cCtx.Path("top")) + + if err := runCmd("mkdocs", "--version"); err == nil { + return nil + } + + if cCtx.Bool("upgrade-pip") { + if err := runCmd("pip", "install", "-U", "pip"); err != nil { + return err + } + } + + return runCmd("pip", "install", "-r", "mkdocs-requirements.txt") +} + +func SetMkdocsRemoteActionFunc(cCtx *cli.Context) error { + ghToken := strings.TrimSpace(cCtx.String("github-token")) + if ghToken == "" { + return errors.New("empty github token") + } + + os.Chdir(cCtx.Path("top")) + + if err := runCmd("git", "remote", "rm", "origin"); err != nil { + return err + } + + return runCmd( + "git", "remote", "add", "origin", + fmt.Sprintf("https://x-access-token:%[1]s@github.com/urfave/cli.git", ghToken), + ) +} + +func LintActionFunc(cCtx *cli.Context) error { + top := cCtx.Path("top") + os.Chdir(top) + + out, err := sh(filepath.Join(top, ".local/bin/goimports"), "-l", ".") + if err != nil { + return err + } + + if strings.TrimSpace(out) != "" { + fmt.Fprintln(cCtx.App.ErrWriter, "# ---> goimports -l is non-empty:") + fmt.Fprintln(cCtx.App.ErrWriter, out) + + return errors.New("goimports needed") + } + + return nil +} + func V2Diff(cCtx *cli.Context) error { os.Chdir(cCtx.Path("top")) @@ -393,34 +664,29 @@ func V2Diff(cCtx *cli.Context) error { return err } -func V2Approve(cCtx *cli.Context) error { - top := cCtx.Path("top") +func getSize(sourcePath, builtPath, tags string) (int64, error) { + args := []string{"build"} - return runCmd( - "cp", - "-v", - filepath.Join(top, "godoc-current.txt"), - filepath.Join(top, "testdata", "godoc-v2.x.txt"), - ) -} + if tags != "" { + args = append(args, []string{"-tags", tags}...) + } -func getSize(sourcePath string, builtPath string, tags string) (size int64, err error) { - // build example binary - err = runCmd("go", "build", "-tags", tags, "-o", builtPath, "-ldflags", "-s -w", sourcePath) - if err != nil { + args = append(args, []string{ + "-o", builtPath, + "-ldflags", "-s -w", + sourcePath, + }...) + + if err := runCmd("go", args...); err != nil { fmt.Println("issue getting size for example binary") return 0, err } - // get file info fileInfo, err := os.Stat(builtPath) if err != nil { fmt.Println("issue getting size for example binary") return 0, err } - // size! - size = fileInfo.Size() - - return size, nil + return fileInfo.Size(), nil } diff --git a/internal/genflags/package.go b/internal/genflags/package.go deleted file mode 100644 index 4e5de41..0000000 --- a/internal/genflags/package.go +++ /dev/null @@ -1,34 +0,0 @@ -package genflags - -import ( - _ "embed" - "strings" - - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -var ( - //go:embed generated.gotmpl - TemplateString string - - //go:embed generated_test.gotmpl - TestTemplateString string - - titler = cases.Title(language.Und, cases.NoLower) -) - -func TypeName(goType string, fc *FlagTypeConfig) string { - if fc != nil && strings.TrimSpace(fc.TypeName) != "" { - return strings.TrimSpace(fc.TypeName) - } - - dotSplit := strings.Split(goType, ".") - goType = dotSplit[len(dotSplit)-1] - - if strings.HasPrefix(goType, "[]") { - return titler.String(strings.TrimPrefix(goType, "[]")) + "SliceFlag" - } - - return titler.String(goType) + "Flag" -} diff --git a/internal/genflags/package_test.go b/internal/genflags/package_test.go deleted file mode 100644 index 81759f8..0000000 --- a/internal/genflags/package_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package genflags_test - -import ( - "fmt" - "testing" - - "github.com/urfave/cli/v3/internal/genflags" -) - -func TestTypeName(t *testing.T) { - for _, tc := range []struct { - gt string - fc *genflags.FlagTypeConfig - expected string - }{ - {gt: "int", fc: nil, expected: "IntFlag"}, - {gt: "int", fc: &genflags.FlagTypeConfig{}, expected: "IntFlag"}, - {gt: "int", fc: &genflags.FlagTypeConfig{TypeName: "VeryIntyFlag"}, expected: "VeryIntyFlag"}, - {gt: "[]bool", fc: nil, expected: "BoolSliceFlag"}, - {gt: "[]bool", fc: &genflags.FlagTypeConfig{}, expected: "BoolSliceFlag"}, - {gt: "[]bool", fc: &genflags.FlagTypeConfig{TypeName: "ManyTruthsFlag"}, expected: "ManyTruthsFlag"}, - {gt: "time.Rumination", fc: nil, expected: "RuminationFlag"}, - {gt: "time.Rumination", fc: &genflags.FlagTypeConfig{}, expected: "RuminationFlag"}, - {gt: "time.Rumination", fc: &genflags.FlagTypeConfig{TypeName: "PonderFlag"}, expected: "PonderFlag"}, - } { - t.Run( - fmt.Sprintf("type=%s,cfg=%v", tc.gt, func() string { - if tc.fc != nil { - return tc.fc.TypeName - } - return "nil" - }()), - func(ct *testing.T) { - actual := genflags.TypeName(tc.gt, tc.fc) - if tc.expected != actual { - ct.Errorf("expected %q, got %q", tc.expected, actual) - } - }, - ) - } -} diff --git a/internal/genflags/spec.go b/internal/genflags/spec.go deleted file mode 100644 index 5b1d4d3..0000000 --- a/internal/genflags/spec.go +++ /dev/null @@ -1,105 +0,0 @@ -package genflags - -import ( - "sort" - "strings" -) - -type Spec struct { - FlagTypes map[string]*FlagTypeConfig `yaml:"flag_types"` - PackageName string `yaml:"package_name"` - TestPackageName string `yaml:"test_package_name"` - UrfaveCLINamespace string `yaml:"urfave_cli_namespace"` - UrfaveCLITestNamespace string `yaml:"urfave_cli_test_namespace"` -} - -func (gfs *Spec) SortedFlagTypes() []*FlagType { - typeNames := []string{} - - for name := range gfs.FlagTypes { - if strings.HasPrefix(name, "[]") { - name = strings.TrimPrefix(name, "[]") + "Slice" - } - - typeNames = append(typeNames, name) - } - - sort.Strings(typeNames) - - ret := make([]*FlagType, len(typeNames)) - - for i, typeName := range typeNames { - ret[i] = &FlagType{ - GoType: typeName, - Config: gfs.FlagTypes[typeName], - } - } - - return ret -} - -type FlagTypeConfig struct { - SkipInterfaces []string `yaml:"skip_interfaces"` - StructFields []*FlagStructField `yaml:"struct_fields"` - TypeName string `yaml:"type_name"` - ValuePointer bool `yaml:"value_pointer"` - NoDefaultText bool `yaml:"no_default_text"` -} - -type FlagStructField struct { - Name string - Type string -} - -type FlagType struct { - GoType string - Config *FlagTypeConfig -} - -func (ft *FlagType) StructFields() []*FlagStructField { - if ft.Config == nil || ft.Config.StructFields == nil { - return []*FlagStructField{} - } - - return ft.Config.StructFields -} - -func (ft *FlagType) ValuePointer() bool { - if ft.Config == nil { - return false - } - - return ft.Config.ValuePointer -} - -func (ft *FlagType) TypeName() string { - return TypeName(ft.GoType, ft.Config) -} - -func (ft *FlagType) GenerateFmtStringerInterface() bool { - return ft.skipInterfaceNamed("fmt.Stringer") -} - -func (ft *FlagType) GenerateFlagInterface() bool { - return ft.skipInterfaceNamed("Flag") -} - -func (ft *FlagType) GenerateDefaultText() bool { - return !ft.Config.NoDefaultText -} - -func (ft *FlagType) skipInterfaceNamed(name string) bool { - if ft.Config == nil { - return true - } - - lowName := strings.ToLower(name) - - for _, interfaceName := range ft.Config.SkipInterfaces { - if strings.ToLower(interfaceName) == lowName { - return false - } - } - - return true -} diff --git a/mkdocs.yml b/mkdocs.yml index 73b88c5..02657b9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,9 +9,45 @@ site_url: https://cli.urfave.org/ repo_url: https://github.com/urfave/cli edit_uri: edit/main/docs/ nav: - - Home: index.md - - v2 Manual: v2/index.md - - v1 Manual: v1/index.md + - Home: + - Welcome: index.md + - Contributing: CONTRIBUTING.md + - Code of Conduct: CODE_OF_CONDUCT.md + - Releasing: RELEASING.md + - Security: SECURITY.md + - Migrate v1 to v2: migrate-v1-to-v2.md + - v2 Manual: + - Getting Started: v2/getting-started.md + - Migrating From Older Releases: v2/migrating-from-older-releases.md + - Examples: + - Greet: v2/examples/greet.md + - Arguments: v2/examples/arguments.md + - Flags: v2/examples/flags.md + - Subcommands: v2/examples/subcommands.md + - Subcommands Categories: v2/examples/subcommands-categories.md + - Exit Codes: v2/examples/exit-codes.md + - Combining Short Options: v2/examples/combining-short-options.md + - Bash Completions: v2/examples/bash-completions.md + - Generated Help Text: v2/examples/generated-help-text.md + - Version Flag: v2/examples/version-flag.md + - Timestamp Flag: v2/examples/timestamp-flag.md + - Suggestions: v2/examples/suggestions.md + - Full API Example: v2/examples/full-api-example.md + - v1 Manual: + - Getting Started: v1/getting-started.md + - Migrating to v2: v1/migrating-to-v2.md + - Examples: + - Greet: v1/examples/greet.md + - Arguments: v1/examples/arguments.md + - Flags: v1/examples/flags.md + - Subcommands: v1/examples/subcommands.md + - Subcommands (Categories): v1/examples/subcommands-categories.md + - Exit Codes: v1/examples/exit-codes.md + - Combining Short Options: v1/examples/combining-short-options.md + - Bash Completions: v1/examples/bash-completions.md + - Generated Help Text: v1/examples/generated-help-text.md + - Version Flag: v1/examples/version-flag.md + theme: name: material palette: @@ -25,9 +61,18 @@ theme: toggle: icon: material/brightness-7 name: light mode + features: + - content.code.annotate + - navigation.top + - navigation.instant + - navigation.expand + - navigation.sections + - navigation.tabs + - navigation.tabs.sticky plugins: - git-revision-date-localized - search + - tags # NOTE: this is the recommended configuration from # https://squidfunk.github.io/mkdocs-material/setup/extensions/#recommended-configuration markdown_extensions: diff --git a/sliceflag.go b/sliceflag.go index 89f3e0c..c8670cb 100644 --- a/sliceflag.go +++ b/sliceflag.go @@ -110,17 +110,18 @@ func (x *SliceFlag[T, S, E]) GetDestination() S { return nil } -func (x *SliceFlag[T, S, E]) String() string { return x.Target.String() } -func (x *SliceFlag[T, S, E]) Names() []string { return x.Target.Names() } -func (x *SliceFlag[T, S, E]) IsSet() bool { return x.Target.IsSet() } -func (x *SliceFlag[T, S, E]) IsRequired() bool { return x.Target.IsRequired() } -func (x *SliceFlag[T, S, E]) TakesValue() bool { return x.Target.TakesValue() } -func (x *SliceFlag[T, S, E]) GetUsage() string { return x.Target.GetUsage() } -func (x *SliceFlag[T, S, E]) GetValue() string { return x.Target.GetValue() } -func (x *SliceFlag[T, S, E]) GetDefaultText() string { return x.Target.GetDefaultText() } -func (x *SliceFlag[T, S, E]) GetEnvVars() []string { return x.Target.GetEnvVars() } -func (x *SliceFlag[T, S, E]) IsVisible() bool { return x.Target.IsVisible() } -func (x *SliceFlag[T, S, E]) GetCategory() string { return x.Target.GetCategory() } +func (x *SliceFlag[T, S, E]) String() string { return x.Target.String() } +func (x *SliceFlag[T, S, E]) Names() []string { return x.Target.Names() } +func (x *SliceFlag[T, S, E]) IsSet() bool { return x.Target.IsSet() } +func (x *SliceFlag[T, S, E]) IsRequired() bool { return x.Target.IsRequired() } +func (x *SliceFlag[T, S, E]) TakesValue() bool { return x.Target.TakesValue() } +func (x *SliceFlag[T, S, E]) GetUsage() string { return x.Target.GetUsage() } +func (x *SliceFlag[T, S, E]) GetValue() string { return x.Target.GetValue() } +func (x *SliceFlag[T, S, E]) GetDefaultText() string { return x.Target.GetDefaultText() } +func (x *SliceFlag[T, S, E]) GetEnvVars() []string { return x.Target.GetEnvVars() } +func (x *SliceFlag[T, S, E]) IsVisible() bool { return x.Target.IsVisible() } +func (x *SliceFlag[T, S, E]) GetCategory() string { return x.Target.GetCategory() } +func (x *SliceFlag[T, S, E]) RunAction(c *Context) error { return x.Target.RunAction(c) } func (x *flagValueHook) Set(value string) error { if err := x.value.Set(value); err != nil { diff --git a/suggestions_test.go b/suggestions_test.go index 4c0984e..909e29c 100644 --- a/suggestions_test.go +++ b/suggestions_test.go @@ -124,7 +124,7 @@ func ExampleApp_Suggest() { app := &App{ Name: "greet", Suggest: true, - HideHelp: true, + HideHelp: false, HideHelpCommand: true, CustomAppHelpTemplate: "(this space intentionally left blank)\n", Flags: []Flag{ @@ -149,7 +149,6 @@ func ExampleApp_Suggest_command() { app := &App{ Name: "greet", Suggest: true, - HideHelp: true, HideHelpCommand: true, CustomAppHelpTemplate: "(this space intentionally left blank)\n", Flags: []Flag{ @@ -162,6 +161,7 @@ func ExampleApp_Suggest_command() { Commands: []*Command{ { Name: "neighbors", + HideHelp: false, CustomHelpTemplate: "(this space intentionally left blank)\n", Flags: []Flag{ &BoolFlag{Name: "smiling"}, diff --git a/template.go b/template.go index f3116fd..9e13604 100644 --- a/template.go +++ b/template.go @@ -21,8 +21,8 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} GLOBAL OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} @@ -54,12 +54,10 @@ DESCRIPTION: OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} - {{end}}{{range .Flags}}{{.}} - {{end}}{{end}}{{else}}{{if .VisibleFlags}} + {{end}}{{range .Flags}}{{.}}{{end}}{{end}}{{else}}{{if .VisibleFlags}} OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{end}} + {{range .VisibleFlags}}{{.}}{{end}}{{end}}{{end}} ` // SubcommandHelpTemplate is the text template for the subcommand help topic. @@ -76,12 +74,11 @@ DESCRIPTION: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} + {{range .VisibleFlags}}{{.}}{{end}}{{end}} ` var MarkdownDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionNum }} diff --git a/testdata/godoc-v2.x.txt b/testdata/godoc-v2.x.txt index 1ba48cc..2c8b5e0 100644 --- a/testdata/godoc-v2.x.txt +++ b/testdata/godoc-v2.x.txt @@ -1,28 +1,28 @@ -package cli // import "github.com/urfave/cli/v2" +package cli // import "github.com/urfave/cli/v3" Package cli provides a minimal framework for creating and organizing command line Go applications. cli is designed to be easy to understand and write, the most simple cli application can be written as follows: func main() { - (&cli.App{}).Run(os.Args) + (&cli.App{}).Run(os.Args) } Of course this application does not do much, so let's make this an actual application: - func main() { - app := &cli.App{ - Name: "greet", - Usage: "say a greeting", - Action: func(c *cli.Context) error { - fmt.Println("Greetings") - return nil - }, - } - - app.Run(os.Args) - } + func main() { + app := &cli.App{ + Name: "greet", + Usage: "say a greeting", + Action: func(c *cli.Context) error { + fmt.Println("Greetings") + return nil + }, + } + + app.Run(os.Args) + } VARIABLES @@ -49,8 +49,8 @@ AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlagCategories}} GLOBAL OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} @@ -64,8 +64,8 @@ GLOBAL OPTIONS: COPYRIGHT: {{wrap .Copyright 3}}{{end}} ` - AppHelpTemplate is the text template for the Default help topic. cli.go uses - text/template to render templates. You can render custom help text by + AppHelpTemplate is the text template for the Default help topic. cli.go + uses text/template to render templates. You can render custom help text by setting this variable. var CommandHelpTemplate = `NAME: @@ -82,12 +82,10 @@ DESCRIPTION: OPTIONS:{{range .VisibleFlagCategories}} {{if .Name}}{{.Name}} - {{end}}{{range .Flags}}{{.}} - {{end}}{{end}}{{else}}{{if .VisibleFlags}} + {{end}}{{range .Flags}}{{.}}{{end}}{{end}}{{else}}{{if .VisibleFlags}} OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{end}} + {{range .VisibleFlags}}{{.}}{{end}}{{end}}{{end}} ` CommandHelpTemplate is the text template for the command help topic. cli.go uses text/template to render templates. You can render custom help text by @@ -157,12 +155,11 @@ DESCRIPTION: COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{ $cv := offsetCommands .VisibleCommands 5}}{{range .VisibleCommands}} + {{$s := join .Names ", "}}{{$s}}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp ""}}{{wrap .Usage $cv}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} + {{range .VisibleFlags}}{{.}}{{end}}{{end}} ` SubcommandHelpTemplate is the text template for the subcommand help topic. cli.go uses text/template to render templates. You can render custom help @@ -201,9 +198,9 @@ func DefaultAppComplete(cCtx *Context) func DefaultCompleteWithFlags(cmd *Command) func(cCtx *Context) func FlagNames(name string, aliases []string) []string func HandleAction(action interface{}, cCtx *Context) (err error) - HandleAction attempts to figure out which Action signature was used. If it's - an ActionFunc or a func with the legacy signature for Action, the func is - run! + HandleAction attempts to figure out which Action signature was used. + If it's an ActionFunc or a func with the legacy signature for Action, + the func is run! func HandleExitCoder(err error) HandleExitCoder handles errors implementing ExitCoder by printing their @@ -300,6 +297,8 @@ type App struct { CommandNotFound CommandNotFoundFunc // Execute this function if a usage error occurs OnUsageError OnUsageErrorFunc + // Execute this function when an invalid flag is accessed from the context + InvalidFlagAccessHandler InvalidFlagAccessFunc // Compilation date Compiled time.Time // List of all authors who contributed @@ -360,14 +359,14 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) to generate command-specific flags func (a *App) RunContext(ctx context.Context, arguments []string) (err error) - RunContext is like Run except it takes a Context that will be passed to its - commands and sub-commands. Through this, you can propagate timeouts and + RunContext is like Run except it takes a Context that will be passed to + its commands and sub-commands. Through this, you can propagate timeouts and cancellation requests func (a *App) Setup() - Setup runs initialization code to ensure all data structures are ready for - `Run` or inspection prior to `Run`. It is internally called by `Run`, but - will return early if setup has already happened. + Setup runs initialization code to ensure all data structures are ready + for `Run` or inspection prior to `Run`. It is internally called by `Run`, + but will return early if setup has already happened. func (a *App) ToFishCompletion() (string, error) ToFishCompletion creates a fish completion string for the `*App` The @@ -450,6 +449,10 @@ type BoolFlag struct { Aliases []string EnvVars []string + + Count *int + + Action func(*Context, bool) error } BoolFlag is a flag with type bool @@ -460,7 +463,7 @@ func (f *BoolFlag) Get(ctx *Context) bool Get returns the flag’s value in the given Context. func (f *BoolFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *BoolFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -487,19 +490,14 @@ func (f *BoolFlag) IsVisible() bool func (f *BoolFlag) Names() []string Names returns the names of the flag +func (f *BoolFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *BoolFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *BoolFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false - -type CategorizableFlag interface { - VisibleFlag - - GetCategory() string -} - CategorizableFlag is an interface that allows us to potentially use a flag - in a categorized representation. + TakesValue returns true if the flag takes a value, otherwise false type Command struct { // The name of the command @@ -629,6 +627,9 @@ func (cCtx *Context) Args() Args func (cCtx *Context) Bool(name string) bool Bool looks up the value of a local BoolFlag, returns false if not found +func (cCtx *Context) Count(name string) int + Count returns the num of occurences of this flag + func (cCtx *Context) Duration(name string) time.Duration Duration looks up the value of a local DurationFlag, returns 0 if not found @@ -698,30 +699,22 @@ func (cCtx *Context) Uint(name string) uint func (cCtx *Context) Uint64(name string) uint64 Uint64 looks up the value of a local Uint64Flag, returns 0 if not found -func (cCtx *Context) Value(name string) interface{} - Value returns the value of the flag corresponding to `name` - -type DocGenerationFlag interface { - Flag - - // TakesValue returns true if the flag takes a value, otherwise false - TakesValue() bool - - // GetUsage returns the usage string for the flag - GetUsage() string +func (cCtx *Context) Uint64Slice(name string) []uint64 + Uint64Slice looks up the value of a local Uint64SliceFlag, returns nil if + not found - // GetValue returns the flags value as string representation and an empty - // string if the flag takes no value at all. - GetValue() string +func (cCtx *Context) UintSlice(name string) []uint + UintSlice looks up the value of a local UintSliceFlag, returns nil if not + found - // GetDefaultText returns the default text for this flag - GetDefaultText() string +func (cCtx *Context) Value(name string) interface{} + Value returns the value of the flag corresponding to `name` - // GetEnvVars returns the env vars for this flag - GetEnvVars() []string +type Countable interface { + Count() int } - DocGenerationFlag is an interface that allows documentation generation for - the flag + Countable is an interface to enable detection of flag values which support + repetitive flags type DurationFlag struct { Name string @@ -740,6 +733,8 @@ type DurationFlag struct { Aliases []string EnvVars []string + + Action func(*Context, time.Duration) error } DurationFlag is a flag with type time.Duration @@ -750,7 +745,7 @@ func (f *DurationFlag) Get(ctx *Context) time.Duration Get returns the flag’s value in the given Context. func (f *DurationFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *DurationFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -777,11 +772,14 @@ func (f *DurationFlag) IsVisible() bool func (f *DurationFlag) Names() []string Names returns the names of the flag +func (f *DurationFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *DurationFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *DurationFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type ErrorFormatter interface { Format(s fmt.State, verb rune) @@ -799,9 +797,9 @@ func Exit(message interface{}, exitCode int) ExitCoder Exit wraps a message and exit code into an error, which by default is handled with a call to os.Exit during default error handling. - This is the simplest way to trigger a non-zero exit code for an App without - having to call os.Exit manually. During testing, this behavior can be - avoided by overiding the ExitErrHandler function on an App or the + This is the simplest way to trigger a non-zero exit code for an App + without having to call os.Exit manually. During testing, this behavior + can be avoided by overiding the ExitErrHandler function on an App or the package-global OsExiter function. func NewExitError(message interface{}, exitCode int) ExitCoder @@ -816,10 +814,42 @@ type ExitErrHandlerFunc func(cCtx *Context, err error) type Flag interface { fmt.Stringer + // Apply Flag settings to the given flag set Apply(*flag.FlagSet) error + + // All possible names for this flag Names() []string + + // Whether the flag has been set or not IsSet() bool + + // whether the flag is a required flag or not + IsRequired() bool + + // IsVisible returns true if the flag is not hidden, otherwise false + IsVisible() bool + + // Returns the category of the flag + GetCategory() string + + // GetUsage returns the usage string for the flag + GetUsage() string + + // GetEnvVars returns the env vars for this flag + GetEnvVars() []string + + // TakesValue returns true if the flag takes a value, otherwise false + TakesValue() bool + + // GetDefaultText returns the default text for this flag + GetDefaultText() string + + // GetValue returns the flags value as string representation and an empty + // string if the flag takes no value at all. + GetValue() string + + RunAction(*Context) error } Flag is a common interface related to parsing flags in cli. For more advanced flag parsing techniques, it is recommended that this interface be @@ -913,6 +943,8 @@ type Float64Flag struct { Aliases []string EnvVars []string + + Action func(*Context, float64) error } Float64Flag is a flag with type float64 @@ -923,7 +955,7 @@ func (f *Float64Flag) Get(ctx *Context) float64 Get returns the flag’s value in the given Context. func (f *Float64Flag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *Float64Flag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -950,11 +982,14 @@ func (f *Float64Flag) IsVisible() bool func (f *Float64Flag) Names() []string Names returns the names of the flag +func (f *Float64Flag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Float64Flag) String() string String returns a readable representation of this value (for usage defaults) func (f *Float64Flag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type Float64Slice struct { // Has unexported fields. @@ -996,6 +1031,8 @@ type Float64SliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []float64) error } Float64SliceFlag is a flag with type *Float64Slice @@ -1006,7 +1043,7 @@ func (f *Float64SliceFlag) Get(ctx *Context) []float64 Get returns the flag’s value in the given Context. func (f *Float64SliceFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *Float64SliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1035,6 +1072,9 @@ func (f *Float64SliceFlag) IsVisible() bool func (f *Float64SliceFlag) Names() []string Names returns the names of the flag +func (f *Float64SliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Float64SliceFlag) SetDestination(slice []float64) func (f *Float64SliceFlag) SetValue(slice []float64) @@ -1064,16 +1104,18 @@ type GenericFlag struct { HasBeenSet bool Value Generic - Destination *Generic + Destination Generic Aliases []string EnvVars []string TakesFile bool + + Action func(*Context, interface{}) error } GenericFlag is a flag with type Generic -func (f GenericFlag) Apply(set *flag.FlagSet) error +func (f *GenericFlag) Apply(set *flag.FlagSet) error Apply takes the flagset and calls Set on the generic flag with the value provided by the user for parsing by the flag @@ -1081,7 +1123,7 @@ func (f *GenericFlag) Get(ctx *Context) interface{} Get returns the flag’s value in the given Context. func (f *GenericFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *GenericFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1108,11 +1150,14 @@ func (f *GenericFlag) IsVisible() bool func (f *GenericFlag) Names() []string Names returns the names of the flag +func (f *GenericFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *GenericFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *GenericFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type Int64Flag struct { Name string @@ -1131,6 +1176,10 @@ type Int64Flag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, int64) error } Int64Flag is a flag with type int64 @@ -1141,7 +1190,7 @@ func (f *Int64Flag) Get(ctx *Context) int64 Get returns the flag’s value in the given Context. func (f *Int64Flag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *Int64Flag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1168,11 +1217,14 @@ func (f *Int64Flag) IsVisible() bool func (f *Int64Flag) Names() []string Names returns the names of the flag +func (f *Int64Flag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Int64Flag) String() string String returns a readable representation of this value (for usage defaults) func (f *Int64Flag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type Int64Slice struct { // Has unexported fields. @@ -1214,6 +1266,8 @@ type Int64SliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []int64) error } Int64SliceFlag is a flag with type *Int64Slice @@ -1224,7 +1278,7 @@ func (f *Int64SliceFlag) Get(ctx *Context) []int64 Get returns the flag’s value in the given Context. func (f *Int64SliceFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *Int64SliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1253,6 +1307,9 @@ func (f *Int64SliceFlag) IsVisible() bool func (f *Int64SliceFlag) Names() []string Names returns the names of the flag +func (f *Int64SliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Int64SliceFlag) SetDestination(slice []int64) func (f *Int64SliceFlag) SetValue(slice []int64) @@ -1261,7 +1318,7 @@ func (f *Int64SliceFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *Int64SliceFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type IntFlag struct { Name string @@ -1280,6 +1337,10 @@ type IntFlag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, int) error } IntFlag is a flag with type int @@ -1290,7 +1351,7 @@ func (f *IntFlag) Get(ctx *Context) int Get returns the flag’s value in the given Context. func (f *IntFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *IntFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1317,11 +1378,14 @@ func (f *IntFlag) IsVisible() bool func (f *IntFlag) Names() []string Names returns the names of the flag +func (f *IntFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *IntFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *IntFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type IntSlice struct { // Has unexported fields. @@ -1367,6 +1431,8 @@ type IntSliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []int) error } IntSliceFlag is a flag with type *IntSlice @@ -1377,7 +1443,7 @@ func (f *IntSliceFlag) Get(ctx *Context) []int Get returns the flag’s value in the given Context. func (f *IntSliceFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *IntSliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1406,6 +1472,9 @@ func (f *IntSliceFlag) IsVisible() bool func (f *IntSliceFlag) Names() []string Names returns the names of the flag +func (f *IntSliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *IntSliceFlag) SetDestination(slice []int) func (f *IntSliceFlag) SetValue(slice []int) @@ -1414,7 +1483,11 @@ func (f *IntSliceFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *IntSliceFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false + +type InvalidFlagAccessFunc func(*Context, string) + InvalidFlagAccessFunc is executed when an invalid flag is accessed from the + context. type MultiError interface { error @@ -1431,8 +1504,8 @@ type MultiInt64Flag = SliceFlag[*Int64SliceFlag, []int64, int64] directly, as Value and/or Destination. See also SliceFlag. type MultiIntFlag = SliceFlag[*IntSliceFlag, []int, int] - MultiIntFlag extends IntSliceFlag with support for using slices directly, as - Value and/or Destination. See also SliceFlag. + MultiIntFlag extends IntSliceFlag with support for using slices directly, + as Value and/or Destination. See also SliceFlag. type MultiStringFlag = SliceFlag[*StringSliceFlag, []string, string] MultiStringFlag extends StringSliceFlag with support for using slices @@ -1465,6 +1538,8 @@ type PathFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, Path) error } PathFlag is a flag with type Path @@ -1475,7 +1550,7 @@ func (f *PathFlag) Get(ctx *Context) string Get returns the flag’s value in the given Context. func (f *PathFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *PathFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1502,20 +1577,14 @@ func (f *PathFlag) IsVisible() bool func (f *PathFlag) Names() []string Names returns the names of the flag +func (f *PathFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *PathFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *PathFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false - -type RequiredFlag interface { - Flag - - IsRequired() bool -} - RequiredFlag is an interface that allows us to mark flags as required it - allows flags required flags to be backwards compatible with the Flag - interface + TakesValue returns true if the flag takes a value, otherwise false type Serializer interface { Serialize() string @@ -1527,9 +1596,9 @@ type SliceFlag[T SliceFlagTarget[E], S ~[]E, E any] struct { Value S Destination *S } - SliceFlag extends implementations like StringSliceFlag and IntSliceFlag with - support for using slices directly, as Value and/or Destination. See also - SliceFlagTarget, MultiStringFlag, MultiFloat64Flag, MultiInt64Flag, + SliceFlag extends implementations like StringSliceFlag and IntSliceFlag + with support for using slices directly, as Value and/or Destination. + See also SliceFlagTarget, MultiStringFlag, MultiFloat64Flag, MultiInt64Flag, MultiIntFlag. func (x *SliceFlag[T, S, E]) Apply(set *flag.FlagSet) error @@ -1554,6 +1623,8 @@ func (x *SliceFlag[T, S, E]) IsVisible() bool func (x *SliceFlag[T, S, E]) Names() []string +func (x *SliceFlag[T, S, E]) RunAction(c *Context) error + func (x *SliceFlag[T, S, E]) SetDestination(slice S) func (x *SliceFlag[T, S, E]) SetValue(slice S) @@ -1564,10 +1635,6 @@ func (x *SliceFlag[T, S, E]) TakesValue() bool type SliceFlagTarget[E any] interface { Flag - RequiredFlag - DocGenerationFlag - VisibleFlag - CategorizableFlag // SetValue should propagate the given slice to the target, ideally as a new value. // Note that a nil slice should nil/clear any existing value (modelled as ~[]E). @@ -1602,6 +1669,8 @@ type StringFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, string) error } StringFlag is a flag with type string @@ -1612,7 +1681,7 @@ func (f *StringFlag) Get(ctx *Context) string Get returns the flag’s value in the given Context. func (f *StringFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *StringFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1639,11 +1708,14 @@ func (f *StringFlag) IsVisible() bool func (f *StringFlag) Names() []string Names returns the names of the flag +func (f *StringFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *StringFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *StringFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type StringSlice struct { // Has unexported fields. @@ -1687,6 +1759,8 @@ type StringSliceFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, []string) error } StringSliceFlag is a flag with type *StringSlice @@ -1697,7 +1771,7 @@ func (f *StringSliceFlag) Get(ctx *Context) []string Get returns the flag’s value in the given Context. func (f *StringSliceFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *StringSliceFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1726,6 +1800,9 @@ func (f *StringSliceFlag) IsVisible() bool func (f *StringSliceFlag) Names() []string Names returns the names of the flag +func (f *StringSliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *StringSliceFlag) SetDestination(slice []string) func (f *StringSliceFlag) SetValue(slice []string) @@ -1734,7 +1811,7 @@ func (f *StringSliceFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *StringSliceFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type SuggestCommandFunc func(commands []*Command, provided string) string @@ -1790,6 +1867,8 @@ type TimestampFlag struct { Layout string Timezone *time.Location + + Action func(*Context, *time.Time) error } TimestampFlag is a flag with type *Timestamp @@ -1800,7 +1879,7 @@ func (f *TimestampFlag) Get(ctx *Context) *time.Time Get returns the flag’s value in the given Context. func (f *TimestampFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *TimestampFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1827,11 +1906,14 @@ func (f *TimestampFlag) IsVisible() bool func (f *TimestampFlag) Names() []string Names returns the names of the flag +func (f *TimestampFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *TimestampFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *TimestampFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false type Uint64Flag struct { Name string @@ -1850,6 +1932,10 @@ type Uint64Flag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, uint64) error } Uint64Flag is a flag with type uint64 @@ -1860,7 +1946,7 @@ func (f *Uint64Flag) Get(ctx *Context) uint64 Get returns the flag’s value in the given Context. func (f *Uint64Flag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *Uint64Flag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1887,10 +1973,101 @@ func (f *Uint64Flag) IsVisible() bool func (f *Uint64Flag) Names() []string Names returns the names of the flag +func (f *Uint64Flag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *Uint64Flag) String() string String returns a readable representation of this value (for usage defaults) func (f *Uint64Flag) TakesValue() bool + TakesValue returns true if the flag takes a value, otherwise false + +type Uint64Slice struct { + // Has unexported fields. +} + Uint64Slice wraps []int64 to satisfy flag.Value + +func NewUint64Slice(defaults ...uint64) *Uint64Slice + NewUint64Slice makes an *Uint64Slice with default values + +func (i *Uint64Slice) Get() interface{} + Get returns the slice of ints set by this flag + +func (i *Uint64Slice) Serialize() string + Serialize allows Uint64Slice to fulfill Serializer + +func (i *Uint64Slice) Set(value string) error + Set parses the value into an integer and appends it to the list of values + +func (i *Uint64Slice) String() string + String returns a readable representation of this value (for usage defaults) + +func (i *Uint64Slice) Value() []uint64 + Value returns the slice of ints set by this flag + +type Uint64SliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Uint64Slice + Destination *Uint64Slice + + Aliases []string + EnvVars []string + + Action func(*Context, []uint64) error +} + Uint64SliceFlag is a flag with type *Uint64Slice + +func (f *Uint64SliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *Uint64SliceFlag) Get(ctx *Context) []uint64 + Get returns the flag’s value in the given Context. + +func (f *Uint64SliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *Uint64SliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *Uint64SliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *Uint64SliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *Uint64SliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *Uint64SliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *Uint64SliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *Uint64SliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *Uint64SliceFlag) Names() []string + Names returns the names of the flag + +func (f *Uint64SliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + +func (f *Uint64SliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *Uint64SliceFlag) TakesValue() bool TakesValue returns true of the flag takes a value, otherwise false type UintFlag struct { @@ -1910,6 +2087,10 @@ type UintFlag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, uint) error } UintFlag is a flag with type uint @@ -1920,7 +2101,7 @@ func (f *UintFlag) Get(ctx *Context) uint Get returns the flag’s value in the given Context. func (f *UintFlag) GetCategory() string - GetCategory returns the category for the flag + GetCategory returns the category of the flag func (f *UintFlag) GetDefaultText() string GetDefaultText returns the default text for this flag @@ -1947,29 +2128,116 @@ func (f *UintFlag) IsVisible() bool func (f *UintFlag) Names() []string Names returns the names of the flag +func (f *UintFlag) RunAction(c *Context) error + RunAction executes flag action if set + func (f *UintFlag) String() string String returns a readable representation of this value (for usage defaults) func (f *UintFlag) TakesValue() bool - TakesValue returns true of the flag takes a value, otherwise false + TakesValue returns true if the flag takes a value, otherwise false -type VisibleFlag interface { - Flag +type UintSlice struct { + // Has unexported fields. +} + UintSlice wraps []int to satisfy flag.Value - // IsVisible returns true if the flag is not hidden, otherwise false - IsVisible() bool +func NewUintSlice(defaults ...uint) *UintSlice + NewUintSlice makes an *UintSlice with default values + +func (i *UintSlice) Get() interface{} + Get returns the slice of ints set by this flag + +func (i *UintSlice) Serialize() string + Serialize allows UintSlice to fulfill Serializer + +func (i *UintSlice) Set(value string) error + Set parses the value into an integer and appends it to the list of values + +func (i *UintSlice) SetUint(value uint) + TODO: Consistently have specific Set function for Int64 and Float64 ? SetInt + directly adds an integer to the list of values + +func (i *UintSlice) String() string + String returns a readable representation of this value (for usage defaults) + +func (i *UintSlice) Value() []uint + Value returns the slice of ints set by this flag + +type UintSliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *UintSlice + Destination *UintSlice + + Aliases []string + EnvVars []string + + Action func(*Context, []uint) error } - VisibleFlag is an interface that allows to check if a flag is visible + UintSliceFlag is a flag with type *UintSlice + +func (f *UintSliceFlag) Apply(set *flag.FlagSet) error + Apply populates the flag given the flag set and environment + +func (f *UintSliceFlag) Get(ctx *Context) []uint + Get returns the flag’s value in the given Context. + +func (f *UintSliceFlag) GetCategory() string + GetCategory returns the category for the flag + +func (f *UintSliceFlag) GetDefaultText() string + GetDefaultText returns the default text for this flag + +func (f *UintSliceFlag) GetEnvVars() []string + GetEnvVars returns the env vars for this flag + +func (f *UintSliceFlag) GetUsage() string + GetUsage returns the usage string for the flag + +func (f *UintSliceFlag) GetValue() string + GetValue returns the flags value as string representation and an empty + string if the flag takes no value at all. + +func (f *UintSliceFlag) IsRequired() bool + IsRequired returns whether or not the flag is required + +func (f *UintSliceFlag) IsSet() bool + IsSet returns whether or not the flag has been set through env or file + +func (f *UintSliceFlag) IsVisible() bool + IsVisible returns true if the flag is not hidden, otherwise false + +func (f *UintSliceFlag) Names() []string + Names returns the names of the flag + +func (f *UintSliceFlag) RunAction(c *Context) error + RunAction executes flag action if set + +func (f *UintSliceFlag) String() string + String returns a readable representation of this value (for usage defaults) + +func (f *UintSliceFlag) TakesValue() bool + TakesValue returns true of the flag takes a value, otherwise false type VisibleFlagCategory interface { // Name returns the category name string Name() string // Flags returns a slice of VisibleFlag sorted by name - Flags() []VisibleFlag + Flags() []Flag } VisibleFlagCategory is a category containing flags. -package altsrc // import "github.com/urfave/cli/v2/altsrc" +package altsrc // import "github.com/urfave/cli/v3/altsrc" FUNCTIONS @@ -1986,9 +2254,9 @@ func InitInputSource(flags []cli.Flag, createInputSource func() (InputSourceCont that are supported by the input source func InitInputSourceWithContext(flags []cli.Flag, createInputSource func(cCtx *cli.Context) (InputSourceContext, error)) cli.BeforeFunc - InitInputSourceWithContext is used to to setup an InputSourceContext on a - cli.Command Before method. It will create a new input source based on the - func provided with potentially using existing cli.Context values to + InitInputSourceWithContext is used to to setup an InputSourceContext on + a cli.Command Before method. It will create a new input source based on + the func provided with potentially using existing cli.Context values to initialize itself. If there is no error it will then apply the new input source to any flags that are supported by the input source diff --git a/zz_generated.flags.go b/zz_generated.flags.go index 685d97d..68934c7 100644 --- a/zz_generated.flags.go +++ b/zz_generated.flags.go @@ -22,6 +22,8 @@ type Float64SliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []float64) error } // IsSet returns whether or not the flag has been set through env or file @@ -86,12 +88,14 @@ type GenericFlag struct { HasBeenSet bool Value Generic - Destination *Generic + Destination Generic Aliases []string EnvVars []string TakesFile bool + + Action func(*Context, interface{}) error } // String returns a readable representation of this value (for usage defaults) @@ -165,6 +169,8 @@ type Int64SliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []int64) error } // IsSet returns whether or not the flag has been set through env or file @@ -233,6 +239,8 @@ type IntSliceFlag struct { Aliases []string EnvVars []string + + Action func(*Context, []int) error } // IsSet returns whether or not the flag has been set through env or file @@ -303,6 +311,8 @@ type PathFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, Path) error } // String returns a readable representation of this value (for usage defaults) @@ -370,6 +380,8 @@ type StringSliceFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, []string) error } // IsSet returns whether or not the flag has been set through env or file @@ -442,6 +454,8 @@ type TimestampFlag struct { Layout string Timezone *time.Location + + Action func(*Context, *time.Time) error } // String returns a readable representation of this value (for usage defaults) @@ -497,6 +511,90 @@ func (f *TimestampFlag) GetDefaultText() string { return f.GetValue() } +// Uint64SliceFlag is a flag with type *Uint64Slice +type Uint64SliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *Uint64Slice + Destination *Uint64Slice + + Aliases []string + EnvVars []string + + Action func(*Context, []uint64) error +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Uint64SliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *Uint64SliceFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Uint64SliceFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Uint64SliceFlag) IsVisible() bool { + return !f.Hidden +} + +// UintSliceFlag is a flag with type *UintSlice +type UintSliceFlag struct { + Name string + + Category string + DefaultText string + FilePath string + Usage string + + Required bool + Hidden bool + HasBeenSet bool + + Value *UintSlice + Destination *UintSlice + + Aliases []string + EnvVars []string + + Action func(*Context, []uint) error +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *UintSliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// Names returns the names of the flag +func (f *UintSliceFlag) Names() []string { + return FlagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *UintSliceFlag) IsRequired() bool { + return f.Required +} + +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *UintSliceFlag) IsVisible() bool { + return !f.Hidden +} + // BoolFlag is a flag with type bool type BoolFlag struct { Name string @@ -515,6 +613,10 @@ type BoolFlag struct { Aliases []string EnvVars []string + + Count *int + + Action func(*Context, bool) error } // String returns a readable representation of this value (for usage defaults) @@ -580,6 +682,8 @@ type Float64Flag struct { Aliases []string EnvVars []string + + Action func(*Context, float64) error } // String returns a readable representation of this value (for usage defaults) @@ -653,6 +757,10 @@ type IntFlag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, int) error } // String returns a readable representation of this value (for usage defaults) @@ -726,6 +834,10 @@ type Int64Flag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, int64) error } // String returns a readable representation of this value (for usage defaults) @@ -801,6 +913,8 @@ type StringFlag struct { EnvVars []string TakesFile bool + + Action func(*Context, string) error } // String returns a readable representation of this value (for usage defaults) @@ -866,6 +980,8 @@ type DurationFlag struct { Aliases []string EnvVars []string + + Action func(*Context, time.Duration) error } // String returns a readable representation of this value (for usage defaults) @@ -939,6 +1055,10 @@ type UintFlag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, uint) error } // String returns a readable representation of this value (for usage defaults) @@ -1012,6 +1132,10 @@ type Uint64Flag struct { Aliases []string EnvVars []string + + Base int + + Action func(*Context, uint64) error } // String returns a readable representation of this value (for usage defaults) diff --git a/zz_generated.flags_test.go b/zz_generated.flags_test.go index a0769a2..be6dadf 100644 --- a/zz_generated.flags_test.go +++ b/zz_generated.flags_test.go @@ -160,6 +160,20 @@ func TestTimestampFlag_SatisfiesFmtStringerInterface(t *testing.T) { _ = f.String() } +func TestUint64SliceFlag_SatisfiesFlagInterface(t *testing.T) { + var f cli.Flag = &cli.Uint64SliceFlag{} + + _ = f.IsSet() + _ = f.Names() +} + +func TestUintSliceFlag_SatisfiesFlagInterface(t *testing.T) { + var f cli.Flag = &cli.UintSliceFlag{} + + _ = f.IsSet() + _ = f.Names() +} + func TestBoolFlag_SatisfiesFlagInterface(t *testing.T) { var f cli.Flag = &cli.BoolFlag{}