diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 294eda2..b2c39fd 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -22,6 +22,6 @@ jobs: with: go-version: ${{ matrix.go }} - name: golangci-lint - uses: golangci/golangci-lint-action@v7 + uses: golangci/golangci-lint-action@v6 with: - version: v1.60.3 + version: v1.64.8 diff --git a/.golangci.yml b/.golangci.yml index 55bafb2..5b79723 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,52 +1,4 @@ -# This file contains all available configuration options -# with their default values (in comments). - -# Options for analysis running. run: - # The default concurrency value is the number of available CPU. - # concurrency: 4 - - # Timeout for analysis, e.g. 30s, 5m. - # Default: 1m - # timeout: 5m - - # Exit code when at least one issue was found. - # Default: 1 - # issues-exit-code: 2 - - # Include test files or not. - # Default: true - # tests: false - - # List of build tags, all linters use it. - # Default: []. - # build-tags: - # - mytag - - # Which dirs to skip: issues from them won't be reported. - # Can use regexp here: `generated.*`, regexp is applied on full path. - # Default value is empty list, - # but default dirs are skipped independently of this option's value (see skip-dirs-use-default). - # "/" will be replaced by current OS file path separator to properly work on Windows. - # skip-dirs: - # - src/external_libs - # - autogenerated_by_my_lib - - # Enables skipping of directories: - # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ - # Default: true - # skip-dirs-use-default: false - - # Which files to skip: they will be analyzed, but issues from them won't be reported. - # Default value is empty list, - # but there is no need to include all autogenerated files, - # we confidently recognize autogenerated files. - # If it's not please let us know. - # "/" will be replaced by current OS file path separator to properly work on Windows. - # skip-files: - # - ".*\\.my\\.go$" - # - lib/bad.go - # If set we pass it to "go list -mod={option}". From "go help modules": # If invoked with -mod=readonly, the go command is disallowed from the implicit # automatic updating of go.mod described above. Instead, it fails when any changes @@ -60,132 +12,7 @@ run: # By default, it isn't set. modules-download-mode: readonly - # Allow multiple parallel golangci-lint instances running. - # If false (default) - golangci-lint acquires file lock on start. - # allow-parallel-runners: false - -# output configuration options -output: - # Format: colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions - # - # Multiple can be specified by separating them by comma, output can be provided - # for each of them by separating format name and path by colon symbol. - # Output path can be either `stdout`, `stderr` or path to the file to write to. - # Example: "checkstyle:report.json,colored-line-number" - # - # Default: colored-line-number - # format: github-actions,colored-line-number - - # Print lines of code with issue. - # Default: true - # print-issued-lines: false - - # Print linter name in the end of issue text. - # Default: true - # print-linter-name: false - - # Make issues output unique by line. - # Default: true - # uniq-by-line: false - - # Add a prefix to the output file references. - # Default is no prefix. - # path-prefix: "" - - # Sort results by: filepath, line and column. - # sort-results: false - - -# All available settings of specific linters. linters-settings: - bidichk: - # The following configurations check for all mentioned invisible unicode runes. - # All runes are enabled by default. - # left-to-right-embedding: false - # right-to-left-embedding: false - # pop-directional-formatting: false - # left-to-right-override: false - # right-to-left-override: false - # left-to-right-isolate: false - # right-to-left-isolate: false - # first-strong-isolate: false - # pop-directional-isolate: false - - # cyclop: - # The maximal code complexity to report. - # max-complexity: 10 - # The maximal average package complexity. - # If it's higher than 0.0 (float) the check is enabled - # Default: 0.0 - # package-average: 0.5 - # Should ignore tests. - # Default: false - # skip-tests: true - - # decorder: - # Required order of `type`, `const`, `var` and `func` declarations inside a file. - # Default: types before constants before variables before functions. - # dec-order: - # - type - # - const - # - var - # - func - - # If true, order of declarations is not checked at all. - # Default: true (disabled) - # disable-dec-order-check: false - - # If true, `init` func can be anywhere in file (does not have to be declared before all other functions). - # Default: true (disabled) - # disable-init-func-first-check: false - - # If true, multiple global `type`, `const` and `var` declarations are allowed. - # Default: true (disabled) - # disable-dec-num-check: false - - # depguard: - # Kind of list is passed in. - # Allowed values: allowlist|denylist - # Default: denylist - # list-type: allowlist - - # Check the list against standard lib. - # Default: false - # include-go-root: true - - # A list of packages for the list type specified. - # Default: [] - # packages: - # - github.com/sirupsen/logrus - - # A list of packages for the list type specified. - # Specify an error message to output when a denied package is used. - # Default: [] - # packages-with-error-message: - # - github.com/sirupsen/logrus: 'logging is allowed only by logutils.Log' - - # Create additional guards that follow the same configuration pattern. - # Results from all guards are aggregated together. - # additional-guards: - # - list-type: denylist - # include-go-root: false - # packages: - # - github.com/stretchr/testify - # Specify rules by which the linter ignores certain files for consideration. - # ignore-file-rules: - # - "**/*_test.go" - # - "**/mock/**/*.go" - - dogsled: - # Checks assignments with too many blank identifiers. - # Default: 2 - # max-blank-identifiers: 3 - - # dupl: - # Tokens count to trigger issue. - # Default: 150 - # threshold: 100 - errcheck: # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. # Such cases aren't reported by default. @@ -197,23 +24,6 @@ linters-settings: # Default: false check-blank: true - # DEPRECATED comma-separated list of pairs of the form pkg:regex - # - # the regex is used to ignore names within pkg. (default "fmt:.*"). - # see https://github.com/kisielk/errcheck#the-deprecated-method for details - # ignore: fmt:.*,io/ioutil:^Read.* - - # To disable the errcheck built-in exclude list. - # See `-excludeonly` option in https://github.com/kisielk/errcheck#excluding-functions for details. - # Default: false - # disable-default-exclusions: true - - # DEPRECATED use exclude-functions instead. - # - # Path to a file containing a list of functions to exclude from checking. - # See https://github.com/kisielk/errcheck#excluding-functions for details. - # exclude: /path/to/file.txt - # List of functions to exclude from checking, where each entry is a single function to exclude. # See https://github.com/kisielk/errcheck#excluding-functions for details. exclude-functions: @@ -221,345 +31,26 @@ linters-settings: - encoding/json.MarshalIndent errchkjson: - # With check-error-free-encoding set to true, errchkjson does warn about errors - # from json encoding functions that are safe to be ignored, - # because they are not possible to happen. - # - # if check-error-free-encoding is set to true and errcheck linter is enabled, - # it is recommended to add the following exceptions to prevent from false positives: - # - # linters-settings: - # errcheck: - # exclude-functions: - # - encoding/json.Marshal - # - encoding/json.MarshalIndent - # - # Default: false - # check-error-free-encoding: true - # Issue on struct encoding that doesn't have exported fields. # Default: false report-no-exported: true - errorlint: - # Check whether fmt.Errorf uses the %w verb for formatting errors. - # See the https://github.com/polyfloyd/go-errorlint for caveats. - # Default: true - # errorf: false - # Check for plain type assertions and type switches. - # Default: true - # asserts: false - # Check for plain error comparisons. - # Default: true - # comparison: false - exhaustive: # Check switch statements in generated files also. # Default: false check-generated: true - # Presence of "default" case in switch statements satisfies exhaustiveness, - # even if all enum members are not listed. - # Default: false - # default-signifies-exhaustive: true - # Enum members matching the supplied regex do not have to be listed in - # switch statements to satisfy exhaustiveness. - # Default: "" - # ignore-enum-members: "Example.+" - # Consider enums only in package scopes, not in inner scopes. - # Default: false - # package-scope-only: true - - # exhaustivestruct: - # Struct Patterns is list of expressions to match struct packages and names. - # The struct packages have the form `example.com/package.ExampleStruct`. - # The matching patterns can use matching syntax from https://pkg.go.dev/path#Match. - # If this list is empty, all structs are tested. - # struct-patterns: - # - '*.Test' - # - 'example.com/package.ExampleStruct' - - # forbidigo: - # Forbid the following identifiers (list of regexp). - # forbid: - # - ^print.*$ - # - 'fmt\.Print.*' - # Optionally put comments at the end of the regex, surrounded by `(# )?` - # Escape any special characters. - # - 'fmt\.Print.*(# Do not commit print statements\.)?' - # Exclude godoc examples from forbidigo checks. - # Default: true - # exclude_godoc_examples: false - - # funlen: - # Checks the number of lines in a function. - # If lower than 0, disable the check. - # Default: 60 - # lines: -1 - # Checks the number of statements in a function. - # If lower than 0, disable the check. - # Default: 40 - # statements: -1 - - # gci: - # DEPRECATED: use `sections` and `prefix(github.com/org/project)` instead. - # local-prefixes: github.com/org/project - - # Checks that no inline Comments are present. - # Default: false - # no-inline-comments: true - - # Checks that no prefix Comments(comment lines above an import) are present. - # Default: false - # no-prefix-comments: true - - # Section configuration to compare against. - # Section names are case-insensitive and may contain parameters in (). - # Default: ["standard", "default"] - # sections: - # - standard # Captures all standard packages if they do not match another section. - # - default # Contains all imports that could not be matched to another section type. - # - comment(your text here) # Prints the specified indented comment. - # - newLine # Prints an empty line - # - prefix(github.com/org/project) # Groups all imports with the specified Prefix. - - # Separators that should be present between sections. - # Default: ["newLine"] - # section-separators: - # - newLine - - # gocognit: - # Minimal code complexity to report - # Default: 30 (but we recommend 10-20) - # min-complexity: 10 - - goconst: - # Minimal length of string constant. - # Default: 3 - # min-len: 2 - # Minimum occurrences of constant string count to trigger issue. - # Default: 3 - # min-occurrences: 2 - # Ignore test files. - # Default: false - # ignore-tests: true - # Look for existing constants matching the values. - # Default: true - # match-constant: false - # Search also for duplicated numbers. - # Default: false - # numbers: true - # Minimum value, only works with goconst.numbers - # Default: 3 - # min: 2 - # Maximum value, only works with goconst.numbers - # Default: 3 - # max: 2 - # Ignore when constant is not used as function argument. - # Default: true - # ignore-calls: false - - # gocritic: - # Which checks should be enabled; can't be combined with 'disabled-checks'. - # See https://go-critic.github.io/overview#checks-overview. - # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`. - # By default, list of stable checks is used. - # enabled-checks: - # - nestingReduce - # - unnamedResult - # - ruleguard - # - truncateCmp - - # Which checks should be disabled; can't be combined with 'enabled-checks'. - # Default: [] - # disabled-checks: - # - regexpMust - - # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. - # See https://github.com/go-critic/go-critic#usage -> section "Tags". - # Default: [] - # enabled-tags: - # - diagnostic - # - style - # - performance - # - experimental - # - opinionated - # disabled-tags: - # - diagnostic - # - style - # - performance - # - experimental - # - opinionated - - # Settings passed to gocritic. - # The settings key is the name of a supported gocritic checker. - # The list of supported checkers can be find in https://go-critic.github.io/overview. - # settings: - # Must be valid enabled check name. - # captLocal: - # Whether to restrict checker to params only. - # Default: true - # paramsOnly: false - # elseif: - # Whether to skip balanced if-else pairs. - # Default: true - # skipBalanced: false - # hugeParam: - # Size in bytes that makes the warning trigger. - # Default: 80 - # sizeThreshold: 70 - # nestingReduce: - # Min number of statements inside a branch to trigger a warning. - # Default: 5 - # bodyWidth: 4 - # rangeExprCopy: - # Size in bytes that makes the warning trigger. - # Default: 512 - # sizeThreshold: 516 - # Whether to check test functions - # Default: true - # skipTestFuncs: false - # rangeValCopy: - # Size in bytes that makes the warning trigger. - # Default: 128 - # sizeThreshold: 32 - # Whether to check test functions. - # Default: true - # skipTestFuncs: false - # ruleguard: - # Enable debug to identify which 'Where' condition was rejected. - # The value of the parameter is the name of a function in a ruleguard file. - # - # When a rule is evaluated: - # If: - # The Match() clause is accepted; and - # One of the conditions in the Where() clause is rejected, - # Then: - # ruleguard prints the specific Where() condition that was rejected. - # - # The flag is passed to the ruleguard 'debug-group' argument. - # debug: 'emptyDecl' - # Deprecated, use 'failOn' param. - # If set to true, identical to failOn='all', otherwise failOn='' - # failOnError: false - # Determines the behavior when an error occurs while parsing ruleguard files. - # If flag is not set, log error and skip rule files that contain an error. - # If flag is set, the value must be a comma-separated list of error conditions. - # - 'all': fail on all errors. - # - 'import': ruleguard rule imports a package that cannot be found. - # - 'dsl': gorule file does not comply with the ruleguard DSL. - # failOn: dsl - # Comma-separated list of file paths containing ruleguard rules. - # If a path is relative, it is relative to the directory where the golangci-lint command is executed. - # The special '${configDir}' variable is substituted with the absolute directory containing the golangci config file. - # Glob patterns such as 'rules-*.go' may be specified. - # rules: '${configDir}/ruleguard/rules-*.go,${configDir}/myrule1.go' - # tooManyResultsChecker: - # Maximum number of results. - # Default: 5 - # maxResults: 10 - # truncateCmp: - # Whether to skip int/uint/uintptr types. - # Default: true - # skipArchDependent: false - # underef: - # Whether to skip (*x).method() calls where x is a pointer receiver. - # Default: true - # skipRecvDeref: false - # unnamedResult: - # Whether to check exported functions. - # checkExported: true - - # gocyclo: - # Minimal code complexity to report. - # Default: 30 (but we recommend 10-20) - # min-complexity: 10 - - # godot: - # Comments to be checked: `declarations`, `toplevel`, or `all`. - # Default: declarations - # scope: toplevel - # List of regexps for excluding particular comment lines from check. - # exclude: - # Exclude todo and fixme comments. - # - "^fixme:" - # - "^todo:" - # Check that each sentence ends with a period. - # Default: true - # period: false - # Check that each sentence starts with a capital letter. - # Default: false - # capital: true - - # godox: - # Report any comments starting with keywords, this is useful for TODO or FIXME comments that - # might be left in the code accidentally and should be resolved before merging. - # Default: TODO, BUG, and FIXME. - # keywords: - # - NOTE - # - OPTIMIZE # marks code that should be optimized before merging - # - HACK # marks hack-around that should be removed before merging gofmt: # Simplify code: gofmt with `-s` option. # Default: true simplify: false - # gofumpt: - # Select the Go version to target. - # Default: 1.15 - # lang-version: "1.17" - - # Choose whether to use the extra rules. - # Default: false - # extra-rules: true - - # goheader: - # Supports two types 'const` and `regexp`. - # Values can be used recursively. - # values: - # const: - # Define here const type values in format k:v. - # For example: - # COMPANY: MY COMPANY - # regexp: - # Define here regexp type values. - # for example: - # AUTHOR: .*@mycompany\.com - # The template use for checking. - # template: |- - # Put here copyright header template for source code files - # For example: - # Note: {{ YEAR }} is a builtin value that returns the year relative to the current machine time. - # - # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }} - # SPDX-License-Identifier: Apache-2.0 - - # Licensed under the Apache License, Version 2.0 (the "License"); - # you may not use this file except in compliance with the License. - # You may obtain a copy of the License at: - - # http://www.apache.org/licenses/LICENSE-2.0 - - # Unless required by applicable law or agreed to in writing, software - # distributed under the License is distributed on an "AS IS" BASIS, - # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - # See the License for the specific language governing permissions and - # limitations under the License. - # As alternative of directive 'template', you may put the path to file with the template source. - # Useful if you need to load the template from a specific file. - # template-path: /path/to/my/template.tmpl - goimports: # Put imports beginning with prefix after 3rd-party packages. # It's a comma-separated list of prefixes. - local-prefixes: lockbox.dev + local-prefixes: impractical.co - # golint: - # Minimal confidence for issues. - # Default: 0.8 - # min-confidence: 0.7 - - gomnd: + mnd: # List of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. checks: - argument @@ -568,272 +59,27 @@ linters-settings: - operation - return - assign - # List of numbers to exclude from analysis. - # The numbers should be written as string. - # Values always ignored: "1", "1.0", "0" and "0.0" - # ignored-numbers: - # - '0666' - # - '0755' - # - '42' - # List of file patterns to exclude from analysis. - # Values always ignored: `.+_test.go` - # ignored-files: - # - 'magic1_.*.go' - # List of function patterns to exclude from analysis. - # Values always ignored: `time.Time` - # ignored-functions: - # - 'math.*' - # - 'http.StatusText' - - gomoddirectives: - # Allow local `replace` directives. - # replace-local: false - # List of allowed `replace` directives. - # Default: [] - # replace-allow-list: - # - launchpad.net/gocheck - # Allow to not explain why the version has been retracted in the `retract` directives. - # Default: false - # retract-allow-no-explanation: false - # Forbid the use of the `exclude` directives. - # Default: false - # exclude-forbidden: false - - # gomodguard: - # allowed: - # List of allowed modules. - # modules: - # - gopkg.in/yaml.v2 - # List of allowed module domains. - # domains: - # - golang.org - # blocked: - # List of blocked modules. - # modules: - # Blocked module. - # - github.com/uudashr/go-module: - # Recommended modules that should be used instead. (Optional) - # recommendations: - # - golang.org/x/mod - # Reason why the recommended module should be used. (Optional) - # reason: "`mod` is the official go.mod parser library." - # List of blocked module version constraints. - # versions: - # Blocked module with version constraint. - # - github.com/mitchellh/go-homedir: - # Version constraint, see https://github.com/Masterminds/semver#basic-comparisons. - # version: "< 1.1.0" - # Reason why the version constraint exists. (Optional) - # reason: "testing if blocked version constraint works." - # Set to true to raise lint issues for packages that are loaded from a local path via replace directive. - # local_replace_directives: false gosimple: # https://staticcheck.io/docs/options#checks checks: [ "all" ] gosec: - # To select a subset of rules to run. - # Available rules: https://github.com/securego/gosec#available-rules - # includes: - # - G101 - # - G102 - # - G103 - # - G104 - # - G106 - # - G107 - # - G108 - # - G109 - # - G110 - # - G201 - # - G202 - # - G203 - # - G204 - # - G301 - # - G302 - # - G303 - # - G304 - # - G305 - # - G306 - # - G307 - # - G401 - # - G402 - # - G403 - # - G404 - # - G501 - # - G502 - # - G503 - # - G504 - # - G505 - # - G601 - # To specify a set of rules to explicitly exclude. # Available rules: https://github.com/securego/gosec#available-rules excludes: - # - G101 - # - G102 - # - G103 - G104 # handled by errcheck - # - G106 - # - G107 - # - G108 - # - G109 - # - G110 - # - G201 - # - G202 - # - G203 - # - G204 - # - G301 - # - G302 - # - G303 - # - G304 - # - G305 - # - G306 - # - G307 - # - G401 - # - G402 - # - G403 - # - G404 - # - G501 - # - G502 - # - G503 - # - G504 - # - G505 - # - G601 - - # Exclude generated files - # Default: false - # exclude-generated: true - - # Filter out the issues with a lower severity than the given value. - # Valid options are: low, medium, high. - # Default: low - # severity: medium - - # Filter out the issues with a lower confidence than the given value. - # Valid options are: low, medium, high. - # Default: low - # confidence: medium - - # To specify the configuration of rules. - # The configuration of rules is not fully documented by gosec: - # https://github.com/securego/gosec#configuration - # https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/rules/rulelist.go#L60-L102 - # config: - # G306: "0600" - # G101: - # pattern: "(?i)example" - # ignore_entropy: false - # entropy_threshold: "80.0" - # per_char_threshold: "3.0" - # truncate: "32" govet: - # Settings per analyzer. - # settings: - # Analyzer name, run `go tool vet help` to see all analyzers. - # printf: - # Run `go tool vet help printf` to see available settings for `printf` analyzer. - # funcs: - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - - # Disable all analyzers. - # Default: false - # disable-all: true - # Enable analyzers by name. - # Run `go tool vet help` to see all analyzers. - # enable: - # - asmdecl - # - assign - # - atomic - # - atomicalign - # - bools - # - buildtag - # - cgocall - # - composites - # - copylocks - # - deepequalerrors - # - errorsas - # - fieldalignment - # - findcall - # - framepointer - # - httpresponse - # - ifaceassert - # - loopclosure - # - lostcancel - # - nilfunc - # - nilness - # - printf - # - reflectvaluecompare - # - shadow - # - shift - # - sigchanyzer - # - sortslice - # - stdmethods - # - stringintconv - # - structtag - # - testinggoroutine - # - tests - # - unmarshal - # - unreachable - # - unsafeptr - # - unusedresult - # - unusedwrite - # Enable all analyzers. # Default: false enable-all: true # Disable analyzers by name. # Run `go tool vet help` to see all analyzers. disable: - # - asmdecl - # - assign - # - atomic - # - atomicalign - # - bools - # - buildtag - # - cgocall - # - composites - # - copylocks - # - deepequalerrors - # - errorsas - fieldalignment - # - findcall - # - framepointer - # - httpresponse - # - ifaceassert - # - loopclosure - # - lostcancel - # - nilfunc - # - nilness - # - printf - # - reflectvaluecompare - # - shadow - # - shift - # - sigchanyzer - # - sortslice - # - stdmethods - # - stringintconv - # - structtag - # - testinggoroutine - # - tests - # - unmarshal - # - unreachable - # - unsafeptr - # - unusedresult - # - unusedwrite grouper: - # Require the use of a single global 'const' declaration only. - # Default: false - # const-require-single-const: true - # Require the use of grouped global 'const' declarations. - # Default: false - # const-require-grouping: true - # Require the use of a single 'import' declaration only. # Default: false import-require-single-import: true @@ -841,142 +87,7 @@ linters-settings: # Default: false import-require-grouping: true - # Require the use of a single global 'type' declaration only. - # Default: false - # type-require-single-type: true - # Require the use of grouped global 'type' declarations. - # Default: false - # type-require-grouping: true - - # Require the use of a single global 'var' declaration only. - # Default: false - # var-require-single-var: true - # Require the use of grouped global 'var' declarations. - # Default: false - # var-require-grouping: true - - # ifshort: - # Maximum length of variable declaration measured in number of lines, after which linter won't suggest using short syntax. - # Has higher priority than max-decl-chars. - # Default: 1 - # max-decl-lines: 2 - # Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax. - # Default: 30 - # max-decl-chars: 40 - - importas: - # Do not allow unaliased imports of aliased packages. - # Default: false - # no-unaliased: true - # Do not allow non-required aliases. - # Default: false - # no-extra-aliases: true - # List of aliases - # alias: - # Using `servingv1` alias for `knative.dev/serving/pkg/apis/serving/v1` package. - # - pkg: knative.dev/serving/pkg/apis/serving/v1 - # alias: servingv1 - # Using `autoscalingv1alpha1` alias for `knative.dev/serving/pkg/apis/autoscaling/v1alpha1` package. - # - pkg: knative.dev/serving/pkg/apis/autoscaling/v1alpha1 - # alias: autoscalingv1alpha1 - # You can specify the package path by regular expression, - # and alias by regular expression expansion syntax like below. - # see https://github.com/julz/importas#use-regular-expression for details - # - pkg: knative.dev/serving/pkg/apis/(\w+)/(v[\w\d]+) - # alias: $1$2 - - ireturn: - # ireturn allows using `allow` and `reject` settings at the same time. - # Both settings are lists of the keywords and regular expressions matched to interface or package names. - # keywords: - # - `empty` for `interface{}` - # - `error` for errors - # - `stdlib` for standard library - # - `anon` for anonymous interfaces - - # By default, it allows using errors, empty interfaces, anonymous interfaces, - # and interfaces provided by the standard library. - # allow: - # - anon - # - error - # - empty - # - stdlib - # You can specify idiomatic endings for interface - # - (or|er)$ - - # reject-list of interfaces - # reject: - # - github.com\/user\/package\/v4\.Type - - #lll: - # Max line length, lines longer will be reported. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option. - # Default: 120. - # line-length: 120 - # Tab width in spaces. - # Default: 1 - # tab-width: 1 - - maintidx: - # Show functions with maintainability index lower than N. - # A high index indicates better maintainability (it's kind of the opposite of complexity). - # Default: 20 - under: 100 - - makezero: - # Allow only slices initialized with a length of zero. - # Default: false - # always: false - - maligned: - # Print struct with more effective memory layout or not. - # Default: false - suggest-new: true - - misspell: - # Correct spellings using locale preferences for US or UK. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - # Default is to use a neutral variety of English. - # locale: US - # ignore-words: - # - someword - - nakedret: - # Make an issue if func has more lines of code than this setting, and it has naked returns. - # Default: 30 - # max-func-lines: 31 - - # nestif: - # Minimal complexity of if statements to report. - # Default: 5 - # min-complexity: 4 - - nilnil: - # Checks that there is no simultaneous return of `nil` error and an invalid value. - # Default: ptr, func, iface, map, chan - # checked-types: - # - ptr - # - func - # - iface - # - map - # - chan - - #nlreturn: - # Size of the block (including return statement that is still "OK") - # so no return split required. - # Default: 1 - # block-size: 2 - nolintlint: - # Disable to ensure that all nolint directives actually have an effect. - # Default: false - # allow-unused: true - # Disable to ensure that nolint directives don't have a leading space. - # Default: true - allow-leading-space: false - # Exclude following linters from requiring an explanation. - # Default: [] - # allow-no-explanation: [ ] # Enable to require an explanation of nonzero length after each nolint directive. # Default: false require-explanation: true @@ -984,80 +95,19 @@ linters-settings: # Default: false require-specific: true - # prealloc: - # IMPORTANT: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - - # Report pre-allocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # Default: true - # simple: false - # Report pre-allocation suggestions on range loops. - # Default: true - # range-loops: false - # Report pre-allocation suggestions on for loops. - # Default: false - # for-loops: true - predeclared: - # Comma-separated list of predeclared identifiers to not report on. - # Default: "" - # ignore: "new,int" # Include method names and field names (i.e., qualified names) in checks. # Default: false q: true - # promlinter: - # Promlinter cannot infer all metrics name in static analysis. - # Enable strict mode will also include the errors caused by failing to parse the args. - # Default: false - # strict: true - # Please refer to https://github.com/yeya24/promlinter#usage for detailed usage. - # disabled-linters: - # - Help - # - MetricUnits - # - Counter - # - HistogramSummaryReserved - # - MetricTypeInName - # - ReservedChars - # - CamelCase - # - UnitAbbreviations - revive: - # Maximum number of open files at the same time. - # See https://github.com/mgechev/revive#command-line-flags - # Defaults to unlimited. - # max-open-files: 2048 - - # When set to false, ignores files with "GENERATED" header, similar to golint. - # See https://github.com/mgechev/revive#available-rules for details. - # Default: false - # ignore-generated-header: true - - # Sets the default severity. - # See https://github.com/mgechev/revive#configuration - # Default: warning - # severity: error - - # Enable all available rules. - # Default: false - # enable-all-rules: true - # Sets the default failure confidence. # This means that linting errors with less than 0.8 confidence will be ignored. # Default: 0.8 confidence: 0.8 rules: - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant - # - name: add-constant - # severity: warning - # disabled: false - # arguments: - # - maxLitCount: "3" - # allowStrs: '""' - # allowInts: "0,1,2" - # allowFloats: "0.0,0.,1.0,1.,2.0,2." - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#argument-limit - name: argument-limit severity: warning disabled: false @@ -1066,15 +116,6 @@ linters-settings: - name: atomic severity: warning disabled: false - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#banned-characters - # - name: banned-characters - # severity: warning - # disabled: false - # arguments: ["Ω","Σ","σ", "7"] - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bare-return - # - name: bare-return - # severity: warning - # disabled: false # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#blank-imports - name: blank-imports severity: warning @@ -1087,11 +128,6 @@ linters-settings: - name: call-to-gc severity: warning disabled: false - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity - # - name: cognitive-complexity - # severity: warning - # disabled: false - # arguments: [ 7 ] # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-naming - name: confusing-naming severity: warning @@ -1112,11 +148,6 @@ linters-settings: - name: context-keys-type severity: warning disabled: false - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic - # - name: cyclomatic - # severity: warning - # disabled: false - # arguments: [ 3 ] # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit - name: deep-exit severity: warning @@ -1174,12 +205,6 @@ linters-settings: arguments: - "checkPrivateReceivers" - "sayRepetitiveInsteadOfStutters" - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#file-header - # - name: file-header - # severity: warning - # disabled: false - # arguments: - # - This is the text that must appear at the top of source files. # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter - name: flag-parameter severity: warning @@ -1189,11 +214,6 @@ linters-settings: severity: warning disabled: false arguments: [ 3 ] - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-length - # - name: function-length - # severity: warning - # disabled: false - # arguments: [ 10, 0 ] # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#get-return - name: get-return severity: warning @@ -1214,35 +234,14 @@ linters-settings: - name: indent-error-flow severity: warning disabled: false - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blacklist - # - name: imports-blacklist - # severity: warning - # disabled: false - # arguments: - # - "crypto/md5" - # - "crypto/sha1" # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing - name: import-shadowing severity: warning disabled: false - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit - # - name: line-length-limit - # severity: warning - # disabled: false - # arguments: [ 80 ] - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-public-structs - # - name: max-public-structs - # severity: warning - # disabled: false - # arguments: [ 3 ] # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-parameter - name: modifies-parameter severity: warning disabled: false - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-value-receiver - # - name: modifies-value-receiver - # severity: warning - # disabled: false # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs - name: nested-structs severity: warning @@ -1279,20 +278,6 @@ linters-settings: - name: string-of-int severity: warning disabled: false - # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format - # - name: string-format - # severity: warning - # disabled: false - # arguments: - # - - 'core.WriteError[1].Message' - # - '/^([^A-Z]|$)/' - # - must not start with a capital letter - # - - 'fmt.Errorf[0]' - # - '/(^|[^\.!?])$/' - # - must not end in punctuation - # - - panic - # - '/^[^\n]*$/' - # - must not contain line breaks # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag - name: struct-tag severity: warning @@ -1363,110 +348,11 @@ linters-settings: severity: warning disabled: false - rowserrcheck: - # packages: - # - github.com/jmoiron/sqlx - staticcheck: # https://staticcheck.io/docs/options#checks checks: [ "all" ] - # stylecheck: - # Select the Go version to target. - # Default: 1.13 - # go: "1.15" - # https://staticcheck.io/docs/options#checks - # checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022" ] - # https://staticcheck.io/docs/options#dot_import_whitelist - # dot-import-whitelist: - # - fmt - # https://staticcheck.io/docs/options#initialisms - # initialisms: [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" ] - # https://staticcheck.io/docs/options#http_status_code_whitelist - # http-status-code-whitelist: [ "200", "400", "404", "500" ] - - #tagliatelle: - # Check the struck tag name case. - case: - # Use the struct field name to check the name of the struct tag. - # Default: false - # use-field-name: true - rules: - # Any struct tag type can be used. - # Support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` - #json: goCamel - #yaml: camel - #xml: camel - #hcl: snake - # bson: camel - # avro: snake - # mapstructure: kebab - - tenv: - # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. - # By default, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. - all: true - - testpackage: - # regexp pattern to skip files - # skip-regexp: (export|internal)_test\.go - - #thelper: - # The following configurations enable all checks. - # All checks are enabled by default. - # test: - # first: false - # name: false - # begin: false - # benchmark: - # first: false - # name: false - # begin: false - # tb: - # first: false - # name: false - # begin: false - - unparam: - # Inspect exported functions. - # - # Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - # - # Default: false - # check-exported: true - - varcheck: - # Check usage of exported fields and variables. - # Default: false - # exported-fields: true - varnamelen: - # The longest distance, in source lines, that is being considered a "small scope". - # Variables used in at most this many lines will be ignored. - # Default: 5 - # max-distance: 6 - # The minimum length of a variable's name that is considered "long". - # Variable names that are at least this long will be ignored. - # Default: 3 - # min-name-length: 2 - # Check method receiver names. - # Default: false - # check-receiver: true - # Check named return values. - # Default: false - # check-return: true - # Ignore "ok" variables that hold the bool return value of a type assertion. - # Default: false - # ignore-type-assert-ok: true - # Ignore "ok" variables that hold the bool return value of a map index. - # Default: false - # ignore-map-index-ok: true - # Ignore "ok" variables that hold the bool return value of a channel receive. - # Default: false - # ignore-chan-recv-ok: true # Optional list of variable names that should be ignored completely. # Default: [] ignore-names: @@ -1487,106 +373,7 @@ linters-settings: # - i int # - const C - # whitespace: - # Enforces newlines (or comments) after every multi-line if statement. - # Default: false - # multi-if: true - # Enforces newlines (or comments) after every multi-line function signature. - # Default: false - # multi-func: true - - # wrapcheck: - # An array of strings that specify substrings of signatures to ignore. - # If this set, it will override the default set of ignored signatures. - # See https://github.com/tomarrell/wrapcheck#configuration for more information. - # ignoreSigs: - # - .Errorf( - # - errors.New( - # - errors.Unwrap( - # - .Wrap( - # - .Wrapf( - # - .WithMessage( - # - .WithMessagef( - # - .WithStack( - # ignoreSigRegexps: - # - \.New.*Error\( - # ignorePackageGlobs: - # - encoding/* - # - github.com/pkg/* - - # wsl: - # See https://github.com/bombsimon/wsl/blob/master/doc/configuration.md for documentation of available settings. - # These are the defaults for `golangci-lint`. - - # Controls if you may cuddle assignments and anything without needing an empty line between them. - # Default: false - # allow-assign-and-anything: false - - # Controls if you may cuddle assignments and calls without needing an empty line between them. - # Default: true - # allow-assign-and-call: true - - # Controls if you're allowed to cuddle multiple declarations. - # This is false by default to encourage you to group them in one var block. - # One major benefit with this is that if the variables are assigned the assignments will be tabulated. - # Default: false - # allow-cuddle-declarations: false - - # Controls if you may cuddle assignments even if they span over multiple lines. - # Default: true - # allow-multiline-assign: true - - # This option allows whitespace after each comment group that begins a block. - # Default: false - # allow-separated-leading-comment: false - - # Controls if blocks can end with comments. - # This is not encouraged sine it's usually code smell but might be useful do improve understanding or learning purposes. - # To be allowed there must be no whitespace between the comment and the last statement or the comment and the closing brace. - # Default: false - # allow-trailing-comment: false - - # Can be set to force trailing newlines at the end of case blocks to improve readability. - # If the number of lines (including comments) in a case block exceeds this number - # a linter error will be yielded if the case does not end with a newline. - # Default: 0 - # force-case-trailing-whitespace: 0 - - # Enforces that an `if` statement checking an error variable is cuddled - # with the line that assigned that error variable. - # Default: false - # force-err-cuddling: false - - # Enforces that an assignment which is actually a short declaration (using `:=`) - # is only allowed to cuddle with other short declarations, and not plain assignments, blocks, etc. - # This rule helps make declarations stand out by themselves, much the same as grouping var statement. - # Default: false - # force-short-decl-cuddling: false - - # Controls if the checks for slice append should be "strict" - # in the sense that it will only allow these assignments to be cuddled with variables being appended. - # Default: true - # strict-append: true - - # The custom section can be used to define linter plugins to be loaded at runtime. - # See README documentation for more info. - # custom: - # Each custom linter should have a unique name. - # example: - # The path to the plugin *.so. Can be absolute or local. - # Required for each custom linter. - # path: /path/to/example.so - # The description of the linter. - # Optional. - # description: This is an example usage of a plugin linter. - # Intended to point to the repo location of the linter. - # Optional. - # original-url: github.com/golangci/example-linter - linters: - # Disable all linters. - # Default: false - # disable-all: true # Enable specific linter # https://golangci-lint.run/usage/linters/#enabled-by-default-linters enable: @@ -1596,12 +383,7 @@ linters: - containedctx - contextcheck - copyloopvar - # - cyclop - # - deadcode - # - decorder - # - depguard - dogsled - # - dupl - durationcheck - err113 - errcheck @@ -1609,24 +391,10 @@ linters: - errname - errorlint - exhaustive - # - exhaustivestruct - # - forbidigo - forcetypeassert - # - funlen - # - gci - # - gochecknoglobals - # - gochecknoinits - # - gocognit - goconst - # - gocritic - # - gocyclo - # - godot - # - godox - gofmt - # - gofumpt - # - goheader - goimports - # - golint - gomoddirectives - gomodguard - goprintffuncname @@ -1634,174 +402,32 @@ linters: - gosimple - govet - grouper - # - ifshort - # - importas - ineffassign - # - interfacer - ireturn - # - lll - # - maintidx - makezero - # - maligned - misspell - mnd - nakedret - # - nestif - nilerr - nilnil - # - nlreturn - noctx - nolintlint - paralleltest - # - prealloc - predeclared - # - promlinter - revive - rowserrcheck - # - scopelint - sqlclosecheck - staticcheck - # - structcheck - # - stylecheck - # - tagliatelle - - tenv - # - testpackage - # - thelper - tparallel - typecheck - unconvert - unparam - unused - # - varcheck + - usetesting - varnamelen - wastedassign - # - whitespace - # - wrapcheck - # - wsl - - # Enable all available linters. - # Default: false - # enable-all: true - # Disable specific linter - # https://golangci-lint.run/usage/linters/#disabled-by-default-linters--e--enable - # disable: - # - asciicheck - # - bidichk - # - bodyclose - # - contextcheck - # - cyclop - # - deadcode - # - depguard - # - dogsled - # - dupl - # - durationcheck - # - errcheck - # - errname - # - errorlint - # - exhaustive - # - exhaustivestruct - # - exportloopref - # - forbidigo - # - forcetypeassert - # - funlen - # - gci - # - gochecknoglobals - # - gochecknoinits - # - gocognit - # - goconst - # - gocritic - # - gocyclo - # - godot - # - godox - # - goerr113 - # - gofmt - # - gofumpt - # - goheader - # - goimports - # - golint - # - gomnd - # - gomoddirectives - # - gomodguard - # - goprintffuncname - # - gosec - # - gosimple - # - govet - # - ifshort - # - importas - # - ineffassign - # - interfacer - # - ireturn - # - lll - # - makezero - # - maligned - # - misspell - # - nakedret - # - nestif - # - nilerr - # - nilnil - # - nlreturn - # - noctx - # - nolintlint - # - paralleltest - # - prealloc - # - predeclared - # - promlinter - # - revive - # - rowserrcheck - # - scopelint - # - sqlclosecheck - # - staticcheck - # - structcheck - # - stylecheck - # - tagliatelle - # - tenv - # - testpackage - # - thelper - # - tparallel - # - typecheck - # - unconvert - # - unparam - # - unused - # - varcheck - # - varnamelen - # - wastedassign - # - whitespace - # - wrapcheck - # - wsl - - # Enable presets. - # https://golangci-lint.run/usage/linters - # presets: - # - bugs - # - comment - # - complexity - # - error - # - format - # - import - # - metalinter - # - module - # - performance - # - sql - # - style - # - test - # - unused - - # Run only fast linters from enabled linters set (first run won't be fast) - # Default: false - # fast: true - issues: - # List of regexps of issue texts to exclude. - # - # But independently of this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. - # To list all excluded by default patterns execute `golangci-lint run --help` - # - # Default: [] - # exclude: - # - abcdef - # Excluding configuration per-path, per-linter, per-text and per-source exclude-rules: # False positive: https://github.com/kunwardeep/paralleltest/issues/8. @@ -1809,38 +435,12 @@ issues: - paralleltest text: "does not use range value in test Run" - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via `nolint` comments. - # - path: internal/hmac/ - # text: "weak cryptographic primitive" - # linters: - # - gosec - - # Exclude some `staticcheck` messages. - # - linters: - # - staticcheck - # text: "SA9003:" - - # Exclude `lll` issues for long lines with `go:generate`. - # - linters: - # - lll - # source: "^//go:generate " - # Independently of option `exclude` we use default exclude patterns, # it can be disabled by this option. # To list all excluded by default patterns execute `golangci-lint run --help`. # Default: true. exclude-use-default: false - # If set to true exclude and exclude-rules regular expressions become case-sensitive. - # Default: false - # exclude-case-sensitive: false - - # The list of ids of default excludes to include or disable. - # Default: [] - # include: - # - EXC0002 # disable excluding of issues about comments from golint. - # Maximum issues count per one linter. # Set to 0 to disable. # Default: 50 @@ -1850,50 +450,3 @@ issues: # Set to 0 to disable. # Default: 3 max-same-issues: 0 - - # Show only new issues: if there are unstaged changes or untracked files, - # only those changes are analyzed, else only changes in HEAD~ are analyzed. - # It's a super-useful option for integration of golangci-lint into existing large codebase. - # It's not practical to fix all existing issues at the moment of integration: - # much better don't allow issues in new code. - # - # Default: false. - # new: true - - # Show only new issues created after git revision `REV`. - # new-from-rev: HEAD - - # Show only new issues created in git patch with set file path. - # new-from-patch: path/to/patch/file - - # Fix found issues (if it's supported by the linter). - # fix: true - - -# severity: - # Set the default severity for issues. - # - # If severity rules are defined and the issues do not match or no severity is provided to the rule - # this will be the default severity applied. - # Severities should match the supported severity names of the selected out format. - # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity - # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity - # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message - # - # Default value is an empty string. - # default-severity: error - - # If set to true `severity-rules` regular expressions become case-sensitive. - # Default: false - # case-sensitive: true - - # When a list of severity rules are provided, severity information will be added to lint issues. - # Severity rules have the same filtering capability as exclude rules - # except you are allowed to specify one matcher per severity rule. - # Only affects out formats that support setting severity information. - # - # Default: [] - # rules: - # - linters: - # - dupl - # severity: info diff --git a/application.go b/application.go index 48f63b6..8df2dc2 100644 --- a/application.go +++ b/application.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "strings" ) var ( @@ -16,6 +15,23 @@ var ( // Application is the root definition of a CLI. type Application struct { + // Name is the name of the binary this application builds. Used when + // generating help output. + Name string + + // Version is the version of this application. Used when generating + // help output. + Version string + + // Description is a short, one-line description of the command, used + // when generating help output. + Description string + + // DetailedDescription is one or more paragraphs describing the + // application, what it does, and what it's used for. Used when + // generating help output. + DetailedDescription string + // Commands are the commands that the application supports. Commands []Command @@ -24,6 +40,33 @@ type Application struct { Flags []FlagDef Handler HandlerBuilder + + // HandlerMiddleware holds the middleware to run before executing the + // HandlerBuilder of the Application. If any return false, the + // HandlerBuilder will not be run. The context.Context and *Response + // passed to the middleware will also be passed to the HandlerBuilder + // and Handler; everything else is not guaranteed to persist any + // changes the middleware makes. + // + // Middleware is largely intended to support use cases like checking + // for a help flag being passed and printing the help output, or other + // scenarios where invocation-time information (what command is being + // run, what flags are set) is necessary but all or many handlers + // should have consistent behavior. + HandlerMiddleware []Middleware + + // GlobalMiddleware holds the middleware to run before executing the + // any HandlerBuilder. If any return false, the HandlerBuilder will not + // be run. The context.Context and *Response passed to the middleware + // will also be passed to the HandlerBuilder and Handler; everything + // else is not guaranteed to persist any changes the middleware makes. + // + // Middleware is largely intended to support use cases like checking + // for a help flag being passed and printing the help output, or other + // scenarios where invocation-time information (what command is being + // run, what flags are set) is necessary but all or many handlers + // should have consistent behavior. + GlobalMiddleware []Middleware } // Run executes the invoked command. It routes the input to the appropriate @@ -51,9 +94,66 @@ func (app Application) Run(ctx context.Context, opts ...RunOption) int { return 1 } + for _, middleware := range app.GlobalMiddleware { + if !middleware.Intercept(ctx, result.Flags, result.Args, DefParams{ + CommandPath: result.CommandPath, + Command: result.Command, + AcceptableFlags: result.DefinedFlags, + App: app, + }, resp) { + return resp.Code + } + } + + if result.Command == nil { + if app.Handler == nil { + HelpHandler{ + App: app, + Flags: app.Flags, + }.Handle(ctx, resp) + return resp.Code + } + + for _, middleware := range app.HandlerMiddleware { + if !middleware.Intercept(ctx, result.Flags, result.Args, DefParams{ + CommandPath: result.CommandPath, + Command: result.Command, + AcceptableFlags: result.DefinedFlags, + App: app, + }, resp) { + return resp.Code + } + } + } + if result.Command.Handler == nil { - fmt.Fprintln(resp.Error, "invalid command:", strings.Join(options.Args, " ")) //nolint:errcheck // if there's an error, we can't do anything - return 1 + HelpHandler{ + App: app, + CmdPath: result.CommandPath, + Cmd: result.Command, + Flags: result.DefinedFlags, + }.Handle(ctx, resp) + return resp.Code + } + + for _, middleware := range result.Command.Middleware { + if !middleware.Intercept(ctx, result.Flags, result.Args, DefParams{ + CommandPath: result.CommandPath, + Command: result.Command, + AcceptableFlags: result.DefinedFlags, + App: app, + }, resp) { + return resp.Code + } + } + + if injector, ok := result.Command.Handler.(DefInjector); ok { + injector.InjectDefs(ctx, DefParams{ + CommandPath: result.CommandPath, + Command: result.Command, + AcceptableFlags: result.DefinedFlags, + App: app, + }, resp) } // Build makes us a handler, parsing all the input and injecting it diff --git a/application_test.go b/application_test.go index a9d560d..56c31fb 100644 --- a/application_test.go +++ b/application_test.go @@ -89,18 +89,18 @@ func ExampleApplication() { // output: // this is help information // 0 - // map[quux:[{true hello}]] [] + // map[quux:[{true hello quux quux}]] [] // 0 - // map[quux:[{true hello}]] [] + // map[quux:[{true hello quux quux}]] [] // 0 - // map[quux:[{true hello}]] [] + // map[quux:[{true hello quux quux}]] [] // 0 - // map[quux:[{true hello}]] [] + // map[quux:[{true hello quux quux}]] [] // 0 - // map[quux:[{true hello}]] [] + // map[quux:[{true hello quux quux}]] [] // 0 - // map[quux:[{true hello}]] [] + // map[quux:[{true hello quux quux}]] [] // 0 - // map[baaz:[{false }] quux:[{true hello}]] [] + // map[baaz:[{false baaz baaz}] quux:[{true hello quux quux}]] [] // 0 } diff --git a/command.go b/command.go index 1f775f6..6ea9a45 100644 --- a/command.go +++ b/command.go @@ -33,14 +33,27 @@ type Command struct { Aliases []string // Description is a short, one-line description of the command, used - // when generating the SubcommandsHelp output. + // when generating help output. Description string + // DetailedDescription is one or more paragraphs describing the + // command, what it does, and what it's used for. Used when generating + // help output. + DetailedDescription string + + // UsageExample is an example of how the command is meant to be used, + // displayed as part of help output. + UsageExample string + // Hidden indicates whether a command should be included in // SubcommandsHelp output or not. If set to true, the command will be // omitted from SubcommandsHelp output. Hidden bool + // ArgsAccepted indicates whether free input is expected as part of + // this command. If true, this command cannot have any subcommands. + ArgsAccepted bool + // Flags holds definitions for the flags, if any, that this command // accepts. Flags []FlagDef @@ -54,9 +67,18 @@ type Command struct { // used. Handler HandlerBuilder - // ArgsAccepted indicates whether free input is expected as part of - // this command. If true, this command cannot have any subcommands. - ArgsAccepted bool + // Middleware holds the middleware to run before executing the Command. + // If any return false, the Handler will not be run. The + // context.Context and *Response passed to the middleware will also be + // passed to the HandlerBuilder and Handler; everything else is not + // guaranteed to persist any changes the middleware makes. + // + // Middleware is largely intended to support use cases like checking + // for a help flag being passed and printing the help output, or other + // scenarios where invocation-time information (what command is being + // run, what flags are set) is necessary but all or many handlers + // should have consistent behavior. + Middleware []Middleware } // Validate determines whether a [Command] has a valid definition or not. diff --git a/def_params.go b/def_params.go new file mode 100644 index 0000000..905a81b --- /dev/null +++ b/def_params.go @@ -0,0 +1,13 @@ +package clif + +// DefParams holds a parameter bundle of the definitions that are relevant to a +// given execution. A definition is relevant to a given execution if it's a +// [Command] in the command path of a [Command] that has been routed to, the +// [Command] that has been routed to, the root [Application], or a [FlagDef] +// that is set on any of those [Command]s or the [Application]. +type DefParams struct { + CommandPath []Command + Command *Command + AcceptableFlags []FlagDef + App Application +} diff --git a/flag_def.go b/flag_def.go index 0c3245e..1ccc56f 100644 --- a/flag_def.go +++ b/flag_def.go @@ -12,14 +12,16 @@ import ( type FlagDef struct { // Name is the name of the flag. It's what will be surfaced in // documentation and what the user will use when applying the flag to a - // command. Names must be unique across all commands, or the parser - // won't know which command to apply the flag to. + // command. Names must be unique across the command and all parent or + // child commands, or the parser won't know which command to apply the + // flag to. Name string // Aliases holds any alternative names the flag should accept from the // user. Aliases are not surfaced in documentation, by default. Aliases - // must be unique across all other aliases and names for all commands, - // or the parser won't know which command to apply the flag to. + // must be unique across all other aliases and names for the command + // and all parent or child commands, or the parser won't know which + // command to apply the flag to. Aliases []string // Description is a user-friendly description of what the flag does and diff --git a/flag_reflect_bool.go b/flag_reflect_bool.go index 291d659..cb19ad8 100644 --- a/flag_reflect_bool.go +++ b/flag_reflect_bool.go @@ -8,7 +8,7 @@ import ( func newValueFromBoolean(_ context.Context, flag FlagValue, target reflect.Value) (reflect.Value, error) { value := true - if flag.Set { + if flag.HasValue { parsed, err := strconv.ParseBool(flag.Raw) if err != nil { return target, err diff --git a/flag_reflect_bool_test.go b/flag_reflect_bool_test.go index ee520aa..599bf9a 100644 --- a/flag_reflect_bool_test.go +++ b/flag_reflect_bool_test.go @@ -16,15 +16,15 @@ func TestNewValueFromBoolean_success(t *testing.T) { expected bool } testCases := map[string]testCase{ - "true": {input: FlagValue{Set: true, Raw: "true"}, expected: true}, - "TRUE": {input: FlagValue{Set: true, Raw: "TRUE"}, expected: true}, - "t": {input: FlagValue{Set: true, Raw: "t"}, expected: true}, - "1": {input: FlagValue{Set: true, Raw: "1"}, expected: true}, - "false": {input: FlagValue{Set: true, Raw: "false"}, expected: false}, - "FALSE": {input: FlagValue{Set: true, Raw: "FALSE"}, expected: false}, - "f": {input: FlagValue{Set: true, Raw: "f"}, expected: false}, - "0": {input: FlagValue{Set: true, Raw: "0"}, expected: false}, - "toggle": {input: FlagValue{Set: false, Raw: ""}, expected: true}, + "true": {input: FlagValue{HasValue: true, Raw: "true"}, expected: true}, + "TRUE": {input: FlagValue{HasValue: true, Raw: "TRUE"}, expected: true}, + "t": {input: FlagValue{HasValue: true, Raw: "t"}, expected: true}, + "1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: true}, + "false": {input: FlagValue{HasValue: true, Raw: "false"}, expected: false}, + "FALSE": {input: FlagValue{HasValue: true, Raw: "FALSE"}, expected: false}, + "f": {input: FlagValue{HasValue: true, Raw: "f"}, expected: false}, + "0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: false}, + "toggle": {input: FlagValue{HasValue: false, Raw: ""}, expected: true}, } for name, test := range testCases { t.Run(name, func(t *testing.T) { @@ -61,7 +61,7 @@ func TestNewValueFromBoolean_error(t *testing.T) { ctx := context.Background() var target bool - _, err := newValueFromBoolean(ctx, FlagValue{Set: true, Raw: input}, reflect.ValueOf(target)) + _, err := newValueFromBoolean(ctx, FlagValue{HasValue: true, Raw: input}, reflect.ValueOf(target)) if err == nil { t.Fatal("Expected error, got none") } diff --git a/flag_reflect_number_big_test.go b/flag_reflect_number_big_test.go index a49ec4a..62e1558 100644 --- a/flag_reflect_number_big_test.go +++ b/flag_reflect_number_big_test.go @@ -20,10 +20,10 @@ func TestNewValueFromBigInt_success(t *testing.T) { } testCases := map[string]testCase{ // *big.Int - "*big.Int/0": {input: FlagValue{Set: true, Raw: "0"}, expected: big.NewInt(0), target: big.NewInt(0)}, - "*big.Int/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: big.NewInt(-1), target: big.NewInt(0)}, - "*big.Int/1": {input: FlagValue{Set: true, Raw: "1"}, expected: big.NewInt(1), target: big.NewInt(0)}, - "*big.Int/max-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(math.MaxInt64, 10)}, expected: big.NewInt(math.MaxInt64), target: big.NewInt(0)}, + "*big.Int/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: big.NewInt(0), target: big.NewInt(0)}, + "*big.Int/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: big.NewInt(-1), target: big.NewInt(0)}, + "*big.Int/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: big.NewInt(1), target: big.NewInt(0)}, + "*big.Int/max-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(math.MaxInt64, 10)}, expected: big.NewInt(math.MaxInt64), target: big.NewInt(0)}, } for name, test := range testCases { t.Run(name, func(t *testing.T) { @@ -61,7 +61,7 @@ func TestNewValueFromBigInt_error_syntax(t *testing.T) { ctx := context.Background() var target *big.Int - _, err := newValueFromBigInt(ctx, FlagValue{Set: true, Raw: input}, reflect.ValueOf(target)) + _, err := newValueFromBigInt(ctx, FlagValue{HasValue: true, Raw: input}, reflect.ValueOf(target)) if err == nil { t.Fatal("Expected error, got none") } @@ -69,8 +69,8 @@ func TestNewValueFromBigInt_error_syntax(t *testing.T) { if !errors.As(err, &conversionErr) { t.Fatalf("Expected clif.InvalidConversionError, got %T: %v", err, err) } - if !conversionErr.Source.Set || conversionErr.Source.Raw != input { - t.Fatalf("Expected conversion error's source to be set with a value of %q, got set: %v value: %q", input, conversionErr.Source.Set, conversionErr.Source.Raw) + if !conversionErr.Source.HasValue || conversionErr.Source.Raw != input { + t.Fatalf("Expected conversion error's source to be set with a value of %q, got set: %v value: %q", input, conversionErr.Source.HasValue, conversionErr.Source.Raw) } if !conversionErr.Target.Equal(reflect.ValueOf(target)) { t.Fatalf("Expected conversion error's target to be %v, got %v", target, conversionErr.Target.Interface()) @@ -89,10 +89,10 @@ func TestNewValueFromBigFloat_success(t *testing.T) { } testCases := map[string]testCase{ // *big.Float - "*big.Float/0": {input: FlagValue{Set: true, Raw: "0"}, expected: big.NewFloat(0), target: big.NewFloat(0)}, - "*big.Float/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: big.NewFloat(-1), target: big.NewFloat(0)}, - "*big.Float/1": {input: FlagValue{Set: true, Raw: "1"}, expected: big.NewFloat(1), target: big.NewFloat(0)}, - "*big.Float/max-float": {input: FlagValue{Set: true, Raw: strconv.FormatFloat(math.MaxFloat64, 'f', -1, 64)}, expected: big.NewFloat(math.MaxFloat64), target: big.NewFloat(0)}, + "*big.Float/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: big.NewFloat(0), target: big.NewFloat(0)}, + "*big.Float/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: big.NewFloat(-1), target: big.NewFloat(0)}, + "*big.Float/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: big.NewFloat(1), target: big.NewFloat(0)}, + "*big.Float/max-float": {input: FlagValue{HasValue: true, Raw: strconv.FormatFloat(math.MaxFloat64, 'f', -1, 64)}, expected: big.NewFloat(math.MaxFloat64), target: big.NewFloat(0)}, } for name, test := range testCases { t.Run(name, func(t *testing.T) { @@ -130,7 +130,7 @@ func TestNewValueFromBigFloat_error_syntax(t *testing.T) { ctx := context.Background() var target *big.Float - _, err := newValueFromBigFloat(ctx, FlagValue{Set: true, Raw: input}, reflect.ValueOf(target)) + _, err := newValueFromBigFloat(ctx, FlagValue{HasValue: true, Raw: input}, reflect.ValueOf(target)) if err == nil { t.Fatal("Expected error, got none") } @@ -138,8 +138,8 @@ func TestNewValueFromBigFloat_error_syntax(t *testing.T) { if !errors.As(err, &conversionErr) { t.Fatalf("Expected clif.InvalidConversionError, got %T: %v", err, err) } - if !conversionErr.Source.Set || conversionErr.Source.Raw != input { - t.Fatalf("Expected conversion error's source to be set with a value of %q, got set: %v value: %q", input, conversionErr.Source.Set, conversionErr.Source.Raw) + if !conversionErr.Source.HasValue || conversionErr.Source.Raw != input { + t.Fatalf("Expected conversion error's source to be set with a value of %q, got set: %v value: %q", input, conversionErr.Source.HasValue, conversionErr.Source.Raw) } if !conversionErr.Target.Equal(reflect.ValueOf(target)) { t.Fatalf("Expected conversion error's target to be %v, got %v", target, conversionErr.Target.Interface()) diff --git a/flag_reflect_number_float_test.go b/flag_reflect_number_float_test.go index 6182831..6a7b5ad 100644 --- a/flag_reflect_number_float_test.go +++ b/flag_reflect_number_float_test.go @@ -20,23 +20,23 @@ func TestNewValueFromFloat_success(t *testing.T) { } testCases := map[string]testCase{ // float32 - "float32/0": {input: FlagValue{Set: true, Raw: "0"}, expected: float64(0), target: float32(0)}, - "float32/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: float64(-1), target: float32(0)}, - "float32/1": {input: FlagValue{Set: true, Raw: "1"}, expected: float64(1), target: float32(0)}, - "float32/max-float": {input: FlagValue{Set: true, Raw: strconv.FormatFloat(float64(math.MaxFloat32), 'f', -1, 32)}, expected: math.MaxFloat32, target: float32(0)}, + "float32/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: float64(0), target: float32(0)}, + "float32/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: float64(-1), target: float32(0)}, + "float32/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: float64(1), target: float32(0)}, + "float32/max-float": {input: FlagValue{HasValue: true, Raw: strconv.FormatFloat(float64(math.MaxFloat32), 'f', -1, 32)}, expected: math.MaxFloat32, target: float32(0)}, // float64 - "float64/0": {input: FlagValue{Set: true, Raw: "0"}, expected: float64(0), target: float64(0)}, - "float64/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: float64(-1), target: float64(0)}, - "float64/1": {input: FlagValue{Set: true, Raw: "1"}, expected: float64(1), target: float64(0)}, - "float64/max-float": {input: FlagValue{Set: true, Raw: strconv.FormatFloat(float64(math.MaxFloat64), 'f', -1, 64)}, expected: math.MaxFloat64, target: float64(0)}, - "float64/inf": {input: FlagValue{Set: true, Raw: "inf"}, expected: math.Inf(1), target: float64(0)}, - "float64/infinity": {input: FlagValue{Set: true, Raw: "infinity"}, expected: math.Inf(1), target: float64(0)}, - "float64/-inf": {input: FlagValue{Set: true, Raw: "-inf"}, expected: math.Inf(-1), target: float64(0)}, - "float64/-infinity": {input: FlagValue{Set: true, Raw: "-infinity"}, expected: math.Inf(-1), target: float64(0)}, - "float64/+inf": {input: FlagValue{Set: true, Raw: "+inf"}, expected: math.Inf(1), target: float64(0)}, - "float64/+infinity": {input: FlagValue{Set: true, Raw: "+infinity"}, expected: math.Inf(1), target: float64(0)}, - "float64/nan": {input: FlagValue{Set: true, Raw: "NaN"}, expected: math.NaN(), target: float64(0)}, + "float64/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: float64(0), target: float64(0)}, + "float64/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: float64(-1), target: float64(0)}, + "float64/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: float64(1), target: float64(0)}, + "float64/max-float": {input: FlagValue{HasValue: true, Raw: strconv.FormatFloat(float64(math.MaxFloat64), 'f', -1, 64)}, expected: math.MaxFloat64, target: float64(0)}, + "float64/inf": {input: FlagValue{HasValue: true, Raw: "inf"}, expected: math.Inf(1), target: float64(0)}, + "float64/infinity": {input: FlagValue{HasValue: true, Raw: "infinity"}, expected: math.Inf(1), target: float64(0)}, + "float64/-inf": {input: FlagValue{HasValue: true, Raw: "-inf"}, expected: math.Inf(-1), target: float64(0)}, + "float64/-infinity": {input: FlagValue{HasValue: true, Raw: "-infinity"}, expected: math.Inf(-1), target: float64(0)}, + "float64/+inf": {input: FlagValue{HasValue: true, Raw: "+inf"}, expected: math.Inf(1), target: float64(0)}, + "float64/+infinity": {input: FlagValue{HasValue: true, Raw: "+infinity"}, expected: math.Inf(1), target: float64(0)}, + "float64/nan": {input: FlagValue{HasValue: true, Raw: "NaN"}, expected: math.NaN(), target: float64(0)}, } for name, test := range testCases { t.Run(name, func(t *testing.T) { @@ -80,7 +80,7 @@ func TestNewValueFromFloat_error_syntax(t *testing.T) { ctx := context.Background() var target float64 - _, err := newValueFromFloat(ctx, FlagValue{Set: true, Raw: input}, reflect.ValueOf(target)) + _, err := newValueFromFloat(ctx, FlagValue{HasValue: true, Raw: input}, reflect.ValueOf(target)) if err == nil { t.Fatal("Expected error, got none") } @@ -120,7 +120,7 @@ func TestNewValueFromFloat_error_overflow(t *testing.T) { ctx := context.Background() - _, err := newValueFromFloat(ctx, FlagValue{Set: true, Raw: test.value.String()}, reflect.ValueOf(test.target)) + _, err := newValueFromFloat(ctx, FlagValue{HasValue: true, Raw: test.value.String()}, reflect.ValueOf(test.target)) if err == nil { t.Fatal("Expected error, got none") } diff --git a/flag_reflect_number_int_test.go b/flag_reflect_number_int_test.go index aa00b7a..98b6f21 100644 --- a/flag_reflect_number_int_test.go +++ b/flag_reflect_number_int_test.go @@ -20,39 +20,39 @@ func TestNewValueFromInt_success(t *testing.T) { } testCases := map[string]testCase{ // int - "int/0": {input: FlagValue{Set: true, Raw: "0"}, expected: int64(0), target: int(0)}, - "int/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: int64(-1), target: int(0)}, - "int/1": {input: FlagValue{Set: true, Raw: "1"}, expected: int64(1), target: int(0)}, - "int/max-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MaxInt), 10)}, expected: math.MaxInt, target: int(0)}, - "int/min-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MinInt), 10)}, expected: math.MinInt, target: int(0)}, + "int/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: int64(0), target: int(0)}, + "int/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: int64(-1), target: int(0)}, + "int/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: int64(1), target: int(0)}, + "int/max-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MaxInt), 10)}, expected: math.MaxInt, target: int(0)}, + "int/min-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MinInt), 10)}, expected: math.MinInt, target: int(0)}, // int8 - "int8/0": {input: FlagValue{Set: true, Raw: "0"}, expected: int64(0), target: int8(0)}, - "int8/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: int64(-1), target: int8(0)}, - "int8/1": {input: FlagValue{Set: true, Raw: "1"}, expected: int64(1), target: int8(0)}, - "int8/max-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MaxInt8), 10)}, expected: math.MaxInt8, target: int8(0)}, - "int8/min-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MinInt8), 10)}, expected: math.MinInt8, target: int8(0)}, + "int8/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: int64(0), target: int8(0)}, + "int8/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: int64(-1), target: int8(0)}, + "int8/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: int64(1), target: int8(0)}, + "int8/max-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MaxInt8), 10)}, expected: math.MaxInt8, target: int8(0)}, + "int8/min-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MinInt8), 10)}, expected: math.MinInt8, target: int8(0)}, // int16 - "int16/0": {input: FlagValue{Set: true, Raw: "0"}, expected: int64(0), target: int16(0)}, - "int16/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: int64(-1), target: int16(0)}, - "int16/1": {input: FlagValue{Set: true, Raw: "1"}, expected: int64(1), target: int16(0)}, - "int16/max-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MaxInt16), 10)}, expected: math.MaxInt16, target: int16(0)}, - "int16/min-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MinInt16), 10)}, expected: math.MinInt16, target: int16(0)}, + "int16/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: int64(0), target: int16(0)}, + "int16/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: int64(-1), target: int16(0)}, + "int16/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: int64(1), target: int16(0)}, + "int16/max-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MaxInt16), 10)}, expected: math.MaxInt16, target: int16(0)}, + "int16/min-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MinInt16), 10)}, expected: math.MinInt16, target: int16(0)}, // int32 - "int32/0": {input: FlagValue{Set: true, Raw: "0"}, expected: int64(0), target: int32(0)}, - "int32/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: int64(-1), target: int32(0)}, - "int32/1": {input: FlagValue{Set: true, Raw: "1"}, expected: int64(1), target: int32(0)}, - "int32/max-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MaxInt32), 10)}, expected: math.MaxInt32, target: int32(0)}, - "int32/min-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MinInt32), 10)}, expected: math.MinInt32, target: int32(0)}, + "int32/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: int64(0), target: int32(0)}, + "int32/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: int64(-1), target: int32(0)}, + "int32/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: int64(1), target: int32(0)}, + "int32/max-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MaxInt32), 10)}, expected: math.MaxInt32, target: int32(0)}, + "int32/min-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MinInt32), 10)}, expected: math.MinInt32, target: int32(0)}, // int64 - "int64/0": {input: FlagValue{Set: true, Raw: "0"}, expected: int64(0), target: int64(0)}, - "int64/-1": {input: FlagValue{Set: true, Raw: "-1"}, expected: int64(-1), target: int64(0)}, - "int64/1": {input: FlagValue{Set: true, Raw: "1"}, expected: int64(1), target: int64(0)}, - "int64/max-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MaxInt64), 10)}, expected: math.MaxInt64, target: int64(0)}, - "int64/min-int": {input: FlagValue{Set: true, Raw: strconv.FormatInt(int64(math.MinInt64), 10)}, expected: math.MinInt64, target: int64(0)}, + "int64/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: int64(0), target: int64(0)}, + "int64/-1": {input: FlagValue{HasValue: true, Raw: "-1"}, expected: int64(-1), target: int64(0)}, + "int64/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: int64(1), target: int64(0)}, + "int64/max-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MaxInt64), 10)}, expected: math.MaxInt64, target: int64(0)}, + "int64/min-int": {input: FlagValue{HasValue: true, Raw: strconv.FormatInt(int64(math.MinInt64), 10)}, expected: math.MinInt64, target: int64(0)}, } for name, test := range testCases { t.Run(name, func(t *testing.T) { @@ -88,7 +88,7 @@ func TestNewValueFromInt_error_syntax(t *testing.T) { ctx := context.Background() var target int - _, err := newValueFromInt(ctx, FlagValue{Set: true, Raw: input}, reflect.ValueOf(target)) + _, err := newValueFromInt(ctx, FlagValue{HasValue: true, Raw: input}, reflect.ValueOf(target)) if err == nil { t.Fatal("Expected error, got none") } @@ -160,7 +160,7 @@ func TestNewValueFromInt_error_overflow(t *testing.T) { ctx := context.Background() - _, err := newValueFromInt(ctx, FlagValue{Set: true, Raw: test.value.String()}, reflect.ValueOf(test.target)) + _, err := newValueFromInt(ctx, FlagValue{HasValue: true, Raw: test.value.String()}, reflect.ValueOf(test.target)) if err == nil { t.Fatal("Expected error, got none") } diff --git a/flag_reflect_number_uint_test.go b/flag_reflect_number_uint_test.go index 8beb2d4..ea5f003 100644 --- a/flag_reflect_number_uint_test.go +++ b/flag_reflect_number_uint_test.go @@ -20,29 +20,29 @@ func TestNewValueFromUint_success(t *testing.T) { } testCases := map[string]testCase{ // uint - "uint/0": {input: FlagValue{Set: true, Raw: "0"}, expected: uint64(0), target: uint(0)}, - "uint/1": {input: FlagValue{Set: true, Raw: "1"}, expected: uint64(1), target: uint(0)}, - "uint/max-uint": {input: FlagValue{Set: true, Raw: strconv.FormatUint(uint64(math.MaxUint), 10)}, expected: math.MaxUint, target: uint(0)}, + "uint/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: uint64(0), target: uint(0)}, + "uint/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: uint64(1), target: uint(0)}, + "uint/max-uint": {input: FlagValue{HasValue: true, Raw: strconv.FormatUint(uint64(math.MaxUint), 10)}, expected: math.MaxUint, target: uint(0)}, // uint8 - "uint8/0": {input: FlagValue{Set: true, Raw: "0"}, expected: uint64(0), target: uint8(0)}, - "uint8/1": {input: FlagValue{Set: true, Raw: "1"}, expected: uint64(1), target: uint8(0)}, - "uint8/max-uint": {input: FlagValue{Set: true, Raw: strconv.FormatUint(uint64(math.MaxUint8), 10)}, expected: math.MaxUint8, target: uint8(0)}, + "uint8/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: uint64(0), target: uint8(0)}, + "uint8/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: uint64(1), target: uint8(0)}, + "uint8/max-uint": {input: FlagValue{HasValue: true, Raw: strconv.FormatUint(uint64(math.MaxUint8), 10)}, expected: math.MaxUint8, target: uint8(0)}, // uint16 - "uint16/0": {input: FlagValue{Set: true, Raw: "0"}, expected: uint64(0), target: uint16(0)}, - "uint16/1": {input: FlagValue{Set: true, Raw: "1"}, expected: uint64(1), target: uint16(0)}, - "uint16/max-uint": {input: FlagValue{Set: true, Raw: strconv.FormatUint(uint64(math.MaxUint16), 10)}, expected: math.MaxUint16, target: uint16(0)}, + "uint16/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: uint64(0), target: uint16(0)}, + "uint16/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: uint64(1), target: uint16(0)}, + "uint16/max-uint": {input: FlagValue{HasValue: true, Raw: strconv.FormatUint(uint64(math.MaxUint16), 10)}, expected: math.MaxUint16, target: uint16(0)}, // uint32 - "uint32/0": {input: FlagValue{Set: true, Raw: "0"}, expected: uint64(0), target: uint32(0)}, - "uint32/1": {input: FlagValue{Set: true, Raw: "1"}, expected: uint64(1), target: uint32(0)}, - "uint32/max-uint": {input: FlagValue{Set: true, Raw: strconv.FormatUint(uint64(math.MaxUint32), 10)}, expected: math.MaxUint32, target: uint32(0)}, + "uint32/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: uint64(0), target: uint32(0)}, + "uint32/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: uint64(1), target: uint32(0)}, + "uint32/max-uint": {input: FlagValue{HasValue: true, Raw: strconv.FormatUint(uint64(math.MaxUint32), 10)}, expected: math.MaxUint32, target: uint32(0)}, // uint64 - "uint64/0": {input: FlagValue{Set: true, Raw: "0"}, expected: uint64(0), target: uint64(0)}, - "uint64/1": {input: FlagValue{Set: true, Raw: "1"}, expected: uint64(1), target: uint64(0)}, - "uint64/max-uint": {input: FlagValue{Set: true, Raw: strconv.FormatUint(uint64(math.MaxUint64), 10)}, expected: math.MaxUint64, target: uint64(0)}, + "uint64/0": {input: FlagValue{HasValue: true, Raw: "0"}, expected: uint64(0), target: uint64(0)}, + "uint64/1": {input: FlagValue{HasValue: true, Raw: "1"}, expected: uint64(1), target: uint64(0)}, + "uint64/max-uint": {input: FlagValue{HasValue: true, Raw: strconv.FormatUint(uint64(math.MaxUint64), 10)}, expected: math.MaxUint64, target: uint64(0)}, } for name, test := range testCases { t.Run(name, func(t *testing.T) { @@ -79,7 +79,7 @@ func TestNewValueFromUint_error_syntax(t *testing.T) { ctx := context.Background() var target uint - _, err := newValueFromUint(ctx, FlagValue{Set: true, Raw: input}, reflect.ValueOf(target)) + _, err := newValueFromUint(ctx, FlagValue{HasValue: true, Raw: input}, reflect.ValueOf(target)) if err == nil { t.Fatal("Expected error, got none") } @@ -131,7 +131,7 @@ func TestNewValueFromUint_error_overflow(t *testing.T) { ctx := context.Background() - _, err := newValueFromUint(ctx, FlagValue{Set: true, Raw: test.value.String()}, reflect.ValueOf(test.target)) + _, err := newValueFromUint(ctx, FlagValue{HasValue: true, Raw: test.value.String()}, reflect.ValueOf(test.target)) if err == nil { t.Fatal("Expected error, got none") } diff --git a/flag_reflect_time.go b/flag_reflect_time.go index 888982b..62da3ad 100644 --- a/flag_reflect_time.go +++ b/flag_reflect_time.go @@ -10,7 +10,7 @@ import ( func newValueFromTime(_ context.Context, flag FlagValue, target reflect.Value) (reflect.Value, error) { value := time.Time{} - if flag.Set { + if flag.HasValue { parsed, err := dateparser.Parse(nil, flag.Raw) if err != nil { return target, err diff --git a/flag_reflect_time_test.go b/flag_reflect_time_test.go index 1cfb092..9942ad4 100644 --- a/flag_reflect_time_test.go +++ b/flag_reflect_time_test.go @@ -15,9 +15,9 @@ func TestNewValueFromTime_success(t *testing.T) { expected time.Time } testCases := map[string]testCase{ - "6 July 2020": {input: FlagValue{Set: true, Raw: "6 July 2020"}, expected: time.Date(2020, time.July, 6, 0, 0, 0, 0, time.UTC)}, - "07/06/2020": {input: FlagValue{Set: true, Raw: "07/06/2020"}, expected: time.Date(2020, time.July, 6, 0, 0, 0, 0, time.UTC)}, - "07/06/20": {input: FlagValue{Set: true, Raw: "07/06/20"}, expected: time.Date(2020, time.July, 6, 0, 0, 0, 0, time.UTC)}, + "6 July 2020": {input: FlagValue{HasValue: true, Raw: "6 July 2020"}, expected: time.Date(2020, time.July, 6, 0, 0, 0, 0, time.UTC)}, + "07/06/2020": {input: FlagValue{HasValue: true, Raw: "07/06/2020"}, expected: time.Date(2020, time.July, 6, 0, 0, 0, 0, time.UTC)}, + "07/06/20": {input: FlagValue{HasValue: true, Raw: "07/06/20"}, expected: time.Date(2020, time.July, 6, 0, 0, 0, 0, time.UTC)}, } for name, test := range testCases { t.Run(name, func(t *testing.T) { @@ -57,7 +57,7 @@ func TestNewValueFromTime_error(t *testing.T) { ctx := context.Background() var target bool - _, err := newValueFromTime(ctx, FlagValue{Set: true, Raw: input}, reflect.ValueOf(target)) + _, err := newValueFromTime(ctx, FlagValue{HasValue: true, Raw: input}, reflect.ValueOf(target)) if err == nil { t.Fatal("Expected error, got none") } diff --git a/flag_set.go b/flag_set.go index 86b3be1..3d4b90c 100644 --- a/flag_set.go +++ b/flag_set.go @@ -19,10 +19,11 @@ type FlagSet map[string]FlagValues // the types supported by [FlagValues.As]. // // If a pointer to a struct, the `flag` struct tag will control which fields a -// flag key parses into. Unexported struct fields cannot be parsed into. All -// exported struct fields must have a `flag` struct tag; use "-" as a struct -// tag to not parse into a field. Field types can be any type supported by -// [FlagValues.As]. +// flag key parses into. The value of the tag should be the canonical name +// (i.e., not an alias) of the flag that should be parsed into that field. +// Unexported struct fields cannot be parsed into. All exported struct fields +// must have a `flag` struct tag; use "-" as a struct tag to not parse into a +// field. Field types can be any type supported by [FlagValues.As]. // // If a pointer to a type that implements [FlagSetSetter], the `SetFromFlagSet` // method will be called and the [FlagSet] will be passed. diff --git a/flag_value.go b/flag_value.go index b1549cb..abc2b49 100644 --- a/flag_value.go +++ b/flag_value.go @@ -7,14 +7,21 @@ import ( // FlagValue holds the value of a flag specified at runtime. type FlagValue struct { - // Set indicates whether the flag had a value set. If false, it + // HasValue indicates whether the flag had a value set. If false, it // indicates that the flag was used as a toggle, without a value, i.e. // --flag. If true, it indicates the flag was used with a value, i.e. // --flag=value or --flag value. - Set bool + HasValue bool // Raw holds the value the flag was given, if Set is true. Raw string + + // Key is the key used to invoke the flag, which could be an alias. + Key string + + // CanonicalKey is the Name of the flag in the FlagDef, the canonical + // way to refer to the flag. + CanonicalKey string } // As parses the value of the [FlagValue] into the target, which must be a diff --git a/fprintf_or_panic.go b/fprintf_or_panic.go new file mode 100644 index 0000000..9fb828e --- /dev/null +++ b/fprintf_or_panic.go @@ -0,0 +1,20 @@ +package clif + +import ( + "fmt" + "io" +) + +func fprintOrPanic(w io.Writer, a ...any) { + _, err := fmt.Fprint(w, a...) + if err != nil { + panic(fmt.Sprintf("Error writing response: %s", err)) + } +} + +func fprintlnOrPanic(w io.Writer, a ...any) { + _, err := fmt.Fprintln(w, a...) + if err != nil { + panic(fmt.Sprintf("Error writing response: %s", err)) + } +} diff --git a/go.mod b/go.mod index 31ccf09..92034b2 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.3 require ( github.com/google/go-cmp v0.7.0 github.com/markusmobius/go-dateparser v1.2.3 + github.com/mitchellh/go-wordwrap v1.0.1 ) require ( diff --git a/go.sum b/go.sum index 8d1a82e..77b681a 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw= github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/handler.go b/handler.go index c95f86a..73341ba 100644 --- a/handler.go +++ b/handler.go @@ -21,3 +21,12 @@ type HandlerBuilder interface { // appropriate handler type. Build(ctx context.Context, flags FlagSet, args []string, resp *Response) Handler } + +// DefInjector is an interface that a [HandlerBuilder] can optionally +// implement. If implemented, the InjectDefs method will be called to make +// the [DefParams] available at runtime to the [HandlerBuilder] before +// [HandlerBuilder.Build] is called. +type DefInjector interface { + HandlerBuilder + InjectDefs(ctx context.Context, defs DefParams, resp *Response) +} diff --git a/help.go b/help.go new file mode 100644 index 0000000..44015bb --- /dev/null +++ b/help.go @@ -0,0 +1,230 @@ +package clif + +import ( + "context" + "strings" + + "github.com/mitchellh/go-wordwrap" +) + +// HelpMiddleware provides a middleware that, when any of the specified flags +// are passed, will print the help output and prevent any other [Handler] or +// [Middleware] from executing. +type HelpMiddleware struct { + Flags []string +} + +func (middleware HelpMiddleware) Intercept(ctx context.Context, flags FlagSet, _ []string, defs DefParams, resp *Response) bool { //nolint:revive // that's the way interfaces go sometimes, this really needs 5 arguments + var showHelp bool + for _, flag := range middleware.Flags { + helpFlag, ok := flags[flag] + if !ok { + continue + } + err := helpFlag.As(ctx, &showHelp) + if err != nil { + fprintlnOrPanic(resp.Error, "Error checking if the", helpFlag, "flag is set:", err.Error()) + resp.Code = 1 + return false + } + if showHelp { + break + } + } + if !showHelp { + return true + } + HelpHandler{ + App: defs.App, + CmdPath: defs.CommandPath, + Cmd: defs.Command, + Flags: defs.AcceptableFlags, + }.Handle(ctx, resp) + return false +} + +// HelpHandler is a [Handler] that writes help output to the [Response.Output]. +// The help output is not guaranteed to be stable between versions of clif. +type HelpHandler struct { + App Application + CmdPath []Command + Cmd *Command + Flags []FlagDef +} + +// Handle is a method that will be called when the command is executed. +// It should contain the business logic of the command. +func (handler HelpHandler) Handle(ctx context.Context, resp *Response) { + if handler.Cmd == nil { + handler.applicationHelp(ctx, resp) + return + } + handler.commandHelp(ctx, resp) +} + +func (handler HelpHandler) applicationHelp(_ context.Context, resp *Response) { + fprintOrPanic(resp.Output, handler.App.Name) + if handler.App.Version != "" { + fprintOrPanic(resp.Output, " ", handler.App.Version) + } + if len(handler.App.Description) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, wrapAndPad(handler.App.Description)) + } + if len(handler.App.DetailedDescription) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "DESCRIPTION") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, wrapAndPad(handler.App.DetailedDescription)) + } + var commands []Command + for _, cmd := range handler.App.Commands { + if cmd.Hidden { + continue + } + commands = append(commands, cmd) + } + if len(commands) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "COMMANDS") + fprintOrPanic(resp.Output, "\n") + var commandTable [][2]string + for _, cmd := range commands { + commandTable = append(commandTable, [2]string{cmd.Name, cmd.Description}) + } + fprintOrPanic(resp.Output, makeWrappedAndPaddedTable(commandTable)) + } + if len(handler.Flags) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "FLAGS") + fprintOrPanic(resp.Output, "\n") + var flagTable [][2]string + for _, flag := range handler.Flags { + flagTable = append(flagTable, [2]string{flag.Name, flag.Description}) + } + fprintOrPanic(resp.Output, makeWrappedAndPaddedTable(flagTable)) + } + fprintOrPanic(resp.Output, "\n") +} + +func (handler HelpHandler) commandHelp(_ context.Context, resp *Response) { + if handler.App.Name != "" { + fprintOrPanic(resp.Output, handler.App.Name, " ") + } + for pos, cmd := range handler.CmdPath { + fprintOrPanic(resp.Output, cmd.Name) + if len(handler.CmdPath) > pos+1 { + fprintOrPanic(resp.Output, " ") + } + } + if len(handler.Cmd.Description) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, wrapAndPad(handler.Cmd.Description)) + } + if len(handler.Cmd.UsageExample) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "USAGE") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, wrapAndPad(handler.Cmd.UsageExample)) + } + if len(handler.Cmd.DetailedDescription) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "DESCRIPTION") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, wrapAndPad(handler.Cmd.DetailedDescription)) + } + var commands []Command + for _, cmd := range handler.Cmd.Subcommands { + if cmd.Hidden { + continue + } + commands = append(commands, cmd) + } + if len(commands) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "SUBCOMMANDS") + fprintOrPanic(resp.Output, "\n") + var commandTable [][2]string + for _, cmd := range commands { + commandTable = append(commandTable, [2]string{cmd.Name, cmd.Description}) + } + fprintOrPanic(resp.Output, makeWrappedAndPaddedTable(commandTable)) + } + if len(handler.Flags) > 0 { + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "\n") + fprintOrPanic(resp.Output, "FLAGS") + fprintOrPanic(resp.Output, "\n") + var flagTable [][2]string + for _, flag := range handler.Flags { + flagTable = append(flagTable, [2]string{flag.Name, flag.Description}) + } + fprintOrPanic(resp.Output, makeWrappedAndPaddedTable(flagTable)) + } + fprintOrPanic(resp.Output, "\n") +} + +func wrapAndPad(input string) string { + pad := " " + limit := uint(80) //nolint:mnd // 80 is a legitimate magic number, because terminals are weird + if uint(len(pad)) > limit { + panic("padding exceeds limit") + } + limit = limit - uint(len(pad)) + wrapped := wordwrap.WrapString(input, limit) + padded := strings.ReplaceAll(wrapped, "\n", "\n"+pad) + // strip the trailing pad + padded = strings.TrimSuffix(padded, pad) + // make sure the first line gets padded + padded = pad + padded + return padded +} + +func makeWrappedAndPaddedTable(input [][2]string) string { + pad := " " + limit := uint(80) //nolint:mnd // 80 is a legitimate magic number, because terminals are weird + if uint(len(pad)) > limit { + panic("padding exceeds limit") + } + limit = limit - uint(len(pad)) + var maxCol1 int + for _, row := range input { + if len(row[0]) > maxCol1 { + maxCol1 = len(row[0]) + } + } + if uint(maxCol1+len(pad)+len(pad)) >= limit { //nolint:gosec // len() can't return a negative integer + panic("padding and first column exceeds limit") + } + limit = limit - uint(maxCol1+len(pad)+len(pad)) //nolint:gosec // len() can't return a negative integer + var result strings.Builder + for _, row := range input { + result.WriteString(pad) + result.WriteString(row[0]) + for range maxCol1 - len(row[0]) { + result.WriteString(" ") + } + wrapped := wordwrap.WrapString(row[1], limit) + wrappedLines := strings.Split(wrapped, "\n") + for pos, line := range wrappedLines { + if pos != 0 { + result.WriteString(pad) + for range maxCol1 { + result.WriteString(" ") + } + } + result.WriteString(" ") + result.WriteString(line) + result.WriteString("\n") + } + } + return strings.TrimSuffix(result.String(), "\n") +} diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..1ed7e7b --- /dev/null +++ b/middleware.go @@ -0,0 +1,20 @@ +package clif + +import ( + "context" +) + +// Middleware provides an interface for specifying code to run after the +// appropriate [Handler] to execute is identified but before the [Handler] is +// executed. +type Middleware interface { + // Intercept is the code that will run when the Middleware is executed. + // All the runtime parameters available to a Handler are available, + // along with DefParams describing the Application, the Command to + // execute (if any), the parents of that Command (if any), and the + // acceptable flags for that particular Command. + // + // If Intercept returns false, execution will halt and the Handler and + // any other Middleware will not be executed. + Intercept(ctx context.Context, flags FlagSet, args []string, defs DefParams, resp *Response) bool +} diff --git a/route.go b/route.go index 9932fe6..d1d9c77 100644 --- a/route.go +++ b/route.go @@ -10,10 +10,23 @@ import ( // [FlagSet] and arguments to pass to it, based on the parsing done by // [Route]. type RouteResult struct { - // Command is the Command that Route believes should be run. - Command Command + // Command is the Command that Route believes should be run. If nil, no + // command was passed and the top-level Application handler should be + // called. A nil value does not indicate a command was specified but + // couldn't be found; that will be an explicit error. + Command *Command + + // CommandPath is the list of Commands that were invoked to reach + // Command, including Command itself. + CommandPath []Command + // Flags are the flags that should be applied to that command. Flags FlagSet + + // DefinedFlags are the FlagDefs that are defined along the + // CommandPath. + DefinedFlags []FlagDef + // Args are the positional arguments that should be passed to that // command. Args []string @@ -59,7 +72,11 @@ func Route(ctx context.Context, app Application, input []string) (RouteResult, e router := inputRouter{ // a stub command to start our command tree off, just the // top-level subcommands of the CLI itself - cmd: Command{ + cmd: &Command{ + // give this command an invalid name so we can detect + // if it's still the active command after router.route + // runs + Name: "--placeholder--", Subcommands: app.Commands, }, // gotta initialize the flags map so we don't panic on write @@ -72,18 +89,27 @@ func Route(ctx context.Context, app Application, input []string) (RouteResult, e return RouteResult{}, err } + if router.cmd != nil && router.cmd.Name == "--placeholder--" { + router.cmd = nil + } + // build a map of flags the commands we visited actually accept so we // can validate the passed flags are acceptable acceptedFlags := map[string]FlagDef{} + var cmdPath []Command + var resultFlagDefs []FlagDef for _, def := range app.Flags { acceptedFlags[def.Name] = def + resultFlagDefs = append(resultFlagDefs, def) for _, alias := range def.Aliases { acceptedFlags[alias] = def } } for _, entry := range router.path { + cmdPath = append(cmdPath, entry.cmd) for _, def := range entry.cmd.Flags { acceptedFlags[def.Name] = def + resultFlagDefs = append(resultFlagDefs, def) for _, alias := range def.Aliases { acceptedFlags[alias] = def } @@ -100,10 +126,13 @@ func Route(ctx context.Context, app Application, input []string) (RouteResult, e // a problem return RouteResult{}, UnknownFlagNameError(flag) } - flagWithoutLeadingHypens := strings.TrimPrefix(strings.TrimPrefix(flag, "-"), "-") + flagWithoutLeadingHyphens := strings.TrimPrefix(strings.TrimPrefix(flag, "-"), "-") + nameWithoutLeadingHyphens := strings.TrimPrefix(strings.TrimPrefix(def.Name, "-"), "-") for _, value := range values { val := FlagValue{ - Set: value != nil, + HasValue: value != nil, + Key: flagWithoutLeadingHyphens, + CanonicalKey: nameWithoutLeadingHyphens, } if value != nil { // if we have a non-nil value but this flag can @@ -120,9 +149,9 @@ func Route(ctx context.Context, app Application, input []string) (RouteResult, e // be used as a toggle, that's a problem return RouteResult{}, MissingFlagValueError(flag) } - flagValues[flagWithoutLeadingHypens] = append(flagValues[flagWithoutLeadingHypens], val) + flagValues[nameWithoutLeadingHyphens] = append(flagValues[nameWithoutLeadingHyphens], val) } - if len(flagValues[flagWithoutLeadingHypens]) > 1 && !def.AllowMultiple { + if len(flagValues[nameWithoutLeadingHyphens]) > 1 && !def.AllowMultiple { // if we have more than one value but only accept a // single value, that's a problem return RouteResult{}, TooManyFlagValuesError(flag) @@ -152,8 +181,9 @@ func Route(ctx context.Context, app Application, input []string) (RouteResult, e envVal := os.Getenv(envVar) if envVal != "" { flagValues[keyWithoutLeadingHyphens] = append(flagValues[keyWithoutLeadingHyphens], FlagValue{ - Set: true, - Raw: envVal, + HasValue: true, + Raw: envVal, + CanonicalKey: strings.TrimPrefix(strings.TrimPrefix(def.Name, "-"), "-"), }) break } @@ -171,9 +201,11 @@ func Route(ctx context.Context, app Application, input []string) (RouteResult, e } return RouteResult{ - Command: router.cmd, - Flags: flagValues, - Args: router.args, + Command: router.cmd, + CommandPath: cmdPath, + Flags: flagValues, + DefinedFlags: resultFlagDefs, + Args: router.args, }, nil } diff --git a/route_test.go b/route_test.go index 1f61323..a6a9ee6 100644 --- a/route_test.go +++ b/route_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "impractical.co/clif" ) @@ -33,9 +34,9 @@ func TestRoute(t *testing.T) { expectedCmdName: "hello", expectedFlags: clif.FlagSet{ "name": { - {Set: true, Raw: "foo"}, - {Set: true, Raw: "bar"}, - {Set: true, Raw: "baaz"}, + {HasValue: true, Raw: "foo", Key: "name", CanonicalKey: "name"}, + {HasValue: true, Raw: "bar", Key: "name", CanonicalKey: "name"}, + {HasValue: true, Raw: "baaz", Key: "name", CanonicalKey: "name"}, }, }, }, diff --git a/router.go b/router.go index 62903f3..a71d280 100644 --- a/router.go +++ b/router.go @@ -12,7 +12,7 @@ type commandPathEntry struct { } type inputRouter struct { - cmd Command + cmd *Command flags map[string][]*string args []string path []commandPathEntry @@ -27,7 +27,7 @@ func (router *inputRouter) route(ctx context.Context) error { if err != nil { return err } - router.cmd = sub + router.cmd = &sub router.path = append(router.path, commandPathEntry{ cmd: sub, name: token.value, diff --git a/version.go b/version.go new file mode 100644 index 0000000..0dad8a3 --- /dev/null +++ b/version.go @@ -0,0 +1,36 @@ +package clif + +import ( + "context" +) + +// VersionMiddleware provides a middleware that, when any of the specified +// flags are passed, will print the version output and prevent any other +// [Handler] or [Middleware] from executing. +type VersionMiddleware struct { + Flags []string +} + +func (middleware VersionMiddleware) Intercept(ctx context.Context, flags FlagSet, _ []string, defs DefParams, resp *Response) bool { //nolint:revive // that's the way interfaces go sometimes, this really needs 5 arguments + var showVersion bool + for _, flag := range middleware.Flags { + versionFlag, ok := flags[flag] + if !ok { + continue + } + err := versionFlag.As(ctx, &showVersion) + if err != nil { + fprintlnOrPanic(resp.Error, "Error checking if the", versionFlag, "flag is set:", err.Error()) + resp.Code = 1 + return false + } + if showVersion { + break + } + } + if !showVersion { + return true + } + fprintlnOrPanic(resp.Output, defs.App.Name, defs.App.Version) + return false +}