diff --git a/go.mod b/go.mod index 984bdbb1da..230bee3751 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/bmatcuk/doublestar v1.1.1 github.com/compose-spec/compose-go v1.2.2 github.com/containers/storage v1.59.0 - github.com/creack/pty v1.1.18 + github.com/creack/pty v1.1.24 github.com/distribution/reference v0.6.0 github.com/docker/cli v28.2.2+incompatible github.com/docker/docker v28.3.3+incompatible @@ -67,7 +67,7 @@ require ( k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.110.1 k8s.io/kubectl v0.29.0 - mvdan.cc/sh/v3 v3.5.1 + mvdan.cc/sh/v3 v3.12.0 sigs.k8s.io/yaml v1.4.0 ) diff --git a/go.sum b/go.sum index b25334c17c..540b005944 100644 --- a/go.sum +++ b/go.sum @@ -105,8 +105,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -161,8 +161,6 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= -github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fujiwara/shapeio v1.0.0 h1:xG5D9oNqCSUUbryZ/jQV3cqe1v2suEjwPIcEg1gKM8M= github.com/fujiwara/shapeio v1.0.0/go.mod h1:LmEmu6L/8jetyj1oewewFb7bZCNRwE7wLCUNzDLaLVA= @@ -191,6 +189,8 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -483,8 +483,8 @@ github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDa github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0= @@ -815,8 +815,8 @@ k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI= k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -mvdan.cc/sh/v3 v3.5.1 h1:hmP3UOw4f+EYexsJjFxvU38+kn+V/s2CclXHanIBkmQ= -mvdan.cc/sh/v3 v3.5.1/go.mod h1:1JcoyAKm1lZw/2bZje/iYKWicU/KMd0rsyJeKHnsK4E= +mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= +mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= diff --git a/vendor/github.com/creack/pty/.editorconfig b/vendor/github.com/creack/pty/.editorconfig new file mode 100644 index 0000000000..349f67aa2d --- /dev/null +++ b/vendor/github.com/creack/pty/.editorconfig @@ -0,0 +1,54 @@ +root = true + +# Sane defaults. +[*] +# Always use unix end of line. +end_of_line = lf +# Always insert a new line at the end of files. +insert_final_newline = true +# Don't leave trailing whitespaces. +trim_trailing_whitespace = true +# Default to utf8 encoding. +charset = utf-8 +# Space > tab for consistent aligns. +indent_style = space +# Default to 2 spaces for indent/tabs. +indent_size = 2 +# Flag long lines. +max_line_length = 140 + +# Explicitly define settings for commonly used files. + +[*.go] +indent_style = tab +indent_size = 8 + +[*.feature] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.tf] +indent_style = space +indent_size = 2 + +[*.md] +# Don't check line lenghts in files. +max_line_length = 0 + +[{Makefile,*.mk}] +indent_style = tab +indent_size = 8 + +[{Dockerfile,Dockerfile.*}] +indent_size = 4 + +[*.sql] +indent_size = 2 diff --git a/vendor/github.com/creack/pty/.golangci.yml b/vendor/github.com/creack/pty/.golangci.yml new file mode 100644 index 0000000000..f023e0f76a --- /dev/null +++ b/vendor/github.com/creack/pty/.golangci.yml @@ -0,0 +1,324 @@ +--- +# Reference: https://golangci-lint.run/usage/configuration/ +run: + timeout: 5m + # modules-download-mode: vendor + + # Include test files. + tests: true + + skip-dirs: [] + + skip-files: [] + +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number". + format: colored-line-number + print-issued-lines: true + print-linter-name: true + +# Linter specific settings. See below in the `linter.enable` section for details on what each linter is doing. +linters-settings: + dogsled: + # Checks assignments with too many blank identifiers. Default is 2. + max-blank-identifiers: 2 + + dupl: + # Tokens count to trigger issue. + threshold: 150 + + errcheck: + # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. + # Enabled as this is often overlooked by developers. + check-type-assertions: true + # Report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`. + # Disabled as we consider that if the developer did type `_`, it was on purpose. + # Note that while this isn't enforced by the linter, each and every case of ignored error should + # be accompanied with a comment explaining why that error is being discarded. + check-blank: false + + exhaustive: + # Indicates that switch statements are to be considered exhaustive if a + # 'default' case is present, even if all enum members aren't listed in the + # switch. + default-signifies-exhaustive: false + + funlen: + # funlen checks the number of lines/statements in a function. + # While is is always best to keep functions short for readability, maintainability and testing, + # the default are a bit too strict (60 lines / 40 statements), increase it to be more flexible. + lines: 160 + statements: 70 + + # NOTE: We don't set `gci` for import order as it supports only one prefix. Use `goimports.local-prefixes` instead. + + gocognit: + # Minimal code complexity to report, defaults to 30 in gocognit, defaults 10 in golangci. + # Use 15 as it allows for some flexibility while preventing too much complexity. + # NOTE: Similar to gocyclo. + min-complexity: 35 + + nestif: + # Minimal complexity of if statements to report. + min-complexity: 8 + + goconst: + # Minimal length of string constant. + min-len: 4 + # Minimal occurrences count to trigger. + # Increase the default from 3 to 5 as small number of const usage can reduce readability instead of improving it. + min-occurrences: 5 + + gocritic: + # Which checks should be disabled; can't be combined with 'enabled-checks'. + # See https://go-critic.github.io/overview#checks-overview + # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` + disabled-checks: + - hugeParam # Very strict check on the size of variables being copied. Too strict for most developer. + # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. + # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". + enabled-tags: + - diagnostic + - style + - opinionated + - performance + settings: + rangeValCopy: + sizeThreshold: 1024 # Increase the allowed copied bytes in range. + + cyclop: + max-complexity: 35 + + gocyclo: + # Similar check as gocognit. + # NOTE: We might be able to remove this linter as it is redundant with gocyclo. It is in golangci-lint, so we keep it for now. + min-complexity: 35 + + godot: + # Check all top-level comments, not only declarations. + check-all: true + + gofmt: + # simplify code: gofmt with `-s` option. + simplify: true + + # NOTE: the goheader settings are set per-project. + + goimports: + # Put imports beginning with prefix after 3rd-party packages. + # It's a comma-separated list of prefixes. + local-prefixes: "github.com/creack/pty" + + golint: + # Minimal confidence for issues, default is 0.8. + min-confidence: 0.8 + + gosimple: + # Select the Go version to target. The default is '1.13'. + go: "1.18" + # https://staticcheck.io/docs/options#checks + checks: ["all"] + + gosec: + + govet: + # Enable all available checks from go vet. + enable-all: false + # Report about shadowed variables. + check-shadowing: true + + # NOTE: depguard is disabled as it is very slow and made redundant by gomodguard. + + lll: + # Make sure everyone is on the same level, fix the tab width to go's default. + tab-width: 8 + # Increase the default max line length to give more flexibility. Forcing newlines can reduce readability instead of improving it. + line-length: 180 + + misspell: + locale: US + ignore-words: + + nakedret: + # Make an issue if func has more lines of code than this setting and it has naked returns; default is 30. + # NOTE: Consider setting this to 1 to prevent naked returns. + max-func-lines: 30 + + nolintlint: + # Prevent ununsed directive to avoid stale comments. + allow-unused: false + # Require an explanation of nonzero length after each nolint directive. + require-explanation: true + # Exclude following linters from requiring an explanation. + # NOTE: It is strongly discouraged to put anything in there. + allow-no-explanation: [] + # Enable to require nolint directives to mention the specific linter being suppressed. This ensurce the developer understand the reason being the error. + require-specific: true + + prealloc: + # NOTE: For most programs usage of prealloc will be a premature optimization. + # Keep thing simple, pre-alloc what is obvious and profile the program for more complex scenarios. + # + simple: true # Checkonly on simple loops that have no returns/breaks/continues/gotos in them. + range-loops: true # Check range loops, true by default + for-loops: false # Check suggestions on for loops, false by default + + rowserrcheck: + packages: [] + + staticcheck: + # Select the Go version to target. The default is '1.13'. + go: "1.18" + # https://staticcheck.io/docs/options#checks + checks: ["all"] + + stylecheck: + # Select the Go version to target. The default is '1.13'. + go: "1.18" + # https://staticcheck.io/docs/options#checks + checks: ["all"] # "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] + + tagliatelle: + # Check the struck tag name case. + case: + # Use the struct field name to check the name of the struct tag. + use-field-name: false + rules: + # Any struct tag type can be used. + # support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` + json: snake + firestore: camel + yaml: camel + xml: camel + bson: camel + avro: snake + mapstructure: kebab + envconfig: upper + + unparam: + # Don't create an error if an exported code have static params being used. It is often expected in libraries. + # NOTE: It would be nice if this linter would differentiate between a main package and a lib. + check-exported: true + + unused: {} + + whitespace: + multi-if: false # Enforces newlines (or comments) after every multi-line if statement + multi-func: false # Enforces newlines (or comments) after every multi-line function signature + +# Run `golangci-lint help linters` to get the full list of linter with their description. +linters: + disable-all: true + # NOTE: enable-all is deprecated because too many people don't pin versions... + # We still require explicit documentation on why some linters are disabled. + # disable: + # - depguard # Go linter that checks if package imports are in a list of acceptable packages [fast: true, auto-fix: false] + # - exhaustivestruct # Checks if all struct's fields are initialized [fast: true, auto-fix: false] + # - forbidigo # Forbids identifiers [fast: true, auto-fix: false] + # - gci # Gci control golang package import order and make it always deterministic. [fast: true, auto-fix: true] + # - godox # Tool for detection of FIXME, TODO and other comment keywords [fast: true, auto-fix: false] + # - goerr113 # Golang linter to check the errors handling expressions [fast: true, auto-fix: false] + # - golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes [fast: false, auto-fix: false] + # - gomnd # An analyzer to detect magic numbers. [fast: true, auto-fix: false] + # - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. [fast: true, auto-fix: false] + # - interfacer # Linter that suggests narrower interface types [fast: false, auto-fix: false] + # - maligned # Tool to detect Go structs that would take less memory if their fields were sorted [fast: false, auto-fix: false] + # - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity [fast: true, auto-fix: false] + # - scopelint # Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] + # - wrapcheck # Checks that errors returned from external packages are wrapped [fast: false, auto-fix: false] + # - wsl # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] + + # disable-reasons: + # - depguard # Checks whitelisted/blacklisted import path, but runs way too slow. Not that useful. + # - exhaustivestruct # Good concept, but not mature enough (errors on not assignable fields like locks) and too noisy when using AWS SDK as most fields are unused. + # - forbidigo # Great idea, but too strict out of the box. Probably will re-enable soon. + # - gci # Conflicts with goimports/gofumpt. + # - godox # Don't fail when finding TODO, FIXME, etc. + # - goerr113 # Too many false positives. + # - golint # Deprecated (since v1.41.0) due to: The repository of the linter has been archived by the owner. Replaced by revive. + # - gomnd # Checks for magic numbers. Disabled due to too many false positives not configurable (03/01/2020 v1.23.7). + # - gomoddirectives # Doesn't support //nolint to whitelist. + # - interfacer # Deprecated (since v1.38.0) due to: The repository of the linter has been archived by the owner. + # - maligned # Deprecated (since v1.38.0) due to: The repository of the linter has been archived by the owner. Replaced by govet 'fieldalignment'. + # - nlreturn # Actually reduces readability in most cases. + # - scopelint # Deprecated (since v1.39.0) due to: The repository of the linter has been deprecated by the owner. Replaced by exportloopref. + # - wrapcheck # Good concept, but always warns for http coded errors. Need to re-enable and whitelist our error package. + # - wsl # Forces to add newlines around blocks. Lots of false positives, not that useful. + + enable: + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers [fast: true, auto-fix: false] + - bodyclose # checks whether HTTP response body is closed successfully [fast: false, auto-fix: false] + - cyclop # checks function and package cyclomatic complexity [fast: false, auto-fix: false] + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) [fast: true, auto-fix: false] + - dupl # Tool for code clone detection [fast: true, auto-fix: false] + - durationcheck # check for two durations multiplied together [fast: false, auto-fix: false] + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] + - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] + - errorlint # go-errorlint is a source code linter for Go software that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. [fast: false, auto-fix: false] + - exhaustive # check exhaustiveness of enum switch statements [fast: false, auto-fix: false] + - exportloopref # checks for pointers to enclosing loop variables [fast: false, auto-fix: false] + - forcetypeassert # finds forced type assertions [fast: true, auto-fix: false] + - funlen # Tool for detection of long functions [fast: true, auto-fix: false] + - gochecknoglobals # check that no global variables exist [fast: true, auto-fix: false] + - gochecknoinits # Checks that no init functions are present in Go code [fast: true, auto-fix: false] + - gocognit # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] + - goconst # Finds repeated strings that could be replaced by a constant [fast: true, auto-fix: false] + - gocritic # Provides many diagnostics that check for bugs, performance and style issues. [fast: false, auto-fix: false] + - gocyclo # Computes and checks the cyclomatic complexity of functions [fast: true, auto-fix: false] + - godot # Check if comments end in a period [fast: true, auto-fix: true] + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification [fast: true, auto-fix: true] + - gofumpt # Gofumpt checks whether code was gofumpt-ed. [fast: true, auto-fix: true] + - goheader # Checks is file header matches to pattern [fast: true, auto-fix: false] + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports [fast: true, auto-fix: true] + - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. [fast: true, auto-fix: false] + - goprintffuncname # Checks that printf-like functions are named with `f` at the end [fast: true, auto-fix: false] + - gosec # (gas): Inspects source code for security problems [fast: false, auto-fix: false] + - gosimple # (megacheck): Linter for Go source code that specializes in simplifying a code [fast: false, auto-fix: false] + - govet # (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false] + - importas # Enforces consistent import aliases [fast: false, auto-fix: false] + - ineffassign # Detects when assignments to existing variables are not used [fast: true, auto-fix: false] + - lll # Reports long lines [fast: true, auto-fix: false] + - makezero # Finds slice declarations with non-zero initial length [fast: false, auto-fix: false] + - misspell # Finds commonly misspelled English words in comments [fast: true, auto-fix: true] + - nakedret # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] + - nestif # Reports deeply nested if statements [fast: true, auto-fix: false] + - nilerr # Finds the code that returns nil even if it checks that the error is not nil. [fast: false, auto-fix: false] + - noctx # noctx finds sending http request without context.Context [fast: false, auto-fix: false] + - nolintlint # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] + - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test [fast: true, auto-fix: false] + - prealloc # Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] + - predeclared # find code that shadows one of Go's predeclared identifiers [fast: true, auto-fix: false] + - promlinter # Check Prometheus metrics naming via promlint [fast: true, auto-fix: false] + - revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint. [fast: false, auto-fix: false] + # Disabled due to generic. Work in progress upstream. + # - rowserrcheck # checks whether Err of rows is checked successfully [fast: false, auto-fix: false] + # Disabled due to generic. Work in progress upstream. + # - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. [fast: false, auto-fix: false] + - staticcheck # (megacheck): Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] + - stylecheck # Stylecheck is a replacement for golint [fast: false, auto-fix: false] + # Disabled due to generic. Work in progress upstream. + # - tagliatelle # Checks the struct tags. [fast: true, auto-fix: false] + # - testpackage # linter that makes you use a separate _test package [fast: true, auto-fix: false] + - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers [fast: false, auto-fix: false] + - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes [fast: false, auto-fix: false] + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] + - unconvert # Remove unnecessary type conversions [fast: false, auto-fix: false] + - unparam # Reports unused function parameters [fast: false, auto-fix: false] + # Disabled due to way too many false positive in go1.20. + # - unused # (megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] + # Disabled due to generic. Work in progress upstream. + # - wastedassign # wastedassign finds wasted assignment statements. [fast: false, auto-fix: false] + - whitespace # Tool for detection of leading and trailing whitespace [fast: true, auto-fix: true] + +issues: + exclude: + # Allow shadowing of 'err'. + - 'shadow: declaration of "err" shadows declaration' + # Allow shadowing of `ctx`. + - 'shadow: declaration of "ctx" shadows declaration' + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-per-linter: 10 + # Disable default excludes. Always be explicit on what we exclude. + exclude-use-default: false + # Exclude some linters from running on tests files. + exclude-rules: [] diff --git a/vendor/github.com/creack/pty/Dockerfile.golang b/vendor/github.com/creack/pty/Dockerfile.golang index 2ee82a3a1f..b6153421c0 100644 --- a/vendor/github.com/creack/pty/Dockerfile.golang +++ b/vendor/github.com/creack/pty/Dockerfile.golang @@ -1,4 +1,4 @@ -ARG GOVERSION=1.14 +ARG GOVERSION=1.18.2 FROM golang:${GOVERSION} # Set base env. diff --git a/vendor/github.com/creack/pty/Dockerfile.riscv b/vendor/github.com/creack/pty/Dockerfile.riscv deleted file mode 100644 index 7a30c94d03..0000000000 --- a/vendor/github.com/creack/pty/Dockerfile.riscv +++ /dev/null @@ -1,23 +0,0 @@ -# NOTE: Using 1.13 as a base to build the RISCV compiler, the resulting version is based on go1.6. -FROM golang:1.13 - -# Clone and complie a riscv compatible version of the go compiler. -RUN git clone https://review.gerrithub.io/riscv/riscv-go /riscv-go -# riscvdev branch HEAD as of 2019-06-29. -RUN cd /riscv-go && git checkout 04885fddd096d09d4450726064d06dd107e374bf -ENV PATH=/riscv-go/misc/riscv:/riscv-go/bin:$PATH -RUN cd /riscv-go/src && GOROOT_BOOTSTRAP=$(go env GOROOT) ./make.bash -ENV GOROOT=/riscv-go - -# Set the base env. -ENV GOOS=linux GOARCH=riscv CGO_ENABLED=0 GOFLAGS='-v -ldflags=-s -ldflags=-w' - -# Pre compile the stdlib. -RUN go build -a std - -# Add the code to the image. -WORKDIR pty -ADD . . - -# Build the lib. -RUN go build diff --git a/vendor/github.com/creack/pty/README.md b/vendor/github.com/creack/pty/README.md index a4fe7670d4..b6a1cf5685 100644 --- a/vendor/github.com/creack/pty/README.md +++ b/vendor/github.com/creack/pty/README.md @@ -10,7 +10,7 @@ go get github.com/creack/pty ## Examples -Note that those examples are for demonstration purpose only, to showcase how to use the library. They are not meant to be used in any kind of production environment. +Note that those examples are for demonstration purpose only, to showcase how to use the library. They are not meant to be used in any kind of production environment. If you want to **set deadlines to work** and `Close()` **interrupting** `Read()` on the returned `*os.File`, you will need to call `syscall.SetNonblock` manually. ### Command diff --git a/vendor/github.com/creack/pty/ioctl.go b/vendor/github.com/creack/pty/ioctl.go index 3cabedd96a..7b6b770b7f 100644 --- a/vendor/github.com/creack/pty/ioctl.go +++ b/vendor/github.com/creack/pty/ioctl.go @@ -1,19 +1,28 @@ -//go:build !windows && !solaris && !aix -// +build !windows,!solaris,!aix +//go:build !windows && go1.12 +// +build !windows,go1.12 package pty -import "syscall" +import "os" -const ( - TIOCGWINSZ = syscall.TIOCGWINSZ - TIOCSWINSZ = syscall.TIOCSWINSZ -) +func ioctl(f *os.File, cmd, ptr uintptr) error { + return ioctlInner(f.Fd(), cmd, ptr) // Fall back to blocking io. +} + +// NOTE: Unused. Keeping for reference. +func ioctlNonblock(f *os.File, cmd, ptr uintptr) error { + sc, e := f.SyscallConn() + if e != nil { + return ioctlInner(f.Fd(), cmd, ptr) // Fall back to blocking io (old behavior). + } + + ch := make(chan error, 1) + defer close(ch) -func ioctl(fd, cmd, ptr uintptr) error { - _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr) - if e != 0 { + e = sc.Control(func(fd uintptr) { ch <- ioctlInner(fd, cmd, ptr) }) + if e != nil { return e } - return nil + e = <-ch + return e } diff --git a/vendor/github.com/creack/pty/ioctl_inner.go b/vendor/github.com/creack/pty/ioctl_inner.go new file mode 100644 index 0000000000..272b50b971 --- /dev/null +++ b/vendor/github.com/creack/pty/ioctl_inner.go @@ -0,0 +1,20 @@ +//go:build !windows && !solaris && !aix +// +build !windows,!solaris,!aix + +package pty + +import "syscall" + +// Local syscall const values. +const ( + TIOCGWINSZ = syscall.TIOCGWINSZ + TIOCSWINSZ = syscall.TIOCSWINSZ +) + +func ioctlInner(fd, cmd, ptr uintptr) error { + _, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, cmd, ptr) + if e != 0 { + return e + } + return nil +} diff --git a/vendor/github.com/creack/pty/ioctl_legacy.go b/vendor/github.com/creack/pty/ioctl_legacy.go new file mode 100644 index 0000000000..f7e923cd07 --- /dev/null +++ b/vendor/github.com/creack/pty/ioctl_legacy.go @@ -0,0 +1,10 @@ +//go:build !windows && !go1.12 +// +build !windows,!go1.12 + +package pty + +import "os" + +func ioctl(f *os.File, cmd, ptr uintptr) error { + return ioctlInner(f.Fd(), cmd, ptr) // fall back to blocking io (old behavior) +} diff --git a/vendor/github.com/creack/pty/ioctl_solaris.go b/vendor/github.com/creack/pty/ioctl_solaris.go index bff22dad0b..6fd8bfeee5 100644 --- a/vendor/github.com/creack/pty/ioctl_solaris.go +++ b/vendor/github.com/creack/pty/ioctl_solaris.go @@ -40,7 +40,7 @@ type strioctl struct { // Defined in asm_solaris_amd64.s. func sysvicall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) -func ioctl(fd, cmd, ptr uintptr) error { +func ioctlInner(fd, cmd, ptr uintptr) error { if _, _, errno := sysvicall6(uintptr(unsafe.Pointer(&procioctl)), 3, fd, cmd, ptr, 0, 0, 0); errno != 0 { return errno } diff --git a/vendor/github.com/creack/pty/ioctl_unsupported.go b/vendor/github.com/creack/pty/ioctl_unsupported.go index 2449a27ee7..e17908d44a 100644 --- a/vendor/github.com/creack/pty/ioctl_unsupported.go +++ b/vendor/github.com/creack/pty/ioctl_unsupported.go @@ -8,6 +8,6 @@ const ( TIOCSWINSZ = 0 ) -func ioctl(fd, cmd, ptr uintptr) error { +func ioctlInner(fd, cmd, ptr uintptr) error { return ErrUnsupported } diff --git a/vendor/github.com/creack/pty/pty_darwin.go b/vendor/github.com/creack/pty/pty_darwin.go index 9bdd71d08d..eadf6ab7c7 100644 --- a/vendor/github.com/creack/pty/pty_darwin.go +++ b/vendor/github.com/creack/pty/pty_darwin.go @@ -46,7 +46,7 @@ func open() (pty, tty *os.File, err error) { func ptsname(f *os.File) (string, error) { n := make([]byte, _IOC_PARM_LEN(syscall.TIOCPTYGNAME)) - err := ioctl(f.Fd(), syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0]))) + err := ioctl(f, syscall.TIOCPTYGNAME, uintptr(unsafe.Pointer(&n[0]))) if err != nil { return "", err } @@ -60,9 +60,9 @@ func ptsname(f *os.File) (string, error) { } func grantpt(f *os.File) error { - return ioctl(f.Fd(), syscall.TIOCPTYGRANT, 0) + return ioctl(f, syscall.TIOCPTYGRANT, 0) } func unlockpt(f *os.File) error { - return ioctl(f.Fd(), syscall.TIOCPTYUNLK, 0) + return ioctl(f, syscall.TIOCPTYUNLK, 0) } diff --git a/vendor/github.com/creack/pty/pty_dragonfly.go b/vendor/github.com/creack/pty/pty_dragonfly.go index aa916aadf1..12803de043 100644 --- a/vendor/github.com/creack/pty/pty_dragonfly.go +++ b/vendor/github.com/creack/pty/pty_dragonfly.go @@ -45,17 +45,17 @@ func open() (pty, tty *os.File, err error) { } func grantpt(f *os.File) error { - _, err := isptmaster(f.Fd()) + _, err := isptmaster(f) return err } func unlockpt(f *os.File) error { - _, err := isptmaster(f.Fd()) + _, err := isptmaster(f) return err } -func isptmaster(fd uintptr) (bool, error) { - err := ioctl(fd, syscall.TIOCISPTMASTER, 0) +func isptmaster(f *os.File) (bool, error) { + err := ioctl(f, syscall.TIOCISPTMASTER, 0) return err == nil, err } @@ -68,7 +68,7 @@ func ptsname(f *os.File) (string, error) { name := make([]byte, _C_SPECNAMELEN) fa := fiodgnameArg{Name: (*byte)(unsafe.Pointer(&name[0])), Len: _C_SPECNAMELEN, Pad_cgo_0: [4]byte{0, 0, 0, 0}} - err := ioctl(f.Fd(), ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa))) + err := ioctl(f, ioctl_FIODNAME, uintptr(unsafe.Pointer(&fa))) if err != nil { return "", err } diff --git a/vendor/github.com/creack/pty/pty_freebsd.go b/vendor/github.com/creack/pty/pty_freebsd.go index bcd3b6f90f..47afcfeec8 100644 --- a/vendor/github.com/creack/pty/pty_freebsd.go +++ b/vendor/github.com/creack/pty/pty_freebsd.go @@ -44,8 +44,8 @@ func open() (pty, tty *os.File, err error) { return p, t, nil } -func isptmaster(fd uintptr) (bool, error) { - err := ioctl(fd, syscall.TIOCPTMASTER, 0) +func isptmaster(f *os.File) (bool, error) { + err := ioctl(f, syscall.TIOCPTMASTER, 0) return err == nil, err } @@ -55,7 +55,7 @@ var ( ) func ptsname(f *os.File) (string, error) { - master, err := isptmaster(f.Fd()) + master, err := isptmaster(f) if err != nil { return "", err } @@ -68,7 +68,7 @@ func ptsname(f *os.File) (string, error) { buf = make([]byte, n) arg = fiodgnameArg{Len: n, Buf: (*byte)(unsafe.Pointer(&buf[0]))} ) - if err := ioctl(f.Fd(), ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil { + if err := ioctl(f, ioctlFIODGNAME, uintptr(unsafe.Pointer(&arg))); err != nil { return "", err } diff --git a/vendor/github.com/creack/pty/pty_linux.go b/vendor/github.com/creack/pty/pty_linux.go index a3b368f561..e7e01c0aa5 100644 --- a/vendor/github.com/creack/pty/pty_linux.go +++ b/vendor/github.com/creack/pty/pty_linux.go @@ -40,7 +40,7 @@ func open() (pty, tty *os.File, err error) { func ptsname(f *os.File) (string, error) { var n _C_uint - err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) //nolint:gosec // Expected unsafe pointer for Syscall call. + err := ioctl(f, syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))) //nolint:gosec // Expected unsafe pointer for Syscall call. if err != nil { return "", err } @@ -49,6 +49,6 @@ func ptsname(f *os.File) (string, error) { func unlockpt(f *os.File) error { var u _C_int - // use TIOCSPTLCK with a pointer to zero to clear the lock - return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) //nolint:gosec // Expected unsafe pointer for Syscall call. + // use TIOCSPTLCK with a pointer to zero to clear the lock. + return ioctl(f, syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u))) //nolint:gosec // Expected unsafe pointer for Syscall call. } diff --git a/vendor/github.com/creack/pty/pty_netbsd.go b/vendor/github.com/creack/pty/pty_netbsd.go index 2b20d944c2..dd5611dbd7 100644 --- a/vendor/github.com/creack/pty/pty_netbsd.go +++ b/vendor/github.com/creack/pty/pty_netbsd.go @@ -47,7 +47,7 @@ func ptsname(f *os.File) (string, error) { * ioctl(fd, TIOCPTSNAME, &pm) == -1 ? NULL : pm.sn; */ var ptm ptmget - if err := ioctl(f.Fd(), uintptr(ioctl_TIOCPTSNAME), uintptr(unsafe.Pointer(&ptm))); err != nil { + if err := ioctl(f, uintptr(ioctl_TIOCPTSNAME), uintptr(unsafe.Pointer(&ptm))); err != nil { return "", err } name := make([]byte, len(ptm.Sn)) @@ -65,5 +65,5 @@ func grantpt(f *os.File) error { * from grantpt(3): Calling grantpt() is equivalent to: * ioctl(fd, TIOCGRANTPT, 0); */ - return ioctl(f.Fd(), uintptr(ioctl_TIOCGRANTPT), 0) + return ioctl(f, uintptr(ioctl_TIOCGRANTPT), 0) } diff --git a/vendor/github.com/creack/pty/pty_openbsd.go b/vendor/github.com/creack/pty/pty_openbsd.go index 031367a85b..337c39f3f1 100644 --- a/vendor/github.com/creack/pty/pty_openbsd.go +++ b/vendor/github.com/creack/pty/pty_openbsd.go @@ -9,6 +9,17 @@ import ( "unsafe" ) +func cInt8ToString(in []int8) string { + var s []byte + for _, v := range in { + if v == 0 { + break + } + s = append(s, byte(v)) + } + return string(s) +} + func open() (pty, tty *os.File, err error) { /* * from ptm(4): @@ -25,12 +36,12 @@ func open() (pty, tty *os.File, err error) { defer p.Close() var ptm ptmget - if err := ioctl(p.Fd(), uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil { + if err := ioctl(p, uintptr(ioctl_PTMGET), uintptr(unsafe.Pointer(&ptm))); err != nil { return nil, nil, err } - pty = os.NewFile(uintptr(ptm.Cfd), "/dev/ptm") - tty = os.NewFile(uintptr(ptm.Sfd), "/dev/ptm") + pty = os.NewFile(uintptr(ptm.Cfd), cInt8ToString(ptm.Cn[:])) + tty = os.NewFile(uintptr(ptm.Sfd), cInt8ToString(ptm.Sn[:])) return pty, tty, nil } diff --git a/vendor/github.com/creack/pty/pty_solaris.go b/vendor/github.com/creack/pty/pty_solaris.go index 37f933e600..4e22416b01 100644 --- a/vendor/github.com/creack/pty/pty_solaris.go +++ b/vendor/github.com/creack/pty/pty_solaris.go @@ -65,7 +65,7 @@ func open() (pty, tty *os.File, err error) { } func ptsname(f *os.File) (string, error) { - dev, err := ptsdev(f.Fd()) + dev, err := ptsdev(f) if err != nil { return "", err } @@ -84,12 +84,12 @@ func unlockpt(f *os.File) error { icLen: 0, icDP: nil, } - return ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr))) + return ioctl(f, I_STR, uintptr(unsafe.Pointer(&istr))) } func minor(x uint64) uint64 { return x & 0377 } -func ptsdev(fd uintptr) (uint64, error) { +func ptsdev(f *os.File) (uint64, error) { istr := strioctl{ icCmd: ISPTM, icTimeout: 0, @@ -97,14 +97,33 @@ func ptsdev(fd uintptr) (uint64, error) { icDP: nil, } - if err := ioctl(fd, I_STR, uintptr(unsafe.Pointer(&istr))); err != nil { + if err := ioctl(f, I_STR, uintptr(unsafe.Pointer(&istr))); err != nil { return 0, err } - var status syscall.Stat_t - if err := syscall.Fstat(int(fd), &status); err != nil { + var errors = make(chan error, 1) + var results = make(chan uint64, 1) + defer close(errors) + defer close(results) + + var err error + var sc syscall.RawConn + sc, err = f.SyscallConn() + if err != nil { + return 0, err + } + err = sc.Control(func(fd uintptr) { + var status syscall.Stat_t + if err := syscall.Fstat(int(fd), &status); err != nil { + results <- 0 + errors <- err + } + results <- uint64(minor(status.Rdev)) + errors <- nil + }) + if err != nil { return 0, err } - return uint64(minor(status.Rdev)), nil + return <-results, <-errors } type ptOwn struct { @@ -113,7 +132,7 @@ type ptOwn struct { } func grantpt(f *os.File) error { - if _, err := ptsdev(f.Fd()); err != nil { + if _, err := ptsdev(f); err != nil { return err } pto := ptOwn{ @@ -127,7 +146,7 @@ func grantpt(f *os.File) error { icLen: int32(unsafe.Sizeof(strioctl{})), icDP: unsafe.Pointer(&pto), } - if err := ioctl(f.Fd(), I_STR, uintptr(unsafe.Pointer(&istr))); err != nil { + if err := ioctl(f, I_STR, uintptr(unsafe.Pointer(&istr))); err != nil { return errors.New("access denied") } return nil @@ -145,8 +164,8 @@ func streamsPush(f *os.File, mod string) error { // but since we are not using libc or XPG4.2, we should not be // double-pushing modules - if err := ioctl(f.Fd(), I_FIND, uintptr(unsafe.Pointer(&buf[0]))); err != nil { + if err := ioctl(f, I_FIND, uintptr(unsafe.Pointer(&buf[0]))); err != nil { return nil } - return ioctl(f.Fd(), I_PUSH, uintptr(unsafe.Pointer(&buf[0]))) + return ioctl(f, I_PUSH, uintptr(unsafe.Pointer(&buf[0]))) } diff --git a/vendor/github.com/creack/pty/pty_unsupported.go b/vendor/github.com/creack/pty/pty_unsupported.go index c771020fae..0971dc74e1 100644 --- a/vendor/github.com/creack/pty/pty_unsupported.go +++ b/vendor/github.com/creack/pty/pty_unsupported.go @@ -1,5 +1,5 @@ -//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris -// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris +//go:build !linux && !darwin && !freebsd && !dragonfly && !netbsd && !openbsd && !solaris && !zos +// +build !linux,!darwin,!freebsd,!dragonfly,!netbsd,!openbsd,!solaris,!zos package pty diff --git a/vendor/github.com/creack/pty/pty_zos.go b/vendor/github.com/creack/pty/pty_zos.go new file mode 100644 index 0000000000..18e61e1963 --- /dev/null +++ b/vendor/github.com/creack/pty/pty_zos.go @@ -0,0 +1,141 @@ +//go:build zos +// +build zos + +package pty + +import ( + "os" + "runtime" + "syscall" + "unsafe" +) + +const ( + SYS_UNLOCKPT = 0x37B + SYS_GRANTPT = 0x37A + SYS_POSIX_OPENPT = 0xC66 + SYS_FCNTL = 0x18C + SYS___PTSNAME_A = 0x718 + + SETCVTON = 1 + + O_NONBLOCK = 0x04 + + F_SETFL = 4 + F_CONTROL_CVT = 13 +) + +type f_cnvrt struct { + Cvtcmd int32 + Pccsid int16 + Fccsid int16 +} + +func open() (pty, tty *os.File, err error) { + ptmxfd, err := openpt(os.O_RDWR | syscall.O_NOCTTY) + if err != nil { + return nil, nil, err + } + + // Needed for z/OS so that the characters are not garbled if ptyp* is untagged + cvtreq := f_cnvrt{Cvtcmd: SETCVTON, Pccsid: 0, Fccsid: 1047} + if _, err = fcntl(uintptr(ptmxfd), F_CONTROL_CVT, uintptr(unsafe.Pointer(&cvtreq))); err != nil { + return nil, nil, err + } + + p := os.NewFile(uintptr(ptmxfd), "/dev/ptmx") + if p == nil { + return nil, nil, err + } + + // In case of error after this point, make sure we close the ptmx fd. + defer func() { + if err != nil { + _ = p.Close() // Best effort. + } + }() + + sname, err := ptsname(ptmxfd) + if err != nil { + return nil, nil, err + } + + _, err = grantpt(ptmxfd) + if err != nil { + return nil, nil, err + } + + if _, err = unlockpt(ptmxfd); err != nil { + return nil, nil, err + } + + ptsfd, err := syscall.Open(sname, os.O_RDWR|syscall.O_NOCTTY, 0) + if err != nil { + return nil, nil, err + } + + if _, err = fcntl(uintptr(ptsfd), F_CONTROL_CVT, uintptr(unsafe.Pointer(&cvtreq))); err != nil { + return nil, nil, err + } + + t := os.NewFile(uintptr(ptsfd), sname) + if err != nil { + return nil, nil, err + } + + return p, t, nil +} + +func openpt(oflag int) (fd int, err error) { + r0, _, e1 := runtime.CallLeFuncWithErr(runtime.GetZosLibVec()+SYS_POSIX_OPENPT<<4, uintptr(oflag)) + fd = int(r0) + if e1 != 0 { + err = syscall.Errno(e1) + } + return +} + +func fcntl(fd uintptr, cmd int, arg uintptr) (val int, err error) { + r0, _, e1 := runtime.CallLeFuncWithErr(runtime.GetZosLibVec()+SYS_FCNTL<<4, uintptr(fd), uintptr(cmd), arg) + val = int(r0) + if e1 != 0 { + err = syscall.Errno(e1) + } + return +} + +func ptsname(fd int) (name string, err error) { + r0, _, e1 := runtime.CallLeFuncWithPtrReturn(runtime.GetZosLibVec()+SYS___PTSNAME_A<<4, uintptr(fd)) + name = u2s(unsafe.Pointer(r0)) + if e1 != 0 { + err = syscall.Errno(e1) + } + return +} + +func grantpt(fildes int) (rc int, err error) { + r0, _, e1 := runtime.CallLeFuncWithErr(runtime.GetZosLibVec()+SYS_GRANTPT<<4, uintptr(fildes)) + rc = int(r0) + if e1 != 0 { + err = syscall.Errno(e1) + } + return +} + +func unlockpt(fildes int) (rc int, err error) { + r0, _, e1 := runtime.CallLeFuncWithErr(runtime.GetZosLibVec()+SYS_UNLOCKPT<<4, uintptr(fildes)) + rc = int(r0) + if e1 != 0 { + err = syscall.Errno(e1) + } + return +} + +func u2s(cstr unsafe.Pointer) string { + str := (*[1024]uint8)(cstr) + i := 0 + for str[i] != 0 { + i++ + } + return string(str[:i]) +} diff --git a/vendor/github.com/creack/pty/test_crosscompile.sh b/vendor/github.com/creack/pty/test_crosscompile.sh index 47e8b10643..40df89add6 100644 --- a/vendor/github.com/creack/pty/test_crosscompile.sh +++ b/vendor/github.com/creack/pty/test_crosscompile.sh @@ -25,9 +25,9 @@ cross() { set -e -cross linux amd64 386 arm arm64 ppc64 ppc64le s390x mips mipsle mips64 mips64le +cross linux amd64 386 arm arm64 ppc64 ppc64le s390x mips mipsle mips64 mips64le riscv64 cross darwin amd64 arm64 -cross freebsd amd64 386 arm arm64 +cross freebsd amd64 386 arm arm64 riscv64 cross netbsd amd64 386 arm arm64 cross openbsd amd64 386 arm arm64 cross dragonfly amd64 @@ -45,10 +45,6 @@ if ! hash docker; then return fi -echo2 "Build for linux." -echo2 " - linux/riscv" -docker build -t creack-pty-test -f Dockerfile.riscv . - # Golang dropped support for darwin 32bits since go1.15. Make sure the lib still compile with go1.14 on those archs. echo2 "Build for darwin (32bits)." echo2 " - darwin/386" diff --git a/vendor/github.com/creack/pty/winsize.go b/vendor/github.com/creack/pty/winsize.go index 57323f40ab..cfa3e5f391 100644 --- a/vendor/github.com/creack/pty/winsize.go +++ b/vendor/github.com/creack/pty/winsize.go @@ -10,10 +10,7 @@ func InheritSize(pty, tty *os.File) error { if err != nil { return err } - if err := Setsize(tty, size); err != nil { - return err - } - return nil + return Setsize(tty, size) } // Getsize returns the number of rows (lines) and cols (positions diff --git a/vendor/github.com/creack/pty/winsize_unix.go b/vendor/github.com/creack/pty/winsize_unix.go index 5d99c3dd9d..8dbbcda0f0 100644 --- a/vendor/github.com/creack/pty/winsize_unix.go +++ b/vendor/github.com/creack/pty/winsize_unix.go @@ -11,16 +11,16 @@ import ( // Winsize describes the terminal size. type Winsize struct { - Rows uint16 // ws_row: Number of rows (in cells) - Cols uint16 // ws_col: Number of columns (in cells) - X uint16 // ws_xpixel: Width in pixels - Y uint16 // ws_ypixel: Height in pixels + Rows uint16 // ws_row: Number of rows (in cells). + Cols uint16 // ws_col: Number of columns (in cells). + X uint16 // ws_xpixel: Width in pixels. + Y uint16 // ws_ypixel: Height in pixels. } // Setsize resizes t to s. func Setsize(t *os.File, ws *Winsize) error { //nolint:gosec // Expected unsafe pointer for Syscall call. - return ioctl(t.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(ws))) + return ioctl(t, syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(ws))) } // GetsizeFull returns the full terminal size description. @@ -28,7 +28,7 @@ func GetsizeFull(t *os.File) (size *Winsize, err error) { var ws Winsize //nolint:gosec // Expected unsafe pointer for Syscall call. - if err := ioctl(t.Fd(), syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws))); err != nil { + if err := ioctl(t, syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(&ws))); err != nil { return nil, err } return &ws, nil diff --git a/vendor/github.com/creack/pty/ztypes_freebsd_riscv64.go b/vendor/github.com/creack/pty/ztypes_freebsd_riscv64.go new file mode 100644 index 0000000000..b3c544098c --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_freebsd_riscv64.go @@ -0,0 +1,13 @@ +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs types_freebsd.go + +package pty + +const ( + _C_SPECNAMELEN = 0x3f +) + +type fiodgnameArg struct { + Len int32 + Buf *byte +} diff --git a/vendor/github.com/creack/pty/ztypes_openbsd_32bit_int.go b/vendor/github.com/creack/pty/ztypes_openbsd_32bit_int.go index 1eb0948167..811312dd35 100644 --- a/vendor/github.com/creack/pty/ztypes_openbsd_32bit_int.go +++ b/vendor/github.com/creack/pty/ztypes_openbsd_32bit_int.go @@ -1,5 +1,4 @@ -//go:build (386 || amd64 || arm || arm64 || mips64) && openbsd -// +build 386 amd64 arm arm64 mips64 +//go:build openbsd // +build openbsd package pty diff --git a/vendor/github.com/creack/pty/ztypes_ppc.go b/vendor/github.com/creack/pty/ztypes_ppc.go new file mode 100644 index 0000000000..ff0b8fd838 --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_ppc.go @@ -0,0 +1,9 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/github.com/creack/pty/ztypes_sparcx.go b/vendor/github.com/creack/pty/ztypes_sparcx.go new file mode 100644 index 0000000000..06e44311df --- /dev/null +++ b/vendor/github.com/creack/pty/ztypes_sparcx.go @@ -0,0 +1,12 @@ +//go:build sparc || sparc64 +// +build sparc sparc64 + +// Code generated by cmd/cgo -godefs; DO NOT EDIT. +// cgo -godefs types.go + +package pty + +type ( + _C_int int32 + _C_uint uint32 +) diff --git a/vendor/modules.txt b/vendor/modules.txt index 70b280e2df..339a9e282e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -109,8 +109,8 @@ github.com/containers/storage/pkg/unshare # github.com/cpuguy83/go-md2man/v2 v2.0.7 ## explicit; go 1.12 github.com/cpuguy83/go-md2man/v2/md2man -# github.com/creack/pty v1.1.18 -## explicit; go 1.13 +# github.com/creack/pty v1.1.24 +## explicit; go 1.18 github.com/creack/pty # github.com/davecgh/go-spew v1.1.1 ## explicit @@ -1426,8 +1426,8 @@ k8s.io/utils/net k8s.io/utils/pointer k8s.io/utils/ptr k8s.io/utils/strings/slices -# mvdan.cc/sh/v3 v3.5.1 -## explicit; go 1.17 +# mvdan.cc/sh/v3 v3.12.0 +## explicit; go 1.23.0 mvdan.cc/sh/v3/expand mvdan.cc/sh/v3/fileutil mvdan.cc/sh/v3/interp diff --git a/vendor/mvdan.cc/sh/v3/expand/arith.go b/vendor/mvdan.cc/sh/v3/expand/arith.go index 1e48a709bc..f01856136a 100644 --- a/vendor/mvdan.cc/sh/v3/expand/arith.go +++ b/vendor/mvdan.cc/sh/v3/expand/arith.go @@ -6,14 +6,18 @@ package expand import ( "fmt" "strconv" + "strings" "mvdan.cc/sh/v3/syntax" ) +// TODO(v4): the arithmetic APIs should return int64 for portability with 32-bit systems, +// even if Bash only supports native int sizes. + func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { - switch x := expr.(type) { + switch expr := expr.(type) { case *syntax.Word: - str, err := Literal(cfg, x) + str, err := Literal(cfg, expr) if err != nil { return 0, err } @@ -30,33 +34,33 @@ func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { str = val } // default to 0 - return atoi(str), nil + return int(atoi(str)), nil case *syntax.ParenArithm: - return Arithm(cfg, x.X) + return Arithm(cfg, expr.X) case *syntax.UnaryArithm: - switch x.Op { + switch expr.Op { case syntax.Inc, syntax.Dec: - name := x.X.(*syntax.Word).Lit() + name := expr.X.(*syntax.Word).Lit() old := atoi(cfg.envGet(name)) val := old - if x.Op == syntax.Inc { + if expr.Op == syntax.Inc { val++ } else { val-- } - if err := cfg.envSet(name, strconv.Itoa(val)); err != nil { + if err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil { return 0, err } - if x.Post { - return old, nil + if expr.Post { + return int(old), nil } - return val, nil + return int(val), nil } - val, err := Arithm(cfg, x.X) + val, err := Arithm(cfg, expr.X) if err != nil { return 0, err } - switch x.Op { + switch expr.Op { case syntax.Not: return oneIf(val == 0), nil case syntax.BitNegation: @@ -67,34 +71,34 @@ func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { return -val, nil } case *syntax.BinaryArithm: - switch x.Op { + switch expr.Op { case syntax.Assgn, syntax.AddAssgn, syntax.SubAssgn, syntax.MulAssgn, syntax.QuoAssgn, syntax.RemAssgn, syntax.AndAssgn, syntax.OrAssgn, syntax.XorAssgn, syntax.ShlAssgn, syntax.ShrAssgn: - return cfg.assgnArit(x) + return cfg.assgnArit(expr) case syntax.TernQuest: // TernColon can't happen here - cond, err := Arithm(cfg, x.X) + cond, err := Arithm(cfg, expr.X) if err != nil { return 0, err } - b2 := x.Y.(*syntax.BinaryArithm) // must have Op==TernColon + b2 := expr.Y.(*syntax.BinaryArithm) // must have Op==TernColon if cond == 1 { return Arithm(cfg, b2.X) } return Arithm(cfg, b2.Y) } - left, err := Arithm(cfg, x.X) + left, err := Arithm(cfg, expr.X) if err != nil { return 0, err } - right, err := Arithm(cfg, x.Y) + right, err := Arithm(cfg, expr.Y) if err != nil { return 0, err } - return binArit(x.Op, left, right), nil + return binArit(expr.Op, left, right) default: - panic(fmt.Sprintf("unexpected arithm expr: %T", x)) + panic(fmt.Sprintf("unexpected arithm expr: %T", expr)) } } @@ -105,20 +109,21 @@ func oneIf(b bool) int { return 0 } -// atoi is just a shorthand for strconv.Atoi that ignores the error, -// just like shells do. -func atoi(s string) int { - n, _ := strconv.Atoi(s) +// atoi is like [strconv.ParseInt](s, 10, 64), but it ignores errors and trims whitespace. +func atoi(s string) int64 { + s = strings.TrimSpace(s) + n, _ := strconv.ParseInt(s, 10, 64) return n } func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) { name := b.X.(*syntax.Word).Lit() val := atoi(cfg.envGet(name)) - arg, err := Arithm(cfg, b.Y) + arg_, err := Arithm(cfg, b.Y) if err != nil { return 0, err } + arg := int64(arg_) switch b.Op { case syntax.Assgn: val = arg @@ -129,8 +134,14 @@ func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) { case syntax.MulAssgn: val *= arg case syntax.QuoAssgn: + if arg == 0 { + return 0, fmt.Errorf("division by zero") + } val /= arg case syntax.RemAssgn: + if arg == 0 { + return 0, fmt.Errorf("division by zero") + } val %= arg case syntax.AndAssgn: val &= arg @@ -143,10 +154,10 @@ func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) { case syntax.ShrAssgn: val >>= uint(arg) } - if err := cfg.envSet(name, strconv.Itoa(val)); err != nil { + if err := cfg.envSet(name, strconv.FormatInt(val, 10)); err != nil { return 0, err } - return val, nil + return int(val), nil } func intPow(a, b int) int { @@ -161,48 +172,54 @@ func intPow(a, b int) int { return p } -func binArit(op syntax.BinAritOperator, x, y int) int { +func binArit(op syntax.BinAritOperator, x, y int) (int, error) { switch op { case syntax.Add: - return x + y + return x + y, nil case syntax.Sub: - return x - y + return x - y, nil case syntax.Mul: - return x * y + return x * y, nil case syntax.Quo: - return x / y + if y == 0 { + return 0, fmt.Errorf("division by zero") + } + return x / y, nil case syntax.Rem: - return x % y + if y == 0 { + return 0, fmt.Errorf("division by zero") + } + return x % y, nil case syntax.Pow: - return intPow(x, y) + return intPow(x, y), nil case syntax.Eql: - return oneIf(x == y) + return oneIf(x == y), nil case syntax.Gtr: - return oneIf(x > y) + return oneIf(x > y), nil case syntax.Lss: - return oneIf(x < y) + return oneIf(x < y), nil case syntax.Neq: - return oneIf(x != y) + return oneIf(x != y), nil case syntax.Leq: - return oneIf(x <= y) + return oneIf(x <= y), nil case syntax.Geq: - return oneIf(x >= y) + return oneIf(x >= y), nil case syntax.And: - return x & y + return x & y, nil case syntax.Or: - return x | y + return x | y, nil case syntax.Xor: - return x ^ y + return x ^ y, nil case syntax.Shr: - return x >> uint(y) + return x >> uint(y), nil case syntax.Shl: - return x << uint(y) + return x << uint(y), nil case syntax.AndArit: - return oneIf(x != 0 && y != 0) + return oneIf(x != 0 && y != 0), nil case syntax.OrArit: - return oneIf(x != 0 || y != 0) + return oneIf(x != 0 || y != 0), nil default: // syntax.Comma // x is executed but its result discarded - return y + return y, nil } } diff --git a/vendor/mvdan.cc/sh/v3/expand/braces.go b/vendor/mvdan.cc/sh/v3/expand/braces.go index e0363aa2fa..b4d977c874 100644 --- a/vendor/mvdan.cc/sh/v3/expand/braces.go +++ b/vendor/mvdan.cc/sh/v3/expand/braces.go @@ -5,12 +5,13 @@ package expand import ( "strconv" + "strings" "mvdan.cc/sh/v3/syntax" ) // Braces performs brace expansion on a word, given that it contains any -// syntax.BraceExp parts. For example, the word with a brace expansion +// [syntax.BraceExp] parts. For example, the word with a brace expansion // "foo{bar,baz}" will return two literal words, "foobar" and "foobaz". // // Note that the resulting words may share word parts. @@ -25,8 +26,13 @@ func Braces(word *syntax.Word) []*syntax.Word { } if br.Sequence { chars := false - from, err1 := strconv.Atoi(br.Elems[0].Lit()) - to, err2 := strconv.Atoi(br.Elems[1].Lit()) + + fromLit := br.Elems[0].Lit() + toLit := br.Elems[1].Lit() + zeros := max(extraLeadingZeros(fromLit), extraLeadingZeros(toLit)) + + from, err1 := strconv.Atoi(fromLit) + to, err2 := strconv.Atoi(toLit) if err1 != nil || err2 != nil { chars = true from = int(br.Elems[0].Lit()[0]) @@ -57,7 +63,7 @@ func Braces(word *syntax.Word) []*syntax.Word { if chars { lit.Value = string(rune(n)) } else { - lit.Value = strconv.Itoa(n) + lit.Value = strings.Repeat("0", zeros) + strconv.Itoa(n) } next.Parts = append([]syntax.WordPart{lit}, next.Parts...) exp := Braces(&next) @@ -83,3 +89,12 @@ func Braces(word *syntax.Word) []*syntax.Word { } return []*syntax.Word{{Parts: left}} } + +func extraLeadingZeros(s string) int { + for i, r := range s { + if r != '0' { + return i + } + } + return 0 // "0" has no extra leading zeros +} diff --git a/vendor/mvdan.cc/sh/v3/expand/environ.go b/vendor/mvdan.cc/sh/v3/expand/environ.go index 68ba384e1e..b7305d065c 100644 --- a/vendor/mvdan.cc/sh/v3/expand/environ.go +++ b/vendor/mvdan.cc/sh/v3/expand/environ.go @@ -4,8 +4,9 @@ package expand import ( + "cmp" "runtime" - "sort" + "slices" "strings" ) @@ -16,6 +17,8 @@ type Environ interface { // set, use Variable.IsSet. Get(name string) Variable + // TODO(v4): make Each below a func that returns an iterator. + // Each iterates over all the currently set variables, calling the // supplied function on each variable. Iteration is stopped if the // function returns false. @@ -29,6 +32,11 @@ type Environ interface { Each(func(name string, vr Variable) bool) } +// TODO(v4): [WriteEnviron.Set] below is overloaded to the point that correctly +// implementing both sides of the interface is tricky. In particular, some operations +// such as `export foo` or `readonly foo` alter the attributes but not the value, +// and `foo=bar` or `foo=[3]=baz` alter the value but not the attributes. + // WriteEnviron is an extension on Environ that supports modifying and deleting // variables. type WriteEnviron interface { @@ -36,40 +44,58 @@ type WriteEnviron interface { // Set sets a variable by name. If !vr.IsSet(), the variable is being // unset; otherwise, the variable is being replaced. // - // It is the implementation's responsibility to handle variable - // attributes correctly. For example, changing an exported variable's - // value does not unexport it, and overwriting a name reference variable - // should modify its target. + // The given variable can have the kind [KeepValue] to replace an existing + // variable's attributes without changing its value at all. + // This is helpful to implement `readonly foo=bar; export foo`, + // as the second declaration needs to clearly signal that the value is not modified. // // An error may be returned if the operation is invalid, such as if the // name is empty or if we're trying to overwrite a read-only variable. Set(name string, vr Variable) error } +//go:generate stringer -type=ValueKind + +// ValueKind describes which kind of value the variable holds. +// While most unset variables will have an [Unknown] kind, an unset variable may +// have a kind associated too, such as via `declare -a foo` resulting in [Indexed]. type ValueKind uint8 const ( - Unset ValueKind = iota + // Unknown is used for unset variables which do not have a kind yet. + Unknown ValueKind = iota + // String describes plain string variables, such as `foo=bar`. String + // NameRef describes variables which reference another by name, such as `declare -n foo=foo2`. NameRef + // Indexed describes indexed array variables, such as `foo=(bar baz)`. Indexed + // Associative describes associative array variables, such as `foo=([bar]=x [baz]=y)`. Associative + + // KeepValue is used by [WriteEnviron.Set] to signal that we are changing attributes + // about a variable, such as exporting it, without changing its value at all. + KeepValue + + // Deprecated: use [Unknown], as tracking whether or not a variable is set + // is now done via [Variable.Set]. + // Otherwise it was impossible to describe an unset variable with a known kind + // such as `declare -A foo`. + Unset = Unknown ) // Variable describes a shell variable, which can have a number of attributes // and a value. -// -// A Variable is unset if its Kind field is Unset, which can be checked via -// Variable.IsSet. The zero value of a Variable is thus a valid unset variable. -// -// If a variable is set, its Value field will be a []string if it is an indexed -// array, a map[string]string if it's an associative array, or a string -// otherwise. type Variable struct { + // Set is true when the variable has been set to a value, + // which may be empty. + Set bool + Local bool Exported bool ReadOnly bool + // Kind defines which of the value fields below should be used. Kind ValueKind Str string // Used when Kind is String or NameRef. @@ -77,10 +103,17 @@ type Variable struct { Map map[string]string // Used when Kind is Associative. } -// IsSet returns whether the variable is set. An empty variable is set, but an -// undeclared variable is not. +// IsSet reports whether the variable has been set to a value. +// The zero value of a Variable is unset. func (v Variable) IsSet() bool { - return v.Kind != Unset + return v.Set +} + +// Declared reports whether the variable has been declared. +// Declared variables may not be set; `export foo` is exported but not set to a value, +// and `declare -a foo` is an indexed array but not set to a value. +func (v Variable) Declared() bool { + return v.Set || v.Local || v.Exported || v.ReadOnly || v.Kind != Unknown } // String returns the variable's value as a string. In general, this only makes @@ -108,7 +141,7 @@ const maxNameRefDepth = 100 // name that was followed and the variable that it points to. func (v Variable) Resolve(env Environ) (string, Variable) { name := "" - for i := 0; i < maxNameRefDepth; i++ { + for range maxNameRefDepth { if v.Kind != NameRef { return name, v } @@ -119,7 +152,7 @@ func (v Variable) Resolve(env Environ) (string, Variable) { } // FuncEnviron wraps a function mapping variable names to their string values, -// and implements Environ. Empty strings returned by the function will be +// and implements [Environ]. Empty strings returned by the function will be // treated as unset variables. All variables will be exported. // // Note that the returned Environ's Each method will be a no-op. @@ -134,12 +167,12 @@ func (f funcEnviron) Get(name string) Variable { if value == "" { return Variable{} } - return Variable{Exported: true, Kind: String, Str: value} + return Variable{Set: true, Exported: true, Kind: String, Str: value} } func (f funcEnviron) Each(func(name string, vr Variable) bool) {} -// ListEnviron returns an Environ with the supplied variables, in the form +// ListEnviron returns an [Environ] with the supplied variables, in the form // "key=value". All variables will be exported. The last value in pairs is used // if multiple values are present. // @@ -149,23 +182,23 @@ func ListEnviron(pairs ...string) Environ { return listEnvironWithUpper(runtime.GOOS == "windows", pairs...) } -// listEnvironWithUpper implements ListEnviron, but letting the tests specify +// listEnvironWithUpper implements [ListEnviron], but letting the tests specify // whether to uppercase all names or not. func listEnvironWithUpper(upper bool, pairs ...string) Environ { - list := append([]string{}, pairs...) + list := slices.Clone(pairs) if upper { // Uppercase before sorting, so that we can remove duplicates // without the need for linear search nor a map. for i, s := range list { - if sep := strings.IndexByte(s, '='); sep > 0 { - list[i] = strings.ToUpper(s[:sep]) + s[sep:] + if name, val, ok := strings.Cut(s, "="); ok { + list[i] = strings.ToUpper(name) + "=" + val } } } - sort.SliceStable(list, func(i, j int) bool { - isep := strings.IndexByte(list[i], '=') - jsep := strings.IndexByte(list[j], '=') + slices.SortStableFunc(list, func(a, b string) int { + isep := strings.IndexByte(a, '=') + jsep := strings.IndexByte(b, '=') if isep < 0 { isep = 0 } else { @@ -176,22 +209,20 @@ func listEnvironWithUpper(upper bool, pairs ...string) Environ { } else { jsep += 1 } - return list[i][:isep] < list[j][:jsep] + return strings.Compare(a[:isep], b[:jsep]) }) last := "" for i := 0; i < len(list); { - s := list[i] - sep := strings.IndexByte(s, '=') - if sep <= 0 { + name, _, ok := strings.Cut(list[i], "=") + if name == "" || !ok { // invalid element; remove it - list = append(list[:i], list[i+1:]...) + list = slices.Delete(list, i, i+1) continue } - name := s[:sep] if last == name { // duplicate; the last one wins - list = append(list[:i-1], list[i:]...) + list = slices.Delete(list, i-1, i) continue } last = name @@ -204,23 +235,35 @@ func listEnvironWithUpper(upper bool, pairs ...string) Environ { type listEnviron []string func (l listEnviron) Get(name string) Variable { - prefix := name + "=" - i := sort.SearchStrings(l, prefix) - if i < len(l) && strings.HasPrefix(l[i], prefix) { - return Variable{Exported: true, Kind: String, Str: strings.TrimPrefix(l[i], prefix)} + eqpos := len(name) + endpos := len(name) + 1 + i, ok := slices.BinarySearchFunc(l, name, func(l, name string) int { + if len(l) < endpos { + // Too short; see if we are before or after the name. + return strings.Compare(l, name) + } + // Compare the name prefix, then the equal character. + c := strings.Compare(l[:eqpos], name) + eq := l[eqpos] + if c == 0 { + return cmp.Compare(eq, '=') + } + return c + }) + if ok { + return Variable{Set: true, Exported: true, Kind: String, Str: l[i][endpos:]} } return Variable{} } func (l listEnviron) Each(fn func(name string, vr Variable) bool) { for _, pair := range l { - i := strings.IndexByte(pair, '=') - if i < 0 { + name, value, ok := strings.Cut(pair, "=") + if !ok { // should never happen; see listEnvironWithUpper panic("expand.listEnviron: did not expect malformed name-value pair: " + pair) } - name, value := pair[:i], pair[i+1:] - if !fn(name, Variable{Exported: true, Kind: String, Str: value}) { + if !fn(name, Variable{Set: true, Exported: true, Kind: String, Str: value}) { return } } diff --git a/vendor/mvdan.cc/sh/v3/expand/expand.go b/vendor/mvdan.cc/sh/v3/expand/expand.go index 2619d846fd..48ebfbfcb0 100644 --- a/vendor/mvdan.cc/sh/v3/expand/expand.go +++ b/vendor/mvdan.cc/sh/v3/expand/expand.go @@ -4,19 +4,21 @@ package expand import ( - "bytes" + "cmp" "errors" "fmt" "io" "io/fs" + "iter" + "maps" "os" "os/user" "path/filepath" "regexp" "runtime" + "slices" "strconv" "strings" - "syscall" "mvdan.cc/sh/v3/pattern" "mvdan.cc/sh/v3/syntax" @@ -40,7 +42,7 @@ type Config struct { Env Environ // CmdSubst expands a command substitution node, writing its standard - // output to the provided io.Writer. + // output to the provided [io.Writer]. // // If nil, encountering a command substitution will result in an // UnexpectedCommandError. @@ -52,18 +54,26 @@ type Config struct { // this field might change until #451 is completely fixed. ProcSubst func(*syntax.ProcSubst) (string, error) - // TODO(v4): update to os.Readdir with fs.DirEntry. - // We could possibly expose that as a preferred ReadDir2 before then, - // to allow users to opt into better performance in v3. + // TODO(v4): replace ReadDir with ReadDir2. - // ReadDir is used for file path globbing. If nil, globbing is disabled. - // Use ioutil.ReadDir to use the filesystem directly. - ReadDir func(string) ([]os.FileInfo, error) + // ReadDir is the older form of [ReadDir2], before io/fs. + // + // Deprecated: use ReadDir2 instead. + ReadDir func(string) ([]fs.FileInfo, error) + + // ReadDir2 is used for file path globbing. + // If nil, and [ReadDir] is nil as well, globbing is disabled. + // Use [os.ReadDir] to use the filesystem directly. + ReadDir2 func(string) ([]fs.DirEntry, error) // GlobStar corresponds to the shell option that allows globbing with // "**". GlobStar bool + // NoCaseGlob corresponds to the shell option that causes case-insensitive + // pattern matching in pathname expansion. + NoCaseGlob bool + // NullGlob corresponds to the shell option that allows globbing // patterns which match nothing to result in zero fields. NullGlob bool @@ -72,7 +82,7 @@ type Config struct { // as errors. NoUnset bool - bufferAlloc bytes.Buffer // TODO: use strings.Builder + bufferAlloc strings.Builder fieldAlloc [4]fieldPart fieldsAlloc [4][]fieldPart @@ -83,7 +93,7 @@ type Config struct { } // UnexpectedCommandError is returned if a command substitution is encountered -// when Config.CmdSubst is nil. +// when [Config.CmdSubst] is nil. type UnexpectedCommandError struct { Node *syntax.CmdSubst } @@ -94,18 +104,31 @@ func (u UnexpectedCommandError) Error() string { var zeroConfig = &Config{} +// TODO: note that prepareConfig is modifying the user's config in place, +// which doesn't feel right - we should make a copy. + func prepareConfig(cfg *Config) *Config { - if cfg == nil { - cfg = zeroConfig - } - if cfg.Env == nil { - cfg.Env = FuncEnviron(func(string) string { return "" }) - } + cfg = cmp.Or(cfg, zeroConfig) + cfg.Env = cmp.Or(cfg.Env, FuncEnviron(func(string) string { return "" })) cfg.ifs = " \t\n" if vr := cfg.Env.Get("IFS"); vr.IsSet() { cfg.ifs = vr.String() } + + if cfg.ReadDir != nil && cfg.ReadDir2 == nil { + cfg.ReadDir2 = func(path string) ([]fs.DirEntry, error) { + infos, err := cfg.ReadDir(path) + if err != nil { + return nil, err + } + entries := make([]fs.DirEntry, len(infos)) + for i, info := range infos { + entries[i] = fs.FileInfoToDirEntry(info) + } + return entries, nil + } + } return cfg } @@ -126,7 +149,7 @@ func (cfg *Config) ifsJoin(strs []string) string { return strings.Join(strs, sep) } -func (cfg *Config) strBuilder() *bytes.Buffer { +func (cfg *Config) strBuilder() *strings.Builder { b := &cfg.bufferAlloc b.Reset() return b @@ -141,10 +164,10 @@ func (cfg *Config) envSet(name, value string) error { if !ok { return fmt.Errorf("environment is read-only") } - return wenv.Set(name, Variable{Kind: String, Str: value}) + return wenv.Set(name, Variable{Set: true, Kind: String, Str: value}) } -// Literal expands a single shell word. It is similar to Fields, but the result +// Literal expands a single shell word. It is similar to [Fields], but the result // is a single string. This is the behavior when a word is used as the value in // a shell variable assignment, for example. // @@ -162,8 +185,8 @@ func Literal(cfg *Config, word *syntax.Word) (string, error) { return cfg.fieldJoin(field), nil } -// Document expands a single shell word as if it were within double quotes. It -// is similar to Literal, but without brace expansion, tilde expansion, and +// Document expands a single shell word as if it were a here-document body. +// It is similar to [Literal], but without brace expansion, tilde expansion, and // globbing. // // The config specifies shell expansion options; nil behaves the same as an @@ -173,36 +196,37 @@ func Document(cfg *Config, word *syntax.Word) (string, error) { return "", nil } cfg = prepareConfig(cfg) - field, err := cfg.wordField(word.Parts, quoteDouble) + field, err := cfg.wordField(word.Parts, quoteHeredoc) if err != nil { return "", err } return cfg.fieldJoin(field), nil } -const patMode = pattern.Filenames | pattern.Braces - -// Pattern expands a single shell word as a pattern, using syntax.QuotePattern +// Pattern expands a single shell word as a pattern, using [pattern.QuoteMeta] // on any non-quoted parts of the input word. The result can be used on -// syntax.TranslatePattern directly. +// [pattern.Regexp] directly. // // The config specifies shell expansion options; nil behaves the same as an // empty config. func Pattern(cfg *Config, word *syntax.Word) (string, error) { + if word == nil { + return "", nil + } cfg = prepareConfig(cfg) field, err := cfg.wordField(word.Parts, quoteNone) if err != nil { return "", err } - buf := cfg.strBuilder() + sb := cfg.strBuilder() for _, part := range field { if part.quote > quoteNone { - buf.WriteString(pattern.QuoteMeta(part.val, patMode)) + sb.WriteString(pattern.QuoteMeta(part.val, 0)) } else { - buf.WriteString(part.val) + sb.WriteString(part.val) } } - return buf.String(), nil + return sb.String(), nil } // Format expands a format string with a number of arguments, following the @@ -214,7 +238,17 @@ func Pattern(cfg *Config, word *syntax.Word) (string, error) { // empty config. func Format(cfg *Config, format string, args []string) (string, int, error) { cfg = prepareConfig(cfg) - buf := cfg.strBuilder() + sb := cfg.strBuilder() + + consumed, err := formatInto(sb, format, args) + if err != nil { + return "", 0, err + } + + return sb.String(), consumed, err +} + +func formatInto(sb *strings.Builder, format string, args []string) (int, error) { var fmts []byte initialArgs := len(args) @@ -244,34 +278,35 @@ formatLoop: i++ switch c = format[i]; c { case 'a': // bell - buf.WriteByte('\a') + sb.WriteByte('\a') case 'b': // backspace - buf.WriteByte('\b') + sb.WriteByte('\b') case 'e', 'E': // escape - buf.WriteByte('\x1b') + sb.WriteByte('\x1b') case 'f': // form feed - buf.WriteByte('\f') + sb.WriteByte('\f') case 'n': // new line - buf.WriteByte('\n') + sb.WriteByte('\n') case 'r': // carriage return - buf.WriteByte('\r') + sb.WriteByte('\r') case 't': // horizontal tab - buf.WriteByte('\t') + sb.WriteByte('\t') case 'v': // vertical tab - buf.WriteByte('\v') + sb.WriteByte('\v') case '\\', '\'', '"', '?': // just the character - buf.WriteByte(c) + sb.WriteByte(c) case '0', '1', '2', '3', '4', '5', '6', '7': digits := readDigits(3, false) // if digits don't fit in 8 bits, 0xff via strconv n, _ := strconv.ParseUint(digits, 8, 8) - buf.WriteByte(byte(n)) + sb.WriteByte(byte(n)) case 'x', 'u', 'U': i++ max := 2 - if c == 'u' { + switch c { + case 'u': max = 4 - } else if c == 'U' { + case 'U': max = 8 } digits := readDigits(max, true) @@ -285,21 +320,21 @@ formatLoop: } if c == 'x' { // always as a single byte - buf.WriteByte(byte(n)) + sb.WriteByte(byte(n)) } else { - buf.WriteRune(rune(n)) + sb.WriteRune(rune(n)) } break } fallthrough default: // no escape sequence - buf.WriteByte('\\') - buf.WriteByte(c) + sb.WriteByte('\\') + sb.WriteByte(c) } case len(fmts) > 0: switch c { case '%': - buf.WriteByte('%') + sb.WriteByte('%') fmts = nil case 'c': var b byte @@ -310,22 +345,30 @@ formatLoop: b = arg[0] } } - buf.WriteByte(b) + sb.WriteByte(b) fmts = nil case '+', '-', ' ': if len(fmts) > 1 { - return "", 0, fmt.Errorf("invalid format char: %c", c) + return 0, fmt.Errorf("invalid format char: %c", c) } fmts = append(fmts, c) case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': fmts = append(fmts, c) - case 's', 'd', 'i', 'u', 'o', 'x': + case 's', 'b', 'd', 'i', 'u', 'o', 'x': arg := "" if len(args) > 0 { arg, args = args[0], args[1:] } - var farg interface{} = arg - if c != 's' { + var farg any + if c == 'b' { + // Passing in nil for args ensures that % format + // strings aren't processed; only escape sequences + // will be handled. + _, err := formatInto(sb, arg, nil) + if err != nil { + return 0, err + } + } else if c != 's' { n, _ := strconv.ParseInt(arg, 0, 0) if c == 'i' || c == 'd' { farg = int(n) @@ -335,25 +378,29 @@ formatLoop: if c == 'i' || c == 'u' { c = 'd' } + } else { + farg = arg + } + if farg != nil { + fmts = append(fmts, c) + fmt.Fprintf(sb, string(fmts), farg) } - fmts = append(fmts, c) - fmt.Fprintf(buf, string(fmts), farg) fmts = nil default: - return "", 0, fmt.Errorf("invalid format char: %c", c) + return 0, fmt.Errorf("invalid format char: %c", c) } case args != nil && c == '%': // if args == nil, we are not doing format // arguments fmts = []byte{c} default: - buf.WriteByte(c) + sb.WriteByte(c) } } if len(fmts) > 0 { - return "", 0, fmt.Errorf("missing format char") + return 0, fmt.Errorf("missing format char") } - return buf.String(), initialArgs - len(args), nil + return initialArgs - len(args), nil } func (cfg *Config) fieldJoin(parts []fieldPart) string { @@ -363,70 +410,91 @@ func (cfg *Config) fieldJoin(parts []fieldPart) string { case 1: // short-cut without a string copy return parts[0].val } - buf := cfg.strBuilder() + sb := cfg.strBuilder() for _, part := range parts { - buf.WriteString(part.val) + sb.WriteString(part.val) } - return buf.String() + return sb.String() } func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) { - buf := cfg.strBuilder() + sb := cfg.strBuilder() for _, part := range parts { if part.quote > quoteNone { - buf.WriteString(pattern.QuoteMeta(part.val, patMode)) + sb.WriteString(pattern.QuoteMeta(part.val, 0)) continue } - buf.WriteString(part.val) - if pattern.HasMeta(part.val, patMode) { + sb.WriteString(part.val) + if pattern.HasMeta(part.val, 0) { glob = true } } if glob { // only copy the string if it will be used - escaped = buf.String() + escaped = sb.String() } return escaped, glob } -// Fields expands a number of words as if they were arguments in a shell +// Fields is a pre-iterators API which now wraps [FieldsSeq]. +func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) { + var fields []string + for s, err := range FieldsSeq(cfg, words...) { + if err != nil { + return nil, err + } + fields = append(fields, s) + } + return fields, nil +} + +// FieldsSeq expands a number of words as if they were arguments in a shell // command. This includes brace expansion, tilde expansion, parameter expansion, // command substitution, arithmetic expansion, and quote removal. -func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) { +func FieldsSeq(cfg *Config, words ...*syntax.Word) iter.Seq2[string, error] { cfg = prepareConfig(cfg) - fields := make([]string, 0, len(words)) dir := cfg.envGet("PWD") - for _, word := range words { - word := *word // make a copy, since SplitBraces replaces the Parts slice - afterBraces := []*syntax.Word{&word} - if syntax.SplitBraces(&word) { - afterBraces = Braces(&word) - } - for _, word2 := range afterBraces { - wfields, err := cfg.wordFields(word2.Parts) - if err != nil { - return nil, err + return func(yield func(string, error) bool) { + for _, word := range words { + word := *word // make a copy, since SplitBraces replaces the Parts slice + afterBraces := []*syntax.Word{&word} + if syntax.SplitBraces(&word) { + afterBraces = Braces(&word) } - for _, field := range wfields { - path, doGlob := cfg.escapedGlobField(field) - var matches []string - var syntaxError *pattern.SyntaxError - if doGlob && cfg.ReadDir != nil { - matches, err = cfg.glob(dir, path) - if !errors.As(err, &syntaxError) { + for _, word2 := range afterBraces { + wfields, err := cfg.wordFields(word2.Parts) + if err != nil { + yield("", err) + return + } + for _, field := range wfields { + path, doGlob := cfg.escapedGlobField(field) + if doGlob && cfg.ReadDir2 != nil { + // Note that globbing requires keeping a slice state, so it doesn't + // really benefit from using an iterator. + matches, err := cfg.glob(dir, path) if err != nil { - return nil, err - } - if len(matches) > 0 || cfg.NullGlob { - fields = append(fields, matches...) + // We avoid [errors.As] as it allocates, + // and we know that [Config.glob] returns [pattern.Regexp] errors without wrapping. + if _, ok := err.(*pattern.SyntaxError); !ok { + yield("", err) + return + } + } else if len(matches) > 0 || cfg.NullGlob { + for _, m := range matches { + if !yield(m, nil) { + return + } + } continue } } + if !yield(cfg.fieldJoin(field), nil) { + return + } } - fields = append(fields, cfg.fieldJoin(field)) } } } - return fields, nil } type fieldPart struct { @@ -439,48 +507,53 @@ type quoteLevel uint const ( quoteNone quoteLevel = iota quoteDouble + quoteHeredoc quoteSingle ) func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) { var field []fieldPart for i, wp := range wps { - switch x := wp.(type) { + switch wp := wp.(type) { case *syntax.Lit: - s := x.Value + s := wp.Value if i == 0 && ql == quoteNone { - if prefix, rest := cfg.expandUser(s); prefix != "" { + if prefix, rest := cfg.expandUser(s, len(wps) > 1); prefix != "" { // TODO: return two separate fieldParts, // like in wordFields? s = prefix + rest } } - if ql == quoteDouble && strings.Contains(s, "\\") { - buf := cfg.strBuilder() + if (ql == quoteDouble || ql == quoteHeredoc) && strings.Contains(s, "\\") { + sb := cfg.strBuilder() for i := 0; i < len(s); i++ { b := s[i] if b == '\\' && i+1 < len(s) { switch s[i+1] { - case '"', '\\', '$', '`': // special chars - continue + case '"': + if ql != quoteDouble { + break + } + fallthrough + case '\\', '$', '`': // special chars + i++ + b = s[i] // write the special char, skipping the backslash } } - buf.WriteByte(b) + sb.WriteByte(b) } - s = buf.String() - } - if i := strings.IndexByte(s, '\x00'); i >= 0 { - s = s[:i] + s = sb.String() } + s, _, _ = strings.Cut(s, "\x00") field = append(field, fieldPart{val: s}) case *syntax.SglQuoted: - fp := fieldPart{quote: quoteSingle, val: x.Value} - if x.Dollar { + fp := fieldPart{quote: quoteSingle, val: wp.Value} + if wp.Dollar { fp.val, _, _ = Format(cfg, fp.val, nil) } field = append(field, fp) case *syntax.DblQuoted: - wfield, err := cfg.wordField(x.Parts, quoteDouble) + wfield, err := cfg.wordField(wp.Parts, quoteDouble) if err != nil { return nil, err } @@ -489,31 +562,33 @@ func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, field = append(field, part) } case *syntax.ParamExp: - val, err := cfg.paramExp(x) + val, err := cfg.paramExp(wp) if err != nil { return nil, err } field = append(field, fieldPart{val: val}) case *syntax.CmdSubst: - val, err := cfg.cmdSubst(x) + val, err := cfg.cmdSubst(wp) if err != nil { return nil, err } field = append(field, fieldPart{val: val}) case *syntax.ArithmExp: - n, err := Arithm(cfg, x.X) + n, err := Arithm(cfg, wp.X) if err != nil { return nil, err } field = append(field, fieldPart{val: strconv.Itoa(n)}) case *syntax.ProcSubst: - path, err := cfg.ProcSubst(x) + path, err := cfg.ProcSubst(wp) if err != nil { return nil, err } field = append(field, fieldPart{val: path}) + case *syntax.ExtGlob: + return nil, fmt.Errorf("extended globbing is not supported") default: - panic(fmt.Sprintf("unhandled word part: %T", x)) + panic(fmt.Sprintf("unhandled word part: %T", wp)) } } return field, nil @@ -523,14 +598,12 @@ func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) { if cfg.CmdSubst == nil { return "", UnexpectedCommandError{Node: cs} } - buf := cfg.strBuilder() - if err := cfg.CmdSubst(buf, cs); err != nil { + sb := cfg.strBuilder() + if err := cfg.CmdSubst(sb, cs); err != nil { return "", err } - out := buf.String() - if strings.IndexByte(out, '\x00') >= 0 { - out = strings.ReplaceAll(out, "\x00", "") - } + out := sb.String() + out = strings.ReplaceAll(out, "\x00", "") return strings.TrimRight(out, "\n"), nil } @@ -546,19 +619,30 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { curField = nil } splitAdd := func(val string) { - for i, field := range strings.FieldsFunc(val, cfg.ifsRune) { - if i > 0 { + fieldStart := -1 + for i, r := range val { + if cfg.ifsRune(r) { + if fieldStart >= 0 { // ending a field + curField = append(curField, fieldPart{val: val[fieldStart:i]}) + fieldStart = -1 + } flush() + } else { + if fieldStart < 0 { // starting a new field + fieldStart = i + } } - curField = append(curField, fieldPart{val: field}) + } + if fieldStart >= 0 { // ending a field without IFS + curField = append(curField, fieldPart{val: val[fieldStart:]}) } } for i, wp := range wps { - switch x := wp.(type) { + switch wp := wp.(type) { case *syntax.Lit: - s := x.Value + s := wp.Value if i == 0 { - prefix, rest := cfg.expandUser(s) + prefix, rest := cfg.expandUser(s, len(wps) > 1) curField = append(curField, fieldPart{ quote: quoteSingle, val: prefix, @@ -566,7 +650,7 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { s = rest } if strings.Contains(s, "\\") { - buf := cfg.strBuilder() + sb := cfg.strBuilder() for i := 0; i < len(s); i++ { b := s[i] if b == '\\' { @@ -575,21 +659,21 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { } b = s[i] } - buf.WriteByte(b) + sb.WriteByte(b) } - s = buf.String() + s = sb.String() } curField = append(curField, fieldPart{val: s}) case *syntax.SglQuoted: allowEmpty = true - fp := fieldPart{quote: quoteSingle, val: x.Value} - if x.Dollar { + fp := fieldPart{quote: quoteSingle, val: wp.Value} + if wp.Dollar { fp.val, _, _ = Format(cfg, fp.val, nil) } curField = append(curField, fp) case *syntax.DblQuoted: - if len(x.Parts) == 1 { - pe, _ := x.Parts[0].(*syntax.ParamExp) + if len(wp.Parts) == 1 { + pe, _ := wp.Parts[0].(*syntax.ParamExp) if elems := cfg.quotedElemFields(pe); elems != nil { for i, elem := range elems { if i > 0 { @@ -604,7 +688,7 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { } } allowEmpty = true - wfield, err := cfg.wordField(x.Parts, quoteDouble) + wfield, err := cfg.wordField(wp.Parts, quoteDouble) if err != nil { return nil, err } @@ -613,31 +697,33 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { curField = append(curField, part) } case *syntax.ParamExp: - val, err := cfg.paramExp(x) + val, err := cfg.paramExp(wp) if err != nil { return nil, err } splitAdd(val) case *syntax.CmdSubst: - val, err := cfg.cmdSubst(x) + val, err := cfg.cmdSubst(wp) if err != nil { return nil, err } splitAdd(val) case *syntax.ArithmExp: - n, err := Arithm(cfg, x.X) + n, err := Arithm(cfg, wp.X) if err != nil { return nil, err } curField = append(curField, fieldPart{val: strconv.Itoa(n)}) case *syntax.ProcSubst: - path, err := cfg.ProcSubst(x) + path, err := cfg.ProcSubst(wp) if err != nil { return nil, err } splitAdd(path) + case *syntax.ExtGlob: + return nil, fmt.Errorf("extended globbing is not supported") default: - panic(fmt.Sprintf("unhandled word part: %T", x)) + panic(fmt.Sprintf("unhandled word part: %T", wp)) } } flush() @@ -648,31 +734,51 @@ func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { } // quotedElemFields returns the list of elements resulting from a quoted -// parameter expansion if it was in the form of ${*}, ${@}, ${foo[*], ${foo[@]}, -// or ${!foo@}. +// parameter expansion that should be treated especially, like "${foo[@]}". func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string { if pe == nil || pe.Length || pe.Width { return nil } + name := pe.Param.Value if pe.Excl { - if pe.Names == syntax.NamesPrefixWords { + switch pe.Names { + case syntax.NamesPrefixWords: // "${!prefix@}" return cfg.namesByPrefix(pe.Param.Value) + case syntax.NamesPrefix: // "${!prefix*}" + return nil + } + switch nodeLit(pe.Index) { + case "@": // "${!name[@]}" + switch vr := cfg.Env.Get(name); vr.Kind { + case Indexed: + // TODO: if an indexed array only has elements 0 and 10, + // we should not return all indices in between those. + keys := make([]string, 0, len(vr.List)) + for key := range vr.List { + keys = append(keys, strconv.Itoa(key)) + } + return keys + case Associative: + return slices.Collect(maps.Keys(vr.Map)) + } } return nil } - name := pe.Param.Value switch name { - case "*": + case "*": // "${*}" return []string{cfg.ifsJoin(cfg.Env.Get(name).List)} - case "@": + case "@": // "${@}" return cfg.Env.Get(name).List } switch nodeLit(pe.Index) { - case "@": - if vr := cfg.Env.Get(name); vr.Kind == Indexed { + case "@": // "${name[@]}" + switch vr := cfg.Env.Get(name); vr.Kind { + case Indexed: return vr.List + case Associative: + return slices.Collect(maps.Values(vr.Map)) } - case "*": + case "*": // "${name[*]}" if vr := cfg.Env.Get(name); vr.Kind == Indexed { return []string{cfg.ifsJoin(vr.List)} } @@ -680,19 +786,26 @@ func (cfg *Config) quotedElemFields(pe *syntax.ParamExp) []string { return nil } -func (cfg *Config) expandUser(field string) (prefix, rest string) { - if len(field) == 0 || field[0] != '~' { +func (cfg *Config) expandUser(field string, moreFields bool) (prefix, rest string) { + name, ok := strings.CutPrefix(field, "~") + if !ok { + // No tilde prefix to expand, e.g. "foo". + return "", field + } + i := strings.IndexByte(name, '/') + if i < 0 && moreFields { + // There is a tilde prefix, but followed by more fields, e.g. "~'foo'". + // We only proceed if an unquoted slash was found in this field, e.g. "~/'foo'". return "", field } - name := field[1:] - if i := strings.Index(name, "/"); i >= 0 { + if i >= 0 { rest = name[i:] name = name[:i] } if name == "" { // Current user; try via "HOME", otherwise fall back to the // system's appropriate home dir env var. Don't use os/user, as - // that's overkill. We can't use os.UserHomeDir, because we want + // that's overkill. We can't use [os.UserHomeDir], because we want // to use cfg.Env, and we always want to check "HOME" first. if vr := cfg.Env.Get("HOME"); vr.IsSet() { @@ -732,7 +845,7 @@ func findAllIndex(pat, name string, n int) [][]int { var rxGlobStar = regexp.MustCompile(".*") -// pathJoin2 is a simpler version of filepath.Join without cleaning the result, +// pathJoin2 is a simpler version of [filepath.Join] without cleaning the result, // since that's needed for globbing. func pathJoin2(elem1, elem2 string) string { if elem1 == "" { @@ -745,7 +858,7 @@ func pathJoin2(elem1, elem2 string) string { } // pathSplit splits a file path into its elements, retaining empty ones. Before -// splitting, slashes are replaced with filepath.Separator, so that splitting +// splitting, slashes are replaced with [filepath.Separator], so that splitting // Unix paths on Windows works as well. func pathSplit(path string) []string { path = filepath.FromSlash(path) @@ -769,11 +882,11 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { // TODO: as an optimization, we could do chunks of the path all at once, // like doing a single stat for "/foo/bar" in "/foo/bar/*". - // TODO: Another optimization would be to reduce the number of ReadDir calls. + // TODO: Another optimization would be to reduce the number of ReadDir2 calls. // For example, /foo/* can end up doing one duplicate call: // - // ReadDir("/foo") to ensure that "/foo/" exists and only matches a directory - // ReadDir("/foo") glob "*" + // ReadDir2("/foo") to ensure that "/foo/" exists and only matches a directory + // ReadDir2("/foo") glob "*" for i, part := range parts { // Keep around for debugging. @@ -786,7 +899,7 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { matches[i] = pathJoin2(dir, part) } continue - case !pattern.HasMeta(part, patMode): + case !pattern.HasMeta(part, 0): var newMatches []string for _, dir := range matches { match := dir @@ -794,17 +907,15 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { match = filepath.Join(base, match) } match = pathJoin2(match, part) - // We can't use ReadDir on the parent and match the directory + // We can't use [Config.ReadDir2] on the parent and match the directory // entry by name, because short paths on Windows break that. - // Our only option is to ReadDir on the directory entry itself, + // Our only option is to [Config.ReadDir2] on the directory entry itself, // which can be wasteful if we only want to see if it exists, // but at least it's correct in all scenarios. - if _, err := cfg.ReadDir(match); err != nil { - const errPathNotFound = syscall.Errno(3) // from syscall/types_windows.go, to avoid a build tag - var pathErr *os.PathError - if runtime.GOOS == "windows" && errors.As(err, &pathErr) && pathErr.Err == errPathNotFound { - // Unfortunately, os.File.Readdir on a regular file on - // Windows returns an error that satisfies ErrNotExist. + if _, err := cfg.ReadDir2(match); err != nil { + if isWindowsErrPathNotFound(err) { + // Unfortunately, [os.File.Readdir] on a regular file on + // Windows returns an error that satisfies [fs.ErrNotExist]. // Luckily, it returns a special "path not found" rather // than the normal "file not found" for missing files, // so we can use that knowledge to work around the bug. @@ -827,10 +938,10 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { // and to avoid recursion, we use a slice as a stack. // Since we pop from the back, we populate the stack backwards. stack := make([]string, 0, len(matches)) - for i := len(matches) - 1; i >= 0; i-- { + for _, match := range slices.Backward(matches) { // "a/**" should match "a/ a/b a/b/cfg ..."; // note how the zero-match case has a trailing separator. - stack = append(stack, pathJoin2(matches[i], "")) + stack = append(stack, pathJoin2(match, "")) } matches = matches[:0] var newMatches []string // to reuse its capacity @@ -846,17 +957,21 @@ func (cfg *Config) glob(base, pat string) ([]string, error) { // If dir is not a directory, we keep the stack as-is and continue. newMatches = newMatches[:0] newMatches, _ = cfg.globDir(base, dir, rxGlobStar, wantDir, newMatches) - for i := len(newMatches) - 1; i >= 0; i-- { - stack = append(stack, newMatches[i]) + for _, match := range slices.Backward(newMatches) { + stack = append(stack, match) } } continue } - expr, err := pattern.Regexp(part, pattern.Filenames) + mode := pattern.Filenames | pattern.EntireString | pattern.NoGlobStar + if cfg.NoCaseGlob { + mode |= pattern.NoGlobCase + } + expr, err := pattern.Regexp(part, mode) if err != nil { return nil, err } - rx := regexp.MustCompile("^" + expr + "$") + rx := regexp.MustCompile(expr) var newMatches []string for _, dir := range matches { newMatches, err = cfg.globDir(base, dir, rx, wantDir, newMatches) @@ -874,7 +989,7 @@ func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, ma if !filepath.IsAbs(dir) { fullDir = filepath.Join(base, dir) } - infos, err := cfg.ReadDir(fullDir) + infos, err := cfg.ReadDir2(fullDir) if err != nil { // We still want to return matches, for the sake of reusing slices. return matches, err @@ -883,22 +998,19 @@ func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, ma name := info.Name() if !wantDir { // No filtering. - } else if mode := info.Mode(); mode&os.ModeSymlink != 0 { + } else if mode := info.Type(); mode&os.ModeSymlink != 0 { // We need to know if the symlink points to a directory. - // This requires an extra syscall, as ReadDir on the parent directory + // This requires an extra syscall, as [Config.ReadDir] on the parent directory // does not follow symlinks for each of the directory entries. // ReadDir is somewhat wasteful here, as we only want its error result, - // but we could try to reuse its result as per the TODO in Config.glob. - if _, err := cfg.ReadDir(filepath.Join(fullDir, info.Name())); err != nil { + // but we could try to reuse its result as per the TODO in [Config.glob]. + if _, err := cfg.ReadDir2(filepath.Join(fullDir, info.Name())); err != nil { continue } } else if !mode.IsDir() { // Not a symlink nor a directory. continue } - if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' { - continue - } if rx.MatchString(name) { matches = append(matches, pathJoin2(dir, name)) } @@ -906,7 +1018,8 @@ func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, ma return matches, nil } -// ReadFields TODO write doc. +// ReadFields splits and returns n fields from s, like the "read" shell builtin. +// If raw is set, backslash escape sequences are not interpreted. // // The config specifies shell expansion options; nil behaves the same as an // empty config. diff --git a/vendor/mvdan.cc/sh/v3/expand/expand_nonwindows.go b/vendor/mvdan.cc/sh/v3/expand/expand_nonwindows.go new file mode 100644 index 0000000000..38b1b4cb56 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/expand/expand_nonwindows.go @@ -0,0 +1,8 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +//go:build !windows + +package expand + +func isWindowsErrPathNotFound(error) bool { return false } diff --git a/vendor/mvdan.cc/sh/v3/expand/expand_windows.go b/vendor/mvdan.cc/sh/v3/expand/expand_windows.go new file mode 100644 index 0000000000..8596381240 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/expand/expand_windows.go @@ -0,0 +1,15 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +package expand + +import ( + "errors" + "os" + "syscall" +) + +func isWindowsErrPathNotFound(err error) bool { + var pathErr *os.PathError + return errors.As(err, &pathErr) && pathErr.Err == syscall.ERROR_PATH_NOT_FOUND +} diff --git a/vendor/mvdan.cc/sh/v3/expand/param.go b/vendor/mvdan.cc/sh/v3/expand/param.go index da77715512..757a5dff0b 100644 --- a/vendor/mvdan.cc/sh/v3/expand/param.go +++ b/vendor/mvdan.cc/sh/v3/expand/param.go @@ -5,8 +5,9 @@ package expand import ( "fmt" + "maps" "regexp" - "sort" + "slices" "strconv" "strings" "unicode" @@ -65,13 +66,13 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { // This is the only parameter expansion that the environment // interface cannot satisfy. line := uint64(cfg.curParam.Pos().Line()) - vr = Variable{Kind: String, Str: strconv.FormatUint(line, 10)} + vr = Variable{Set: true, Kind: String, Str: strconv.FormatUint(line, 10)} default: vr = cfg.Env.Get(name) } orig := vr _, vr = vr.Resolve(cfg.Env) - if cfg.NoUnset && vr.Kind == Unset && !overridingUnset(pe) { + if cfg.NoUnset && !vr.IsSet() && !overridingUnset(pe) { return "", UnsetParameterError{ Node: pe, Message: "unbound variable", @@ -106,7 +107,7 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { switch nodeLit(index) { case "@", "*": switch vr.Kind { - case Unset: + case Unknown: elems = nil indexAllElements = true case Indexed: @@ -160,23 +161,23 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { strs = cfg.namesByPrefix(pe.Param.Value) case orig.Kind == NameRef: strs = append(strs, orig.Str) - case vr.Kind == Indexed: + case pe.Index != nil && vr.Kind == Indexed: for i, e := range vr.List { if e != "" { strs = append(strs, strconv.Itoa(i)) } } - case vr.Kind == Associative: - for k := range vr.Map { - strs = append(strs, k) - } - case !syntax.ValidName(str): + case pe.Index != nil && vr.Kind == Associative: + strs = slices.AppendSeq(strs, maps.Keys(vr.Map)) + case !vr.IsSet(): return "", fmt.Errorf("invalid indirect expansion") + case str == "": + return "", nil default: vr = cfg.Env.Get(str) strs = append(strs, vr.String()) } - sort.Strings(strs) + slices.Sort(strs) str = strings.Join(strs, " ") case pe.Slice != nil: if callVarInd { @@ -197,13 +198,15 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { if pe.Slice.Length != nil { str = str[:slicePos(sliceLen)] } - } else { // elems are already sliced - } + } // else, elems are already sliced case pe.Repl != nil: orig, err := Pattern(cfg, pe.Repl.Orig) if err != nil { return "", err } + if orig == "" { + break // nothing to replace + } with, err := Literal(cfg, pe.Repl.With) if err != nil { return "", err @@ -213,15 +216,15 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { n = -1 } locs := findAllIndex(orig, str, n) - buf := cfg.strBuilder() + sb := cfg.strBuilder() last := 0 for _, loc := range locs { - buf.WriteString(str[last:loc[0]]) - buf.WriteString(with) + sb.WriteString(str[last:loc[0]]) + sb.WriteString(with) last = loc[1] } - buf.WriteString(str[last:]) - str = buf.String() + sb.WriteString(str[last:]) + str = sb.String() case pe.Exp != nil: arg, err := Literal(cfg, pe.Exp.Word) if err != nil { @@ -395,11 +398,7 @@ func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) { case Associative: switch lit := nodeLit(idx); lit { case "@", "*": - strs := make([]string, 0, len(vr.Map)) - for _, val := range vr.Map { - strs = append(strs, val) - } - sort.Strings(strs) + strs := slices.Sorted(maps.Values(vr.Map)) if lit == "*" { return cfg.ifsJoin(strs), nil } @@ -416,11 +415,10 @@ func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) { func (cfg *Config) namesByPrefix(prefix string) []string { var names []string - cfg.Env.Each(func(name string, vr Variable) bool { + for name := range cfg.Env.Each { if strings.HasPrefix(name, prefix) { names = append(names, name) } - return true - }) + } return names } diff --git a/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go b/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go new file mode 100644 index 0000000000..b98f03ef32 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go @@ -0,0 +1,28 @@ +// Code generated by "stringer -type=ValueKind"; DO NOT EDIT. + +package expand + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Unknown-0] + _ = x[String-1] + _ = x[NameRef-2] + _ = x[Indexed-3] + _ = x[Associative-4] + _ = x[KeepValue-5] +} + +const _ValueKind_name = "UnknownStringNameRefIndexedAssociativeKeepValue" + +var _ValueKind_index = [...]uint8{0, 7, 13, 20, 27, 38, 47} + +func (i ValueKind) String() string { + if i >= ValueKind(len(_ValueKind_index)-1) { + return "ValueKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ValueKind_name[_ValueKind_index[i]:_ValueKind_index[i+1]] +} diff --git a/vendor/mvdan.cc/sh/v3/fileutil/file.go b/vendor/mvdan.cc/sh/v3/fileutil/file.go index 629724892e..9c22445d57 100644 --- a/vendor/mvdan.cc/sh/v3/fileutil/file.go +++ b/vendor/mvdan.cc/sh/v3/fileutil/file.go @@ -1,19 +1,18 @@ // Copyright (c) 2016, Daniel Martí // See LICENSE for licensing information -// Package fileutil contains code to work with shell files, also known -// as shell scripts. +// Package fileutil allows inspecting shell files, such as detecting whether a +// file may be shell or extracting its shebang. package fileutil import ( "io/fs" - "os" "regexp" "strings" ) var ( - shebangRe = regexp.MustCompile(`^#!\s?/(usr/)?bin/(env\s+)?(sh|bash|mksh|bats|zsh)(\s|$)`) + shebangRe = regexp.MustCompile(`^#![ \t]*/(usr/)?bin/(env[ \t]+)?(sh|bash|mksh|bats|zsh)(\s|$)`) extRe = regexp.MustCompile(`\.(sh|bash|mksh|bats|zsh)$`) ) @@ -49,8 +48,8 @@ const ( ConfNotScript ScriptConfidence = iota // ConfIfShebang describes files which might be shell scripts, depending - // on the shebang line in the file's contents. Since CouldBeScript only - // works on os.FileInfo, the answer in this case can't be final. + // on the shebang line in the file's contents. Since [CouldBeScript] only + // works on [fs.FileInfo], the answer in this case can't be final. ConfIfShebang // ConfIsScript describes files which are definitely shell scripts, @@ -60,42 +59,26 @@ const ( // CouldBeScript is a shortcut for CouldBeScript2(fs.FileInfoToDirEntry(info)). // -// Deprecated: prefer CouldBeScript2, which usually requires fewer syscalls. -func CouldBeScript(info os.FileInfo) ScriptConfidence { - // TODO: once we drop support for Go 1.16, - // make use of this Go 1.17 API instead: - // return CouldBeScript2(fs.FileInfoToDirEntry(info)) - - name := info.Name() - switch { - case info.IsDir(), name[0] == '.': - return ConfNotScript - case info.Mode()&os.ModeSymlink != 0: - return ConfNotScript - case extRe.MatchString(name): - return ConfIsScript - case strings.IndexByte(name, '.') > 0: - return ConfNotScript // different extension - default: - return ConfIfShebang - } +// Deprecated: prefer [CouldBeScript2], which usually requires fewer syscalls. +func CouldBeScript(info fs.FileInfo) ScriptConfidence { + return CouldBeScript2(fs.FileInfoToDirEntry(info)) } // CouldBeScript2 reports how likely a directory entry is to be a shell script. -// It discards directories, symlinks, hidden files and files with non-shell -// extensions. +// It discards directories and other non-regular files like symbolic links, +// filenames beginning with '.', and files with non-shell extensions. func CouldBeScript2(entry fs.DirEntry) ScriptConfidence { name := entry.Name() switch { - case entry.IsDir(), name[0] == '.': - return ConfNotScript - case entry.Type()&os.ModeSymlink != 0: - return ConfNotScript + case name[0] == '.': + return ConfNotScript // '.' prefix (hidden file) + case !entry.Type().IsRegular(): + return ConfNotScript // dir, symlink, named pipes, etc case extRe.MatchString(name): - return ConfIsScript + return ConfIsScript // shell extension case strings.IndexByte(name, '.') > 0: - return ConfNotScript // different extension + return ConfNotScript // non-shell extension default: - return ConfIfShebang + return ConfIfShebang // no extension; read and look for a shebang } } diff --git a/vendor/mvdan.cc/sh/v3/interp/api.go b/vendor/mvdan.cc/sh/v3/interp/api.go index 0648bd9db6..795112837a 100644 --- a/vendor/mvdan.cc/sh/v3/interp/api.go +++ b/vendor/mvdan.cc/sh/v3/interp/api.go @@ -4,6 +4,13 @@ // Package interp implements an interpreter that executes shell // programs. It aims to support POSIX, but its support is not complete // yet. It also supports some Bash features. +// +// The interpreter generally aims to behave like Bash, +// but it does not support all of its features. +// +// The interpreter currently aims to behave like a non-interactive shell, +// which is how most shells run scripts, and is more useful to machines. +// In the future, it may gain an option to behave like an interactive shell. package interp import ( @@ -11,43 +18,52 @@ import ( "errors" "fmt" "io" - "math/rand" + "io/fs" + "maps" "os" "path/filepath" + "slices" "strconv" - "sync" "time" - "golang.org/x/sync/errgroup" - "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) // A Runner interprets shell programs. It can be reused, but it is not safe for -// concurrent use. You should typically use New to build a new Runner. +// concurrent use. Use [New] to build a new Runner. // // Note that writes to Stdout and Stderr may be concurrent if background -// commands are used. If you plan on using an io.Writer implementation that +// commands are used. If you plan on using an [io.Writer] implementation that // isn't safe for concurrent use, consider a workaround like hiding writes // behind a mutex. // -// To create a Runner, use New. Runner's exported fields are meant to be -// configured via runner options; once a Runner has been created, the fields -// should be treated as read-only. +// Runner's exported fields are meant to be configured via [RunnerOption]; +// once a Runner has been created, the fields should be treated as read-only. type Runner struct { // Env specifies the initial environment for the interpreter, which must - // be non-nil. + // not be nil. It can only be set via [Env]. + // + // If it includes a TMPDIR variable describing an absolute directory, + // it is used as the directory in which to create temporary files needed + // for the interpreter's use, such as named pipes for process substitutions. + // Otherwise, [os.TempDir] is used. Env expand.Environ + // writeEnv overlays [Runner.Env] so that we can write environment variables + // as an overlay. writeEnv expand.WriteEnviron // Dir specifies the working directory of the command, which must be an - // absolute path. + // absolute path. It can only be set via [Dir]. Dir string + // tempDir is either $TMPDIR from [Runner.Env], or [os.TempDir]. + tempDir string + // Params are the current shell parameters, e.g. from running a shell // file or calling a function. Accessible via the $@/$* family of vars. + // It can only be set via [Params]. Params []string // Separate maps - note that bash allows a name to be both a var and a @@ -64,28 +80,31 @@ type Runner struct { // arguments. It may be nil. callHandler CallHandlerFunc - // execHandler is a function responsible for executing programs. It must be non-nil. + // execHandler is responsible for executing programs. It must not be nil. execHandler ExecHandlerFunc - // openHandler is a function responsible for opening files. It must be non-nil. + // execMiddlewares grows with calls to [ExecHandlers], + // and is used to construct execHandler when Reset is first called. + // The slice is needed to preserve the relative order of middlewares. + execMiddlewares []func(ExecHandlerFunc) ExecHandlerFunc + + // openHandler is a function responsible for opening files. It must not be nil. openHandler OpenHandlerFunc // readDirHandler is a function responsible for reading directories during // glob expansion. It must be non-nil. - readDirHandler ReadDirHandlerFunc + readDirHandler ReadDirHandlerFunc2 // statHandler is a function responsible for getting file stat. It must be non-nil. statHandler StatHandlerFunc - stdin io.Reader + stdin *os.File // e.g. the read end of a pipe stdout io.Writer stderr io.Writer ecfg *expand.Config ectx context.Context // just so that Runner.Subshell can use it again - lastExpandExit int // used to surface exit codes while expanding fields - // didReset remembers whether the runner has ever been reset. This is // used so that Reset is automatically called when running any program // or node for the first time on a Runner. @@ -93,45 +112,46 @@ type Runner struct { usedNew bool - // rand is used mainly to generate temporary files. - rand *rand.Rand - - // wgProcSubsts allows waiting for any process substitution sub-shells - // to finish running. - wgProcSubsts sync.WaitGroup - filename string // only if Node was a File // >0 to break or continue out of N enclosing loops breakEnclosing, contnEnclosing int - inLoop bool - inFunc bool - inSource bool - noErrExit bool + inLoop bool + inFunc bool + inSource bool + handlingTrap bool // whether we're currently in a trap callback // track if a sourced script set positional parameters sourceSetParams bool - err error // current shell exit code or fatal error - handlingTrap bool // whether we're currently in a trap callback - shellExited bool // whether the shell needs to exit + // noErrExit prevents failing commands from triggering [optErrExit], + // such as the condition in a [syntax.IfClause]. + noErrExit bool - // The current and last exit status code. They can only be different if + // The current and last exit statuses. They can only be different if // the interpreter is in the middle of running a statement. In that - // scenario, 'exit' is the status code for the statement being run, and - // 'lastExit' corresponds to the previous statement that was run. - exit int - lastExit int + // scenario, 'exit' is the status for the current statement being run, + // and 'lastExit' corresponds to the previous statement that was run. + exit exitStatus + lastExit exitStatus - bgShells errgroup.Group + lastExpandExit exitStatus // used to surface exit statuses while expanding fields + + // bgProcs holds all background shells spawned by this runner. + // Their PIDs are 1-indexed, from 1 to len(bgProcs), with a "g" prefix + // to distinguish them from real PIDs on the host operating system. + // + // Note that each shell only tracks its direct children; + // subshells do not share nor inherit the background PIDs they can wait for. + bgProcs []bgProc opts runnerOpts origDir string origParams []string origOpts runnerOpts - origStdin io.Reader + origStdin *os.File origStdout io.Writer origStderr io.Writer @@ -151,6 +171,75 @@ type Runner struct { callbackExit string } +// exitStatus holds the state of the shell after running one command. +// Beyond the exit status code, it also holds whether the shell should return or exit, +// as well as any Go error values that should be given back to the user. +// +// TODO(v4): consider replacing ExitStatus with a struct like this, +// so that an [ExecHandlerFunc] can e.g. mimic `exit 0` or fatal errors +// with specific exit codes. +type exitStatus struct { + // code is the exit status code. + code uint8 + + // TODO: consider an enum, as only one of these should be set at a time + returning bool // whether the current function `return`ed + exiting bool // whether the current shell is exiting + fatalExit bool // whether the current shell is exiting due to a fatal error; err below must not be nil + + // err is a fatal error if fatal is true, or a non-fatal custom error from a handler. + // Used so that running a single statement with a custom handler + // which returns a non-fatal Go error, such as a Go error wrapping [NewExitStatus], + // can be returned by [Runner.Run] without being lost entirely. + err error +} + +func (e *exitStatus) ok() bool { return e.code == 0 } + +func (e *exitStatus) oneIf(b bool) { + if b { + e.code = 1 + } else { + e.code = 0 + } +} + +func (e *exitStatus) fatal(err error) { + if !e.fatalExit && err != nil { + e.exiting = true + e.fatalExit = true + e.err = err + if e.code == 0 { + e.code = 1 + } + } +} + +func (e *exitStatus) fromHandlerError(err error) { + if err != nil { + var exit errBuiltinExitStatus + var es ExitStatus + if errors.As(err, &exit) { + *e = exitStatus(exit) + } else if errors.As(err, &es) { + e.err = err + e.code = uint8(es) + } else { + e.fatal(err) // handler's custom fatal error + } + } else { + e.code = 0 + } +} + +type bgProc struct { + // closed when the background process finishes, + // after which point the result fields below are set. + done chan struct{} + + exit *exitStatus +} + type alias struct { args []*syntax.Word blank bool @@ -174,17 +263,22 @@ func (r *Runner) optByFlag(flag byte) *bool { func New(opts ...RunnerOption) (*Runner, error) { r := &Runner{ usedNew: true, - execHandler: DefaultExecHandler(2 * time.Second), openHandler: DefaultOpenHandler(), - readDirHandler: DefaultReadDirHandler(), + readDirHandler: DefaultReadDirHandler2(), statHandler: DefaultStatHandler(), } r.dirStack = r.dirBootstrap[:0] + // turn "on" the default Bash options + for i, opt := range bashOptsTable { + r.opts[len(shellOptsTable)+i] = opt.defaultState + } + for _, opt := range opts { if err := opt(r); err != nil { return nil, err } } + // Set the default fallbacks, if necessary. if r.Env == nil { Env(nil)(r) @@ -200,11 +294,14 @@ func New(opts ...RunnerOption) (*Runner, error) { return r, nil } -// RunnerOption is a function which can be passed to New to alter Runner behaviour. -// To apply option to existing Runner call it directly, -// for example interp.Params("-e")(runner). +// RunnerOption can be passed to [New] to alter a [Runner]'s behaviour. +// It can also be applied directly on an existing Runner, +// such as interp.Params("-e")(runner). +// Note that options cannot be applied once Run or Reset have been called. type RunnerOption func(*Runner) error +// TODO: enforce the rule above via didReset. + // Env sets the interpreter's environment. If nil, a copy of the current // process's environment is used. func Env(env expand.Environ) RunnerOption { @@ -245,6 +342,16 @@ func Dir(path string) RunnerOption { } } +// Interactive configures the interpreter to behave like an interactive shell, +// akin to Bash. Currently, this only enables the expansion of aliases, +// but later on it should also change other behavior. +func Interactive(enabled bool) RunnerOption { + return func(r *Runner) error { + r.opts[optExpandAliases] = enabled + return nil + } +} + // Params populates the shell options and parameters. For example, Params("-e", // "--", "foo") will set the "-e" option and the parameters ["foo"], and // Params("+e") will unset the "-e" option and leave the parameters untouched. @@ -274,7 +381,7 @@ func Params(args ...string) RunnerOption { value := fp.value() if value == "" && enable { for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) + r.printOptLine(opt.name, r.opts[i], true) } continue } @@ -288,7 +395,7 @@ func Params(args ...string) RunnerOption { } continue } - opt := r.optByName(value, false) + _, opt := r.optByName(value, false) if opt == nil { return fmt.Errorf("invalid option: %q", value) } @@ -308,7 +415,7 @@ func Params(args ...string) RunnerOption { } } -// CallHandler sets the call handler. See CallHandlerFunc for more info. +// CallHandler sets the call handler. See [CallHandlerFunc] for more info. func CallHandler(f CallHandlerFunc) RunnerOption { return func(r *Runner) error { r.callHandler = f @@ -316,7 +423,11 @@ func CallHandler(f CallHandlerFunc) RunnerOption { } } -// ExecHandler sets the command execution handler. See ExecHandlerFunc for more info. +// ExecHandler sets one command execution handler, +// which replaces [DefaultExecHandler](2 * time.Second). +// +// Deprecated: use [ExecHandlers] instead, which allows chaining handlers more easily +// like middleware functions. func ExecHandler(f ExecHandlerFunc) RunnerOption { return func(r *Runner) error { r.execHandler = f @@ -324,7 +435,36 @@ func ExecHandler(f ExecHandlerFunc) RunnerOption { } } -// OpenHandler sets file open handler. See OpenHandlerFunc for more info. +// ExecHandlers appends middlewares to handle command execution. +// The middlewares are chained from first to last, and the first is called by the runner. +// Each middleware is expected to call the "next" middleware at most once. +// +// For example, a middleware may implement only some commands. +// For those commands, it can run its logic and avoid calling "next". +// For any other commands, it can call "next" with the original parameters. +// +// Another common example is a middleware which always calls "next", +// but runs custom logic either before or after that call. +// For instance, a middleware could change the arguments to the "next" call, +// or it could print log lines before or after the call to "next". +// +// The last exec handler is always [DefaultExecHandler](2 * time.Second). +func ExecHandlers(middlewares ...func(next ExecHandlerFunc) ExecHandlerFunc) RunnerOption { + return func(r *Runner) error { + r.execMiddlewares = append(r.execMiddlewares, middlewares...) + return nil + } +} + +// TODO: consider porting the middleware API in [ExecHandlers] to [OpenHandler], +// [ReadDirHandler2], and [StatHandler]. + +// TODO(v4): now that [ExecHandlers] allows calling a next handler with changed +// arguments, one of the two advantages of [CallHandler] is gone. The other is the +// ability to work with builtins; if we make [ExecHandlers] work with builtins, we +// could join both APIs. + +// OpenHandler sets file open handler. See [OpenHandlerFunc] for more info. func OpenHandler(f OpenHandlerFunc) RunnerOption { return func(r *Runner) error { r.openHandler = f @@ -332,15 +472,35 @@ func OpenHandler(f OpenHandlerFunc) RunnerOption { } } -// ReadDirHandler sets the read directory handler. See ReadDirHandlerFunc for more info. +// ReadDirHandler sets the read directory handler. See [ReadDirHandlerFunc] for more info. +// +// Deprecated: use [ReadDirHandler2]. func ReadDirHandler(f ReadDirHandlerFunc) RunnerOption { + return func(r *Runner) error { + r.readDirHandler = func(ctx context.Context, path string) ([]fs.DirEntry, error) { + infos, err := f(ctx, path) + if err != nil { + return nil, err + } + entries := make([]fs.DirEntry, len(infos)) + for i, info := range infos { + entries[i] = fs.FileInfoToDirEntry(info) + } + return entries, nil + } + return nil + } +} + +// ReadDirHandler2 sets the read directory handler. See [ReadDirHandlerFunc2] for more info. +func ReadDirHandler2(f ReadDirHandlerFunc2) RunnerOption { return func(r *Runner) error { r.readDirHandler = f return nil } } -// StatHandler sets the stat handler. See StatHandlerFunc for more info. +// StatHandler sets the stat handler. See [StatHandlerFunc] for more info. func StatHandler(f StatHandlerFunc) RunnerOption { return func(r *Runner) error { r.statHandler = f @@ -348,12 +508,45 @@ func StatHandler(f StatHandlerFunc) RunnerOption { } } +func stdinFile(r io.Reader) (*os.File, error) { + switch r := r.(type) { + case *os.File: + return r, nil + case nil: + return nil, nil + default: + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + go func() { + io.Copy(pw, r) + pw.Close() + }() + return pr, nil + } +} + // StdIO configures an interpreter's standard input, standard output, and // standard error. If out or err are nil, they default to a writer that discards // the output. +// +// Note that providing a non-nil standard input other than [*os.File] will require +// an [os.Pipe] and spawning a goroutine to copy into it, +// as an [os.File] is the only way to share a reader with subprocesses. +// This may cause the interpreter to consume the entire reader. +// See [os/exec.Cmd.Stdin]. +// +// When providing an [*os.File] as standard input, consider using an [os.Pipe] +// as it has the best chance to support cancellable reads via [os.File.SetReadDeadline], +// so that cancelling the runner's context can stop a blocked standard input read. func StdIO(in io.Reader, out, err io.Writer) RunnerOption { return func(r *Runner) error { - r.stdin = in + stdin, _err := stdinFile(in) + if _err != nil { + return _err + } + r.stdin = stdin if out == nil { out = io.Discard } @@ -366,28 +559,38 @@ func StdIO(in io.Reader, out, err io.Writer) RunnerOption { } } -func (r *Runner) optByName(name string, bash bool) *bool { +// optByName returns the matching runner's option index and status +func (r *Runner) optByName(name string, bash bool) (index int, status *bool) { if bash { - for i, optName := range bashOptsTable { - if optName == name { - return &r.opts[len(shellOptsTable)+i] + for i, opt := range bashOptsTable { + if opt.name == name { + index = len(shellOptsTable) + i + return index, &r.opts[index] } } } for i, opt := range &shellOptsTable { if opt.name == name { - return &r.opts[i] + return i, &r.opts[i] } } - return nil + return 0, nil } type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool -var shellOptsTable = [...]struct { +type shellOpt struct { flag byte name string -}{ +} + +type bashOpt struct { + name string + defaultState bool // Bash's default value for this option + supported bool // whether we support the option's non-default state +} + +var shellOptsTable = [...]shellOpt{ // sorted alphabetically by name; use a space for the options // that have no flag form {'a', "allexport"}, @@ -399,17 +602,119 @@ var shellOptsTable = [...]struct { {' ', "pipefail"}, } -var bashOptsTable = [...]string{ - // sorted alphabetically by name - "expand_aliases", - "globstar", - "nullglob", +var bashOptsTable = [...]bashOpt{ + // supported options, sorted alphabetically by name + { + name: "expand_aliases", + defaultState: false, + supported: true, + }, + { + name: "globstar", + defaultState: false, + supported: true, + }, + { + name: "nocaseglob", + defaultState: false, + supported: true, + }, + { + name: "nullglob", + defaultState: false, + supported: true, + }, + // unsupported options, sorted alphabetically by name + {name: "assoc_expand_once"}, + {name: "autocd"}, + {name: "cdable_vars"}, + {name: "cdspell"}, + {name: "checkhash"}, + {name: "checkjobs"}, + { + name: "checkwinsize", + defaultState: true, + }, + { + name: "cmdhist", + defaultState: true, + }, + {name: "compat31"}, + {name: "compat32"}, + {name: "compat40"}, + {name: "compat41"}, + {name: "compat42"}, + {name: "compat44"}, + {name: "compat43"}, + {name: "compat44"}, + { + name: "complete_fullquote", + defaultState: true, + }, + {name: "direxpand"}, + {name: "dirspell"}, + {name: "dotglob"}, + {name: "execfail"}, + {name: "extdebug"}, + {name: "extglob"}, + { + name: "extquote", + defaultState: true, + }, + {name: "failglob"}, + { + name: "force_fignore", + defaultState: true, + }, + {name: "globasciiranges"}, + {name: "gnu_errfmt"}, + {name: "histappend"}, + {name: "histreedit"}, + {name: "histverify"}, + { + name: "hostcomplete", + defaultState: true, + }, + {name: "huponexit"}, + { + name: "inherit_errexit", + defaultState: true, + }, + { + name: "interactive_comments", + defaultState: true, + }, + {name: "lastpipe"}, + {name: "lithist"}, + {name: "localvar_inherit"}, + {name: "localvar_unset"}, + {name: "login_shell"}, + {name: "mailwarn"}, + {name: "no_empty_cmd_completion"}, + {name: "nocasematch"}, + { + name: "progcomp", + defaultState: true, + }, + {name: "progcomp_alias"}, + { + name: "promptvars", + defaultState: true, + }, + {name: "restricted_shell"}, + {name: "shift_verbose"}, + { + name: "sourcepath", + defaultState: true, + }, + {name: "xpg_echo"}, } // To access the shell options arrays without a linear search when we // know which option we're after at compile time. First come the shell options, // then the bash options. const ( + // These correspond to indexes in [shellOptsTable] optAllExport = iota optErrExit optNoExec @@ -418,8 +723,11 @@ const ( optXTrace optPipeFail + // These correspond to indexes (offset by the above seven items) of + // supported options in [bashOptsTable] optExpandAliases optGlobStar + optNoCaseGlob optNullGlob ) @@ -441,17 +749,38 @@ func (r *Runner) Reset() { r.origStdin = r.stdin r.origStdout = r.stdout r.origStderr = r.stderr + + if r.execHandler != nil && len(r.execMiddlewares) > 0 { + panic("interp.ExecHandler should be replaced with interp.ExecHandlers, not mixed") + } + if r.execHandler == nil { + r.execHandler = DefaultExecHandler(2 * time.Second) + } + // Middlewares are chained from first to last, and each can call the + // next in the chain, so we need to construct the chain backwards. + for _, mw := range slices.Backward(r.execMiddlewares) { + r.execHandler = mw(r.execHandler) + } + // Fill tempDir; only need to do this once given that Env will not change. + if dir := r.Env.Get("TMPDIR").String(); filepath.IsAbs(dir) { + r.tempDir = dir + } else { + r.tempDir = os.TempDir() + } + // Clean it as we will later do a string prefix match. + r.tempDir = filepath.Clean(r.tempDir) } // reset the internal state *r = Runner{ Env: r.Env, + tempDir: r.tempDir, callHandler: r.callHandler, execHandler: r.execHandler, openHandler: r.openHandler, readDirHandler: r.readDirHandler, statHandler: r.statHandler, - // These can be set by functions like Dir or Params, but + // These can be set by functions like [Dir] or [Params], but // builtins can overwrite them; reset the fields to whatever the // constructor set up. Dir: r.origDir, @@ -469,16 +798,19 @@ func (r *Runner) Reset() { origStderr: r.origStderr, // emptied below, to reuse the space - Vars: r.Vars, + Vars: r.Vars, + dirStack: r.dirStack[:0], usedNew: r.usedNew, } + // Ensure we stop referencing any pointers before we reuse bgProcs. + clear(r.bgProcs) + r.bgProcs = r.bgProcs[:0] + if r.Vars == nil { r.Vars = make(map[string]expand.Variable) } else { - for k := range r.Vars { - delete(r.Vars, k) - } + clear(r.Vars) } // TODO(v4): Use the supplied Env directly if it implements enough methods. r.writeEnv = &overlayEnviron{parent: r.Env} @@ -487,14 +819,24 @@ func (r *Runner) Reset() { r.setVarString("HOME", home) } if !r.writeEnv.Get("UID").IsSet() { - r.setVar("UID", nil, expand.Variable{ + r.setVar("UID", expand.Variable{ + Set: true, Kind: expand.String, ReadOnly: true, Str: strconv.Itoa(os.Getuid()), }) } + if !r.writeEnv.Get("EUID").IsSet() { + r.setVar("EUID", expand.Variable{ + Set: true, + Kind: expand.String, + ReadOnly: true, + Str: strconv.Itoa(os.Geteuid()), + }) + } if !r.writeEnv.Get("GID").IsSet() { - r.setVar("GID", nil, expand.Variable{ + r.setVar("GID", expand.Variable{ + Set: true, Kind: expand.String, ReadOnly: true, Str: strconv.Itoa(os.Getgid()), @@ -505,70 +847,75 @@ func (r *Runner) Reset() { r.setVarString("OPTIND", "1") r.dirStack = append(r.dirStack, r.Dir) + r.didReset = true } -// exitStatus is a non-zero status code resulting from running a shell node. -type exitStatus uint8 +// ExitStatus is a non-zero status code resulting from running a shell node. +type ExitStatus uint8 -func (s exitStatus) Error() string { return fmt.Sprintf("exit status %d", s) } +func (s ExitStatus) Error() string { return fmt.Sprintf("exit status %d", s) } // NewExitStatus creates an error which contains the specified exit status code. +// +// Deprecated: use [ExitStatus] directly. +// +//go:fix inline func NewExitStatus(status uint8) error { - return exitStatus(status) + return ExitStatus(status) } // IsExitStatus checks whether error contains an exit status and returns it. +// +// Deprecated: use [errors.As] with [ExitStatus] directly. +// +//go:fix inline func IsExitStatus(err error) (status uint8, ok bool) { - var s exitStatus - if errors.As(err, &s) { - return uint8(s), true + var es ExitStatus + if errors.As(err, &es) { + return uint8(es), true } return 0, false } -// Run interprets a node, which can be a *File, *Stmt, or Command. If a non-nil +// Run interprets a node, which can be a [*File], [*Stmt], or [Command]. If a non-nil // error is returned, it will typically contain a command's exit status, which -// can be retrieved with IsExitStatus. +// can be retrieved with [IsExitStatus]. // // Run can be called multiple times synchronously to interpret programs -// incrementally. To reuse a Runner without keeping the internal shell state, +// incrementally. To reuse a [Runner] without keeping the internal shell state, // call Reset. // -// Calling Run on an entire *File implies an exit, meaning that an exit trap may +// Calling Run on an entire [*File] implies an exit, meaning that an exit trap may // run. func (r *Runner) Run(ctx context.Context, node syntax.Node) error { if !r.didReset { r.Reset() } r.fillExpandConfig(ctx) - r.err = nil - r.shellExited = false + r.exit = exitStatus{} r.filename = "" - switch x := node.(type) { + switch node := node.(type) { case *syntax.File: - r.filename = x.Name - r.stmts(ctx, x.Stmts) - if !r.shellExited { - r.exitShell(ctx, r.exit) - } + r.filename = node.Name + r.stmts(ctx, node.Stmts) case *syntax.Stmt: - r.stmt(ctx, x) + r.stmt(ctx, node) case syntax.Command: - r.cmd(ctx, x) + r.cmd(ctx, node) default: - return fmt.Errorf("node can only be File, Stmt, or Command: %T", x) + return fmt.Errorf("node can only be File, Stmt, or Command: %T", node) } - if r.exit != 0 { - r.setErr(NewExitStatus(uint8(r.exit))) + r.trapCallback(ctx, r.callbackExit, "exit") + maps.Insert(r.Vars, r.writeEnv.Each) + // Return the first of: a fatal error, a non-fatal handler error, or the exit code. + if err := r.exit.err; err != nil { + return err } - if r.Vars != nil { - r.writeEnv.Each(func(name string, vr expand.Variable) bool { - r.Vars[name] = vr - return true - }) + if code := r.exit.code; code != 0 { + return ExitStatus(code) } - return r.err + return nil } // Exited reports whether the last Run call should exit an entire shell. This @@ -577,27 +924,35 @@ func (r *Runner) Run(ctx context.Context, node syntax.Node) error { // Note that this state is overwritten at every Run call, so it should be // checked immediately after each Run call. func (r *Runner) Exited() bool { - return r.shellExited + return r.exit.exiting } -// Subshell makes a copy of the given Runner, suitable for use concurrently +// Subshell makes a copy of the given [Runner], suitable for use concurrently // with the original. The copy will have the same environment, including // variables and functions, but they can all be modified without affecting the // original. // -// Subshell is not safe to use concurrently with Run. Orchestrating this is +// Subshell is not safe to use concurrently with [Run]. Orchestrating this is // left up to the caller; no locking is performed. // -// To replace e.g. stdin/out/err, do StdIO(r.stdin, r.stdout, r.stderr)(r) on +// To replace e.g. stdin/out/err, do [StdIO](r.stdin, r.stdout, r.stderr)(r) on // the copy. func (r *Runner) Subshell() *Runner { + return r.subshell(true) +} + +// subshell is like [Runner.subshell], but allows skipping some allocations and copies +// when creating subshells which will not be used concurrently with the parent shell. +// TODO(v4): we should expose this, e.g. SubshellForeground and SubshellBackground. +func (r *Runner) subshell(background bool) *Runner { if !r.didReset { r.Reset() } // Keep in sync with the Runner type. Manually copy fields, to not copy - // sensitive ones like errgroup.Group, and to do deep copies of slices. + // sensitive ones like [errgroup.Group], and to do deep copies of slices. r2 := &Runner{ Dir: r.Dir, + tempDir: r.tempDir, Params: r.Params, callHandler: r.callHandler, execHandler: r.execHandler, @@ -615,39 +970,11 @@ func (r *Runner) Subshell() *Runner { origStdout: r.origStdout, // used for process substitutions } - // Env vars and funcs are copied, since they might be modified. - // TODO(v4): lazy copying? it would probably be enough to add a - // copyOnWrite bool field to Variable, then a Modify method that must be - // used when one needs to modify a variable. ideally with some way to - // catch direct modifications without the use of Modify and panic, - // perhaps via a check when getting or setting vars at some level. - oenv := &overlayEnviron{parent: expand.ListEnviron()} - r.writeEnv.Each(func(name string, vr expand.Variable) bool { - vr2 := vr - // Make deeper copies of List and Map, but ensure that they remain nil - // if they are nil in vr. - vr2.List = append([]string(nil), vr.List...) - if vr.Map != nil { - vr2.Map = make(map[string]string, len(vr.Map)) - for k, vr := range vr.Map { - vr2.Map[k] = vr - } - } - oenv.Set(name, vr2) - return true - }) - r2.writeEnv = oenv - r2.Funcs = make(map[string]*syntax.Stmt, len(r.Funcs)) - for k, v := range r.Funcs { - r2.Funcs[k] = v - } + r2.writeEnv = newOverlayEnviron(r.writeEnv, background) + // Funcs are copied, since they might be modified. + r2.Funcs = maps.Clone(r.Funcs) r2.Vars = make(map[string]expand.Variable) - if l := len(r.alias); l > 0 { - r2.alias = make(map[string]alias, l) - for k, v := range r.alias { - r2.alias[k] = v - } - } + r2.alias = maps.Clone(r.alias) r2.dirStack = append(r2.dirBootstrap[:0], r.dirStack...) r2.fillExpandConfig(r.ectx) diff --git a/vendor/mvdan.cc/sh/v3/interp/builtin.go b/vendor/mvdan.cc/sh/v3/interp/builtin.go index f8161998ed..b1ba07ea7b 100644 --- a/vendor/mvdan.cc/sh/v3/interp/builtin.go +++ b/vendor/mvdan.cc/sh/v3/interp/builtin.go @@ -4,74 +4,99 @@ package interp import ( + "bufio" "bytes" + "cmp" "context" "errors" "fmt" - "io" "os" "path/filepath" + "slices" "strconv" "strings" + "syscall" + "time" + + "golang.org/x/term" "mvdan.cc/sh/v3/expand" "mvdan.cc/sh/v3/syntax" ) -func isBuiltin(name string) bool { +// IsBuiltin returns true if the given word is a shell builtin. +func IsBuiltin(name string) bool { switch name { - case "true", ":", "false", "exit", "set", "shift", "unset", + case ":", "true", "false", "exit", "set", "shift", "unset", "echo", "printf", "break", "continue", "pwd", "cd", "wait", "builtin", "trap", "type", "source", ".", "command", "dirs", "pushd", "popd", "umask", "alias", "unalias", "fg", "bg", "getopts", "eval", "test", "[", "exec", - "return", "read", "shopt": + "return", "read", "mapfile", "readarray", "shopt": return true } return false } -func oneIf(b bool) int { - if b { - return 1 - } - return 0 -} +// TODO: atoi is duplicated in the expand package. -// atoi is just a shorthand for strconv.Atoi that ignores the error, -// just like shells do. -func atoi(s string) int { - n, _ := strconv.Atoi(s) +// atoi is like [strconv.ParseInt](s, 10, 64), but it ignores errors and trims whitespace. +func atoi(s string) int64 { + s = strings.TrimSpace(s) + n, _ := strconv.ParseInt(s, 10, 64) return n } -func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, args []string) int { +type errBuiltinExitStatus exitStatus + +func (e errBuiltinExitStatus) Error() string { + return fmt.Sprintf("builtin exit status %d", e.code) +} + +// Builtin allows [ExecHandlerFunc] implementations to execute any builtin, +// which can be useful for an exec handler to wrap or combine builtin calls. +// +// Note that a non-nil error may be returned in cases where the builtin +// alters the control flow of the runner, even if the builtin did not fail. +// For example, this is the case with `exit 0` or `return`. +func (hc HandlerContext) Builtin(ctx context.Context, args []string) error { + if hc.kind != handlerKindExec { + return fmt.Errorf("HandlerContext.Builtin can only be called via an ExecHandlerFunc") + } + exit := hc.runner.builtin(ctx, hc.Pos, args[0], args[1:]) + if exit != (exitStatus{}) { + return errBuiltinExitStatus(exit) + } + return nil +} + +func (r *Runner) builtin(ctx context.Context, pos syntax.Pos, name string, args []string) (exit exitStatus) { + failf := func(code uint8, format string, args ...any) exitStatus { + r.errf(format, args...) + exit.code = code + return exit + } switch name { - case "true", ":": + case ":", "true": case "false": - return 1 + exit.code = 1 case "exit": - exit := 0 switch len(args) { case 0: exit = r.lastExit case 1: n, err := strconv.Atoi(args[0]) if err != nil { - r.errf("invalid exit status code: %q\n", args[0]) - return 2 + return failf(2, "invalid exit status code: %q\n", args[0]) } - exit = n + exit.code = uint8(n) default: - r.errf("exit cannot take multiple arguments\n") - return 1 + return failf(1, "exit cannot take multiple arguments\n") } - r.exitShell(ctx, exit) - return exit + exit.exiting = true case "set": if err := Params(args...)(r); err != nil { - r.errf("set: %v\n", err) - return 2 + return failf(2, "set: %v\n", err) } r.updateExpandOpts() case "shift": @@ -85,8 +110,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } fallthrough default: - r.errf("usage: shift [n]\n") - return 2 + return failf(2, "usage: shift [n]\n") } if n >= len(r.Params) { r.Params = nil @@ -145,15 +169,13 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } case "printf": if len(args) == 0 { - r.errf("usage: printf format [arguments]\n") - return 2 + return failf(2, "usage: printf format [arguments]\n") } format, args := args[0], args[1:] for { s, n, err := expand.Format(r.ecfg, format, args) if err != nil { - r.errf("%v\n", err) - return 1 + return failf(1, "%v\n", err) } r.out(s) args = args[n:] @@ -163,8 +185,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } case "break", "continue": if !r.inLoop { - r.errf("%s is only useful in a loop", name) - break + return failf(0, "%s is only useful in a loop\n", name) } enclosing := &r.breakEnclosing if name == "continue" { @@ -180,8 +201,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } fallthrough default: - r.errf("usage: %s [n]\n", name) - return 2 + return failf(2, "usage: %s [n]\n", name) } case "pwd": evalSymlinks := false @@ -192,8 +212,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a case "-P": evalSymlinks = true default: - r.errf("invalid option: %q\n", args[0]) - return 2 + return failf(2, "invalid option: %q\n", args[0]) } args = args[1:] } @@ -202,8 +221,8 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a var err error pwd, err = filepath.EvalSymlinks(pwd) if err != nil { - r.setErr(err) - return 1 + exit.fatal(err) // perhaps overly dramatic? + return exit } } r.outf("%s\n", pwd) @@ -222,26 +241,45 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.outf("%s\n", path) } default: - r.errf("usage: cd [dir]\n") - return 2 + return failf(2, "usage: cd [dir]\n") } - return r.changeDir(ctx, path) + exit.code = r.changeDir(ctx, path) case "wait": - if len(args) > 0 { - panic("wait with args not handled yet") + fp := flagParser{remaining: args} + for fp.more() { + switch flag := fp.flag(); flag { + case "-n", "-p": + return failf(2, "wait: unsupported option %q\n", flag) + default: + return failf(2, "wait: invalid option %q\n", flag) + } + } + if len(args) == 0 { + // Note that "wait" without arguments always returns exit status zero. + for _, bg := range r.bgProcs { + <-bg.done + } + break } - err := r.bgShells.Wait() - if _, ok := IsExitStatus(err); err != nil && !ok { - r.setErr(err) + for _, arg := range args { + arg, ok := strings.CutPrefix(arg, "g") + pid := atoi(arg) + if !ok || pid <= 0 || pid > int64(len(r.bgProcs)) { + return failf(1, "wait: pid %s is not a child of this shell\n", arg) + } + bg := r.bgProcs[pid-1] + <-bg.done + exit = *bg.exit } case "builtin": if len(args) < 1 { break } - if !isBuiltin(args[0]) { - return 1 + if !IsBuiltin(args[0]) { + exit.code = 1 + return exit } - return r.builtinCode(ctx, pos, args[0], args[1:]) + exit = r.builtin(ctx, pos, args[0], args[1:]) case "type": anyNotFound := false mode := "" @@ -249,13 +287,11 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a for fp.more() { switch flag := fp.flag(); flag { case "-a", "-f", "-P", "--help": - r.errf("command: NOT IMPLEMENTED\n") - return 3 + return failf(3, "command: NOT IMPLEMENTED\n") case "-p", "-t": mode = flag default: - r.errf("command: invalid option %q\n", flag) - return 2 + return failf(2, "command: invalid option %q\n", flag) } } args := fp.args() @@ -302,7 +338,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } continue } - if isBuiltin(arg) { + if IsBuiltin(arg) { if mode == "-t" { r.out("builtin\n") } else { @@ -324,22 +360,20 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a anyNotFound = true } if anyNotFound { - return 1 + exit.code = 1 } case "eval": src := strings.Join(args, " ") p := syntax.NewParser() file, err := p.Parse(strings.NewReader(src), "") if err != nil { - r.errf("eval: %v\n", err) - return 1 + return failf(1, "eval: %v\n", err) } r.stmts(ctx, file.Stmts) - return r.exit + exit = r.exit case "source", ".": if len(args) < 1 { - r.errf("%v: source: need filename\n", pos) - return 2 + return failf(2, "%v: source: need filename\n", pos) } path, err := scriptFromPathDir(r.Dir, r.writeEnv, args[0]) if err != nil { @@ -351,15 +385,13 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } f, err := r.open(ctx, path, os.O_RDONLY, 0, false) if err != nil { - r.errf("source: %v\n", err) - return 1 + return failf(1, "source: %v\n", err) } defer f.Close() p := syntax.NewParser() file, err := p.Parse(f, path) if err != nil { - r.errf("source: %v\n", err) - return 1 + return failf(1, "source: %v\n", err) } // Keep the current versions of some fields we might modify. @@ -388,15 +420,11 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.sourceSetParams = oldSourceSetParams r.inSource = oldInSource - if code, ok := r.err.(returnStatus); ok { - r.err = nil - return int(code) - } - return r.exit + exit = r.exit + exit.returning = false case "[": if len(args) == 0 || args[len(args)-1] != "]" { - r.errf("%v: [: missing matching ]\n", pos) - return 2 + return failf(2, "%v: [: missing matching ]\n", pos) } args = args[:len(args)-1] fallthrough @@ -412,9 +440,10 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a p.next() expr := p.classicTest("[", false) if parseErr { - return 2 + exit.code = 2 + return exit } - return oneIf(r.bashTest(ctx, expr, true) == "") + exit.oneIf(r.bashTest(ctx, expr, true) == "") case "exec": // TODO: Consider unix.Exec, i.e. actually replacing // the process. It's in theory what a shell should do, @@ -424,9 +453,9 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.keepRedirs = true break } - r.exitShell(ctx, 1) - r.exec(ctx, args) - return r.exit + r.exit.exiting = true + r.exec(ctx, pos, args) + exit = r.exit case "command": show := false fp := flagParser{remaining: args} @@ -435,8 +464,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a case "-v": show = true default: - r.errf("command: invalid option %q\n", flag) - return 2 + return failf(2, "command: invalid option %q\n", flag) } } args := fp.args() @@ -444,16 +472,17 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a break } if !show { - if isBuiltin(args[0]) { - return r.builtinCode(ctx, pos, args[0], args[1:]) + if IsBuiltin(args[0]) { + return r.builtin(ctx, pos, args[0], args[1:]) } - r.exec(ctx, args) - return r.exit + r.exec(ctx, pos, args) + exit = r.exit + return exit } - last := 0 + last := uint8(0) for _, arg := range args { last = 0 - if r.Funcs[arg] != nil || isBuiltin(arg) { + if r.Funcs[arg] != nil || IsBuiltin(arg) { r.outf("%s\n", arg) } else if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil { r.outf("%s\n", path) @@ -461,10 +490,10 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a last = 1 } } - return last + exit.code = last case "dirs": - for i := len(r.dirStack) - 1; i >= 0; i-- { - r.outf("%s", r.dirStack[i]) + for i, dir := range slices.Backward(r.dirStack) { + r.outf("%s", dir) if i > 0 { r.out(" ") } @@ -489,28 +518,28 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a break } if len(r.dirStack) < 2 { - r.errf("pushd: no other directory\n") - return 1 + return failf(1, "pushd: no other directory\n") } newtop := swap() if code := r.changeDir(ctx, newtop); code != 0 { - return code + exit.code = code + return exit } - r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) + r.builtin(ctx, syntax.Pos{}, "dirs", nil) case 1: if change { if code := r.changeDir(ctx, args[0]); code != 0 { - return code + exit.code = code + return exit } r.dirStack = append(r.dirStack, r.Dir) } else { r.dirStack = append(r.dirStack, args[0]) swap() } - r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) + r.builtin(ctx, syntax.Pos{}, "dirs", nil) default: - r.errf("pushd: too many arguments\n") - return 2 + return failf(2, "pushd: too many arguments\n") } case "popd": change := true @@ -521,64 +550,64 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a switch len(args) { case 0: if len(r.dirStack) < 2 { - r.errf("popd: directory stack empty\n") - return 1 + return failf(1, "popd: directory stack empty\n") } oldtop := r.dirStack[len(r.dirStack)-1] r.dirStack = r.dirStack[:len(r.dirStack)-1] if change { newtop := r.dirStack[len(r.dirStack)-1] if code := r.changeDir(ctx, newtop); code != 0 { - return code + exit.code = code + return exit } } else { r.dirStack[len(r.dirStack)-1] = oldtop } - r.builtinCode(ctx, syntax.Pos{}, "dirs", nil) + r.builtin(ctx, syntax.Pos{}, "dirs", nil) default: - r.errf("popd: invalid argument\n") - return 2 + return failf(2, "popd: invalid argument\n") } case "return": if !r.inFunc && !r.inSource { - r.errf("return: can only be done from a func or sourced script\n") - return 1 + return failf(1, "return: can only be done from a func or sourced script\n") } - code := 0 switch len(args) { case 0: case 1: - code = atoi(args[0]) + n, err := strconv.Atoi(args[0]) + if err != nil { + return failf(2, "invalid return status code: %q\n", args[0]) + } + exit.code = uint8(n) default: - r.errf("return: too many arguments\n") - return 2 + return failf(2, "return: too many arguments\n") } - r.setErr(returnStatus(code)) + exit.returning = true case "read": var prompt string raw := false + silent := false fp := flagParser{remaining: args} for fp.more() { switch flag := fp.flag(); flag { + case "-s": + silent = true case "-r": raw = true case "-p": prompt = fp.value() if prompt == "" { - r.errf("read: -p: option requires an argument\n") - return 2 + return failf(2, "read: -p: option requires an argument\n") } default: - r.errf("read: invalid option %q\n", flag) - return 2 + return failf(2, "read: invalid option %q\n", flag) } } args := fp.args() for _, name := range args { if !syntax.ValidName(name) { - r.errf("read: invalid identifier %q\n", name) - return 2 + return failf(2, "read: invalid identifier %q\n", name) } } @@ -586,12 +615,16 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.out(prompt) } - line, err := r.readLine(raw) - if err != nil { - return 1 + var line []byte + var err error + if silent { + // Note that on Windows, syscall.Stdin is of type uintptr. + line, err = term.ReadPassword(int(syscall.Stdin)) + } else { + line, err = r.readLine(ctx, raw) } if len(args) == 0 { - args = append(args, "REPLY") + args = append(args, shellReplyVar) } values := expand.ReadFields(r.ecfg, string(line), len(args), raw) @@ -603,12 +636,16 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.setVarString(name, val) } - return 0 + // We can get data back from readLine and an error at the same time, so + // check err after we process the data. + if err != nil { + exit.code = 1 + return exit + } case "getopts": if len(args) < 2 { - r.errf("getopts: usage: getopts optstring name [arg ...]\n") - return 2 + return failf(2, "getopts: usage: getopts optstring name [arg ...]\n") } optind, _ := strconv.Atoi(r.envGet("OPTIND")) if optind-1 != r.optState.argidx { @@ -620,8 +657,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a optstr := args[0] name := args[1] if !syntax.ValidName(name) { - r.errf("getopts: invalid identifier: %q\n", name) - return 2 + return failf(2, "getopts: invalid identifier: %q\n", name) } args = args[2:] if len(args) == 0 { @@ -647,7 +683,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10)) } - return oneIf(done) + exit.oneIf(done) case "shopt": mode := "" @@ -662,34 +698,46 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a case "-p", "-q": panic(fmt.Sprintf("unhandled shopt flag: %s", flag)) default: - r.errf("shopt: invalid option %q\n", flag) - return 2 + return failf(2, "shopt: invalid option %q\n", flag) } } args := fp.args() + bash := !posixOpts if len(args) == 0 { - if !posixOpts { - for i, name := range bashOptsTable { - r.printOptLine(name, r.opts[len(shellOptsTable)+i]) + if bash { + for i, opt := range bashOptsTable { + r.printOptLine(opt.name, r.opts[len(shellOptsTable)+i], opt.supported) } break } for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) + r.printOptLine(opt.name, r.opts[i], true) } break } for _, arg := range args { - opt := r.optByName(arg, !posixOpts) + i, opt := r.optByName(arg, bash) if opt == nil { - r.errf("shopt: invalid option name %q\n", arg) - return 1 + return failf(1, "shopt: invalid option name %q\n", arg) } + + var ( + bo *bashOpt + supported = true // default for shell options + ) + if bash { + bo = &bashOptsTable[i-len(shellOptsTable)] + supported = bo.supported + } + switch mode { case "-s", "-u": + if bash && !supported { + return failf(1, "shopt: invalid option name %q %q (%q not supported)\n", arg, r.optStatusText(bo.defaultState), r.optStatusText(!bo.defaultState)) + } *opt = mode == "-s" default: // "" - r.printOptLine(arg, *opt) + r.printOptLine(arg, *opt, supported) } } r.updateExpandOpts() @@ -714,9 +762,10 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a show(name, als) } } - for _, name := range args { - i := strings.IndexByte(name, '=') - if i < 1 { // don't save an empty name + argsLoop: + for _, arg := range args { + name, src, ok := strings.Cut(arg, "=") + if !ok { als, ok := r.alias[name] if !ok { r.errf("alias: %q not found\n", name) @@ -729,16 +778,14 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a // TODO: parse any CallExpr perhaps, or even any Stmt parser := syntax.NewParser() var words []*syntax.Word - src := name[i+1:] - if err := parser.Words(strings.NewReader(src), func(w *syntax.Word) bool { + for w, err := range parser.WordsSeq(strings.NewReader(src)) { + if err != nil { + r.errf("alias: could not parse %q: %v\n", src, err) + continue argsLoop + } words = append(words, w) - return true - }); err != nil { - r.errf("alias: could not parse %q: %v", src, err) - continue } - name = name[:i] if r.alias == nil { r.alias = make(map[string]alias) } @@ -758,14 +805,14 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a for fp.more() { switch flag := fp.flag(); flag { case "-l", "-p": - r.errf("trap: %q: NOT IMPLEMENTED flag\n", flag) - return 2 + return failf(2, "trap: %q: NOT IMPLEMENTED flag\n", flag) case "-": // default signal default: r.errf("trap: %q: invalid option\n", flag) r.errf("trap: usage: trap [-lp] [[arg] signal_spec ...]\n") - return 2 + exit.code = 2 + return exit } } args := fp.args() @@ -796,26 +843,101 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a case "EXIT": r.callbackExit = callback default: - r.errf("trap: %s: invalid signal specification\n", arg) - return 2 + return failf(2, "trap: %s: invalid signal specification\n", arg) + } + } + + case "readarray", "mapfile": + dropDelim := false + delim := "\n" + fp := flagParser{remaining: args} + for fp.more() { + switch flag := fp.flag(); flag { + case "-t": + // Remove the delim from each line read + dropDelim = true + case "-d": + if len(fp.remaining) == 0 { + return failf(2, "%s: -d: option requires an argument\n", name) + } + delim = fp.value() + if delim == "" { + // Bash sets the delim to an ASCII NUL if provided with an empty + // string. + delim = "\x00" + } + default: + return failf(2, "%s: invalid option %q\n", name, flag) + } + } + + args := fp.args() + var arrayName string + switch len(args) { + case 0: + arrayName = "MAPFILE" + case 1: + if !syntax.ValidName(args[0]) { + return failf(2, "%s: invalid identifier %q\n", name, args[0]) } + arrayName = args[0] + default: + return failf(2, "%s: Only one array name may be specified, %v\n", name, args) + } + + var vr expand.Variable + vr.Kind = expand.Indexed + scanner := bufio.NewScanner(r.stdin) + scanner.Split(mapfileSplit(delim[0], dropDelim)) + for scanner.Scan() { + vr.List = append(vr.List, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return failf(2, "%s: unable to read, %v\n", name, err) } + r.setVar(arrayName, vr) + default: // "umask", "fg", "bg", - panic(fmt.Sprintf("unhandled builtin: %s", name)) + return failf(2, "%s: unimplemented builtin\n", name) + } + return exit +} + +// mapfileSplit returns a suitable Split function for a [bufio.Scanner]; +// the code is mostly stolen from [bufio.ScanLines]. +func mapfileSplit(delim byte, dropDelim bool) bufio.SplitFunc { + return func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, delim); i >= 0 { + // We have a full newline-terminated line. + if dropDelim { + return i + 1, data[0:i], nil + } else { + return i + 1, data[0 : i+1], nil + } + } + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil } - return 0 } -func (r *Runner) printOptLine(name string, enabled bool) { - status := "off" - if enabled { - status = "on" +func (r *Runner) printOptLine(name string, enabled, supported bool) { + state := r.optStatusText(enabled) + if supported { + r.outf("%s\t%s\n", name, state) + return } - r.outf("%s\t%s\n", name, status) + r.outf("%s\t%s\t(%q not supported)\n", name, state, r.optStatusText(!enabled)) } -func (r *Runner) readLine(raw bool) ([]byte, error) { +func (r *Runner) readLine(ctx context.Context, raw bool) ([]byte, error) { if r.stdin == nil { return nil, errors.New("interp: can't read, there's no stdin") } @@ -823,6 +945,19 @@ func (r *Runner) readLine(raw bool) ([]byte, error) { var line []byte esc := false + stopc := make(chan struct{}) + stop := context.AfterFunc(ctx, func() { + r.stdin.SetReadDeadline(time.Now()) + close(stopc) + }) + defer func() { + if !stop() { + // The AfterFunc was started. + // Wait for it to complete, and reset the file's deadline. + <-stopc + r.stdin.SetReadDeadline(time.Time{}) + } + }() for { var buf [1]byte n, err := r.stdin.Read(buf[:]) @@ -843,25 +978,20 @@ func (r *Runner) readLine(raw bool) ([]byte, error) { esc = false } } - if err == io.EOF && len(line) > 0 { - return line, nil - } if err != nil { - return nil, err + return line, err } } } -func (r *Runner) changeDir(ctx context.Context, path string) int { - if path == "" { - path = "." - } +func (r *Runner) changeDir(ctx context.Context, path string) uint8 { + path = cmp.Or(path, ".") path = r.absPath(path) info, err := r.stat(ctx, path) if err != nil || !info.IsDir() { return 1 } - if !hasPermissionToDir(info) { + if r.access(ctx, path, access_X_OK) != nil { return 1 } r.Dir = path @@ -877,7 +1007,7 @@ func absPath(dir, path string) string { if !filepath.IsAbs(path) { path = filepath.Join(dir, path) } - return filepath.Clean(path) + return filepath.Clean(path) // TODO: this clean is likely unnecessary } func (r *Runner) absPath(path string) string { @@ -987,3 +1117,11 @@ func (g *getopts) next(optstr string, args []string) (opt rune, optarg string, d return opt, optarg, false } + +// optStatusText returns a shell option's status text display +func (r *Runner) optStatusText(status bool) string { + if status { + return "on" + } + return "off" +} diff --git a/vendor/mvdan.cc/sh/v3/interp/handler.go b/vendor/mvdan.cc/sh/v3/interp/handler.go index 881ed83b32..9f0125c26d 100644 --- a/vendor/mvdan.cc/sh/v3/interp/handler.go +++ b/vendor/mvdan.cc/sh/v3/interp/handler.go @@ -7,16 +7,17 @@ import ( "context" "fmt" "io" + "io/fs" "io/ioutil" "os" "os/exec" "path/filepath" "runtime" "strings" - "syscall" "time" "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/syntax" ) // HandlerCtx returns HandlerContext value stored in ctx. @@ -31,9 +32,24 @@ func HandlerCtx(ctx context.Context) HandlerContext { type handlerCtxKey struct{} -// HandlerContext is the data passed to all the handler functions via a context value. -// It contains some of the current state of the Runner. +type handlerKind int + +const ( + _ handlerKind = iota + handlerKindExec // [ExecHandlerFunc] + handlerKindCall // [CallHandlerFunc] + handlerKindOpen // [OpenHandlerFunc] + handlerKindReadDir // [ReadDirHandlerFunc2] +) + +// HandlerContext is the data passed to all the handler functions via [context.WithValue]. +// It contains some of the current state of the [Runner]. type HandlerContext struct { + runner *Runner // for internal use only, e.g. [HandlerContext.Builtin] + + // kind records which type of handler this context was built for. + kind handlerKind + // Env is a read-only version of the interpreter's environment, // including environment variables, global variables, and local function // variables. @@ -42,7 +58,16 @@ type HandlerContext struct { // Dir is the interpreter's current directory. Dir string + // Pos is the source position which relates to the operation, + // such as a [syntax.CallExpr] when calling an [ExecHandlerFunc]. + // It may be invalid if the operation has no relevant position information. + Pos syntax.Pos + + // TODO(v4): use an os.File for stdin below directly. + // Stdin is the interpreter's current standard input reader. + // It is always an [*os.File], but the type here remains an [io.Reader] + // due to backwards compatibility. Stdin io.Reader // Stdout is the interpreter's current standard output writer. Stdout io.Writer @@ -50,13 +75,13 @@ type HandlerContext struct { Stderr io.Writer } -// CallHandlerFunc is a handler which runs on every CallExpr. +// CallHandlerFunc is a handler which runs on every [syntax.CallExpr]. // It is called once variable assignments and field expansion have occurred. // The call's arguments are replaced by what the handler returns, // and then the call is executed by the Runner as usual. // At this time, returning an empty slice without an error is not supported. // -// This handler is similar to ExecHandlerFunc, but has two major differences: +// This handler is similar to [ExecHandlerFunc], but has two major differences: // // First, it runs for all simple commands, including function calls and builtins. // @@ -64,19 +89,24 @@ type HandlerContext struct { // allow running custom code which allows replacing the argument list. // Shell builtins touch on many internals of the Runner, after all. // -// Returning a non-nil error will halt the Runner. +// Returning a non-nil error will halt the [Runner] and will be returned via the API. type CallHandlerFunc func(ctx context.Context, args []string) ([]string, error) +// TODO: consistently treat handler errors as non-fatal by default, +// but have an interface or API to specify fatal errors which should make +// the shell exit with a particular status code. + // ExecHandlerFunc is a handler which executes simple commands. -// It is called for all CallExpr nodes where the first argument is neither a -// declared function nor a builtin. +// It is called for all [syntax.CallExpr] nodes +// where the first argument is neither a declared function nor a builtin. // // Returning a nil error means a zero exit status. -// Other exit statuses can be set with NewExitStatus. -// Any other error will halt the Runner. +// Other exit statuses can be set by returning or wrapping a [NewExitStatus] error, +// and such an error is returned via the API if it is the last statement executed. +// Any other error will halt the [Runner] and will be returned via the API. type ExecHandlerFunc func(ctx context.Context, args []string) error -// DefaultExecHandler returns the ExecHandlerFunc used by default. +// DefaultExecHandler returns the [ExecHandlerFunc] used by default. // It finds binaries in PATH and executes them. // When context is cancelled, an interrupt signal is sent to running processes. // killTimeout is a duration to wait before sending the kill signal. @@ -84,14 +114,14 @@ type ExecHandlerFunc func(ctx context.Context, args []string) error // // On Windows, the kill signal is always sent immediately, // because Go doesn't currently support sending Interrupt on Windows. -// Runner.New sets killTimeout to 2 seconds by default. +// [Runner] defaults to a killTimeout of 2 seconds. func DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc { return func(ctx context.Context, args []string) error { hc := HandlerCtx(ctx) path, err := LookPathDir(hc.Dir, hc.Env, args[0]) if err != nil { fmt.Fprintln(hc.Stderr, err) - return NewExitStatus(127) + return ExitStatus(127) } cmd := exec.Cmd{ Path: path, @@ -105,47 +135,38 @@ func DefaultExecHandler(killTimeout time.Duration) ExecHandlerFunc { err = cmd.Start() if err == nil { - if done := ctx.Done(); done != nil { - go func() { - <-done - - if killTimeout <= 0 || runtime.GOOS == "windows" { - _ = cmd.Process.Signal(os.Kill) - return - } - - // TODO: don't temporarily leak this goroutine - // if the program stops itself with the - // interrupt. - go func() { - time.Sleep(killTimeout) - _ = cmd.Process.Signal(os.Kill) - }() - _ = cmd.Process.Signal(os.Interrupt) - }() - } + stopf := context.AfterFunc(ctx, func() { + if killTimeout <= 0 || runtime.GOOS == "windows" { + _ = cmd.Process.Signal(os.Kill) + return + } + _ = cmd.Process.Signal(os.Interrupt) + // TODO: don't sleep in this goroutine if the program + // stops itself with the interrupt above. + time.Sleep(killTimeout) + _ = cmd.Process.Signal(os.Kill) + }) + defer stopf() err = cmd.Wait() } - switch x := err.(type) { + switch err := err.(type) { case *exec.ExitError: - // started, but errored - default to 1 if OS - // doesn't have exit statuses - if status, ok := x.Sys().(syscall.WaitStatus); ok { - if status.Signaled() { - if ctx.Err() != nil { - return ctx.Err() - } - return NewExitStatus(uint8(128 + status.Signal())) + // Windows and Plan9 do not have support for [syscall.WaitStatus] + // with methods like Signaled and Signal, so for those, [waitStatus] is a no-op. + // Note: [waitStatus] is an alias [syscall.WaitStatus] + if status, ok := err.Sys().(waitStatus); ok && status.Signaled() { + if ctx.Err() != nil { + return ctx.Err() } - return NewExitStatus(uint8(status.ExitStatus())) + return ExitStatus(128 + status.Signal()) } - return NewExitStatus(1) + return ExitStatus(err.ExitCode()) case *exec.Error: // did not start fmt.Fprintf(hc.Stderr, "%v\n", err) - return NewExitStatus(127) + return ExitStatus(127) default: return err } @@ -203,12 +224,12 @@ func findFile(dir, file string, _ []string) (string, error) { return checkStat(dir, file, false) } -// LookPath is deprecated. See LookPathDir. +// LookPath is deprecated; see [LookPathDir]. func LookPath(env expand.Environ, file string) (string, error) { return LookPathDir(env.Get("PWD").String(), env, file) } -// LookPathDir is similar to os/exec.LookPath, with the difference that it uses the +// LookPathDir is similar to [os/exec.LookPath], with the difference that it uses the // provided environment. env is used to fetch relevant environment variables // such as PWD and PATH. // @@ -217,7 +238,7 @@ func LookPathDir(cwd string, env expand.Environ, file string) (string, error) { return lookPathDir(cwd, env, file, findExecutable) } -// findAny defines a function to pass to lookPathDir. +// findAny defines a function to pass to [lookPathDir]. type findAny = func(dir string, file string, exts []string) (string, error) func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (string, error) { @@ -253,7 +274,7 @@ func lookPathDir(cwd string, env expand.Environ, file string, find findAny) (str return "", fmt.Errorf("%q: executable file not found in $PATH", file) } -// scriptFromPathDir is similar to LookPathDir, with the difference that it looks +// scriptFromPathDir is similar to [LookPathDir], with the difference that it looks // for both executable and non-executable files. func scriptFromPathDir(cwd string, env expand.Environ, file string) (string, error) { return lookPathDir(cwd, env, file, findFile) @@ -280,49 +301,79 @@ func pathExts(env expand.Environ) []string { return exts } -// OpenHandlerFunc is a handler which opens files. It is -// called for all files that are opened directly by the shell, such as -// in redirects. Files opened by executed programs are not included. +// OpenHandlerFunc is a handler which opens files. +// It is called for all files that are opened directly by the shell, +// such as in redirects, except for named pipes created by process substitutions. +// Files opened by executed programs are not included. // -// The path parameter may be relative to the current directory, which can be -// fetched via HandlerCtx. +// The path parameter may be relative to the current directory, +// which can be fetched via [HandlerCtx]. // -// Use a return error of type *os.PathError to have the error printed to -// stderr and the exit status set to 1. If the error is of any other type, the -// interpreter will come to a stop. +// Use a return error of type [*os.PathError] to have the error printed to +// stderr and the exit status set to 1. +// Any other error will halt the [Runner] and will be returned via the API. +// +// Note that implementations which do not return [os.File] will cause +// extra files and goroutines for input redirections; see [StdIO]. type OpenHandlerFunc func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) -// DefaultOpenHandler returns an OpenHandlerFunc used by default. It uses os.OpenFile to open files. +// TODO: paths passed to [OpenHandlerFunc] should be cleaned. + +// DefaultOpenHandler returns the [OpenHandlerFunc] used by default. +// It uses [os.OpenFile] to open files. +// +// For the sake of portability, /dev/null opens NUL on Windows. func DefaultOpenHandler() OpenHandlerFunc { return func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { mc := HandlerCtx(ctx) - if path != "" && !filepath.IsAbs(path) { + if runtime.GOOS == "windows" && path == "/dev/null" { + path = "NUL" + // Work around https://go.dev/issue/71752, where Go 1.24 started giving + // "Invalid handle" errors when opening "NUL" with O_TRUNC. + // TODO: hopefully remove this in the future once the bug is fixed. + flag &^= os.O_TRUNC + } else if path != "" && !filepath.IsAbs(path) { path = filepath.Join(mc.Dir, path) } return os.OpenFile(path, flag, perm) } } +// TODO(v4): if this is kept in v4, it most likely needs to use [io/fs.DirEntry] for efficiency + // ReadDirHandlerFunc is a handler which reads directories. It is called during // shell globbing, if enabled. // -// TODO(v4): if this is kept in v4, it most likely needs to use fs.DirEntry for efficiency -type ReadDirHandlerFunc func(ctx context.Context, path string) ([]os.FileInfo, error) +// Deprecated: use [ReadDirHandlerFunc2], which uses [fs.DirEntry]. +type ReadDirHandlerFunc func(ctx context.Context, path string) ([]fs.FileInfo, error) -// DefaultReadDirHandler returns a ReadDirHandlerFunc used by default. It uses ioutil.ReadDir(). +// ReadDirHandlerFunc2 is a handler which reads directories. It is called during +// shell globbing, if enabled. +type ReadDirHandlerFunc2 func(ctx context.Context, path string) ([]fs.DirEntry, error) + +// DefaultReadDirHandler returns the [ReadDirHandlerFunc] used by default. +// It makes use of [ioutil.ReadDir]. func DefaultReadDirHandler() ReadDirHandlerFunc { - return func(ctx context.Context, path string) ([]os.FileInfo, error) { + return func(ctx context.Context, path string) ([]fs.FileInfo, error) { return ioutil.ReadDir(path) } } -// StatHandlerFunc is a handler which gets the file stat. the first argument provides directory to use as -// basedir if name is relative path -type StatHandlerFunc func(ctx context.Context, name string, followSymlinks bool) (os.FileInfo, error) +// DefaultReadDirHandler2 returns the [ReadDirHandlerFunc2] used by default. +// It uses [os.ReadDir]. +func DefaultReadDirHandler2() ReadDirHandlerFunc2 { + return func(ctx context.Context, path string) ([]fs.DirEntry, error) { + return os.ReadDir(path) + } +} + +// StatHandlerFunc is a handler which gets a file's information. +type StatHandlerFunc func(ctx context.Context, name string, followSymlinks bool) (fs.FileInfo, error) -// DefaultStatHandler returns a StatHandlerFunc used by default. It uses os.Stat() +// DefaultStatHandler returns the [StatHandlerFunc] used by default. +// It makes use of [os.Stat] and [os.Lstat], depending on followSymlinks. func DefaultStatHandler() StatHandlerFunc { - return func(ctx context.Context, path string, followSymlinks bool) (os.FileInfo, error) { + return func(ctx context.Context, path string, followSymlinks bool) (fs.FileInfo, error) { if !followSymlinks { return os.Lstat(path) } else { diff --git a/vendor/mvdan.cc/sh/v3/interp/os_notunix.go b/vendor/mvdan.cc/sh/v3/interp/os_notunix.go new file mode 100644 index 0000000000..3197b802fd --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/interp/os_notunix.go @@ -0,0 +1,57 @@ +// Copyright (c) 2017, Andrey Nering +// See LICENSE for licensing information + +//go:build !unix + +package interp + +import ( + "context" + "fmt" + + "mvdan.cc/sh/v3/syntax" +) + +func mkfifo(path string, mode uint32) error { + return fmt.Errorf("unsupported") +} + +// access attempts to emulate [unix.Access] on Windows. +// Windows seems to have a different system of permissions than Unix, +// so for now just rely on what [io/fs.FileInfo] gives us. +func (r *Runner) access(ctx context.Context, path string, mode uint32) error { + info, err := r.lstat(ctx, path) + if err != nil { + return err + } + m := info.Mode() + switch mode { + case access_R_OK: + if m&0o400 == 0 { + return fmt.Errorf("file is not readable") + } + case access_W_OK: + if m&0o200 == 0 { + return fmt.Errorf("file is not writable") + } + case access_X_OK: + if m&0o100 == 0 { + return fmt.Errorf("file is not executable") + } + } + return nil +} + +// unTestOwnOrGrp panics. Under Unix, it implements the -O and -G unary tests, +// but under Windows, it's unclear how to implement those tests, since Windows +// doesn't have the concept of a file owner, just ACLs, and it's unclear how +// to map the one to the other. +func (r *Runner) unTestOwnOrGrp(ctx context.Context, op syntax.UnTestOperator, x string) bool { + panic(fmt.Sprintf("unhandled unary test op: %v", op)) +} + +// waitStatus is a no-op on plan9 and windows. +type waitStatus struct{} + +func (waitStatus) Signaled() bool { return false } +func (waitStatus) Signal() int { return 0 } diff --git a/vendor/mvdan.cc/sh/v3/interp/os_unix.go b/vendor/mvdan.cc/sh/v3/interp/os_unix.go index 9f0fc5225c..214f7e0f0d 100644 --- a/vendor/mvdan.cc/sh/v3/interp/os_unix.go +++ b/vendor/mvdan.cc/sh/v3/interp/os_unix.go @@ -1,57 +1,48 @@ // Copyright (c) 2017, Andrey Nering // See LICENSE for licensing information -//go:build !windows -// +build !windows +//go:build unix package interp import ( - "os" + "context" "os/user" "strconv" "syscall" "golang.org/x/sys/unix" + "mvdan.cc/sh/v3/syntax" ) func mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } -// hasPermissionToDir returns if the OS current user has execute permission -// to the given directory -func hasPermissionToDir(info os.FileInfo) bool { - user, err := user.Current() +// access is similar to checking the permission bits from [io/fs.FileInfo], +// but it also takes into account the current user's role. +func (r *Runner) access(ctx context.Context, path string, mode uint32) error { + // TODO(v4): "access" may need to become part of a handler, like "open" or "stat". + return unix.Access(path, mode) +} + +// unTestOwnOrGrp implements the -O and -G unary tests. If the file does not +// exist, or the current user cannot be retrieved, returns false. +func (r *Runner) unTestOwnOrGrp(ctx context.Context, op syntax.UnTestOperator, x string) bool { + info, err := r.stat(ctx, x) if err != nil { - return false // unknown user; assume no permissions + return false } - uid, err := strconv.Atoi(user.Uid) + u, err := user.Current() if err != nil { + return false } - if uid == 0 { - return true // super-user - } - - st, _ := info.Sys().(*syscall.Stat_t) - if st == nil { - panic("unexpected info.Sys type") - } - perm := info.Mode().Perm() - // user (u) - if perm&0o100 != 0 && st.Uid == uint32(uid) { - return true + if op == syntax.TsUsrOwn { + uid, _ := strconv.Atoi(u.Uid) + return uint32(uid) == info.Sys().(*syscall.Stat_t).Uid } - - gid, _ := strconv.Atoi(user.Gid) - // other users in group (g) - if perm&0o010 != 0 && st.Uid != uint32(uid) && st.Gid == uint32(gid) { - return true - } - // remaining users (o) - if perm&0o001 != 0 && st.Uid != uint32(uid) && st.Gid != uint32(gid) { - return true - } - - return false + gid, _ := strconv.Atoi(u.Gid) + return uint32(gid) == info.Sys().(*syscall.Stat_t).Gid } + +type waitStatus = syscall.WaitStatus diff --git a/vendor/mvdan.cc/sh/v3/interp/os_windows.go b/vendor/mvdan.cc/sh/v3/interp/os_windows.go deleted file mode 100644 index b5b4d2ea07..0000000000 --- a/vendor/mvdan.cc/sh/v3/interp/os_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2017, Andrey Nering -// See LICENSE for licensing information - -package interp - -import ( - "fmt" - "os" -) - -func mkfifo(path string, mode uint32) error { - return fmt.Errorf("unsupported") -} - -// hasPermissionToDir is a no-op on Windows. -func hasPermissionToDir(info os.FileInfo) bool { - return true -} diff --git a/vendor/mvdan.cc/sh/v3/interp/runner.go b/vendor/mvdan.cc/sh/v3/interp/runner.go index 13168aa958..5de92048a5 100644 --- a/vendor/mvdan.cc/sh/v3/interp/runner.go +++ b/vendor/mvdan.cc/sh/v3/interp/runner.go @@ -6,13 +6,19 @@ package interp import ( "bytes" "context" + "errors" "fmt" "io" + "io/fs" + "iter" "math" - "math/rand" + mathrand "math/rand/v2" "os" + "path/filepath" "regexp" "runtime" + "slices" + "strconv" "strings" "sync" "time" @@ -22,6 +28,19 @@ import ( "mvdan.cc/sh/v3/syntax" ) +const ( + // shellReplyPS3Var, or PS3, is a special variable in Bash used by the select command, + // while the shell is awaiting for input. the default value is [shellDefaultPS3] + shellReplyPS3Var = "PS3" + // shellDefaultPS3, or #?, is PS3's default value + shellDefaultPS3 = "#? " + // shellReplyVar, or REPLY, is a special variable in Bash that is used to store the result of + // the select command or of the read command, when no variable name is specified + shellReplyVar = "REPLY" + + fifoNamePrefix = "sh-interp-" +) + func (r *Runner) fillExpandConfig(ctx context.Context) { r.ectx = ctx r.ecfg = &expand.Config{ @@ -44,11 +63,15 @@ func (r *Runner) fillExpandConfig(ctx context.Context) { f.Close() return err } - r2 := r.Subshell() + r2 := r.subshell(false) r2.stdout = w r2.stmts(ctx, cs.Stmts) + r2.exit.exiting = false // subshells don't exit the parent shell r.lastExpandExit = r2.exit - return r2.err + if r2.exit.fatalExit { + return r2.exit.err // surface fatal errors immediately + } + return nil }, ProcSubst: func(ps *syntax.ProcSubst) (string, error) { if runtime.GOOS == "windows" { @@ -58,19 +81,14 @@ func (r *Runner) fillExpandConfig(ctx context.Context) { return os.DevNull, nil } - if r.rand == nil { - r.rand = rand.New(rand.NewSource(time.Now().UnixNano())) - } - dir := os.TempDir() - // We can't atomically create a random unused temporary FIFO. - // Similar to os.CreateTemp, + // Similar to [os.CreateTemp], // keep trying new random paths until one does not exist. // We use a uint64 because a uint32 easily runs into retries. var path string try := 0 for { - path = fmt.Sprintf("%s/sh-interp-%x", dir, r.rand.Uint64()) + path = filepath.Join(r.tempDir, fifoNamePrefix+strconv.FormatUint(mathrand.Uint64(), 16)) err := mkfifo(path, 0o666) if err == nil { break @@ -83,29 +101,38 @@ func (r *Runner) fillExpandConfig(ctx context.Context) { } } - r2 := r.Subshell() + r2 := r.subshell(true) stdout := r.origStdout - r.wgProcSubsts.Add(1) + // TODO: note that `man bash` mentions that `wait` only waits for the last + // process substitution as long as it is $!; the logic here would mean we wait for all of them. + bg := bgProc{ + done: make(chan struct{}), + exit: new(exitStatus), + } + r.bgProcs = append(r.bgProcs, bg) go func() { - defer r.wgProcSubsts.Done() + defer func() { + *bg.exit = r2.exit + close(bg.done) + }() switch ps.Op { case syntax.CmdIn: f, err := os.OpenFile(path, os.O_WRONLY, 0) if err != nil { - r.errf("cannot open fifo for stdout: %v", err) + r.errf("cannot open fifo for stdout: %v\n", err) return } r2.stdout = f defer func() { if err := f.Close(); err != nil { - r.errf("closing stdout fifo: %v", err) + r.errf("closing stdout fifo: %v\n", err) } os.Remove(path) }() default: // syntax.CmdOut f, err := os.OpenFile(path, os.O_RDONLY, 0) if err != nil { - r.errf("cannot open fifo for stdin: %v", err) + r.errf("cannot open fifo for stdin: %v\n", err) return } r2.stdin = f @@ -117,6 +144,7 @@ func (r *Runner) fillExpandConfig(ctx context.Context) { }() } r2.stmts(ctx, ps.Stmts) + r2.exit.exiting = false // subshells don't exit the parent shell }() return path, nil }, @@ -142,22 +170,37 @@ func catShortcutArg(stmt *syntax.Stmt) *syntax.Word { func (r *Runner) updateExpandOpts() { if r.opts[optNoGlob] { - r.ecfg.ReadDir = nil + r.ecfg.ReadDir2 = nil } else { - r.ecfg.ReadDir = func(s string) ([]os.FileInfo, error) { - return r.readDirHandler(r.handlerCtx(context.Background()), s) + r.ecfg.ReadDir2 = func(s string) ([]fs.DirEntry, error) { + return r.readDirHandler(r.handlerCtx(r.ectx, handlerKindReadDir, todoPos), s) } } r.ecfg.GlobStar = r.opts[optGlobStar] + r.ecfg.NoCaseGlob = r.opts[optNoCaseGlob] r.ecfg.NullGlob = r.opts[optNullGlob] r.ecfg.NoUnset = r.opts[optNoUnset] } func (r *Runner) expandErr(err error) { - if err != nil { - r.errf("%v\n", err) - r.exitShell(context.TODO(), 1) + if err == nil { + return + } + errMsg := err.Error() + fmt.Fprintln(r.stderr, errMsg) + switch { + case errors.As(err, &expand.UnsetParameterError{}): + case errMsg == "invalid indirect expansion": + // TODO: These errors are treated as fatal by bash. + // Make the error type reflect that. + case strings.HasSuffix(errMsg, "not supported"): + // TODO: This "has suffix" is a temporary measure until the expand + // package supports all syntax nodes like extended globbing. + default: + return // other cases do not exit } + r.exit.code = 1 + r.exit.exiting = true } func (r *Runner) arithm(expr syntax.ArithmExpr) int { @@ -190,7 +233,7 @@ func (r *Runner) pattern(word *syntax.Word) string { return str } -// expandEnviron exposes Runner's variables to the expand package. +// expandEnviron exposes [Runner]'s variables to the expand package. type expandEnv struct { r *Runner } @@ -202,7 +245,7 @@ func (e expandEnv) Get(name string) expand.Variable { } func (e expandEnv) Set(name string, vr expand.Variable) error { - e.r.setVarInternal(name, vr) + e.r.setVar(name, vr) return nil // TODO: return any errors } @@ -210,41 +253,43 @@ func (e expandEnv) Each(fn func(name string, vr expand.Variable) bool) { e.r.writeEnv.Each(fn) } -func (r *Runner) handlerCtx(ctx context.Context) context.Context { +var todoPos syntax.Pos // for handlerCtx callers where we don't yet have a position + +func (r *Runner) handlerCtx(ctx context.Context, kind handlerKind, pos syntax.Pos) context.Context { hc := HandlerContext{ + runner: r, + kind: kind, Env: &overlayEnviron{parent: r.writeEnv}, Dir: r.Dir, - Stdin: r.stdin, + Pos: pos, Stdout: r.stdout, Stderr: r.stderr, } - return context.WithValue(ctx, handlerCtxKey{}, hc) -} - -func (r *Runner) setErr(err error) { - if r.err == nil { - r.err = err + if r.stdin != nil { // do not leave hc.Stdin as a typed nil + hc.Stdin = r.stdin } + return context.WithValue(ctx, handlerCtxKey{}, hc) } func (r *Runner) out(s string) { io.WriteString(r.stdout, s) } -func (r *Runner) outf(format string, a ...interface{}) { +func (r *Runner) outf(format string, a ...any) { fmt.Fprintf(r.stdout, format, a...) } -func (r *Runner) errf(format string, a ...interface{}) { +func (r *Runner) errf(format string, a ...any) { fmt.Fprintf(r.stderr, format, a...) } func (r *Runner) stop(ctx context.Context) bool { - if r.err != nil || r.Exited() { + // Some traps trigger on exit, so we do want those to run. + if !r.handlingTrap && (r.exit.returning || r.exit.exiting) { return true } if err := ctx.Err(); err != nil { - r.err = err + r.exit.fatal(err) return true } if r.opts[optNoExec] { @@ -257,14 +302,22 @@ func (r *Runner) stmt(ctx context.Context, st *syntax.Stmt) { if r.stop(ctx) { return } - r.exit = 0 + r.exit = exitStatus{} if st.Background { - r2 := r.Subshell() + r2 := r.subshell(true) st2 := *st st2.Background = false - r.bgShells.Go(func() error { - return r2.Run(ctx, &st2) - }) + bg := bgProc{ + done: make(chan struct{}), + exit: new(exitStatus), + } + r.bgProcs = append(r.bgProcs, bg) + go func() { + r2.Run(ctx, &st2) + r2.exit.exiting = false // subshells don't exit the parent shell + *bg.exit = r2.exit + close(bg.done) + }() } else { r.stmtSync(ctx, st) } @@ -272,34 +325,34 @@ func (r *Runner) stmt(ctx context.Context, st *syntax.Stmt) { } func (r *Runner) stmtSync(ctx context.Context, st *syntax.Stmt) { - defer r.wgProcSubsts.Wait() oldIn, oldOut, oldErr := r.stdin, r.stdout, r.stderr for _, rd := range st.Redirs { cls, err := r.redir(ctx, rd) if err != nil { - r.exit = 1 + r.exit.code = 1 break } if cls != nil { defer cls.Close() } } - if r.exit == 0 && st.Cmd != nil { + if r.exit.ok() && st.Cmd != nil { r.cmd(ctx, st.Cmd) } if st.Negated { - r.exit = oneIf(r.exit == 0) - } else if _, ok := st.Cmd.(*syntax.CallExpr); !ok { - } else if r.exit != 0 && !r.noErrExit && r.opts[optErrExit] { - // If the "errexit" option is set and a simple command failed, - // exit the shell. Exceptions: + // TODO: negate the entire [exitStatus] here, wiping errors + r.exit.oneIf(r.exit.ok()) + } else if b, ok := st.Cmd.(*syntax.BinaryCmd); ok && (b.Op == syntax.AndStmt || b.Op == syntax.OrStmt) { + } else if !r.exit.ok() && !r.noErrExit { + r.trapCallback(ctx, r.callbackErr, "error") + // If the "errexit" option is set and a command failed, exit the shell. Exceptions: // // conditions (if , while , etc) - // part of && or || lists - // preceded by ! - r.exitShell(ctx, r.exit) - } else if r.exit != 0 { - r.trapCallback(ctx, r.callbackErr, "error") + // part of && or || lists; excluded via "else" above + // preceded by !; excluded via "else" above + if r.opts[optErrExit] { + r.exit.exiting = true + } } if !r.keepRedirs { r.stdin, r.stdout, r.stderr = oldIn, oldOut, oldErr @@ -314,36 +367,44 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { tracingEnabled := r.opts[optXTrace] trace := r.tracer() - switch x := cm.(type) { + switch cm := cm.(type) { case *syntax.Block: - r.stmts(ctx, x.Stmts) + r.stmts(ctx, cm.Stmts) case *syntax.Subshell: - r2 := r.Subshell() - r2.stmts(ctx, x.Stmts) + r2 := r.subshell(false) + r2.stmts(ctx, cm.Stmts) + r2.exit.exiting = false // subshells don't exit the parent shell r.exit = r2.exit - r.setErr(r2.err) case *syntax.CallExpr: // Use a new slice, to not modify the slice in the alias map. - var args []*syntax.Word - left := x.Args - for len(left) > 0 && r.opts[optExpandAliases] { - als, ok := r.alias[left[0].Lit()] + args := cm.Args + for i := 0; i < len(args); { + if !r.opts[optExpandAliases] { + break + } + als, ok := r.alias[args[i].Lit()] if !ok { break } - args = append(args, als.args...) - left = left[1:] + args = slices.Replace(args, i, i+1, als.args...) if !als.blank { break } + i += len(als.args) } - args = append(args, left...) - r.lastExpandExit = 0 + r.lastExpandExit = exitStatus{} fields := r.fields(args...) if len(fields) == 0 { - for _, as := range x.Assigns { - vr := r.assignVal(as, "") - r.setVar(as.Name.Value, as.Index, vr) + for _, as := range cm.Assigns { + prev := r.lookupVar(as.Name.Value) + // Here we have a naked "foo=bar", so if we inherited a local var from a parent + // function we want to signal that we are modifying the parent var rather than + // creating a new local var via "local foo=bar". + // TODO: there is likely a better way to do this. + prev.Local = false + + vr := r.assignVal(prev, as, "") + r.setVarWithIndex(prev, as.Name.Value, as.Index, vr) if !tracingEnabled { continue @@ -366,7 +427,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { // If interpreting the last expansion like $(foo) failed, // and the expansion and assignments otherwise succeeded, // we need to surface that last exit code. - if r.exit == 0 { + if r.exit.ok() { r.exit = r.lastExpandExit } break @@ -378,45 +439,45 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { } var restores []restoreVar - for _, as := range x.Assigns { + for _, as := range cm.Assigns { name := as.Name.Value - origVr := r.lookupVar(name) + prev := r.lookupVar(name) - vr := r.assignVal(as, "") + vr := r.assignVal(prev, as, "") // Inline command vars are always exported. vr.Exported = true - restores = append(restores, restoreVar{name, origVr}) + restores = append(restores, restoreVar{name, prev}) - r.setVarInternal(name, vr) + r.setVar(name, vr) } trace.call(fields[0], fields[1:]...) trace.newLineFlush() - r.call(ctx, x.Args[0].Pos(), fields) + r.call(ctx, cm.Args[0].Pos(), fields) for _, restore := range restores { - r.setVarInternal(restore.name, restore.vr) + r.setVar(restore.name, restore.vr) } case *syntax.BinaryCmd: - switch x.Op { + switch cm.Op { case syntax.AndStmt, syntax.OrStmt: oldNoErrExit := r.noErrExit r.noErrExit = true - r.stmt(ctx, x.X) + r.stmt(ctx, cm.X) r.noErrExit = oldNoErrExit - if (r.exit == 0) == (x.Op == syntax.AndStmt) { - r.stmt(ctx, x.Y) + if r.exit.ok() == (cm.Op == syntax.AndStmt) { + r.stmt(ctx, cm.Y) } case syntax.Pipe, syntax.PipeAll: pr, pw, err := os.Pipe() if err != nil { - r.setErr(err) + r.exit.fatal(err) // not being able to create a pipe is rare but critical return } - r2 := r.Subshell() + r2 := r.subshell(true) r2.stdout = pw - if x.Op == syntax.PipeAll { + if cm.Op == syntax.PipeAll { r2.stderr = pw } else { r2.stderr = r.stderr @@ -425,47 +486,50 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { var wg sync.WaitGroup wg.Add(1) go func() { - r2.stmt(ctx, x.X) + r2.stmt(ctx, cm.X) + r2.exit.exiting = false // subshells don't exit the parent shell pw.Close() wg.Done() }() - r.stmt(ctx, x.Y) + r.stmt(ctx, cm.Y) pr.Close() wg.Wait() - if r.opts[optPipeFail] && r2.exit != 0 && r.exit == 0 { + if r.opts[optPipeFail] && !r2.exit.ok() && r.exit.ok() { r.exit = r2.exit } - r.setErr(r2.err) + if r2.exit.fatalExit { + r.exit.fatal(r2.exit.err) // surface fatal errors immediately + } } case *syntax.IfClause: oldNoErrExit := r.noErrExit r.noErrExit = true - r.stmts(ctx, x.Cond) + r.stmts(ctx, cm.Cond) r.noErrExit = oldNoErrExit - if r.exit == 0 { - r.stmts(ctx, x.Then) + if r.exit.ok() { + r.stmts(ctx, cm.Then) break } - r.exit = 0 - if x.Else != nil { - r.cmd(ctx, x.Else) + r.exit.code = 0 + if cm.Else != nil { + r.cmd(ctx, cm.Else) } case *syntax.WhileClause: for !r.stop(ctx) { oldNoErrExit := r.noErrExit r.noErrExit = true - r.stmts(ctx, x.Cond) + r.stmts(ctx, cm.Cond) r.noErrExit = oldNoErrExit - stop := (r.exit == 0) == x.Until - r.exit = 0 - if stop || r.loopStmtsBroken(ctx, x.Do) { + stop := r.exit.ok() == cm.Until + r.exit.code = 0 + if stop || r.loopStmtsBroken(ctx, cm.Do) { break } } case *syntax.ForClause: - switch y := x.Loop.(type) { + switch y := cm.Loop.(type) { case *syntax.WordIter: name := y.Name.Value items := r.Params // for i; do ... @@ -475,6 +539,47 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { items = r.fields(y.Items...) // for i in ...; do ... } + if cm.Select { + ps3 := shellDefaultPS3 + if e := r.envGet(shellReplyPS3Var); e != "" { + ps3 = e + } + + prompt := func() []byte { + // display menu + for i, word := range items { + r.errf("%d) %v\n", i+1, word) + } + r.errf("%s", ps3) + + line, err := r.readLine(ctx, true) + if err != nil { + r.exit.code = 1 + return nil + } + return line + } + + retry: + choice := prompt() + if len(choice) == 0 { + goto retry // no reply; try again + } + + reply := string(choice) + r.setVarString(shellReplyVar, reply) + + c, _ := strconv.Atoi(reply) + if c > 0 && c <= len(items) { + r.setVarString(name, items[c-1]) + } + + // execute commands until break or return is encountered + if r.loopStmtsBroken(ctx, cm.Do) { + break + } + } + for _, field := range items { r.setVarString(name, field) trace.stringf("for %s in", y.Name.Value) @@ -487,7 +592,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { trace.string(` "$@"`) } trace.newLineFlush() - if r.loopStmtsBroken(ctx, x.Do) { + if r.loopStmtsBroken(ctx, cm.Do) { break } } @@ -496,7 +601,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { r.arithm(y.Init) } for y.Cond == nil || r.arithm(y.Cond) != 0 { - if r.exit != 0 || r.loopStmtsBroken(ctx, x.Do) { + if !r.exit.ok() || r.loopStmtsBroken(ctx, cm.Do) { break } if y.Post != nil { @@ -505,41 +610,41 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { } } case *syntax.FuncDecl: - r.setFunc(x.Name.Value, x.Body) + r.setFunc(cm.Name.Value, cm.Body) case *syntax.ArithmCmd: - r.exit = oneIf(r.arithm(x.X) == 0) + r.exit.oneIf(r.arithm(cm.X) == 0) case *syntax.LetClause: var val int - for _, expr := range x.Exprs { + for _, expr := range cm.Exprs { val = r.arithm(expr) if !tracingEnabled { continue } - switch v := expr.(type) { + switch expr := expr.(type) { case *syntax.Word: - qs, err := syntax.Quote(r.literal(v), syntax.LangBash) + qs, err := syntax.Quote(r.literal(expr), syntax.LangBash) if err != nil { return } trace.stringf("let %v", qs) case *syntax.BinaryArithm, *syntax.UnaryArithm: - trace.expr(x) + trace.expr(cm) case *syntax.ParenArithm: // TODO } } trace.newLineFlush() - r.exit = oneIf(val == 0) + r.exit.oneIf(val == 0) case *syntax.CaseClause: trace.string("case ") - trace.expr(x.Word) + trace.expr(cm.Word) trace.string(" in") trace.newLineFlush() - str := r.literal(x.Word) - for _, ci := range x.Items { + str := r.literal(cm.Word) + for _, ci := range cm.Items { for _, word := range ci.Patterns { pattern := r.pattern(word) if match(pattern, str) { @@ -549,15 +654,15 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { } } case *syntax.TestClause: - if r.bashTest(ctx, x.X, false) == "" && r.exit == 0 { + if r.bashTest(ctx, cm.X, false) == "" && r.exit.ok() { // to preserve exit status code 2 for regex errors, etc - r.exit = 1 + r.exit.code = 1 } case *syntax.DeclClause: local, global := false, false var modes []string valType := "" - switch x.Variant.Value { + switch cm.Variant.Value { case "declare": // When used in a function, "declare" acts as "local" // unless the "-g" option is used. @@ -565,7 +670,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { case "local": if !r.inFunc { r.errf("local: can only be used in a function\n") - r.exit = 1 + r.exit.code = 1 return } local = true @@ -576,73 +681,73 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { case "nameref": valType = "-n" } - for _, as := range x.Args { - for _, as := range r.flattenAssign(as) { - name := as.Name.Value - if strings.HasPrefix(name, "-") { - switch name { - case "-x", "-r": - modes = append(modes, name) - case "-a", "-A", "-n": - valType = name - case "-g": - global = true - default: - r.errf("declare: invalid option %q\n", name) - r.exit = 2 - return - } - continue - } - if !syntax.ValidName(name) { - r.errf("declare: invalid name %q\n", name) - r.exit = 1 + assignLoop: + for as := range r.flattenAssigns(cm.Args) { + fp := flagParser{remaining: []string{as.Name.Value}} + for fp.more() { + switch flag := fp.flag(); flag { + case "-x", "-r": + modes = append(modes, flag) + case "-a", "-A", "-n": + valType = flag + case "-g": + global = true + default: + r.errf("declare: invalid option %q\n", flag) + r.exit.code = 2 return } - var vr expand.Variable - if !as.Naked { - vr = r.assignVal(as, valType) - } - if global { - vr.Local = false - } else if local { - vr.Local = true - } - for _, mode := range modes { - switch mode { - case "-x": - vr.Exported = true - case "-r": - vr.ReadOnly = true - } - } - if as.Naked { - if vr.Exported || vr.Local || vr.ReadOnly { - r.setVarInternal(name, vr) - } + continue assignLoop + } + name := as.Name.Value + if !syntax.ValidName(name) { + r.errf("declare: invalid name %q\n", name) + r.exit.code = 1 + return + } + vr := r.lookupVar(as.Name.Value) + if as.Naked { + if valType == "-A" { + vr.Kind = expand.Associative } else { - r.setVar(name, as.Index, vr) + vr.Kind = expand.KeepValue } + } else { + vr = r.assignVal(vr, as, valType) + } + if global { + vr.Local = false + } else if local { + vr.Local = true } + for _, mode := range modes { + switch mode { + case "-x": + vr.Exported = true + case "-r": + vr.ReadOnly = true + } + } + r.setVar(name, vr) } case *syntax.TimeClause: start := time.Now() - if x.Stmt != nil { - r.stmt(ctx, x.Stmt) + if cm.Stmt != nil { + r.stmt(ctx, cm.Stmt) } format := "%s\t%s\n" - if x.PosixFormat { + if cm.PosixFormat { format = "%s %s\n" } else { r.outf("\n") } real := time.Since(start) - r.outf(format, "real", elapsedString(real, x.PosixFormat)) + r.outf(format, "real", elapsedString(real, cm.PosixFormat)) // TODO: can we do these? - r.outf(format, "user", elapsedString(0, x.PosixFormat)) - r.outf(format, "sys", elapsedString(0, x.PosixFormat)) + r.outf(format, "user", elapsedString(0, cm.PosixFormat)) + r.outf(format, "sys", elapsedString(0, cm.PosixFormat)) default: - panic(fmt.Sprintf("unhandled command node: %T", x)) + panic(fmt.Sprintf("unhandled command node: %T", cm)) } } @@ -663,53 +768,50 @@ func (r *Runner) trapCallback(ctx context.Context, callback, name string) { // ignore errors in the callback return } + oldExit := r.exit r.stmts(ctx, file.Stmts) + r.exit = oldExit // traps on EXIT or ERR should not modify the result r.handlingTrap = false } -// exitShell exits the current shell session with the given status code. -func (r *Runner) exitShell(ctx context.Context, status int) { - if status != 0 { - r.trapCallback(ctx, r.callbackErr, "error") - } - r.trapCallback(ctx, r.callbackExit, "exit") - - r.shellExited = true - // Restore the original exit status. We ignore the callbacks. - r.exit = status -} - -func (r *Runner) flattenAssign(as *syntax.Assign) []*syntax.Assign { - // Convert "declare $x" into "declare value". - // Don't use syntax.Parser here, as we only want the basic - // splitting by '='. - if as.Name != nil { - return []*syntax.Assign{as} // nothing to do - } - var asgns []*syntax.Assign - for _, field := range r.fields(as.Value) { - as := &syntax.Assign{} - parts := strings.SplitN(field, "=", 2) - as.Name = &syntax.Lit{Value: parts[0]} - if len(parts) == 1 { - as.Naked = true - } else { - as.Value = &syntax.Word{Parts: []syntax.WordPart{ - &syntax.Lit{Value: parts[1]}, - }} +func (r *Runner) flattenAssigns(args []*syntax.Assign) iter.Seq[*syntax.Assign] { + return func(yield func(*syntax.Assign) bool) { + for _, as := range args { + // Convert "declare $x" into "declare value". + // Don't use syntax.Parser here, as we only want the basic + // splitting by '='. + if as.Name != nil { + if !yield(as) { + return + } + continue + } + for _, field := range r.fields(as.Value) { + as := &syntax.Assign{} + name, val, ok := strings.Cut(field, "=") + as.Name = &syntax.Lit{Value: name} + if !ok { + as.Naked = true + } else { + as.Value = &syntax.Word{Parts: []syntax.WordPart{ + &syntax.Lit{Value: val}, + }} + } + if !yield(as) { + return + } + } } - asgns = append(asgns, as) } - return asgns } func match(pat, name string) bool { - expr, err := pattern.Regexp(pat, 0) + expr, err := pattern.Regexp(pat, pattern.EntireString) if err != nil { return false } - rx := regexp.MustCompile("(?m)^" + expr + "$") + rx := regexp.MustCompile(expr) return rx.MatchString(name) } @@ -728,10 +830,22 @@ func (r *Runner) stmts(ctx context.Context, stmts []*syntax.Stmt) { } } -func (r *Runner) hdocReader(rd *syntax.Redirect) io.Reader { +func (r *Runner) hdocReader(rd *syntax.Redirect) (*os.File, error) { + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + // We write to the pipe in a new goroutine, + // as pipe writes may block once the buffer gets full. + // We still construct and buffer the entire heredoc first, + // as doing it concurrently would lead to different semantics and be racy. if rd.Op != syntax.DashHdoc { hdoc := r.document(rd.Hdoc) - return strings.NewReader(hdoc) + go func() { + pw.WriteString(hdoc) + pw.Close() + }() + return pr, nil } var buf bytes.Buffer var cur []syntax.WordPart @@ -758,39 +872,76 @@ func (r *Runner) hdocReader(rd *syntax.Redirect) io.Reader { } } flushLine() - return &buf + go func() { + pw.Write(buf.Bytes()) + pw.Close() + }() + return pr, nil } func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, error) { if rd.Hdoc != nil { - r.stdin = r.hdocReader(rd) - return nil, nil + pr, err := r.hdocReader(rd) + if err != nil { + return nil, err + } + r.stdin = pr + return pr, nil } + orig := &r.stdout if rd.N != nil { switch rd.N.Value { + case "0": + // Note that the input redirects below always use stdin (0) + // because we don't support anything else right now. case "1": + // The default for the output redirects below. case "2": orig = &r.stderr + default: + panic(fmt.Sprintf("unsupported redirect fd: %v", rd.N.Value)) } } arg := r.literal(rd.Word) switch rd.Op { case syntax.WordHdoc: - r.stdin = strings.NewReader(arg + "\n") - return nil, nil + pr, pw, err := os.Pipe() + if err != nil { + return nil, err + } + r.stdin = pr + // We write to the pipe in a new goroutine, + // as pipe writes may block once the buffer gets full. + go func() { + pw.WriteString(arg) + pw.WriteString("\n") + pw.Close() + }() + return pr, nil case syntax.DplOut: switch arg { case "1": *orig = r.stdout case "2": *orig = r.stderr + case "-": + *orig = io.Discard // closing the output writer + default: + panic(fmt.Sprintf("unhandled %v arg: %q", rd.Op, arg)) } return nil, nil case syntax.RdrIn, syntax.RdrOut, syntax.AppOut, syntax.RdrAll, syntax.AppAll: // done further below - // case syntax.DplIn: + case syntax.DplIn: + switch arg { + case "-": + r.stdin = nil // closing the input file + default: + panic(fmt.Sprintf("unhandled %v arg: %q", rd.Op, arg)) + } + return nil, nil default: panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op)) } @@ -807,7 +958,11 @@ func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, err } switch rd.Op { case syntax.RdrIn: - r.stdin = f + stdin, err := stdinFile(f) + if err != nil { + return nil, err + } + r.stdin = stdin case syntax.RdrOut, syntax.AppOut: *orig = f case syntax.RdrAll, syntax.AppAll: @@ -837,20 +992,16 @@ func (r *Runner) loopStmtsBroken(ctx context.Context, stmts []*syntax.Stmt) bool return false } -type returnStatus uint8 - -func (s returnStatus) Error() string { return fmt.Sprintf("return status %d", s) } - func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) { if r.stop(ctx) { return } if r.callHandler != nil { var err error - args, err = r.callHandler(r.handlerCtx(ctx), args) + args, err = r.callHandler(r.handlerCtx(ctx, handlerKindCall, pos), args) if err != nil { // handler's custom fatal error - r.setErr(err) + r.exit.fatal(err) return } } @@ -863,7 +1014,7 @@ func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) { r.inFunc = true // Functions run in a nested scope. - // Note that Runner.exec below does something similar. + // Note that [Runner.exec] below does something similar. origEnv := r.writeEnv r.writeEnv = &overlayEnviron{parent: r.writeEnv, funcScope: true} @@ -873,54 +1024,55 @@ func (r *Runner) call(ctx context.Context, pos syntax.Pos, args []string) { r.Params = oldParams r.inFunc = oldInFunc - if code, ok := r.err.(returnStatus); ok { - r.err = nil - r.exit = int(code) - } + r.exit.returning = false return } - if isBuiltin(name) { - r.exit = r.builtinCode(ctx, pos, name, args[1:]) + if IsBuiltin(name) { + r.exit = r.builtin(ctx, pos, name, args[1:]) return } - r.exec(ctx, args) + r.exec(ctx, pos, args) } -func (r *Runner) exec(ctx context.Context, args []string) { - err := r.execHandler(r.handlerCtx(ctx), args) - if status, ok := IsExitStatus(err); ok { - r.exit = int(status) - return - } - if err != nil { - // handler's custom fatal error - r.setErr(err) - return - } - r.exit = 0 +func (r *Runner) exec(ctx context.Context, pos syntax.Pos, args []string) { + r.exit.fromHandlerError(r.execHandler(r.handlerCtx(ctx, handlerKindExec, pos), args)) } func (r *Runner) open(ctx context.Context, path string, flags int, mode os.FileMode, print bool) (io.ReadWriteCloser, error) { - f, err := r.openHandler(r.handlerCtx(ctx), path, flags, mode) + // If we are opening a FIFO temporary file created by the interpreter itself, + // don't pass this along to the open handler as it will not work at all + // unless [os.OpenFile] is used directly with it. + // Matching by directory and basename prefix isn't perfect, but works. + // + // If we want FIFOs to use a handler in the future, they probably + // need their own separate handler API matching Unix-like semantics. + dir, name := filepath.Split(path) + dir = strings.TrimSuffix(dir, "/") + if dir == r.tempDir && strings.HasPrefix(name, fifoNamePrefix) { + return os.OpenFile(path, flags, mode) + } + + f, err := r.openHandler(r.handlerCtx(ctx, handlerKindOpen, todoPos), path, flags, mode) // TODO: support wrapped PathError returned from openHandler. switch err.(type) { case nil: + return f, nil case *os.PathError: if print { r.errf("%v\n", err) } default: // handler's custom fatal error - r.setErr(err) + r.exit.fatal(err) } - return f, err + return nil, err } -func (r *Runner) stat(ctx context.Context, name string) (os.FileInfo, error) { +func (r *Runner) stat(ctx context.Context, name string) (fs.FileInfo, error) { path := absPath(r.Dir, name) return r.statHandler(ctx, path, true) } -func (r *Runner) lstat(ctx context.Context, name string) (os.FileInfo, error) { +func (r *Runner) lstat(ctx context.Context, name string) (fs.FileInfo, error) { path := absPath(r.Dir, name) return r.statHandler(ctx, path, false) } diff --git a/vendor/mvdan.cc/sh/v3/interp/test.go b/vendor/mvdan.cc/sh/v3/interp/test.go index fb45699c4f..dda46b9b7d 100644 --- a/vendor/mvdan.cc/sh/v3/interp/test.go +++ b/vendor/mvdan.cc/sh/v3/interp/test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "os" - "os/exec" "regexp" "golang.org/x/term" @@ -64,10 +63,20 @@ func (r *Runner) binTest(ctx context.Context, op syntax.BinTestOperator, x, y st case syntax.TsReMatch: re, err := regexp.Compile(y) if err != nil { - r.exit = 2 + r.exit.code = 2 return false } - return re.MatchString(x) + m := re.FindStringSubmatch(x) + if m == nil { + return false + } + vr := expand.Variable{ + Set: true, + Kind: expand.Indexed, + List: m, + } + r.setVar("BASH_REMATCH", vr) + return true case syntax.TsNewer: info1, err1 := r.stat(ctx, x) info2, err2 := r.stat(ctx, y) @@ -117,6 +126,13 @@ func (r *Runner) statMode(ctx context.Context, name string, mode os.FileMode) bo return err == nil && info.Mode()&mode != 0 } +// These are copied from x/sys/unix as we can't import it here. +const ( + access_R_OK = 0x4 + access_W_OK = 0x2 + access_X_OK = 0x1 +) + func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) bool { switch op { case syntax.TsExists: @@ -150,26 +166,17 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) // case syntax.TsUsrOwn: // case syntax.TsModif: case syntax.TsRead: - f, err := r.open(ctx, x, os.O_RDONLY, 0, false) - if err == nil { - f.Close() - } - return err == nil + return r.access(ctx, r.absPath(x), access_R_OK) == nil case syntax.TsWrite: - f, err := r.open(ctx, x, os.O_WRONLY, 0, false) - if err == nil { - f.Close() - } - return err == nil + return r.access(ctx, r.absPath(x), access_W_OK) == nil case syntax.TsExec: - _, err := exec.LookPath(r.absPath(x)) - return err == nil + return r.access(ctx, r.absPath(x), access_X_OK) == nil case syntax.TsNoEmpty: info, err := r.stat(ctx, x) return err == nil && info.Size() > 0 case syntax.TsFdTerm: fd := atoi(x) - var f interface{} + var f any switch fd { case 0: f = r.stdin @@ -179,7 +186,7 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) f = r.stderr } if f, ok := f.(interface{ Fd() uintptr }); ok { - // Support Fd methods such as the one on *os.File. + // Support [os.File.Fd] methods such as the one on [*os.File]. return term.IsTerminal(int(f.Fd())) } // TODO: allow term.IsTerminal here too if running in the @@ -190,7 +197,7 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) case syntax.TsNempStr: return x != "" case syntax.TsOptSet: - if opt := r.optByName(x, false); opt != nil { + if _, opt := r.optByName(x, false); opt != nil { return *opt } return false @@ -200,6 +207,8 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) return r.lookupVar(x).Kind == expand.NameRef case syntax.TsNot: return x == "" + case syntax.TsUsrOwn, syntax.TsGrpOwn: + return r.unTestOwnOrGrp(ctx, op, x) default: panic(fmt.Sprintf("unhandled unary test op: %v", op)) } diff --git a/vendor/mvdan.cc/sh/v3/interp/test_classic.go b/vendor/mvdan.cc/sh/v3/interp/test_classic.go index f0b1b20bd9..905f3a7d17 100644 --- a/vendor/mvdan.cc/sh/v3/interp/test_classic.go +++ b/vendor/mvdan.cc/sh/v3/interp/test_classic.go @@ -19,7 +19,7 @@ type testParser struct { err func(err error) } -func (p *testParser) errf(format string, a ...interface{}) { +func (p *testParser) errf(format string, a ...any) { p.err(fmt.Errorf(format, a...)) } @@ -51,7 +51,7 @@ func (p *testParser) classicTest(fval string, pastAndOr bool) syntax.TestExpr { } else { left = p.classicTest(fval, true) } - if left == nil || p.eof { + if left == nil || p.eof || p.val == ")" { return left } opStr := p.val @@ -76,7 +76,7 @@ func (p *testParser) classicTest(fval string, pastAndOr bool) syntax.TestExpr { } func (p *testParser) testExprBase(fval string) syntax.TestExpr { - if p.eof { + if p.eof || p.val == ")" { return nil } op := testUnaryOp(p.val) @@ -86,6 +86,15 @@ func (p *testParser) testExprBase(fval string) syntax.TestExpr { p.next() u.X = p.classicTest(op.String(), false) return u + case syntax.TsParen: + pe := &syntax.ParenTest{} + p.next() + pe.X = p.classicTest(op.String(), false) + if p.val != ")" { + p.errf("reached %s without matching ( with )", p.val) + } + p.next() + return pe case illegalTok: return p.followWord(fval) default: @@ -108,6 +117,8 @@ func testUnaryOp(val string) syntax.UnTestOperator { switch val { case "!": return syntax.TsNot + case "(": + return syntax.TsParen case "-e", "-a": return syntax.TsExists case "-f": diff --git a/vendor/mvdan.cc/sh/v3/interp/trace.go b/vendor/mvdan.cc/sh/v3/interp/trace.go index 08b6eafceb..d8d38ead6a 100644 --- a/vendor/mvdan.cc/sh/v3/interp/trace.go +++ b/vendor/mvdan.cc/sh/v3/interp/trace.go @@ -14,7 +14,7 @@ import ( type tracer struct { buf bytes.Buffer printer *syntax.Printer - stdout io.Writer + output io.Writer needsPlus bool } @@ -25,7 +25,7 @@ func (r *Runner) tracer() *tracer { return &tracer{ printer: syntax.NewPrinter(), - stdout: r.stdout, + output: r.stderr, needsPlus: true, } } @@ -44,7 +44,7 @@ func (t *tracer) string(s string) { t.buf.WriteString(s) } -func (t *tracer) stringf(f string, a ...interface{}) { +func (t *tracer) stringf(f string, a ...any) { if t == nil { return } @@ -74,7 +74,7 @@ func (t *tracer) flush() { return } - t.stdout.Write(t.buf.Bytes()) + t.output.Write(t.buf.Bytes()) t.buf.Reset() } @@ -102,7 +102,7 @@ func (t *tracer) call(cmd string, args ...string) { if strings.TrimSpace(s) == "" { // fields may be empty for function () {} declarations t.string(cmd) - } else if isBuiltin(cmd) { + } else if IsBuiltin(cmd) { if cmd == "set" { // TODO: only first occurrence of set is not printed, succeeding calls are printed return diff --git a/vendor/mvdan.cc/sh/v3/interp/vars.go b/vendor/mvdan.cc/sh/v3/interp/vars.go index feda4285b8..c15b6de538 100644 --- a/vendor/mvdan.cc/sh/v3/interp/vars.go +++ b/vendor/mvdan.cc/sh/v3/interp/vars.go @@ -4,9 +4,14 @@ package interp import ( + cryptorand "crypto/rand" + "encoding/binary" "fmt" + "maps" + mathrand "math/rand/v2" "os" "runtime" + "slices" "strconv" "strings" @@ -14,12 +19,29 @@ import ( "mvdan.cc/sh/v3/syntax" ) +func newOverlayEnviron(parent expand.Environ, background bool) *overlayEnviron { + oenv := &overlayEnviron{} + if !background { + oenv.parent = parent + } else { + // We could do better here if the parent is also an overlayEnviron; + // measure with profiles or benchmarks before we choose to do so. + oenv.values = make(map[string]expand.Variable) + maps.Insert(oenv.values, parent.Each) + } + return oenv +} + +// overlayEnviron is our main implementation of [expand.WriteEnviron]. type overlayEnviron struct { + // parent is non-nil if [values] is an overlay over a parent environment + // which we can safely reuse without data races, such as non-background subshells + // or function calls. parent expand.Environ values map[string]expand.Variable // We need to know if the current scope is a function's scope, because - // functions can modify global variables. + // functions can modify global variables. When true, [parent] must not be nil. funcScope bool } @@ -27,41 +49,32 @@ func (o *overlayEnviron) Get(name string) expand.Variable { if vr, ok := o.values[name]; ok { return vr } - return o.parent.Get(name) + if o.parent != nil { + return o.parent.Get(name) + } + return expand.Variable{} } func (o *overlayEnviron) Set(name string, vr expand.Variable) error { - // Manipulation of a global var inside a function - if o.funcScope && !vr.Local && !o.values[name].Local { - // "foo=bar" on a global var in a function updates the global scope - if vr.IsSet() { - return o.parent.(expand.WriteEnviron).Set(name, vr) - } - // "foo=bar" followed by "export foo" or "readonly foo" - if vr.Exported || vr.ReadOnly { - prev := o.Get(name) - prev.Exported = prev.Exported || vr.Exported - prev.ReadOnly = prev.ReadOnly || vr.ReadOnly - vr = prev - return o.parent.(expand.WriteEnviron).Set(name, vr) - } - // "unset" is handled below + prev, inOverlay := o.values[name] + // Manipulation of a global var inside a function. + if o.funcScope && !vr.Local && !prev.Local { + // In a function, the parent environment is ours, so it's always read-write. + return o.parent.(expand.WriteEnviron).Set(name, vr) + } + if !inOverlay && o.parent != nil { + prev = o.parent.Get(name) } - prev := o.Get(name) if o.values == nil { o.values = make(map[string]expand.Variable) } - if !vr.IsSet() && (vr.Exported || vr.Local || vr.ReadOnly) { - // marking as exported/local/readonly - prev.Exported = prev.Exported || vr.Exported - prev.Local = prev.Local || vr.Local - prev.ReadOnly = prev.ReadOnly || vr.ReadOnly - vr = prev - o.values[name] = vr - return nil - } - if prev.ReadOnly { + if vr.Kind == expand.KeepValue { + vr.Kind = prev.Kind + vr.Str = prev.Str + vr.List = prev.List + vr.Map = prev.Map + } else if prev.ReadOnly { return fmt.Errorf("readonly variable") } if !vr.IsSet() { // unsetting @@ -71,13 +84,6 @@ func (o *overlayEnviron) Set(name string, vr expand.Variable) error { return nil } delete(o.values, name) - if writeEnv, _ := o.parent.(expand.WriteEnviron); writeEnv != nil { - writeEnv.Set(name, vr) - return nil - } - } else if prev.Exported { - // variable is set and was marked as exported - vr.Exported = true } // modifying the entire variable vr.Local = prev.Local || vr.Local @@ -86,7 +92,9 @@ func (o *overlayEnviron) Set(name string, vr expand.Variable) error { } func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) { - o.parent.Each(f) + if o.parent != nil { + o.parent.Each(f) + } for name, vr := range o.values { if !f(name, vr) { return @@ -96,7 +104,7 @@ func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) { func execEnv(env expand.Environ) []string { list := make([]string, 0, 64) - env.Each(func(name string, vr expand.Variable) bool { + for name, vr := range env.Each { if !vr.IsSet() { // If a variable is set globally but unset in the // runner, we need to ensure it's not part of the final @@ -112,8 +120,7 @@ func execEnv(env expand.Environ) []string { if vr.Exported && vr.Kind == expand.String { list = append(list, name+"="+vr.String()) } - return true - }) + } return list } @@ -133,12 +140,24 @@ func (r *Runner) lookupVar(name string) expand.Variable { } else { vr.List = r.Params } + case "!": + if n := len(r.bgProcs); n > 0 { + vr.Kind, vr.Str = expand.String, "g"+strconv.Itoa(n) + } case "?": - vr.Kind, vr.Str = expand.String, strconv.Itoa(r.lastExit) + vr.Kind, vr.Str = expand.String, strconv.Itoa(int(r.lastExit.code)) case "$": vr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getpid()) case "PPID": vr.Kind, vr.Str = expand.String, strconv.Itoa(os.Getppid()) + case "RANDOM": // not for cryptographic use + vr.Kind, vr.Str = expand.String, strconv.Itoa(mathrand.IntN(32767)) + // TODO: support setting RANDOM to seed it + case "SRANDOM": // pseudo-random generator from the system + var p [4]byte + cryptorand.Read(p[:]) + n := binary.NativeEndian.Uint32(p[:]) + vr.Kind, vr.Str = expand.String, strconv.FormatUint(uint64(n), 10) case "DIRSTACK": vr.Kind, vr.List = expand.Indexed, r.dirStack case "0": @@ -149,23 +168,21 @@ func (r *Runner) lookupVar(name string) expand.Variable { vr.Str = "gosh" } case "1", "2", "3", "4", "5", "6", "7", "8", "9": - vr.Kind = expand.String - i := int(name[0] - '1') - if i < len(r.Params) { + if i := int(name[0] - '1'); i < len(r.Params) { + vr.Kind = expand.String vr.Str = r.Params[i] - } else { - vr.Str = "" } } - if vr.IsSet() { + if vr.Kind != expand.Unknown { + vr.Set = true return vr } - if vr := r.writeEnv.Get(name); vr.IsSet() { + if vr := r.writeEnv.Get(name); vr.Declared() { return vr } if runtime.GOOS == "windows" { upper := strings.ToUpper(name) - if vr := r.writeEnv.Get(upper); vr.IsSet() { + if vr := r.writeEnv.Get(upper); vr.Declared() { return vr } } @@ -179,37 +196,37 @@ func (r *Runner) envGet(name string) string { func (r *Runner) delVar(name string) { if err := r.writeEnv.Set(name, expand.Variable{}); err != nil { r.errf("%s: %v\n", name, err) - r.exit = 1 + r.exit.code = 1 return } } func (r *Runner) setVarString(name, value string) { - r.setVar(name, nil, expand.Variable{Kind: expand.String, Str: value}) + r.setVar(name, expand.Variable{Set: true, Kind: expand.String, Str: value}) } -func (r *Runner) setVarInternal(name string, vr expand.Variable) { +func (r *Runner) setVar(name string, vr expand.Variable) { if r.opts[optAllExport] { vr.Exported = true } if err := r.writeEnv.Set(name, vr); err != nil { r.errf("%s: %v\n", name, err) - r.exit = 1 + r.exit.code = 1 return } } -func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable) { - cur := r.lookupVar(name) - if name2, var2 := cur.Resolve(r.writeEnv); name2 != "" { +func (r *Runner) setVarWithIndex(prev expand.Variable, name string, index syntax.ArithmExpr, vr expand.Variable) { + prev.Set = true + if name2, var2 := prev.Resolve(r.writeEnv); name2 != "" { name = name2 - cur = var2 + prev = var2 } if vr.Kind == expand.String && index == nil { // When assigning a string to an array, fall back to the // zero value for the index. - switch cur.Kind { + switch prev.Kind { case expand.Indexed: index = &syntax.Word{Parts: []syntax.WordPart{ &syntax.Lit{Value: "0"}, @@ -221,7 +238,7 @@ func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable } } if index == nil { - r.setVarInternal(name, vr) + r.setVar(name, vr) return } @@ -230,11 +247,12 @@ func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable valStr := vr.Str var list []string - switch cur.Kind { + switch prev.Kind { case expand.String: - list = append(list, cur.Str) + list = append(list, prev.Str) case expand.Indexed: - list = cur.List + // TODO: only clone when inside a subshell and getting a var from outside for the first time + list = slices.Clone(prev.List) case expand.Associative: // if the existing variable is already an AssocArray, try our // best to convert the key to a string @@ -243,8 +261,14 @@ func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable return } k := r.literal(w) - cur.Map[k] = valStr - r.setVarInternal(name, cur) + + // TODO: only clone when inside a subshell and getting a var from outside for the first time + prev.Map = maps.Clone(prev.Map) + if prev.Map == nil { + prev.Map = make(map[string]string) + } + prev.Map[k] = valStr + r.setVar(name, prev) return } k := r.arithm(index) @@ -252,9 +276,9 @@ func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable list = append(list, "") } list[k] = valStr - cur.Kind = expand.Indexed - cur.List = list - r.setVarInternal(name, cur) + prev.Kind = expand.Indexed + prev.List = list + r.setVar(name, prev) } func (r *Runner) setFunc(name string, body *syntax.Stmt) { @@ -276,13 +300,13 @@ func stringIndex(index syntax.ArithmExpr) bool { return false } -// TODO: make assignVal and setVar consistent with the WriteEnviron interface +// TODO: make assignVal and [setVar] consistent with the [expand.WriteEnviron] interface -func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { - prev := r.lookupVar(as.Name.Value) +func (r *Runner) assignVal(prev expand.Variable, as *syntax.Assign, valType string) expand.Variable { + prev.Set = true if as.Value != nil { s := r.literal(as.Value) - if !as.Append || !prev.IsSet() { + if !as.Append { prev.Kind = expand.String if valType == "-n" { prev.Kind = expand.NameRef @@ -291,7 +315,8 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { return prev } switch prev.Kind { - case expand.String: + case expand.String, expand.Unknown: + prev.Kind = expand.String prev.Str += s case expand.Indexed: if len(prev.List) == 0 { @@ -351,9 +376,7 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { } elemValues[i].index = index index += len(elemValues[i].values) - if index > maxIndex { - maxIndex = index - } + maxIndex = max(maxIndex, index) } // Flatten down the values. strs := make([]string, maxIndex) @@ -368,7 +391,7 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { return prev } switch prev.Kind { - case expand.Unset: + case expand.Unknown: prev.Kind = expand.Indexed prev.List = strs case expand.String: diff --git a/vendor/mvdan.cc/sh/v3/pattern/pattern.go b/vendor/mvdan.cc/sh/v3/pattern/pattern.go index fd80f71721..6c8847673c 100644 --- a/vendor/mvdan.cc/sh/v3/pattern/pattern.go +++ b/vendor/mvdan.cc/sh/v3/pattern/pattern.go @@ -9,11 +9,11 @@ package pattern import ( - "bytes" "fmt" + "io" "regexp" - "strconv" "strings" + "unicode/utf8" ) // Mode can be used to supply a number of options to the package's functions. @@ -29,228 +29,243 @@ func (e SyntaxError) Error() string { return e.msg } func (e SyntaxError) Unwrap() error { return e.err } +// TODO(v4): flip NoGlobStar to be opt-in via GlobStar, matching bash +// TODO(v4): flip EntireString to be opt-out via PartialMatch, as EntireString causes subtle bugs when forgotten +// TODO(v4): rename NoGlobCase to CaseInsensitive for readability + const ( - Shortest Mode = 1 << iota // prefer the shortest match. - Filenames // "*" and "?" don't match slashes; only "**" does - Braces // support "{a,b}" and "{1..4}" + Shortest Mode = 1 << iota // prefer the shortest match. + Filenames // "*" and "?" don't match slashes; only "**" does + EntireString // match the entire string using ^$ delimiters + NoGlobCase // Do case-insensitive match (that is, use (?i) in the regexp) + NoGlobStar // Do not support "**" ) -var numRange = regexp.MustCompile(`^([+-]?\d+)\.\.([+-]?\d+)}`) - // Regexp turns a shell pattern into a regular expression that can be used with -// regexp.Compile. It will return an error if the input pattern was incorrect. -// Otherwise, the returned expression can be passed to regexp.MustCompile. +// [regexp.Compile]. It will return an error if the input pattern was incorrect. +// Otherwise, the returned expression can be passed to [regexp.MustCompile]. // // For example, Regexp(`foo*bar?`, true) returns `foo.*bar.`. // -// Note that this function (and QuoteMeta) should not be directly used with file +// Note that this function (and [QuoteMeta]) should not be directly used with file // paths if Windows is supported, as the path separator on that platform is the // same character as the escaping character for shell patterns. func Regexp(pat string, mode Mode) (string, error) { - any := false -noopLoop: - for _, r := range pat { - switch r { - // including those that need escaping since they are - // regular expression metacharacters - case '*', '?', '[', '\\', '.', '+', '(', ')', '|', - ']', '{', '}', '^', '$': - any = true - break noopLoop + // If there are no special pattern matching or regular expression characters, + // and we don't need to insert extras for the modes affecting non-special characters, + // we can directly return the input string as a short-cut. + if mode&(EntireString|NoGlobCase) == 0 { + needsEscaping := false + noopLoop: + for _, r := range pat { + switch r { + // including those that need escaping since they are + // regular expression metacharacters + case '*', '?', '[', '\\', '.', '+', '(', ')', '|', + ']', '{', '}', '^', '$': + needsEscaping = true + break noopLoop + } + } + if !needsEscaping { + return pat, nil } } - if !any { // short-cut without a string copy - return pat, nil + var sb strings.Builder + // Enable matching `\n` with the `.` metacharacter as globs match `\n` + sb.WriteString("(?s") + if mode&NoGlobCase != 0 { + sb.WriteString("i") } - closingBraces := []int{} - var buf bytes.Buffer -writeLoop: - for i := 0; i < len(pat); i++ { - switch c := pat[i]; c { - case '*': - if mode&Filenames != 0 { - if i++; i < len(pat) && pat[i] == '*' { - if i++; i < len(pat) && pat[i] == '/' { - buf.WriteString("(.*/|)") - } else { - buf.WriteString(".*") - i-- - } + if mode&Shortest != 0 { + sb.WriteString("U") + } + sb.WriteString(")") + if mode&EntireString != 0 { + sb.WriteString("^") + } + sl := stringLexer{s: pat} + for { + if err := regexpNext(&sb, &sl, mode); err == io.EOF { + break + } else if err != nil { + return "", err + } + } + if mode&EntireString != 0 { + sb.WriteString("$") + } + return sb.String(), nil +} + +// stringLexer helps us tokenize a pattern string. +// Note that we can use the null byte '\x00' to signal "no character" as shell strings cannot contain null bytes. +// TODO: should the tokenization be based on runes? e.g: [á-é] +type stringLexer struct { + s string + i int +} + +func (sl *stringLexer) next() byte { + if sl.i >= len(sl.s) { + return '\x00' + } + c := sl.s[sl.i] + sl.i++ + return c +} + +func (sl *stringLexer) last() byte { + if sl.i < 2 { + return '\x00' + } + return sl.s[sl.i-2] +} + +func (sl *stringLexer) peekNext() byte { + if sl.i >= len(sl.s) { + return '\x00' + } + return sl.s[sl.i] +} + +func (sl *stringLexer) peekRest() string { + return sl.s[sl.i:] +} + +func regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error { + switch c := sl.next(); c { + case '\x00': + return io.EOF + case '*': + if mode&Filenames == 0 { + // * - matches anything when not in filename mode + sb.WriteString(".*") + break + } + // "**" only acts as globstar if it is alone as a path element. + singleBefore := sl.i == 1 || sl.last() == '/' + if sl.peekNext() == '*' { + sl.i++ + singleAfter := sl.i == len(sl.s) || sl.peekNext() == '/' + if mode&NoGlobStar == 0 && singleBefore && singleAfter { + if sl.peekNext() == '/' { + // **/ - like "**" but requiring a trailing slash when matching + sl.i++ + sb.WriteString("((/|[^/.][^/]*)*/)?") } else { - buf.WriteString("[^/]*") - i-- + // ** - match any number of slashes or "*" path elements + sb.WriteString("(/|[^/.][^/]*)*") } - } else { - buf.WriteString(".*") - } - if mode&Shortest != 0 { - buf.WriteByte('?') - } - case '?': - if mode&Filenames != 0 { - buf.WriteString("[^/]") - } else { - buf.WriteByte('.') - } - case '\\': - if i++; i >= len(pat) { - return "", &SyntaxError{msg: `\ at end of pattern`} - } - buf.WriteString(regexp.QuoteMeta(string(pat[i]))) - case '[': - name, err := charClass(pat[i:]) - if err != nil { - return "", &SyntaxError{msg: "charClass invalid", err: err} - } - if name != "" { - buf.WriteString(name) - i += len(name) - 1 break } - if mode&Filenames != 0 { - for _, c := range pat[i:] { - if c == ']' { - break - } else if c == '/' { - buf.WriteString("\\[") - continue writeLoop - } - } - } - buf.WriteByte(c) - if i++; i >= len(pat) { - return "", &SyntaxError{msg: "[ was not matched with a closing ]"} - } - switch c = pat[i]; c { - case '!', '^': - buf.WriteByte('^') - if i++; i >= len(pat) { - return "", &SyntaxError{msg: "[ was not matched with a closing ]"} - } - } - if c = pat[i]; c == ']' { - buf.WriteByte(']') - if i++; i >= len(pat) { - return "", &SyntaxError{msg: "[ was not matched with a closing ]"} - } - } - rangeStart := byte(0) - loopBracket: - for ; i < len(pat); i++ { - c = pat[i] - buf.WriteByte(c) - switch c { - case '\\': - if i++; i < len(pat) { - buf.WriteByte(pat[i]) - } - continue - case ']': - break loopBracket - } - if rangeStart != 0 && rangeStart > c { - return "", &SyntaxError{msg: fmt.Sprintf("invalid range: %c-%c", rangeStart, c)} - } - if c == '-' { - rangeStart = pat[i-1] - } else { - rangeStart = 0 + // foo**, **bar, or NoGlobStar - behaves like "*" below + } + // * - matches anything except slashes and leading dots + if singleBefore { + sb.WriteString("([^/.][^/]*)?") + } else { + sb.WriteString("[^/]*") + } + case '?': + if mode&Filenames != 0 { + sb.WriteString("[^/]") + } else { + sb.WriteByte('.') + } + case '\\': + c = sl.next() + if c == '\x00' { + return &SyntaxError{msg: `\ at end of pattern`} + } + sb.WriteString(regexp.QuoteMeta(string(c))) + case '[': + // TODO: surely char classes can be mixed with others, e.g. [[:foo:]xyz] + if name, err := charClass(sl.peekRest()); err != nil { + return &SyntaxError{msg: "charClass invalid", err: err} + } else if name != "" { + sb.WriteByte('[') + sb.WriteString(name) + sl.i += len(name) + break + } + if mode&Filenames != 0 { + for _, c := range sl.peekRest() { + if c == ']' { + break + } else if c == '/' { + sb.WriteString("\\[") + return nil } } - if i >= len(pat) { - return "", &SyntaxError{msg: "[ was not matched with a closing ]"} - } - case '{': - if mode&Braces == 0 { - buf.WriteString(regexp.QuoteMeta(string(c))) - break + } + sb.WriteByte(c) + if c = sl.next(); c == '\x00' { + return &SyntaxError{msg: "[ was not matched with a closing ]"} + } + switch c { + case '!', '^': + sb.WriteByte('^') + if c = sl.next(); c == '\x00' { + return &SyntaxError{msg: "[ was not matched with a closing ]"} } - innerLevel := 1 - commas := false - peekBrace: - for j := i + 1; j < len(pat); j++ { - switch c := pat[j]; c { - case '{': - innerLevel++ - case ',': - commas = true - case '\\': - j++ - case '}': - if innerLevel--; innerLevel > 0 { - continue - } - if !commas { - break peekBrace - } - closingBraces = append(closingBraces, j) - buf.WriteString("(?:") - continue writeLoop - } + } + if c == ']' { + sb.WriteByte(']') + if c = sl.next(); c == '\x00' { + return &SyntaxError{msg: "[ was not matched with a closing ]"} } - if match := numRange.FindStringSubmatch(pat[i+1:]); len(match) == 3 { - start, err1 := strconv.Atoi(match[1]) - end, err2 := strconv.Atoi(match[2]) - if err1 != nil || err2 != nil || start > end { - return "", &SyntaxError{msg: fmt.Sprintf("invalid range: %q", match[0])} + } + for { + sb.WriteByte(c) + switch c { + case '\x00': + return &SyntaxError{msg: "[ was not matched with a closing ]"} + case '\\': + if c = sl.next(); c != '0' { + sb.WriteByte(c) } - // TODO: can we do better here? - buf.WriteString("(?:") - for n := start; n <= end; n++ { - if n > start { - buf.WriteByte('|') - } - fmt.Fprintf(&buf, "%d", n) + case '-': + start := sl.last() + end := sl.peekNext() + // TODO: what about overlapping ranges, like: [a--z] + if end != ']' && start > end { + return &SyntaxError{msg: fmt.Sprintf("invalid range: %c-%c", start, end)} } - buf.WriteByte(')') - i += len(match[0]) - break - } - buf.WriteString(regexp.QuoteMeta(string(c))) - case ',': - if len(closingBraces) == 0 { - buf.WriteString(regexp.QuoteMeta(string(c))) - } else { - buf.WriteByte('|') - } - case '}': - if len(closingBraces) > 0 && closingBraces[len(closingBraces)-1] == i { - buf.WriteByte(')') - closingBraces = closingBraces[:len(closingBraces)-1] - } else { - buf.WriteString(regexp.QuoteMeta(string(c))) - } - default: - if c > 128 { - buf.WriteByte(c) - } else { - buf.WriteString(regexp.QuoteMeta(string(c))) + case ']': + return nil } + c = sl.next() + } + default: + if c > utf8.RuneSelf { + sb.WriteByte(c) + } else { + sb.WriteString(regexp.QuoteMeta(string(c))) } } - return buf.String(), nil + return nil } func charClass(s string) (string, error) { - if strings.HasPrefix(s, "[[.") || strings.HasPrefix(s, "[[=") { + if strings.HasPrefix(s, "[.") || strings.HasPrefix(s, "[=") { return "", fmt.Errorf("collating features not available") } - if !strings.HasPrefix(s, "[[:") { + name, ok := strings.CutPrefix(s, "[:") + if !ok { return "", nil } - name := s[3:] - end := strings.Index(name, ":]]") - if end < 0 { + name, _, ok = strings.Cut(name, ":]]") + if !ok { return "", fmt.Errorf("[[: was not matched with a closing :]]") } - name = name[:end] switch name { case "alnum", "alpha", "ascii", "blank", "cntrl", "digit", "graph", "lower", "print", "punct", "space", "upper", "word", "xdigit": default: return "", fmt.Errorf("invalid character class: %q", name) } - return s[:len(name)+6], nil + return s[:len(name)+5], nil } // HasMeta returns whether a string contains any unescaped pattern @@ -260,9 +275,11 @@ func charClass(s string) (string, error) { // For example, HasMeta(`foo\*bar`) returns false, but HasMeta(`foo*bar`) // returns true. // -// This can be useful to avoid extra work, like TranslatePattern. Note that this -// function cannot be used to avoid QuotePattern, as backslashes are quoted by +// This can be useful to avoid extra work, like [Regexp]. Note that this +// function cannot be used to avoid [QuoteMeta], as backslashes are quoted by // that function but ignored here. +// +// The [Mode] parameter is unused, and will be removed in v4. func HasMeta(pat string, mode Mode) bool { for i := 0; i < len(pat); i++ { switch pat[i] { @@ -270,10 +287,6 @@ func HasMeta(pat string, mode Mode) bool { i++ case '*', '?', '[': return true - case '{': - if mode&Braces != 0 { - return true - } } } return false @@ -283,35 +296,28 @@ func HasMeta(pat string, mode Mode) bool { // given text. The returned string is a pattern that matches the literal text. // // For example, QuoteMeta(`foo*bar?`) returns `foo\*bar\?`. +// +// The [Mode] parameter is unused, and will be removed in v4. func QuoteMeta(pat string, mode Mode) string { - any := false + needsEscaping := false loop: for _, r := range pat { switch r { - case '{': - if mode&Braces == 0 { - continue - } - fallthrough case '*', '?', '[', '\\': - any = true + needsEscaping = true break loop } } - if !any { // short-cut without a string copy + if !needsEscaping { // short-cut without a string copy return pat } - var buf bytes.Buffer + var sb strings.Builder for _, r := range pat { switch r { case '*', '?', '[', '\\': - buf.WriteByte('\\') - case '{': - if mode&Braces != 0 { - buf.WriteByte('\\') - } + sb.WriteByte('\\') } - buf.WriteRune(r) + sb.WriteRune(r) } - return buf.String() + return sb.String() } diff --git a/vendor/mvdan.cc/sh/v3/syntax/braces.go b/vendor/mvdan.cc/sh/v3/syntax/braces.go index dca854fd82..0d32199fb0 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/braces.go +++ b/vendor/mvdan.cc/sh/v3/syntax/braces.go @@ -3,7 +3,10 @@ package syntax -import "strconv" +import ( + "strconv" + "strings" +) var ( litLeftBrace = &Lit{Value: "{"} @@ -23,7 +26,10 @@ var ( // It does not return an error; malformed brace expansions are simply skipped. // For example, the literal word "a{b" is left unchanged. func SplitBraces(word *Word) bool { - any := false + if !strings.Contains(word.Lit(), "{") { + // In the common case where a word has no braces, skip any allocs. + return false + } top := &Word{} acc := top var cur *BraceExp @@ -90,7 +96,6 @@ func SplitBraces(word *Word) bool { if cur == nil { continue } - any = true addlitidx() br := pop() if len(br.Elems) == 1 { @@ -110,7 +115,8 @@ func SplitBraces(word *Word) bool { val := elem.Lit() if _, err := strconv.Atoi(val); err == nil { } else if len(val) == 1 && - 'a' <= val[0] && val[0] <= 'z' { + (('a' <= val[0] && val[0] <= 'z') || + ('A' <= val[0] && val[0] <= 'Z')) { chars[i] = true } else { broken = true @@ -154,9 +160,6 @@ func SplitBraces(word *Word) bool { addLit(&left) } } - if !any { - return false - } // open braces that were never closed fall back to non-braces for acc != top { br := pop() diff --git a/vendor/mvdan.cc/sh/v3/syntax/lexer.go b/vendor/mvdan.cc/sh/v3/syntax/lexer.go index 133cc00d38..f518cf1997 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/lexer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/lexer.go @@ -61,38 +61,43 @@ func (p *Parser) rune() rune { if p.r == '\n' || p.r == escNewl { // p.r instead of b so that newline // character positions don't have col 0. - if p.line++; p.line > lineMax { - p.lineOverflow = true - } + p.line++ p.col = 0 - p.colOverflow = false - } - if p.col += p.w; p.col > colMax { - p.colOverflow = true } + p.col += int64(p.w) bquotes := 0 retry: - if p.bsp < len(p.bs) { + if p.bsp < uint(len(p.bs)) { if b := p.bs[p.bsp]; b < utf8.RuneSelf { p.bsp++ - if b == '\x00' { + switch b { + case '\x00': // Ignore null bytes while parsing, like bash. + p.col++ goto retry - } - if b == '\\' { + case '\r': + if p.peekByte('\n') { // \r\n turns into \n + p.col++ + goto retry + } + case '\\': if p.r == '\\' { } else if p.peekByte('\n') { p.bsp++ p.w, p.r = 1, escNewl return escNewl - } else if p.peekBytes("\r\n") { + } else if p.peekBytes("\r\n") { // \\\r\n turns into \\\n + p.col++ p.bsp += 2 p.w, p.r = 2, escNewl return escNewl } if p.openBquotes > 0 && bquotes < p.openBquotes && - p.bsp < len(p.bs) && bquoteEscaped(p.bs[p.bsp]) { + p.bsp < uint(len(p.bs)) && bquoteEscaped(p.bs[p.bsp]) { + // We turn backquote command substitutions into $(), + // so we remove the extra backslashes needed by the backquotes. bquotes++ + p.col++ goto retry } } @@ -112,9 +117,9 @@ retry: var w int p.r, w = utf8.DecodeRune(p.bs[p.bsp:]) if p.litBs != nil { - p.litBs = append(p.litBs, p.bs[p.bsp:p.bsp+w]...) + p.litBs = append(p.litBs, p.bs[p.bsp:p.bsp+uint(w)]...) } - p.bsp += w + p.bsp += uint(w) if p.r == utf8.RuneError && w == 1 { p.posErr(p.nextPos(), "invalid UTF-8 encoding") } @@ -136,8 +141,8 @@ retry: // had not yet been used at the end of the buffer are slid into the // beginning of the buffer. func (p *Parser) fill() { - p.offs += p.bsp - left := len(p.bs) - p.bsp + p.offs += int64(p.bsp) + left := len(p.bs) - int(p.bsp) copy(p.readBuf[:left], p.readBuf[p.bsp:]) readAgain: n, err := 0, p.readErr @@ -256,7 +261,7 @@ skipSpace: } if p.stopAt != nil && (p.spaced || p.tok == illegalTok || p.stopToken()) { w := utf8.RuneLen(r) - if bytes.HasPrefix(p.bs[p.bsp-w:], p.stopAt) { + if bytes.HasPrefix(p.bs[p.bsp-uint(w):], p.stopAt) { p.r = utf8.RuneSelf p.w = 1 p.tok = _EOF @@ -270,6 +275,18 @@ skipSpace: case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`': p.tok = p.regToken(r) case '#': + // If we're parsing $foo#bar, ${foo}#bar, 'foo'#bar, or "foo"#bar, + // #bar is a continuation of the same word, not a comment. + // TODO: support $(foo)#bar and `foo`#bar as well, which is slightly tricky, + // as we can't easily tell them apart from (foo)#bar and `#bar`, + // where #bar should remain a comment. + if !p.spaced { + switch p.tok { + case _LitWord, rightBrace, sglQuote, dblQuote: + p.advanceLitNone(r) + return + } + } r = p.rune() p.newLit(r) runeLoop: @@ -303,7 +320,7 @@ skipSpace: p.advanceLitNone(r) } case '?', '*', '+', '@', '!': - if p.tokenizeGlob() { + if p.extendedGlob() { switch r { case '?': p.tok = globQuest @@ -359,43 +376,44 @@ skipSpace: } } -// tokenizeGlob determines whether the expression should be tokenized as a glob literal -func (p *Parser) tokenizeGlob() bool { +// extendedGlob determines whether we're parsing a Bash extended globbing expression. +// For example, whether `*` or `@` are followed by `(` to form `@(foo)`. +func (p *Parser) extendedGlob() bool { if p.val == "function" { return false } - // NOTE: empty pattern list is a valid globbing syntax, eg @() - // but we'll operate on the "likelihood" that it is a function; - // only tokenize if its a non-empty pattern list - if p.peekBytes("()") { - return false + if p.peekByte('(') { + // NOTE: empty pattern list is a valid globbing syntax like `@()`, + // but we'll operate on the "likelihood" that it is a function; + // only tokenize if its a non-empty pattern list. + // We do this after peeking for just one byte, so that the input `echo *` + // followed by a newline does not hang an interactive shell parser until + // another byte is input. + return !p.peekBytes("()") } - return p.peekByte('(') + return false } func (p *Parser) peekBytes(s string) bool { - for p.bsp+(len(p.bs)-1) >= len(p.bs) { + peekEnd := int(p.bsp) + len(s) + // TODO: This should loop for slow readers, e.g. those providing one byte at + // a time. Use a loop and test it with [testing/iotest.OneByteReader]. + if peekEnd > len(p.bs) { p.fill() } - bw := p.bsp + len(s) - return bw <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:bw], []byte(s)) + return peekEnd <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:peekEnd], []byte(s)) } func (p *Parser) peekByte(b byte) bool { - if p.bsp == len(p.bs) { + if p.bsp == uint(len(p.bs)) { p.fill() } - return p.bsp < len(p.bs) && p.bs[p.bsp] == b + return p.bsp < uint(len(p.bs)) && p.bs[p.bsp] == b } func (p *Parser) regToken(r rune) token { switch r { case '\'': - if p.openBquotes > 0 { - // bury openBquotes - p.buriedBquotes = p.openBquotes - p.openBquotes = 0 - } p.rune() return sglQuote case '"': @@ -411,9 +429,6 @@ func (p *Parser) regToken(r rune) token { p.rune() return andAnd case '>': - if p.lang == LangPOSIX { - break - } if p.rune() == '>' { p.rune() return appAll @@ -503,7 +518,7 @@ func (p *Parser) regToken(r rune) token { if r = p.rune(); r == '-' { p.rune() return dashHdoc - } else if r == '<' && p.lang != LangPOSIX { + } else if r == '<' { p.rune() return wordHdoc } @@ -801,9 +816,9 @@ func (p *Parser) newLit(r rune) { p.litBs[0] = byte(r) case r > escNewl: w := utf8.RuneLen(r) - p.litBs = append(p.litBuf[:0], p.bs[p.bsp-w:p.bsp]...) + p.litBs = append(p.litBuf[:0], p.bs[p.bsp-uint(w):p.bsp]...) default: - // don't let r == utf8.RuneSelf go to the second case as RuneLen + // don't let r == utf8.RuneSelf go to the second case as [utf8.RuneLen] // would return -1 p.litBs = p.litBuf[:0] } @@ -813,7 +828,7 @@ func (p *Parser) endLit() (s string) { if p.r == utf8.RuneSelf || p.r == escNewl { s = string(p.litBs) } else { - s = string(p.litBs[:len(p.litBs)-int(p.w)]) + s = string(p.litBs[:len(p.litBs)-p.w]) } p.litBs = nil return @@ -917,7 +932,7 @@ loop: tok = _Lit break loop case '?', '*', '+', '@', '!': - if p.tokenizeGlob() { + if p.extendedGlob() { tok = _Lit break loop } @@ -1068,7 +1083,7 @@ func (p *Parser) quotedHdocWord() *Word { if val == "" { return nil } - return p.word(p.wps(p.lit(pos, val))) + return p.wordOne(p.lit(pos, val)) } } } diff --git a/vendor/mvdan.cc/sh/v3/syntax/nodes.go b/vendor/mvdan.cc/sh/v3/syntax/nodes.go index 32518ec877..902e325096 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/nodes.go +++ b/vendor/mvdan.cc/sh/v3/syntax/nodes.go @@ -4,6 +4,7 @@ package syntax import ( + "math" "strconv" "strings" ) @@ -11,11 +12,11 @@ import ( // Node represents a syntax tree node. type Node interface { // Pos returns the position of the first character of the node. Comments - // are ignored, except if the node is a *File. + // are ignored, except if the node is a [*File]. Pos() Pos // End returns the position of the character immediately after the node. // If the character is a newline, the line number won't cross into the - // next line. Comments are ignored, except if the node is a *File. + // next line. Comments are ignored, except if the node is a [*File]. End() Pos } @@ -69,9 +70,17 @@ type Pos struct { offs, lineCol uint32 } -// We used to split line and column numbers evenly in 16 bits, but line numbers -// are significantly more important in practice. Use more bits for them. const ( + // Offsets use 32 bits for a reasonable amount of precision. + // We reserve a few of the highest values to represent types of invalid positions. + // We leave some space before the real uint32 maximum so that we can easily detect + // when arithmetic on invalid positions is done by mistake. + offsetRecovered = math.MaxUint32 - 10 + offsetMax = math.MaxUint32 - 11 + + // We used to split line and column numbers evenly in 16 bits, but line numbers + // are significantly more important in practice. Use more bits for them. + lineBitSize = 18 lineMax = (1 << lineBitSize) - 1 @@ -87,9 +96,12 @@ const ( // NewPos creates a position with the given offset, line, and column. // -// Note that Pos uses a limited number of bits to store these numbers. +// Note that [Pos] uses a limited number of bits to store these numbers. // If line or column overflow their allocated space, they are replaced with 0. func NewPos(offset, line, column uint) Pos { + // Basic protection against offset overflow; + // note that an offset of 0 is valid, so we leave the maximum. + offset = min(offset, offsetMax) if line > lineMax { line = 0 // protect against overflows; rendered as "?" } @@ -103,23 +115,29 @@ func NewPos(offset, line, column uint) Pos { } // Offset returns the byte offset of the position in the original source file. -// Byte offsets start at 0. +// Byte offsets start at 0. Invalid positions always report the offset 0. // -// Note that Offset is not protected against overflows; -// if an input is larger than 4GiB, the offset will wrap around to 0. -func (p Pos) Offset() uint { return uint(p.offs) } +// Offset has basic protection against overflows; if an input is too large, +// offset numbers will stop increasing past a very large number. +func (p Pos) Offset() uint { + if p.offs > offsetMax { + return 0 // invalid + } + return uint(p.offs) +} // Line returns the line number of the position, starting at 1. +// Invalid positions always report the line number 0. // // Line is protected against overflows; if an input has too many lines, extra -// lines will have a line number of 0, rendered as "?" by Pos.String. +// lines will have a line number of 0, rendered as "?" by [Pos.String]. func (p Pos) Line() uint { return uint(p.lineCol >> colBitSize) } // Col returns the column number of the position, starting at 1. It counts in -// bytes. +// bytes. Invalid positions always report the column number 0. // // Col is protected against overflows; if an input line has too many columns, -// extra columns will have a column number of 0, rendered as "?" by Pos.String. +// extra columns will have a column number of 0, rendered as "?" by [Pos.String]. func (p Pos) Col() uint { return uint(p.lineCol & colBitMask) } func (p Pos) String() string { @@ -138,15 +156,36 @@ func (p Pos) String() string { return b.String() } -// IsValid reports whether the position is valid. All positions in nodes -// returned by Parse are valid. -func (p Pos) IsValid() bool { return p != Pos{} } +// IsValid reports whether the position contains useful position information. +// Some positions returned via [Parse] may be invalid: for example, [Stmt.Semicolon] +// will only be valid if a statement contained a closing token such as ';'. +// +// Recovered positions, as reported by [Pos.IsRecovered], are not considered valid +// given that they don't contain position information. +func (p Pos) IsValid() bool { + return p.offs <= offsetMax && p.lineCol != 0 +} + +var recoveredPos = Pos{offs: offsetRecovered} + +// IsRecovered reports whether the position that the token or node belongs to +// was missing in the original input and recovered via [RecoverErrors]. +func (p Pos) IsRecovered() bool { return p == recoveredPos } // After reports whether the position p is after p2. It is a more expressive // version of p.Offset() > p2.Offset(). -func (p Pos) After(p2 Pos) bool { return p.offs > p2.offs } +// It always returns false if p is an invalid position. +func (p Pos) After(p2 Pos) bool { + if !p.IsValid() { + return false + } + return p.offs > p2.offs +} func posAddCol(p Pos, n int) Pos { + if !p.IsValid() { + return p + } // TODO: guard against overflows p.lineCol += uint32(n) p.offs += uint32(n) @@ -209,9 +248,9 @@ func (s *Stmt) End() Pos { // Command represents all nodes that are simple or compound commands, including // function declarations. // -// These are *CallExpr, *IfClause, *WhileClause, *ForClause, *CaseClause, -// *Block, *Subshell, *BinaryCmd, *FuncDecl, *ArithmCmd, *TestClause, -// *DeclClause, *LetClause, *TimeClause, and *CoprocClause. +// These are [*CallExpr], [*IfClause], [*WhileClause], [*ForClause], [*CaseClause], +// [*Block], [*Subshell], [*BinaryCmd], [*FuncDecl], [*ArithmCmd], [*TestClause], +// [*DeclClause], [*LetClause], [*TimeClause], and [*CoprocClause]. type Command interface { Node commandNode() @@ -242,7 +281,7 @@ func (*TestDecl) commandNode() {} // If Index is non-nil, the value will be a word and not an array as nested // arrays are not allowed. // -// If Naked is true and Name is nil, the assignment is part of a DeclClause and +// If Naked is true and Name is nil, the assignment is part of a [DeclClause] and // the argument (in the Value field) will be evaluated at run-time. This // includes parameter expansions, which may expand to assignments or options. type Assign struct { @@ -396,7 +435,7 @@ type ForClause struct { func (f *ForClause) Pos() Pos { return f.ForPos } func (f *ForClause) End() Pos { return posAddCol(f.DonePos, 4) } -// Loop holds either *WordIter or *CStyleLoop. +// Loop holds either [*WordIter] or [*CStyleLoop]. type Loop interface { Node loopNode() @@ -425,7 +464,7 @@ func (w *WordIter) End() Pos { // CStyleLoop represents the behavior of a for clause similar to the C // language. // -// This node will only appear with LangBash. +// This node will only appear with [LangBash]. type CStyleLoop struct { Lparen, Rparen Pos // Init, Cond, Post can each be nil, if the for loop construct omits it. @@ -467,7 +506,7 @@ type Word struct { func (w *Word) Pos() Pos { return w.Parts[0].Pos() } func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() } -// Lit returns the word as a literal value, if the word consists of *Lit nodes +// Lit returns the word as a literal value, if the word consists of [*Lit] nodes // only. An empty string is returned otherwise. Words with multiple literals, // which can appear in some edge cases, are handled properly. // @@ -491,8 +530,8 @@ func (w *Word) Lit() string { // WordPart represents all nodes that can form part of a word. // -// These are *Lit, *SglQuoted, *DblQuoted, *ParamExp, *CmdSubst, *ArithmExp, -// *ProcSubst, and *ExtGlob. +// These are [*Lit], [*SglQuoted], [*DblQuoted], [*ParamExp], [*CmdSubst], [*ArithmExp], +// [*ProcSubst], and [*ExtGlob]. type WordPart interface { Node wordPartNode() @@ -587,21 +626,21 @@ func (p *ParamExp) nakedIndex() bool { return p.Short && p.Index != nil } -// Slice represents a character slicing expression inside a ParamExp. +// Slice represents a character slicing expression inside a [ParamExp]. // -// This node will only appear in LangBash and LangMirBSDKorn. +// This node will only appear with [LangBash] and [LangMirBSDKorn]. type Slice struct { Offset, Length ArithmExpr } -// Replace represents a search and replace expression inside a ParamExp. +// Replace represents a search and replace expression inside a [ParamExp]. type Replace struct { All bool Orig, With *Word } -// Expansion represents string manipulation in a ParamExp other than those -// covered by Replace. +// Expansion represents string manipulation in a [ParamExp] other than those +// covered by [Replace]. type Expansion struct { Op ParExpOperator Word *Word @@ -626,7 +665,7 @@ func (a *ArithmExp) End() Pos { // ArithmCmd represents an arithmetic command. // -// This node will only appear in LangBash and LangMirBSDKorn. +// This node will only appear with [LangBash] and [LangMirBSDKorn]. type ArithmCmd struct { Left, Right Pos Unsigned bool // mksh's ((# expr)) @@ -639,7 +678,7 @@ func (a *ArithmCmd) End() Pos { return posAddCol(a.Right, 2) } // ArithmExpr represents all nodes that form arithmetic expressions. // -// These are *BinaryArithm, *UnaryArithm, *ParenArithm, and *Word. +// These are [*BinaryArithm], [*UnaryArithm], [*ParenArithm], and [*Word]. type ArithmExpr interface { Node arithmExprNode() @@ -652,12 +691,12 @@ func (*Word) arithmExprNode() {} // BinaryArithm represents a binary arithmetic expression. // -// If Op is any assign operator, X will be a word with a single *Lit whose value +// If Op is any assign operator, X will be a word with a single [*Lit] whose value // is a valid name. // // Ternary operators like "a ? b : c" are fit into this structure. Thus, if -// Op==TernQuest, Y will be a *BinaryArithm with Op==TernColon. Op can only be -// TernColon in that scenario. +// Op==[TernQuest], Y will be a [*BinaryArithm] with Op==[TernColon]. +// [TernColon] does not appear in any other scenario. type BinaryArithm struct { OpPos Pos Op BinAritOperator @@ -670,7 +709,7 @@ func (b *BinaryArithm) End() Pos { return b.Y.End() } // UnaryArithm represents an unary arithmetic expression. The unary operator // may come before or after the sub-expression. // -// If Op is Inc or Dec, X will be a word with a single *Lit whose value is a +// If Op is [Inc] or [Dec], X will be a word with a single [*Lit] whose value is a // valid name. type UnaryArithm struct { OpPos Pos @@ -716,7 +755,7 @@ type CaseClause struct { func (c *CaseClause) Pos() Pos { return c.Case } func (c *CaseClause) End() Pos { return posAddCol(c.Esac, 4) } -// CaseItem represents a pattern list (case) within a CaseClause. +// CaseItem represents a pattern list (case) within a [CaseClause]. type CaseItem struct { Op CaseOperator OpPos Pos // unset if it was finished by "esac" @@ -737,7 +776,7 @@ func (c *CaseItem) End() Pos { // TestClause represents a Bash extended test clause. // -// This node will only appear in LangBash and LangMirBSDKorn. +// This node will only appear with [LangBash] and [LangMirBSDKorn]. type TestClause struct { Left, Right Pos @@ -749,7 +788,7 @@ func (t *TestClause) End() Pos { return posAddCol(t.Right, 2) } // TestExpr represents all nodes that form test expressions. // -// These are *BinaryTest, *UnaryTest, *ParenTest, and *Word. +// These are [*BinaryTest], [*UnaryTest], [*ParenTest], and [*Word]. type TestExpr interface { Node testExprNode() @@ -796,7 +835,7 @@ func (p *ParenTest) End() Pos { return posAddCol(p.Rparen, 1) } // Args can contain a mix of regular and naked assignments. The naked // assignments can represent either options or variable names. // -// This node will only appear with LangBash. +// This node will only appear with [LangBash]. type DeclClause struct { // Variant is one of "declare", "local", "export", "readonly", // "typeset", or "nameref". @@ -814,7 +853,7 @@ func (d *DeclClause) End() Pos { // ArrayExpr represents a Bash array expression. // -// This node will only appear with LangBash. +// This node will only appear with [LangBash]. type ArrayExpr struct { Lparen, Rparen Pos @@ -853,7 +892,7 @@ func (a *ArrayElem) End() Pos { // ExtGlob represents a Bash extended globbing expression. Note that these are // parsed independently of whether shopt has been called or not. // -// This node will only appear in LangBash and LangMirBSDKorn. +// This node will only appear with [LangBash] and [LangMirBSDKorn]. type ExtGlob struct { OpPos Pos Op GlobOperator @@ -865,7 +904,7 @@ func (e *ExtGlob) End() Pos { return posAddCol(e.Pattern.End(), 1) } // ProcSubst represents a Bash process substitution. // -// This node will only appear with LangBash. +// This node will only appear with [LangBash]. type ProcSubst struct { OpPos, Rparen Pos Op ProcOperator @@ -880,7 +919,7 @@ func (s *ProcSubst) End() Pos { return posAddCol(s.Rparen, 1) } // TimeClause represents a Bash time clause. PosixFormat corresponds to the -p // flag. // -// This node will only appear in LangBash and LangMirBSDKorn. +// This node will only appear with [LangBash] and [LangMirBSDKorn]. type TimeClause struct { Time Pos PosixFormat bool @@ -897,7 +936,7 @@ func (c *TimeClause) End() Pos { // CoprocClause represents a Bash coproc clause. // -// This node will only appear with LangBash. +// This node will only appear with [LangBash]. type CoprocClause struct { Coproc Pos Name *Word @@ -909,7 +948,7 @@ func (c *CoprocClause) End() Pos { return c.Stmt.End() } // LetClause represents a Bash let clause. // -// This node will only appear in LangBash and LangMirBSDKorn. +// This node will only appear with [LangBash] and [LangMirBSDKorn]. type LetClause struct { Let Pos Exprs []ArithmExpr @@ -920,7 +959,7 @@ func (l *LetClause) End() Pos { return l.Exprs[len(l.Exprs)-1].End() } // BraceExp represents a Bash brace expression, such as "{a,f}" or "{1..10}". // -// This node will only appear as a result of SplitBraces. +// This node will only appear as a result of [SplitBraces]. type BraceExp struct { Sequence bool // {x..y[..incr]} instead of {x,y[,...]} Elems []*Word diff --git a/vendor/mvdan.cc/sh/v3/syntax/parser.go b/vendor/mvdan.cc/sh/v3/syntax/parser.go index 21993b1e35..b5363fcd7a 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/parser.go +++ b/vendor/mvdan.cc/sh/v3/syntax/parser.go @@ -4,9 +4,10 @@ package syntax import ( - "bytes" "fmt" "io" + "iter" + "slices" "strconv" "strings" "unicode/utf8" @@ -24,14 +25,14 @@ func KeepComments(enabled bool) ParserOption { } // LangVariant describes a shell language variant to use when tokenizing and -// parsing shell code. The zero value is LangBash. +// parsing shell code. The zero value is [LangBash]. type LangVariant int const ( // LangBash corresponds to the GNU Bash language, as described in its // manual at https://www.gnu.org/software/bash/manual/bash.html. // - // We currently follow Bash version 5.1. + // We currently follow Bash version 5.2. // // Its string representation is "bash". LangBash LangVariant = iota @@ -44,7 +45,7 @@ const ( // LangMirBSDKorn corresponds to the MirBSD Korn Shell, also known as // mksh, as described at http://www.mirbsd.org/htman/i386/man1/mksh.htm. - // Note that it shares some features with Bash, due to the the shared + // Note that it shares some features with Bash, due to the shared // ancestry that is ksh. // // We currently follow mksh version 59. @@ -63,7 +64,7 @@ const ( // commonly used by end-user applications like shfmt, // which can guess a file's language variant given its filename or shebang. // - // At this time, the Parser does not support LangAuto. + // At this time, [Variant] does not support LangAuto. LangAuto ) @@ -144,7 +145,27 @@ func StopAt(word string) ParserOption { return func(p *Parser) { p.stopAt = []byte(word) } } -// NewParser allocates a new Parser and applies any number of options. +// RecoverErrors allows the parser to skip up to a maximum number of +// errors in the given input on a best-effort basis. +// This can be useful to tab-complete an interactive shell prompt, +// or when providing diagnostics on slightly incomplete shell source. +// +// Currently, this only helps with mandatory tokens from the shell grammar +// which are not present in the input. They result in position fields +// or nodes whose position report [Pos.IsRecovered] as true. +// +// For example, given the input +// +// (foo | +// +// the result will contain two recovered positions; first, the pipe requires +// a statement to follow, and as [Stmt.Pos] reports, the entire node is recovered. +// Second, the subshell needs to be closed, so [Subshell.Rparen] is recovered. +func RecoverErrors(maximum int) ParserOption { + return func(p *Parser) { p.recoverErrorsMax = maximum } +} + +// NewParser allocates a new [Parser] and applies any number of options. func NewParser(options ...ParserOption) *Parser { p := &Parser{} for _, opt := range options { @@ -196,7 +217,7 @@ type wrappedReader struct { *Parser io.Reader - lastLine int + lastLine int64 accumulated []*Stmt fn func([]*Stmt) bool } @@ -230,21 +251,20 @@ func (w *wrappedReader) Read(p []byte) (n int, err error) { // called with said statements. // // If a line ending in an incomplete statement is parsed, the function will be -// called with any fully parsed statements, and Parser.Incomplete will return -// true. +// called with any fully parsed statements, and [Parser.Incomplete] will return true. // // One can imagine a simple interactive shell implementation as follows: // -// fmt.Fprintf(os.Stdout, "$ ") -// parser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool { -// if parser.Incomplete() { -// fmt.Fprintf(os.Stdout, "> ") -// return true -// } -// run(stmts) -// fmt.Fprintf(os.Stdout, "$ ") -// return true -// } +// fmt.Fprintf(os.Stdout, "$ ") +// parser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool { +// if parser.Incomplete() { +// fmt.Fprintf(os.Stdout, "> ") +// return true +// } +// run(stmts) +// fmt.Fprintf(os.Stdout, "$ ") +// return true +// } // // If the callback function returns false, parsing is stopped and the function // is not called again. @@ -269,9 +289,20 @@ func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { }) } -// Words reads and parses words one at a time, calling a function each time one -// is parsed. If the function returns false, parsing is stopped and the function -// is not called again. +// Words is a pre-iterators API which now wraps [Parser.WordsSeq]. +func (p *Parser) Words(r io.Reader, fn func(*Word) bool) error { + for w, err := range p.WordsSeq(r) { + if err != nil { + return err + } + if !fn(w) { + break + } + } + return nil +} + +// WordsSeq reads and parses a sequence of words alongside any error encountered. // // Newlines are skipped, meaning that multi-line input will work fine. If the // parser encounters a token that isn't a word, such as a semicolon, an error @@ -280,23 +311,28 @@ func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { // Note that the lexer doesn't currently tokenize spaces, so it may need to read // a non-space byte such as a newline or a letter before finishing the parsing // of a word. This will be fixed in the future. -func (p *Parser) Words(r io.Reader, fn func(*Word) bool) error { +func (p *Parser) WordsSeq(r io.Reader) iter.Seq2[*Word, error] { p.reset() p.f = &File{} p.src = r - p.rune() - p.next() - for { - p.got(_Newl) - w := p.getWord() - if w == nil { - if p.tok != _EOF { - p.curErr("%s is not a valid word", p.tok) + return func(yield func(*Word, error) bool) { + p.rune() + p.next() + for { + p.got(_Newl) + w := p.getWord() + if w == nil { + if p.tok != _EOF { + p.curErr("%s is not a valid word", p.tok) + } + if p.err != nil { + yield(nil, p.err) + } + return + } + if !yield(w, nil) { + return } - return p.err - } - if !fn(w) { - return nil } } } @@ -339,7 +375,7 @@ func (p *Parser) Arithmetic(r io.Reader) (ArithmExpr, error) { type Parser struct { src io.Reader bs []byte // current chunk of read bytes - bsp int // pos within chunk for the rune after r + bsp uint // pos within chunk for the rune after r; uint helps eliminate bounds checks r rune // next rune w int // width of r @@ -354,15 +390,10 @@ type Parser struct { val string // current value (valid if tok is _Lit*) // position of r, to be converted to Parser.pos later - offs, line, col int + offs, line, col int64 pos Pos // position of tok - // TODO: Guard against offset overflow too. Less likely as it's 32-bit, - // whereas line and col are 16-bit. - lineOverflow bool - colOverflow bool - quote quoteState // current lexer state eqlOffs int // position of '=' in val (a literal) @@ -371,6 +402,9 @@ type Parser struct { stopAt []byte + recoveredErrors int + recoverErrorsMax int + forbidNested bool // list of pending heredoc bodies @@ -381,18 +415,15 @@ type Parser struct { parsingDoc bool // true if using Parser.Document - // openStmts is how many entire statements we're currently parsing. A - // non-zero number means that we require certain tokens or words before - // reaching EOF. - openStmts int + // openNodes tracks how many entire statements or words we're currently parsing. + // A non-zero number means that we require certain tokens or words before + // reaching EOF, used for [Parser.Incomplete]. + openNodes int // openBquotes is how many levels of backquotes are open at the moment. openBquotes int // lastBquoteEsc is how many times the last backquote token was escaped lastBquoteEsc int - // buriedBquotes is like openBquotes, but saved for when the parser - // comes out of single quotes - buriedBquotes int rxOpenParens int rxFirstPart bool @@ -400,29 +431,23 @@ type Parser struct { accComs []Comment curComs *[]Comment - litBatch []Lit - wordBatch []Word - wpsBatch []WordPart - stmtBatch []Stmt - stListBatch []*Stmt - callBatch []callAlloc + litBatch []Lit + wordBatch []wordAlloc readBuf [bufSize]byte litBuf [bufSize]byte litBs []byte } -// Incomplete reports whether the parser is waiting to read more bytes because -// it needs to finish properly parsing a statement. +// Incomplete reports whether the parser needs more input bytes +// to finish properly parsing a statement or word. // // It is only safe to call while the parser is blocked on a read. For an example -// use case, see the documentation for Parser.Interactive. +// use case, see [Parser.Interactive]. func (p *Parser) Incomplete() bool { - // If we're in a quote state other than noState, we're parsing a node - // such as a double-quoted string. - // If there are any open statements, we need to finish them. + // If there are any open nodes, we need to finish them. // If we're constructing a literal, we need to finish it. - return p.quote != noState || p.openStmts > 0 || p.litBs != nil + return p.openNodes > 0 || len(p.litBs) > 0 } const bufSize = 1 << 10 @@ -435,28 +460,36 @@ func (p *Parser) reset() { p.r, p.w = 0, 0 p.err, p.readErr = nil, nil p.quote, p.forbidNested = noState, false - p.openStmts = 0 + p.openNodes = 0 + p.recoveredErrors = 0 p.heredocs, p.buriedHdocs = p.heredocs[:0], 0 + p.hdocStops = nil p.parsingDoc = false - p.openBquotes, p.buriedBquotes = 0, 0 + p.openBquotes = 0 + p.accComs = nil p.accComs, p.curComs = nil, &p.accComs + p.litBatch = nil + p.wordBatch = nil + p.litBs = nil } func (p *Parser) nextPos() Pos { - // TODO: detect offset overflow while lexing as well. + // Basic protection against offset overflow; + // note that an offset of 0 is valid, so we leave the maximum. + offset := min(p.offs+int64(p.bsp)-int64(p.w), offsetMax) var line, col uint - if !p.lineOverflow { + if p.line <= lineMax { line = uint(p.line) } - if !p.colOverflow { + if p.col <= colMax { col = uint(p.col) } - return NewPos(uint(p.offs+p.bsp-int(p.w)), line, col) + return NewPos(uint(offset), line, col) } func (p *Parser) lit(pos Pos, val string) *Lit { if len(p.litBatch) == 0 { - p.litBatch = make([]Lit, 128) + p.litBatch = make([]Lit, 32) } l := &p.litBatch[0] p.litBatch = p.litBatch[1:] @@ -466,56 +499,39 @@ func (p *Parser) lit(pos Pos, val string) *Lit { return l } -func (p *Parser) word(parts []WordPart) *Word { +type wordAlloc struct { + word Word + parts [1]WordPart +} + +func (p *Parser) wordAnyNumber() *Word { if len(p.wordBatch) == 0 { - p.wordBatch = make([]Word, 64) + p.wordBatch = make([]wordAlloc, 32) } - w := &p.wordBatch[0] + alloc := &p.wordBatch[0] p.wordBatch = p.wordBatch[1:] - w.Parts = parts + w := &alloc.word + w.Parts = p.wordParts(alloc.parts[:0]) return w } -func (p *Parser) wps(wp WordPart) []WordPart { - if len(p.wpsBatch) == 0 { - p.wpsBatch = make([]WordPart, 64) - } - wps := p.wpsBatch[:1:1] - p.wpsBatch = p.wpsBatch[1:] - wps[0] = wp - return wps -} - -func (p *Parser) stmt(pos Pos) *Stmt { - if len(p.stmtBatch) == 0 { - p.stmtBatch = make([]Stmt, 64) - } - s := &p.stmtBatch[0] - p.stmtBatch = p.stmtBatch[1:] - s.Position = pos - return s -} - -func (p *Parser) stList() []*Stmt { - if len(p.stListBatch) == 0 { - p.stListBatch = make([]*Stmt, 256) +func (p *Parser) wordOne(part WordPart) *Word { + if len(p.wordBatch) == 0 { + p.wordBatch = make([]wordAlloc, 32) } - stmts := p.stListBatch[:0:4] - p.stListBatch = p.stListBatch[4:] - return stmts -} - -type callAlloc struct { - ce CallExpr - ws [4]*Word + alloc := &p.wordBatch[0] + p.wordBatch = p.wordBatch[1:] + w := &alloc.word + w.Parts = alloc.parts[:1] + w.Parts[0] = part + return w } func (p *Parser) call(w *Word) *CallExpr { - if len(p.callBatch) == 0 { - p.callBatch = make([]callAlloc, 32) + var alloc struct { + ce CallExpr + ws [4]*Word } - alloc := &p.callBatch[0] - p.callBatch = p.callBatch[1:] ce := &alloc.ce ce.Args = alloc.ws[:1] ce.Args[0] = w @@ -573,39 +589,37 @@ func (p *Parser) postNested(s saveState) { } func (p *Parser) unquotedWordBytes(w *Word) ([]byte, bool) { - var buf bytes.Buffer + buf := make([]byte, 0, 4) didUnquote := false for _, wp := range w.Parts { - if p.unquotedWordPart(&buf, wp, false) { - didUnquote = true - } + buf, didUnquote = p.unquotedWordPart(buf, wp, false) } - return buf.Bytes(), didUnquote + return buf, didUnquote } -func (p *Parser) unquotedWordPart(buf *bytes.Buffer, wp WordPart, quotes bool) (quoted bool) { - switch x := wp.(type) { +func (p *Parser) unquotedWordPart(buf []byte, wp WordPart, quotes bool) (_ []byte, quoted bool) { + switch wp := wp.(type) { case *Lit: - for i := 0; i < len(x.Value); i++ { - if b := x.Value[i]; b == '\\' && !quotes { - if i++; i < len(x.Value) { - buf.WriteByte(x.Value[i]) + for i := 0; i < len(wp.Value); i++ { + if b := wp.Value[i]; b == '\\' && !quotes { + if i++; i < len(wp.Value) { + buf = append(buf, wp.Value[i]) } quoted = true } else { - buf.WriteByte(b) + buf = append(buf, b) } } case *SglQuoted: - buf.WriteString(x.Value) + buf = append(buf, []byte(wp.Value)...) quoted = true case *DblQuoted: - for _, wp2 := range x.Parts { - p.unquotedWordPart(buf, wp2, true) + for _, wp2 := range wp.Parts { + buf, _ = p.unquotedWordPart(buf, wp2, true) } quoted = true } - return + return buf, quoted } func (p *Parser) doHeredocs() { @@ -638,14 +652,14 @@ func (p *Parser) doHeredocs() { r.Hdoc = p.getWord() } if r.Hdoc != nil { - lastLine = int(r.Hdoc.End().Line()) + lastLine = int64(r.Hdoc.End().Line()) } if lastLine < p.line { // TODO: It seems like this triggers more often than it // should. Look into it. l := p.lit(p.nextPos(), "") if r.Hdoc == nil { - r.Hdoc = p.word(p.wps(l)) + r.Hdoc = p.wordOne(l) } else { r.Hdoc.Parts = append(r.Hdoc.Parts, l) } @@ -675,6 +689,14 @@ func (p *Parser) gotRsrv(val string) (Pos, bool) { return pos, false } +func (p *Parser) recoverError() bool { + if p.recoveredErrors < p.recoverErrorsMax { + p.recoveredErrors++ + return true + } + return false +} + func readableStr(s string) string { // don't quote tokens like & or } if s != "" && s[0] >= 'a' && s[0] <= 'z' { @@ -701,6 +723,9 @@ func (p *Parser) follow(lpos Pos, left string, tok token) { func (p *Parser) followRsrv(lpos Pos, left, val string) Pos { pos, ok := p.gotRsrv(val) if !ok { + if p.recoverError() { + return recoveredPos + } p.followErr(lpos, left, fmt.Sprintf("%q", val)) } return pos @@ -713,6 +738,9 @@ func (p *Parser) followStmts(left string, lpos Pos, stops ...string) ([]*Stmt, [ newLine := p.got(_Newl) stmts, last := p.stmtList(stops...) if len(stmts) < 1 && !newLine { + if p.recoverError() { + return []*Stmt{{Position: recoveredPos}}, nil + } p.followErr(lpos, left, "a statement list") } return stmts, last @@ -721,6 +749,9 @@ func (p *Parser) followStmts(left string, lpos Pos, stops ...string) ([]*Stmt, [ func (p *Parser) followWordTok(tok token, pos Pos) *Word { w := p.getWord() if w == nil { + if p.recoverError() { + return p.wordOne(&Lit{ValuePos: recoveredPos}) + } p.followErr(pos, tok.String(), "a word") } return w @@ -729,6 +760,9 @@ func (p *Parser) followWordTok(tok token, pos Pos) *Word { func (p *Parser) stmtEnd(n Node, start, end string) Pos { pos, ok := p.gotRsrv(end) if !ok { + if p.recoverError() { + return recoveredPos + } p.posErr(n.Pos(), "%s statement must end with %q", start, end) } return pos @@ -739,7 +773,7 @@ func (p *Parser) quoteErr(lpos Pos, quote token) { p.tok.String(), quote) } -func (p *Parser) matchingErr(lpos Pos, left, right interface{}) { +func (p *Parser) matchingErr(lpos Pos, left, right any) { p.posErr(lpos, "reached %s without matching %s with %s", p.tok.String(), left, right) } @@ -747,6 +781,9 @@ func (p *Parser) matchingErr(lpos Pos, left, right interface{}) { func (p *Parser) matched(lpos Pos, left, right token) Pos { pos := p.pos if !p.got(right) { + if p.recoverError() { + return recoveredPos + } p.matchingErr(lpos, left, right) } return pos @@ -755,7 +792,7 @@ func (p *Parser) matched(lpos Pos, left, right token) Pos { func (p *Parser) errPass(err error) { if p.err == nil { p.err = err - p.bsp = len(p.bs) + 1 + p.bsp = uint(len(p.bs)) + 1 p.r = utf8.RuneSelf p.w = 1 p.tok = _EOF @@ -763,7 +800,7 @@ func (p *Parser) errPass(err error) { } // IsIncomplete reports whether a Parser error could have been avoided with -// extra input bytes. For example, if an io.EOF was encountered while there was +// extra input bytes. For example, if an [io.EOF] was encountered while there was // an unclosed quote or parenthesis. func IsIncomplete(err error) bool { perr, ok := err.(ParseError) @@ -805,8 +842,8 @@ func IsKeyword(word string) bool { // the parser cannot recover. type ParseError struct { Filename string - Pos - Text string + Pos Pos + Text string Incomplete bool } @@ -823,34 +860,40 @@ func (e ParseError) Error() string { // in the current language variant, and what languages support it. type LangError struct { Filename string - Pos + Pos Pos + + // Feature briefly describes which language feature caused the error. Feature string - Langs []LangVariant + // Langs lists some of the language variants which support the feature. + Langs []LangVariant + // LangUsed is the language variant used which led to the error. + LangUsed LangVariant } func (e LangError) Error() string { - var buf bytes.Buffer + var sb strings.Builder if e.Filename != "" { - buf.WriteString(e.Filename + ":") + sb.WriteString(e.Filename + ":") } - buf.WriteString(e.Pos.String() + ": ") - buf.WriteString(e.Feature) + sb.WriteString(e.Pos.String() + ": ") + sb.WriteString(e.Feature) if strings.HasSuffix(e.Feature, "s") { - buf.WriteString(" are a ") + sb.WriteString(" are a ") } else { - buf.WriteString(" is a ") + sb.WriteString(" is a ") } for i, lang := range e.Langs { if i > 0 { - buf.WriteString("/") + sb.WriteString("/") } - buf.WriteString(lang.String()) + sb.WriteString(lang.String()) } - buf.WriteString(" feature") - return buf.String() + sb.WriteString(" feature; tried parsing as ") + sb.WriteString(e.LangUsed.String()) + return sb.String() } -func (p *Parser) posErr(pos Pos, format string, a ...interface{}) { +func (p *Parser) posErr(pos Pos, format string, a ...any) { p.errPass(ParseError{ Filename: p.f.Name, Pos: pos, @@ -859,7 +902,7 @@ func (p *Parser) posErr(pos Pos, format string, a ...interface{}) { }) } -func (p *Parser) curErr(format string, a ...interface{}) { +func (p *Parser) curErr(format string, a ...any) { p.posErr(p.pos, format, a...) } @@ -869,6 +912,7 @@ func (p *Parser) langErr(pos Pos, feature string, langs ...LangVariant) { Pos: pos, Feature: feature, Langs: langs, + LangUsed: p.lang, }) } @@ -904,9 +948,9 @@ loop: if p.tok == _EOF { break } - p.openStmts++ + p.openNodes++ s := p.getStmt(true, false, false) - p.openStmts-- + p.openNodes-- if s == nil { p.invalidStmtStart() break @@ -922,9 +966,6 @@ func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) { var stmts []*Stmt var last []Comment fn := func(s *Stmt) bool { - if stmts == nil { - stmts = p.stList() - } stmts = append(stmts, s) return true } @@ -941,8 +982,7 @@ func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) { // fi // TODO(mvdan): look into deduplicating this with similar logic // in caseItems. - for i := len(p.accComs) - 1; i >= 0; i-- { - c := p.accComs[i] + for i, c := range slices.Backward(p.accComs) { if c.Pos().Col() != p.pos.Col() { break } @@ -968,8 +1008,8 @@ func (p *Parser) invalidStmtStart() { } func (p *Parser) getWord() *Word { - if parts := p.wordParts(); len(parts) > 0 && p.err == nil { - return p.word(parts) + if w := p.wordAnyNumber(); len(w.Parts) > 0 && p.err == nil { + return w } return nil } @@ -984,19 +1024,20 @@ func (p *Parser) getLit() *Lit { return nil } -func (p *Parser) wordParts() (wps []WordPart) { +func (p *Parser) wordParts(wps []WordPart) []WordPart { for { + p.openNodes++ n := p.wordPart() + p.openNodes-- if n == nil { - return - } - if wps == nil { - wps = p.wps(n) - } else { - wps = append(wps, n) + if len(wps) == 0 { + return nil // normalize empty lists into nil + } + return wps } + wps = append(wps, n) if p.spaced { - return + return wps } } } @@ -1009,7 +1050,7 @@ func (p *Parser) ensureNoNested() { func (p *Parser) wordPart() WordPart { switch p.tok { - case _Lit, _LitWord: + case _Lit, _LitWord, _LitRedir: l := p.lit(p.pos, p.val) p.next() return l @@ -1018,12 +1059,12 @@ func (p *Parser) wordPart() WordPart { switch p.r { case '|': if p.lang != LangMirBSDKorn { - p.curErr(`"${|stmts;}" is a mksh feature`) + p.langErr(p.pos, `"${|stmts;}"`, LangMirBSDKorn) } fallthrough case ' ', '\t', '\n': if p.lang != LangMirBSDKorn { - p.curErr(`"${ stmts;}" is a mksh feature`) + p.langErr(p.pos, `"${ stmts;}"`, LangMirBSDKorn) } cs := &CmdSubst{ Left: p.pos, @@ -1130,10 +1171,6 @@ func (p *Parser) wordPart() WordPart { sq.Right = p.nextPos() sq.Value = p.endLit() - // restore openBquotes - p.openBquotes = p.buriedBquotes - p.buriedBquotes = 0 - p.rune() p.next() return sq @@ -1141,6 +1178,10 @@ func (p *Parser) wordPart() WordPart { p.litBs = append(p.litBs, '\\', '\n') case utf8.RuneSelf: p.tok = _EOF + if p.recoverError() { + sq.Right = recoveredPos + return sq + } p.quoteErr(sq.Pos(), sglQuote) return nil } @@ -1178,7 +1219,11 @@ func (p *Parser) wordPart() WordPart { // Like above, the lexer didn't call p.rune for us. p.rune() if !p.got(bckQuote) { - p.quoteErr(cs.Pos(), bckQuote) + if p.recoverError() { + cs.Right = recoveredPos + } else { + p.quoteErr(cs.Pos(), bckQuote) + } } return cs case globQuest, globStar, globPlus, globAt, globExcl: @@ -1214,15 +1259,25 @@ func (p *Parser) wordPart() WordPart { } func (p *Parser) dblQuoted() *DblQuoted { - q := &DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote} + alloc := &struct { + quoted DblQuoted + parts [1]WordPart + }{ + quoted: DblQuoted{Left: p.pos, Dollar: p.tok == dollDblQuote}, + } + q := &alloc.quoted old := p.quote p.quote = dblQuotes p.next() - q.Parts = p.wordParts() + q.Parts = p.wordParts(alloc.parts[:0]) p.quote = old q.Right = p.pos if !p.got(dblQuote) { - p.quoteErr(q.Pos(), dblQuote) + if p.recoverError() { + q.Right = recoveredPos + } else { + p.quoteErr(q.Pos(), dblQuote) + } } return q } @@ -1255,7 +1310,7 @@ func (p *Parser) paramExp() *ParamExp { } case perc: if p.lang != LangMirBSDKorn { - p.posErr(pe.Pos(), `"${%%foo}" is a mksh feature`) + p.langErr(pe.Pos(), `"${%foo}"`, LangMirBSDKorn) } if paramNameOp(p.r) { pe.Width = true @@ -1263,9 +1318,6 @@ func (p *Parser) paramExp() *ParamExp { } case exclMark: if paramNameOp(p.r) { - if p.lang == LangPOSIX { - p.langErr(p.pos, "${!foo}", LangBash, LangMirBSDKorn) - } pe.Excl = true p.next() } @@ -1297,6 +1349,9 @@ func (p *Parser) paramExp() *ParamExp { case _Lit, _LitWord: p.curErr("%s cannot be followed by a word", op) case rightBrace: + if pe.Excl && p.lang == LangPOSIX { + p.langErr(pe.Pos(), `"${!foo}"`, LangBash, LangMirBSDKorn) + } pe.Rbrace = p.pos p.quote = old p.next() @@ -1367,6 +1422,9 @@ func (p *Parser) paramExp() *ParamExp { case p.tok == star && !pe.Excl: p.curErr("not a valid parameter expansion operator: %v", p.tok) case pe.Excl && p.r == '}': + if !p.lang.isBash() { + p.langErr(pe.Pos(), fmt.Sprintf(`"${!foo%s}"`, p.tok), LangBash) + } pe.Names = ParNamesOperator(p.tok) p.next() default: @@ -1380,8 +1438,7 @@ func (p *Parser) paramExp() *ParamExp { p.curErr("not a valid parameter expansion operator: %v", p.tok) } p.quote = old - pe.Rbrace = p.pos - p.matched(pe.Dollar, dollBrace, rightBrace) + pe.Rbrace = p.matched(pe.Dollar, dollBrace, rightBrace) return pe } @@ -1396,7 +1453,7 @@ func (p *Parser) paramExpExp() *Expansion { p.curErr("@ expansion operator requires a literal") } switch p.val { - case "a", "u", "A", "E", "K", "L", "P", "U": + case "a", "k", "u", "A", "E", "K", "L", "P", "U": if !p.lang.isBash() { p.langErr(p.pos, "this expansion operator", LangBash) } @@ -1406,7 +1463,7 @@ func (p *Parser) paramExpExp() *Expansion { } case "Q": default: - p.curErr("invalid @ expansion operator") + p.curErr("invalid @ expansion operator %q", p.val) } } return &Expansion{Op: op, Word: p.getWord()} @@ -1500,7 +1557,7 @@ func (p *Parser) getAssign(needEqual bool) *Assign { left := p.lit(posAddCol(p.pos, 1), p.val[p.eqlOffs+1:]) if left.Value != "" { left.ValuePos = posAddCol(left.ValuePos, p.eqlOffs) - as.Value = p.word(p.wps(left)) + as.Value = p.wordOne(left) } p.next() } else { // foo[x]=bar @@ -1624,6 +1681,9 @@ func (p *Parser) doRedirect(s *Stmt) { if !p.lang.isBash() && r.N != nil && r.N.Value[0] == '{' { p.langErr(r.N.Pos(), "{varname} redirects", LangBash) } + if p.lang == LangPOSIX && (p.tok == rdrAll || p.tok == appAll) { + p.langErr(p.pos, "&> redirects", LangBash, LangMirBSDKorn) + } r.Op, r.OpPos = RedirOperator(p.tok), p.pos p.next() switch r.Op { @@ -1643,6 +1703,11 @@ func (p *Parser) doRedirect(s *Stmt) { } p.doHeredocs() } + case WordHdoc: + if p.lang == LangPOSIX { + p.langErr(r.OpPos, "herestrings", LangBash, LangMirBSDKorn) + } + fallthrough default: r.Word = p.followWordTok(token(r.Op), r.OpPos) } @@ -1650,7 +1715,7 @@ func (p *Parser) doRedirect(s *Stmt) { func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { pos, ok := p.gotRsrv("!") - s := p.stmt(pos) + s := &Stmt{Position: pos} if ok { s.Negated = true if p.stopToken() { @@ -1679,10 +1744,14 @@ func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { p.got(_Newl) b.Y = p.getStmt(false, true, false) if b.Y == nil || p.err != nil { - p.followErr(b.OpPos, b.Op.String(), "a statement") - return nil + if p.recoverError() { + b.Y = &Stmt{Position: recoveredPos} + } else { + p.followErr(b.OpPos, b.Op.String(), "a statement") + return nil + } } - s = p.stmt(s.Position) + s = &Stmt{Position: s.Position} s.Cmd = b s.Comments, b.X.Comments = b.X.Comments, nil } @@ -1751,8 +1820,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { } case "]]": if p.lang != LangPOSIX { - p.curErr(`%q can only be used to close a test`, - p.val) + p.curErr(`%q can only be used to close a test`, p.val) } case "let": if p.lang != LangPOSIX { @@ -1763,7 +1831,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.bashFuncDecl(s) } case "declare": - if p.lang.isBash() { + if p.lang.isBash() { // Note that mksh lacks this one. p.declClause(s) } case "local", "export", "readonly", "typeset", "nameref": @@ -1775,7 +1843,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.timeClause(s) } case "coproc": - if p.lang.isBash() { + if p.lang.isBash() { // Note that mksh lacks this one. p.coprocClause(s) } case "select": @@ -1802,7 +1870,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { } p.funcDecl(s, name, name.ValuePos, true) } else { - p.callExpr(s, p.word(p.wps(name)), false) + p.callExpr(s, p.wordOne(name), false) } case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: @@ -1820,7 +1888,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { p.callExpr(s, nil, true) break } - w := p.word(p.wordParts()) + w := p.wordAnyNumber() if p.got(leftParen) { p.posErr(w.Pos(), "invalid func name") } @@ -1852,11 +1920,15 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { b := &BinaryCmd{OpPos: p.pos, Op: BinCmdOperator(p.tok), X: s} p.next() p.got(_Newl) - if b.Y = p.gotStmtPipe(p.stmt(p.pos), true); b.Y == nil || p.err != nil { - p.followErr(b.OpPos, b.Op.String(), "a statement") - break + if b.Y = p.gotStmtPipe(&Stmt{Position: p.pos}, true); b.Y == nil || p.err != nil { + if p.recoverError() { + b.Y = &Stmt{Position: recoveredPos} + } else { + p.followErr(b.OpPos, b.Op.String(), "a statement") + break + } } - s = p.stmt(s.Position) + s = &Stmt{Position: s.Position} s.Cmd = b s.Comments, b.X.Comments = b.X.Comments, nil // in "! x | y", the bang applies to the entire pipeline @@ -1895,9 +1967,11 @@ func (p *Parser) block(s *Stmt) { b := &Block{Lbrace: p.pos} p.next() b.Stmts, b.Last = p.stmtList("}") - pos, ok := p.gotRsrv("}") - b.Rbrace = pos - if !ok { + if pos, ok := p.gotRsrv("}"); ok { + b.Rbrace = pos + } else if p.recoverError() { + b.Rbrace = recoveredPos + } else { p.matchingErr(b.Lbrace, "{", "}") } s.Cmd = b @@ -2056,7 +2130,7 @@ func (p *Parser) caseClause(s *Stmt) { cc.In = pos cc.Braces = true if p.lang != LangMirBSDKorn { - p.posErr(cc.Pos(), `"case i {" is a mksh feature`) + p.langErr(cc.Pos(), `"case i {"`, LangMirBSDKorn) } end = "}" } else { @@ -2070,7 +2144,7 @@ func (p *Parser) caseClause(s *Stmt) { func (p *Parser) caseItems(stop string) (items []*CaseItem) { p.got(_Newl) - for p.tok != _EOF && !(p.tok == _LitWord && p.val == stop) { + for p.tok != _EOF && (p.tok != _LitWord || p.val != stop) { ci := &CaseItem{} ci.Comments, p.accComs = p.accComs, nil p.got(leftParen) @@ -2104,18 +2178,27 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) { ci.Op = CaseOperator(p.tok) p.next() p.got(_Newl) + + // Split the comments: + // + // case x in + // a) + // foo + // ;; + // # comment for a + // # comment for b + // b) + // [...] split := len(p.accComs) - if p.tok == _LitWord && p.val != stop { - for i := len(p.accComs) - 1; i >= 0; i-- { - c := p.accComs[i] - if c.Pos().Col() != p.pos.Col() { - break - } - split = i + for i, c := range slices.Backward(p.accComs) { + if c.Pos().Col() != p.pos.Col() { + break } + split = i } ci.Comments = append(ci.Comments, p.accComs[:split]...) p.accComs = p.accComs[split:] + items = append(items, ci) } return @@ -2128,7 +2211,7 @@ func (p *Parser) testClause(s *Stmt) { if _, ok := p.gotRsrv("]]"); ok || p.tok == _EOF { p.posErr(tc.Left, "test clause requires at least one expression") } - tc.X = p.testExpr(dblLeftBrack, tc.Left, false) + tc.X = p.testExpr(false) if tc.X == nil { p.followErrExp(tc.Left, "[[") } @@ -2140,13 +2223,13 @@ func (p *Parser) testClause(s *Stmt) { s.Cmd = tc } -func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { +func (p *Parser) testExpr(pastAndOr bool) TestExpr { p.got(_Newl) var left TestExpr if pastAndOr { left = p.testExprBase() } else { - left = p.testExpr(ftok, fpos, true) + left = p.testExpr(true) } if left == nil { return left @@ -2180,7 +2263,7 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { switch b.Op { case AndTest, OrTest: p.next() - if b.Y = p.testExpr(token(b.Op), b.OpPos, false); b.Y == nil { + if b.Y = p.testExpr(false); b.Y == nil { p.followErrExp(b.OpPos, b.Op.String()) } case TsReMatch: @@ -2226,7 +2309,7 @@ func (p *Parser) testExprBase() TestExpr { case exclMark: u := &UnaryTest{OpPos: p.pos, Op: TsNot} p.next() - if u.X = p.testExpr(token(u.Op), u.OpPos, false); u.X == nil { + if u.X = p.testExpr(false); u.X == nil { p.followErrExp(u.OpPos, u.Op.String()) } return u @@ -2241,7 +2324,7 @@ func (p *Parser) testExprBase() TestExpr { case leftParen: pe := &ParenTest{Lparen: p.pos} p.next() - if pe.X = p.testExpr(leftParen, pe.Lparen, false); pe.X == nil { + if pe.X = p.testExpr(false); pe.X == nil { p.followErrExp(pe.Lparen, "(") } pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) @@ -2306,7 +2389,7 @@ func (p *Parser) timeClause(s *Stmt) { if _, ok := p.gotRsrv("-p"); ok { tc.PosixFormat = true } - tc.Stmt = p.gotStmtPipe(p.stmt(p.pos), false) + tc.Stmt = p.gotStmtPipe(&Stmt{Position: p.pos}, false) s.Cmd = tc } @@ -2314,19 +2397,19 @@ func (p *Parser) coprocClause(s *Stmt) { cc := &CoprocClause{Coproc: p.pos} if p.next(); isBashCompoundCommand(p.tok, p.val) { // has no name - cc.Stmt = p.gotStmtPipe(p.stmt(p.pos), false) + cc.Stmt = p.gotStmtPipe(&Stmt{Position: p.pos}, false) s.Cmd = cc return } cc.Name = p.getWord() - cc.Stmt = p.gotStmtPipe(p.stmt(p.pos), false) + cc.Stmt = p.gotStmtPipe(&Stmt{Position: p.pos}, false) if cc.Stmt == nil { if cc.Name == nil { p.posErr(cc.Coproc, "coproc clause requires a command") return } // name was in fact the stmt - cc.Stmt = p.stmt(cc.Name.Pos()) + cc.Stmt = &Stmt{Position: cc.Name.Pos()} cc.Stmt.Cmd = p.call(cc.Name) cc.Name = nil } else if cc.Name != nil { @@ -2402,16 +2485,18 @@ loop: ce.Assigns = append(ce.Assigns, p.getAssign(true)) break } - ce.Args = append(ce.Args, p.word( - p.wps(p.lit(p.pos, p.val)), - )) + // Avoid failing later with the confusing "} can only be used to close a block". + if p.lang == LangPOSIX && p.val == "{" && w != nil && w.Lit() == "function" { + p.langErr(p.pos, `the "function" builtin`, LangBash) + } + ce.Args = append(ce.Args, p.wordOne(p.lit(p.pos, p.val))) p.next() case _Lit: if len(ce.Args) == 0 && p.hasValidIdent() { ce.Assigns = append(ce.Assigns, p.getAssign(true)) break } - ce.Args = append(ce.Args, p.word(p.wordParts())) + ce.Args = append(ce.Args, p.wordAnyNumber()) case bckQuote: if p.backquoteEnd() { break loop @@ -2420,7 +2505,7 @@ loop: case dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: - ce.Args = append(ce.Args, p.word(p.wordParts())) + ce.Args = append(ce.Args, p.wordAnyNumber()) case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: p.doRedirect(s) @@ -2432,6 +2517,12 @@ loop: } fallthrough default: + // Note that we'll only keep the first error that happens. + if len(ce.Args) > 0 { + if cmd := ce.Args[0].Lit(); p.lang == LangPOSIX && isBashCompoundCommand(_LitWord, cmd) { + p.langErr(p.pos, fmt.Sprintf("the %q builtin", cmd), LangBash) + } + } p.curErr("a command can only contain words and redirects; encountered %s", p.tok) } } diff --git a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go index 0021c62d7c..d04f3d896b 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go +++ b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go @@ -189,12 +189,12 @@ func (p *Parser) arithmExprValue(compact bool) ArithmExpr { case _LitWord: l := p.getLit() if p.tok != leftBrack { - x = p.word(p.wps(l)) + x = p.wordOne(l) break } pe := &ParamExp{Dollar: l.ValuePos, Short: true, Param: l} pe.Index = p.eitherIndex() - x = p.word(p.wps(pe)) + x = p.wordOne(pe) case bckQuote: if p.quote == arithmExprLet && p.openBquotes > 0 { return nil @@ -295,11 +295,11 @@ func isArithName(left ArithmExpr) bool { if !ok || len(w.Parts) != 1 { return false } - switch x := w.Parts[0].(type) { + switch wp := w.Parts[0].(type) { case *Lit: - return ValidName(x.Value) + return ValidName(wp.Value) case *ParamExp: - return x.nakedIndex() + return wp.nakedIndex() default: return false } @@ -343,6 +343,9 @@ func (p *Parser) matchedArithm(lpos Pos, left, right token) { func (p *Parser) arithmEnd(ltok token, lpos Pos, old saveState) Pos { if !p.peekArithmEnd() { + if p.recoverError() { + return recoveredPos + } p.arithmMatchingErr(lpos, ltok, dblRightParen) } p.rune() diff --git a/vendor/mvdan.cc/sh/v3/syntax/printer.go b/vendor/mvdan.cc/sh/v3/syntax/printer.go index 7dc183a024..74675ad9cc 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/printer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/printer.go @@ -55,6 +55,11 @@ func SpaceRedirects(enabled bool) PrinterOption { // Note that this feature is best-effort and will only keep the // alignment stable, so it may need some human help the first time it is // run. +// +// Deprecated: this formatting option is flawed and buggy, and often does +// not result in what the user wants when the code gets complex enough. +// The next major version, v4, will remove this feature entirely. +// See: https://github.com/mvdan/sh/issues/658 func KeepPadding(enabled bool) PrinterOption { return func(p *Printer) { if enabled && !p.keepPadding { @@ -84,7 +89,7 @@ func Minify(enabled bool) PrinterOption { // newlines must still appear, such as those following comments or around // here-documents. // -// Print's trailing newline when given a *File is not affected by this option. +// Print's trailing newline when given a [*File] is not affected by this option. func SingleLine(enabled bool) PrinterOption { return func(p *Printer) { p.singleLine = enabled } } @@ -109,9 +114,9 @@ func NewPrinter(opts ...PrinterOption) *Printer { // Print "pretty-prints" the given syntax tree node to the given writer. Writes // to w are buffered. // -// The node types supported at the moment are *File, *Stmt, *Word, *Assign, any -// Command node, and any WordPart node. A trailing newline will only be printed -// when a *File is used. +// The node types supported at the moment are [*File], [*Stmt], [*Word], [*Assign], any +// [Command] node, and any WordPart node. A trailing newline will only be printed +// when a [*File] is used. func (p *Printer) Print(w io.Writer, node Node) error { p.reset() @@ -134,25 +139,25 @@ func (p *Printer) Print(w io.Writer, node Node) error { w = p.tabWriter p.bufWriter.Reset(w) - switch x := node.(type) { + switch node := node.(type) { case *File: - p.stmtList(x.Stmts, x.Last) + p.stmtList(node.Stmts, node.Last) p.newline(Pos{}) case *Stmt: - p.stmtList([]*Stmt{x}, nil) + p.stmtList([]*Stmt{node}, nil) case Command: - p.command(x, nil) + p.command(node, nil) case *Word: - p.line = x.Pos().Line() - p.word(x) + p.line = node.Pos().Line() + p.word(node) case WordPart: - p.line = x.Pos().Line() - p.wordPart(x, nil) + p.line = node.Pos().Line() + p.wordPart(node, nil) case *Assign: - p.line = x.Pos().Line() - p.assigns([]*Assign{x}) + p.line = node.Pos().Line() + p.assigns([]*Assign{node}) default: - return fmt.Errorf("unsupported node type: %T", x) + return fmt.Errorf("unsupported node type: %T", node) } p.flushHeredocs() p.flushComments() @@ -216,7 +221,7 @@ func (c *colCounter) Reset(w io.Writer) { // Printer holds the internal state of the printing mechanism of a // program. type Printer struct { - bufWriter + bufWriter // TODO: embedding this makes the methods part of the API, which we did not intend tabWriter *tabwriter.Writer cols colCounter @@ -280,7 +285,7 @@ func (p *Printer) reset() { } func (p *Printer) spaces(n uint) { - for i := uint(0); i < n; i++ { + for range n { p.WriteByte(' ') } } @@ -308,16 +313,21 @@ func (p *Printer) spacePad(pos Pos) { // wantsNewline reports whether we want to print at least one newline before // printing a node at a given position. A zero position can be given to simply // tell if we want a newline following what's just been printed. -func (p *Printer) wantsNewline(pos Pos) bool { +func (p *Printer) wantsNewline(pos Pos, escapingNewline bool) bool { if p.mustNewline { // We must have a newline here. return true } - if p.singleLine { - // The newline is optional, and singleLine turns it off. + if p.singleLine && len(p.pendingComments) == 0 { + // The newline is optional, and singleLine skips it. + // Don't skip if there are any pending comments, + // as that might move them further down to the wrong place. return false } - // THe newline is optional, and we want it via either wantNewline or via + if escapingNewline && p.minify { + return false + } + // The newline is optional, and we want it via either wantNewline or via // the position's line. return p.wantNewline || pos.Line() > p.line } @@ -349,7 +359,7 @@ func (p *Printer) spacedToken(s string, pos Pos) { } func (p *Printer) semiOrNewl(s string, pos Pos) { - if p.wantsNewline(Pos{}) { + if p.wantsNewline(Pos{}, false) { p.newline(pos) p.indent() } else { @@ -359,7 +369,7 @@ func (p *Printer) semiOrNewl(s string, pos Pos) { if !p.minify { p.space() } - p.line = pos.Line() + p.advanceLine(pos.Line()) } p.WriteString(s) p.wantSpace = spaceRequired @@ -415,14 +425,19 @@ func (p *Printer) indent() { // TODO(mvdan): add an indent call at the end of newline? +// newline prints one newline and advances p.line to pos.Line(). func (p *Printer) newline(pos Pos) { p.flushHeredocs() p.flushComments() p.WriteByte('\n') p.wantSpace = spaceWritten p.wantNewline, p.mustNewline = false, false - if p.line < pos.Line() { - p.line++ + p.advanceLine(pos.Line()) +} + +func (p *Printer) advanceLine(line uint) { + if p.line < line { + p.line = line } } @@ -486,7 +501,7 @@ func (p *Printer) flushHeredocs() { if r.Hdoc != nil { // Overwrite p.line, since printing r.Word again can set // p.line to the beginning of the heredoc again. - p.line = r.Hdoc.End().Line() + p.advanceLine(r.Hdoc.End().Line()) } p.wantSpace = spaceNotRequired } @@ -495,27 +510,32 @@ func (p *Printer) flushHeredocs() { p.mustNewline = true } +// newline prints between zero and two newlines. +// If any newlines are printed, it advances p.line to pos.Line(). func (p *Printer) newlines(pos Pos) { if p.firstLine && len(p.pendingComments) == 0 { p.firstLine = false return // no empty lines at the top } - if !p.wantsNewline(pos) { + if !p.wantsNewline(pos, false) { return } - p.newline(pos) - if pos.Line() > p.line { - if !p.minify { - // preserve single empty lines - p.WriteByte('\n') - } - p.line++ + p.flushHeredocs() + p.flushComments() + p.WriteByte('\n') + p.wantSpace = spaceWritten + p.wantNewline, p.mustNewline = false, false + + l := pos.Line() + if l > p.line+1 && !p.minify { + p.WriteByte('\n') // preserve single empty lines } + p.advanceLine(l) p.indent() } func (p *Printer) rightParen(pos Pos) { - if !p.minify { + if len(p.pendingHdocs) > 0 || !p.minify { p.newlines(pos) } p.WriteByte(')') @@ -523,7 +543,7 @@ func (p *Printer) rightParen(pos Pos) { } func (p *Printer) semiRsrv(s string, pos Pos) { - if p.wantsNewline(pos) { + if p.wantsNewline(pos, false) { p.newlines(pos) } else { if !p.wroteSemi { @@ -567,9 +587,7 @@ func (p *Printer) flushComments() { p.space() } // don't go back one line, which may happen in some edge cases - if p.line < cline { - p.line = cline - } + p.advanceLine(cline) p.WriteByte('#') p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace)) p.wantNewline = true @@ -593,6 +611,14 @@ func (p *Printer) comments(comments ...Comment) { } func (p *Printer) wordParts(wps []WordPart, quoted bool) { + // We disallow unquoted escaped newlines between word parts below. + // However, we want to allow a leading escaped newline for cases such as: + // + // foo <<< \ + // "bar baz" + if !quoted && !p.singleLine && wps[0].Pos().Line() > p.line { + p.bslashNewl() + } for i, wp := range wps { var next WordPart if i+1 < len(wps) { @@ -609,93 +635,93 @@ func (p *Printer) wordParts(wps []WordPart, quoted bool) { p.line++ } p.wordPart(wp, next) - p.line = wp.End().Line() + p.advanceLine(wp.End().Line()) } } func (p *Printer) wordPart(wp, next WordPart) { - switch x := wp.(type) { + switch wp := wp.(type) { case *Lit: - p.writeLit(x.Value) + p.writeLit(wp.Value) case *SglQuoted: - if x.Dollar { + if wp.Dollar { p.WriteByte('$') } p.WriteByte('\'') - p.writeLit(x.Value) + p.writeLit(wp.Value) p.WriteByte('\'') - p.line = x.End().Line() + p.advanceLine(wp.End().Line()) case *DblQuoted: - p.dblQuoted(x) + p.dblQuoted(wp) case *CmdSubst: - p.line = x.Pos().Line() + p.advanceLine(wp.Pos().Line()) switch { - case x.TempFile: + case wp.TempFile: p.WriteString("${") p.wantSpace = spaceRequired - p.nestedStmts(x.Stmts, x.Last, x.Right) + p.nestedStmts(wp.Stmts, wp.Last, wp.Right) p.wantSpace = spaceNotRequired - p.semiRsrv("}", x.Right) - case x.ReplyVar: + p.semiRsrv("}", wp.Right) + case wp.ReplyVar: p.WriteString("${|") - p.nestedStmts(x.Stmts, x.Last, x.Right) + p.nestedStmts(wp.Stmts, wp.Last, wp.Right) p.wantSpace = spaceNotRequired - p.semiRsrv("}", x.Right) + p.semiRsrv("}", wp.Right) // Special case: `# inline comment` - case x.Backquotes && len(x.Stmts) == 0 && - len(x.Last) == 1 && x.Right.Line() == p.line: + case wp.Backquotes && len(wp.Stmts) == 0 && + len(wp.Last) == 1 && wp.Right.Line() == p.line: p.WriteString("`#") - p.WriteString(x.Last[0].Text) + p.WriteString(wp.Last[0].Text) p.WriteString("`") default: p.WriteString("$(") - if len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) { + if len(wp.Stmts) > 0 && startsWithLparen(wp.Stmts[0]) { p.wantSpace = spaceRequired } else { p.wantSpace = spaceNotRequired } - p.nestedStmts(x.Stmts, x.Last, x.Right) - p.rightParen(x.Right) + p.nestedStmts(wp.Stmts, wp.Last, wp.Right) + p.rightParen(wp.Right) } case *ParamExp: litCont := ";" if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" { litCont = nextLit.Value[:1] } - name := x.Param.Value + name := wp.Param.Value switch { case !p.minify: - case x.Excl, x.Length, x.Width: - case x.Index != nil, x.Slice != nil: - case x.Repl != nil, x.Exp != nil: + case wp.Excl, wp.Length, wp.Width: + case wp.Index != nil, wp.Slice != nil: + case wp.Repl != nil, wp.Exp != nil: case len(name) > 1 && !ValidName(name): // ${10} case ValidName(name + litCont): // ${var}cont default: - x2 := *x + x2 := *wp x2.Short = true p.paramExp(&x2) return } - p.paramExp(x) + p.paramExp(wp) case *ArithmExp: p.WriteString("$((") - if x.Unsigned { + if wp.Unsigned { p.WriteString("# ") } - p.arithmExpr(x.X, false, false) + p.arithmExpr(wp.X, false, false) p.WriteString("))") case *ExtGlob: - p.WriteString(x.Op.String()) - p.writeLit(x.Pattern.Value) + p.WriteString(wp.Op.String()) + p.writeLit(wp.Pattern.Value) p.WriteByte(')') case *ProcSubst: // avoid conflict with << and others if p.wantSpace == spaceRequired { p.space() } - p.WriteString(x.Op.String()) - p.nestedStmts(x.Stmts, x.Last, x.Rparen) - p.rightParen(x.Rparen) + p.WriteString(wp.Op.String()) + p.nestedStmts(wp.Stmts, wp.Last, wp.Rparen) + p.rightParen(wp.Rparen) } } @@ -780,23 +806,23 @@ func (p *Printer) paramExp(pe *ParamExp) { } func (p *Printer) loop(loop Loop) { - switch x := loop.(type) { + switch loop := loop.(type) { case *WordIter: - p.writeLit(x.Name.Value) - if x.InPos.IsValid() { + p.writeLit(loop.Name.Value) + if loop.InPos.IsValid() { p.spacedString(" in", Pos{}) - p.wordJoin(x.Items) + p.wordJoin(loop.Items) } case *CStyleLoop: p.WriteString("((") - if x.Init == nil { + if loop.Init == nil { p.space() } - p.arithmExpr(x.Init, false, false) + p.arithmExpr(loop.Init, false, false) p.WriteString("; ") - p.arithmExpr(x.Cond, false, false) + p.arithmExpr(loop.Cond, false, false) p.WriteString("; ") - p.arithmExpr(x.Post, false, false) + p.arithmExpr(loop.Post, false, false) p.WriteString("))") } } @@ -805,40 +831,40 @@ func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) { if p.minify { compact = true } - switch x := expr.(type) { + switch expr := expr.(type) { case *Word: - p.word(x) + p.word(expr) case *BinaryArithm: if compact { - p.arithmExpr(x.X, compact, spacePlusMinus) - p.WriteString(x.Op.String()) - p.arithmExpr(x.Y, compact, false) + p.arithmExpr(expr.X, compact, spacePlusMinus) + p.WriteString(expr.Op.String()) + p.arithmExpr(expr.Y, compact, false) } else { - p.arithmExpr(x.X, compact, spacePlusMinus) - if x.Op != Comma { + p.arithmExpr(expr.X, compact, spacePlusMinus) + if expr.Op != Comma { p.space() } - p.WriteString(x.Op.String()) + p.WriteString(expr.Op.String()) p.space() - p.arithmExpr(x.Y, compact, false) + p.arithmExpr(expr.Y, compact, false) } case *UnaryArithm: - if x.Post { - p.arithmExpr(x.X, compact, spacePlusMinus) - p.WriteString(x.Op.String()) + if expr.Post { + p.arithmExpr(expr.X, compact, spacePlusMinus) + p.WriteString(expr.Op.String()) } else { if spacePlusMinus { - switch x.Op { + switch expr.Op { case Plus, Minus: p.space() } } - p.WriteString(x.Op.String()) - p.arithmExpr(x.X, compact, false) + p.WriteString(expr.Op.String()) + p.arithmExpr(expr.X, compact, false) } case *ParenArithm: p.WriteByte('(') - p.arithmExpr(x.X, false, false) + p.arithmExpr(expr.X, false, false) p.WriteByte(')') } } @@ -855,34 +881,34 @@ func (p *Printer) testExpr(expr TestExpr) { } func (p *Printer) testExprSameLine(expr TestExpr) { - p.line = expr.Pos().Line() - switch x := expr.(type) { + p.advanceLine(expr.Pos().Line()) + switch expr := expr.(type) { case *Word: - p.word(x) + p.word(expr) case *BinaryTest: - p.testExprSameLine(x.X) + p.testExprSameLine(expr.X) p.space() - p.WriteString(x.Op.String()) - switch x.Op { + p.WriteString(expr.Op.String()) + switch expr.Op { case AndTest, OrTest: p.wantSpace = spaceRequired - p.testExpr(x.Y) + p.testExpr(expr.Y) default: p.space() - p.testExprSameLine(x.Y) + p.testExprSameLine(expr.Y) } case *UnaryTest: - p.WriteString(x.Op.String()) + p.WriteString(expr.Op.String()) p.space() - p.testExprSameLine(x.X) + p.testExprSameLine(expr.X) case *ParenTest: p.WriteByte('(') - if startsWithLparen(x.X) { + if startsWithLparen(expr.X) { p.wantSpace = spaceRequired } else { p.wantSpace = spaceNotRequired } - p.testExpr(x.X) + p.testExpr(expr.X) p.WriteByte(')') } } @@ -894,16 +920,16 @@ func (p *Printer) word(w *Word) { func (p *Printer) unquotedWord(w *Word) { for _, wp := range w.Parts { - switch x := wp.(type) { + switch wp := wp.(type) { case *SglQuoted: - p.writeLit(x.Value) + p.writeLit(wp.Value) case *DblQuoted: - p.wordParts(x.Parts, true) + p.wordParts(wp.Parts, true) case *Lit: - for i := 0; i < len(x.Value); i++ { - if b := x.Value[i]; b == '\\' { - if i++; i < len(x.Value) { - p.WriteByte(x.Value[i]) + for i := 0; i < len(wp.Value); i++ { + if b := wp.Value[i]; b == '\\' { + if i++; i < len(wp.Value) { + p.WriteByte(wp.Value[i]) } } else { p.WriteByte(b) @@ -937,7 +963,7 @@ func (p *Printer) casePatternJoin(pats []*Word) { if i > 0 { p.spacedToken("|", Pos{}) } - if p.wantsNewline(w.Pos()) { + if p.wantsNewline(w.Pos(), true) { if !anyNewline { p.incLevel() anyNewline = true @@ -997,7 +1023,7 @@ func (p *Printer) stmt(s *Stmt) { } p.incLevel() for _, r := range s.Redirs[startRedirs:] { - if p.wantsNewline(r.OpPos) { + if p.wantsNewline(r.OpPos, true) { p.bslashNewl() } if p.wantSpace == spaceRequired { @@ -1037,85 +1063,104 @@ func (p *Printer) stmt(s *Stmt) { p.decLevel() } +func (p *Printer) printRedirsUntil(redirs []*Redirect, startRedirs int, pos Pos) int { + for _, r := range redirs[startRedirs:] { + if r.Pos().After(pos) || r.Op == Hdoc || r.Op == DashHdoc { + break + } + if p.wantSpace == spaceRequired { + p.spacePad(r.Pos()) + } + if r.N != nil { + p.writeLit(r.N.Value) + } + p.WriteString(r.Op.String()) + if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { + p.space() + } else { + p.wantSpace = spaceRequired + } + p.word(r.Word) + startRedirs++ + } + return startRedirs +} + func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { - p.line = cmd.Pos().Line() + p.advanceLine(cmd.Pos().Line()) p.spacePad(cmd.Pos()) - switch x := cmd.(type) { + switch cmd := cmd.(type) { case *CallExpr: - p.assigns(x.Assigns) - if len(x.Args) <= 1 { - p.wordJoin(x.Args) - return 0 - } - p.wordJoin(x.Args[:1]) - for _, r := range redirs { - if r.Pos().After(x.Args[1].Pos()) || r.Op == Hdoc || r.Op == DashHdoc { - break - } - if p.wantSpace == spaceRequired { - p.spacePad(r.Pos()) - } - if r.N != nil { - p.writeLit(r.N.Value) - } - p.WriteString(r.Op.String()) - if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { - p.space() - } else { - p.wantSpace = spaceRequired - } - p.word(r.Word) - startRedirs++ + p.assigns(cmd.Assigns) + if len(cmd.Args) > 0 { + startRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[0].Pos()) } - p.wordJoin(x.Args[1:]) + if len(cmd.Args) <= 1 { + p.wordJoin(cmd.Args) + return startRedirs + } + p.wordJoin(cmd.Args[:1]) + startRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[1].Pos()) + p.wordJoin(cmd.Args[1:]) case *Block: p.WriteByte('{') p.wantSpace = spaceRequired // Forbid "foo()\n{ bar; }" p.wantNewline = p.wantNewline || p.funcNextLine - p.nestedStmts(x.Stmts, x.Last, x.Rbrace) - p.semiRsrv("}", x.Rbrace) + p.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rbrace) + p.semiRsrv("}", cmd.Rbrace) case *IfClause: - p.ifClause(x, false) + p.ifClause(cmd, false) case *Subshell: p.WriteByte('(') - if len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) { + stmts := cmd.Stmts + if len(stmts) > 0 && startsWithLparen(stmts[0]) { p.wantSpace = spaceRequired + // Add a space between nested parentheses if we're printing them in a single line, + // to avoid the ambiguity between `((` and `( (`. + if (cmd.Lparen.Line() != stmts[0].Pos().Line() || len(stmts) > 1) && !p.singleLine { + p.wantSpace = spaceNotRequired + + if p.minify { + p.mustNewline = true + } + } } else { p.wantSpace = spaceNotRequired } - p.spacePad(stmtsPos(x.Stmts, x.Last)) - p.nestedStmts(x.Stmts, x.Last, x.Rparen) + + p.spacePad(stmtsPos(cmd.Stmts, cmd.Last)) + p.nestedStmts(cmd.Stmts, cmd.Last, cmd.Rparen) p.wantSpace = spaceNotRequired - p.spacePad(x.Rparen) - p.rightParen(x.Rparen) + p.spacePad(cmd.Rparen) + p.rightParen(cmd.Rparen) case *WhileClause: - if x.Until { - p.spacedString("until", x.Pos()) + if cmd.Until { + p.spacedString("until", cmd.Pos()) } else { - p.spacedString("while", x.Pos()) + p.spacedString("while", cmd.Pos()) } - p.nestedStmts(x.Cond, x.CondLast, Pos{}) - p.semiOrNewl("do", x.DoPos) - p.nestedStmts(x.Do, x.DoLast, x.DonePos) - p.semiRsrv("done", x.DonePos) + p.nestedStmts(cmd.Cond, cmd.CondLast, Pos{}) + p.semiOrNewl("do", cmd.DoPos) + p.nestedStmts(cmd.Do, cmd.DoLast, cmd.DonePos) + p.semiRsrv("done", cmd.DonePos) case *ForClause: - if x.Select { + if cmd.Select { p.WriteString("select ") } else { p.WriteString("for ") } - p.loop(x.Loop) - p.semiOrNewl("do", x.DoPos) - p.nestedStmts(x.Do, x.DoLast, x.DonePos) - p.semiRsrv("done", x.DonePos) + p.loop(cmd.Loop) + p.semiOrNewl("do", cmd.DoPos) + p.nestedStmts(cmd.Do, cmd.DoLast, cmd.DonePos) + p.semiRsrv("done", cmd.DonePos) case *BinaryCmd: - p.stmt(x.X) - if p.minify || p.singleLine || x.Y.Pos().Line() <= p.line { + p.stmt(cmd.X) + if p.minify || p.singleLine || cmd.Y.Pos().Line() <= p.line { // leave p.nestedBinary untouched - p.spacedToken(x.Op.String(), x.OpPos) - p.line = x.Y.Pos().Line() - p.stmt(x.Y) + p.spacedToken(cmd.Op.String(), cmd.OpPos) + p.advanceLine(cmd.Y.Pos().Line()) + p.stmt(cmd.Y) break } indent := !p.nestedBinary @@ -1126,59 +1171,60 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { if len(p.pendingHdocs) == 0 { p.bslashNewl() } - p.spacedToken(x.Op.String(), x.OpPos) - if len(x.Y.Comments) > 0 { + p.spacedToken(cmd.Op.String(), cmd.OpPos) + if len(cmd.Y.Comments) > 0 { p.wantSpace = spaceNotRequired - p.newline(x.Y.Pos()) + p.newline(cmd.Y.Pos()) p.indent() - p.comments(x.Y.Comments...) + p.comments(cmd.Y.Comments...) p.newline(Pos{}) p.indent() } } else { - p.spacedToken(x.Op.String(), x.OpPos) - p.line = x.OpPos.Line() - p.comments(x.Y.Comments...) + p.spacedToken(cmd.Op.String(), cmd.OpPos) + p.advanceLine(cmd.OpPos.Line()) + p.comments(cmd.Y.Comments...) p.newline(Pos{}) p.indent() } - p.line = x.Y.Pos().Line() - _, p.nestedBinary = x.Y.Cmd.(*BinaryCmd) - p.stmt(x.Y) + p.advanceLine(cmd.Y.Pos().Line()) + _, p.nestedBinary = cmd.Y.Cmd.(*BinaryCmd) + p.stmt(cmd.Y) if indent { p.decLevel() } p.nestedBinary = false case *FuncDecl: - if x.RsrvWord { + if cmd.RsrvWord { p.WriteString("function ") } - p.writeLit(x.Name.Value) - if !x.RsrvWord || x.Parens { + p.writeLit(cmd.Name.Value) + if !cmd.RsrvWord || cmd.Parens { p.WriteString("()") } if p.funcNextLine { p.newline(Pos{}) p.indent() - } else if !x.Parens || !p.minify { + } else if !cmd.Parens || !p.minify { p.space() } - p.line = x.Body.Pos().Line() - p.comments(x.Body.Comments...) - p.stmt(x.Body) + p.advanceLine(cmd.Body.Pos().Line()) + p.comments(cmd.Body.Comments...) + p.stmt(cmd.Body) case *CaseClause: p.WriteString("case ") - p.word(x.Word) + p.word(cmd.Word) p.WriteString(" in") + p.advanceLine(cmd.In.Line()) p.wantSpace = spaceRequired if p.swtCaseIndent { p.incLevel() } - if len(x.Items) == 0 { + if len(cmd.Items) == 0 { // Apparently "case x in; esac" is invalid shell. p.mustNewline = true } - for i, ci := range x.Items { + for i, ci := range cmd.Items { var last []Comment for i, c := range ci.Comments { if c.Pos().After(ci.Pos()) { @@ -1203,12 +1249,13 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { (bodyEnd.IsValid() && ci.OpPos.Line() > bodyEnd.Line()) p.nestedStmts(ci.Stmts, ci.Last, ci.OpPos) p.level++ - if !p.minify || i != len(x.Items)-1 { + if !p.minify || i != len(cmd.Items)-1 { if sep { p.newlines(ci.OpPos) p.wantNewline = true } p.spacedToken(ci.Op.String(), ci.OpPos) + p.advanceLine(ci.OpPos.Line()) // avoid ; directly after tokens like ;; p.wroteSemi = true } @@ -1216,58 +1263,58 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.flushComments() p.level-- } - p.comments(x.Last...) + p.comments(cmd.Last...) if p.swtCaseIndent { p.flushComments() p.decLevel() } - p.semiRsrv("esac", x.Esac) + p.semiRsrv("esac", cmd.Esac) case *ArithmCmd: p.WriteString("((") - if x.Unsigned { + if cmd.Unsigned { p.WriteString("# ") } - p.arithmExpr(x.X, false, false) + p.arithmExpr(cmd.X, false, false) p.WriteString("))") case *TestClause: p.WriteString("[[ ") p.incLevel() - p.testExpr(x.X) + p.testExpr(cmd.X) p.decLevel() - p.spacedString("]]", x.Right) + p.spacedString("]]", cmd.Right) case *DeclClause: - p.spacedString(x.Variant.Value, x.Pos()) - p.assigns(x.Args) + p.spacedString(cmd.Variant.Value, cmd.Pos()) + p.assigns(cmd.Args) case *TimeClause: - p.spacedString("time", x.Pos()) - if x.PosixFormat { - p.spacedString("-p", x.Pos()) + p.spacedString("time", cmd.Pos()) + if cmd.PosixFormat { + p.spacedString("-p", cmd.Pos()) } - if x.Stmt != nil { - p.stmt(x.Stmt) + if cmd.Stmt != nil { + p.stmt(cmd.Stmt) } case *CoprocClause: - p.spacedString("coproc", x.Pos()) - if x.Name != nil { + p.spacedString("coproc", cmd.Pos()) + if cmd.Name != nil { p.space() - p.word(x.Name) + p.word(cmd.Name) } p.space() - p.stmt(x.Stmt) + p.stmt(cmd.Stmt) case *LetClause: - p.spacedString("let", x.Pos()) - for _, n := range x.Exprs { + p.spacedString("let", cmd.Pos()) + for _, n := range cmd.Exprs { p.space() p.arithmExpr(n, true, false) } case *TestDecl: - p.spacedString("@test", x.Pos()) + p.spacedString("@test", cmd.Pos()) p.space() - p.word(x.Description) + p.word(cmd.Description) p.space() - p.stmt(x.Body) + p.stmt(cmd.Body) default: - panic(fmt.Sprintf("syntax.Printer: unexpected node type %T", x)) + panic(fmt.Sprintf("syntax.Printer: unexpected node type %T", cmd)) } return startRedirs } @@ -1338,10 +1385,10 @@ func (p *Printer) stmtList(stmts []*Stmt, last []Comment) { // statement. p.comments(c) } - if !p.minify || p.wantSpace == spaceRequired { + if p.mustNewline || !p.minify || p.wantSpace == spaceRequired { p.newlines(pos) } - p.line = pos.Line() + p.advanceLine(pos.Line()) p.comments(midComs...) p.stmt(s) p.comments(endComs...) @@ -1382,7 +1429,7 @@ func (p *Printer) nestedStmts(stmts []*Stmt, last []Comment, closing Pos) { func (p *Printer) assigns(assigns []*Assign) { p.incLevel() for _, a := range assigns { - if p.wantsNewline(a.Pos()) { + if p.wantsNewline(a.Pos(), true) { p.bslashNewl() } else { p.spacePad(a.Pos()) @@ -1401,11 +1448,8 @@ func (p *Printer) assigns(assigns []*Assign) { // Ensure we don't use an escaped newline after '=', // because that can result in indentation, thus // splitting "foo=bar" into "foo= bar". - p.line = a.Value.Pos().Line() - // Similar to the above, we want to print the word as if it were - // quoted, as otherwise escaped newlines could split + p.advanceLine(a.Value.Pos().Line()) p.word(a.Value) - // p.wordParts(a.Value.Parts, true) } else if a.Array != nil { p.wantSpace = spaceNotRequired p.WriteByte('(') @@ -1468,7 +1512,7 @@ func (e *extraIndenter) WriteByte(b byte) error { lineIndent += e.firstChange } e.bufWriter.WriteByte(tabwriter.Escape) - for i := 0; i < lineIndent; i++ { + for range lineIndent { e.bufWriter.WriteByte('\t') } e.bufWriter.WriteByte(tabwriter.Escape) @@ -1478,7 +1522,7 @@ func (e *extraIndenter) WriteByte(b byte) error { } func (e *extraIndenter) WriteString(s string) (int, error) { - for i := 0; i < len(s); i++ { + for i := range len(s) { e.WriteByte(s[i]) } return len(s), nil diff --git a/vendor/mvdan.cc/sh/v3/syntax/simplify.go b/vendor/mvdan.cc/sh/v3/syntax/simplify.go index 6f245918e9..7eef65ef27 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/simplify.go +++ b/vendor/mvdan.cc/sh/v3/syntax/simplify.go @@ -3,19 +3,19 @@ package syntax -import "bytes" +import "strings" // Simplify modifies a node to remove redundant pieces of syntax, and returns // whether any changes were made. // // The changes currently applied are: // -// Remove clearly useless parentheses $(( (expr) )) -// Remove dollars from vars in exprs (($var)) -// Remove duplicate subshells $( (stmts) ) -// Remove redundant quotes [[ "$var" == str ]] -// Merge negations with unary operators [[ ! -n $var ]] -// Use single quotes to shorten literals "\$foo" +// Remove clearly useless parentheses $(( (expr) )) +// Remove dollars from vars in exprs (($var)) +// Remove duplicate subshells $( (stmts) ) +// Remove redundant quotes [[ "$var" == str ]] +// Merge negations with unary operators [[ ! -n $var ]] +// Use single quotes to shorten literals "\$foo" func Simplify(n Node) bool { s := simplifier{} Walk(n, s.visit) @@ -27,63 +27,63 @@ type simplifier struct { } func (s *simplifier) visit(node Node) bool { - switch x := node.(type) { + switch node := node.(type) { case *Assign: - x.Index = s.removeParensArithm(x.Index) + node.Index = s.removeParensArithm(node.Index) // Don't inline params, as x[i] and x[$i] mean // different things when x is an associative // array; the first means "i", the second "$i". case *ParamExp: - x.Index = s.removeParensArithm(x.Index) + node.Index = s.removeParensArithm(node.Index) // don't inline params - same as above. - if x.Slice == nil { + if node.Slice == nil { break } - x.Slice.Offset = s.removeParensArithm(x.Slice.Offset) - x.Slice.Offset = s.inlineSimpleParams(x.Slice.Offset) - x.Slice.Length = s.removeParensArithm(x.Slice.Length) - x.Slice.Length = s.inlineSimpleParams(x.Slice.Length) + node.Slice.Offset = s.removeParensArithm(node.Slice.Offset) + node.Slice.Offset = s.inlineSimpleParams(node.Slice.Offset) + node.Slice.Length = s.removeParensArithm(node.Slice.Length) + node.Slice.Length = s.inlineSimpleParams(node.Slice.Length) case *ArithmExp: - x.X = s.removeParensArithm(x.X) - x.X = s.inlineSimpleParams(x.X) + node.X = s.removeParensArithm(node.X) + node.X = s.inlineSimpleParams(node.X) case *ArithmCmd: - x.X = s.removeParensArithm(x.X) - x.X = s.inlineSimpleParams(x.X) + node.X = s.removeParensArithm(node.X) + node.X = s.inlineSimpleParams(node.X) case *ParenArithm: - x.X = s.removeParensArithm(x.X) - x.X = s.inlineSimpleParams(x.X) + node.X = s.removeParensArithm(node.X) + node.X = s.inlineSimpleParams(node.X) case *BinaryArithm: - x.X = s.inlineSimpleParams(x.X) - x.Y = s.inlineSimpleParams(x.Y) + node.X = s.inlineSimpleParams(node.X) + node.Y = s.inlineSimpleParams(node.Y) case *CmdSubst: - x.Stmts = s.inlineSubshell(x.Stmts) + node.Stmts = s.inlineSubshell(node.Stmts) case *Subshell: - x.Stmts = s.inlineSubshell(x.Stmts) + node.Stmts = s.inlineSubshell(node.Stmts) case *Word: - x.Parts = s.simplifyWord(x.Parts) + node.Parts = s.simplifyWord(node.Parts) case *TestClause: - x.X = s.removeParensTest(x.X) - x.X = s.removeNegateTest(x.X) + node.X = s.removeParensTest(node.X) + node.X = s.removeNegateTest(node.X) case *ParenTest: - x.X = s.removeParensTest(x.X) - x.X = s.removeNegateTest(x.X) + node.X = s.removeParensTest(node.X) + node.X = s.removeNegateTest(node.X) case *BinaryTest: - x.X = s.unquoteParams(x.X) - x.X = s.removeNegateTest(x.X) - if x.Op == TsMatchShort { + node.X = s.unquoteParams(node.X) + node.X = s.removeNegateTest(node.X) + if node.Op == TsMatchShort { s.modified = true - x.Op = TsMatch + node.Op = TsMatch } - switch x.Op { + switch node.Op { case TsMatch, TsNoMatch: // unquoting enables globbing default: - x.Y = s.unquoteParams(x.Y) + node.Y = s.unquoteParams(node.Y) } - x.Y = s.removeNegateTest(x.Y) + node.Y = s.removeNegateTest(node.Y) case *UnaryTest: - x.X = s.unquoteParams(x.X) + node.X = s.unquoteParams(node.X) } return true } @@ -99,7 +99,7 @@ parts: if lit == nil { break } - var buf bytes.Buffer + var sb strings.Builder escaped := false for _, r := range lit.Value { switch r { @@ -118,9 +118,9 @@ parts: } escaped = false } - buf.WriteRune(r) + sb.WriteRune(r) } - newVal := buf.String() + newVal := sb.String() if newVal == lit.Value { break } diff --git a/vendor/mvdan.cc/sh/v3/syntax/tokens.go b/vendor/mvdan.cc/sh/v3/syntax/tokens.go index 6a64b21378..97dec54338 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/tokens.go +++ b/vendor/mvdan.cc/sh/v3/syntax/tokens.go @@ -312,6 +312,7 @@ const ( TsVarSet // -v TsRefVar // -R TsNot = UnTestOperator(exclMark) // ! + TsParen = UnTestOperator(leftParen) // ( ) type BinTestOperator token diff --git a/vendor/mvdan.cc/sh/v3/syntax/walk.go b/vendor/mvdan.cc/sh/v3/syntax/walk.go index be3f20909d..105f1ce0df 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/walk.go +++ b/vendor/mvdan.cc/sh/v3/syntax/walk.go @@ -9,21 +9,6 @@ import ( "reflect" ) -func walkStmts(stmts []*Stmt, last []Comment, f func(Node) bool) { - for _, s := range stmts { - Walk(s, f) - } - for _, c := range last { - Walk(&c, f) - } -} - -func walkWords(words []*Word, f func(Node) bool) { - for _, w := range words { - Walk(w, f) - } -} - // Walk traverses a syntax tree in depth-first order: It starts by calling // f(node); node must not be nil. If f returns true, Walk invokes f // recursively for each of the non-nil children of node, followed by @@ -33,208 +18,191 @@ func Walk(node Node, f func(Node) bool) { return } - switch x := node.(type) { + switch node := node.(type) { case *File: - walkStmts(x.Stmts, x.Last, f) + walkList(node.Stmts, f) + walkComments(node.Last, f) case *Comment: case *Stmt: - for _, c := range x.Comments { - if !x.End().After(c.Pos()) { + for _, c := range node.Comments { + if !node.End().After(c.Pos()) { defer Walk(&c, f) break } Walk(&c, f) } - if x.Cmd != nil { - Walk(x.Cmd, f) - } - for _, r := range x.Redirs { - Walk(r, f) + if node.Cmd != nil { + Walk(node.Cmd, f) } + walkList(node.Redirs, f) case *Assign: - if x.Name != nil { - Walk(x.Name, f) - } - if x.Value != nil { - Walk(x.Value, f) - } - if x.Index != nil { - Walk(x.Index, f) - } - if x.Array != nil { - Walk(x.Array, f) - } + walkNilable(node.Name, f) + walkNilable(node.Value, f) + walkNilable(node.Index, f) + walkNilable(node.Array, f) case *Redirect: - if x.N != nil { - Walk(x.N, f) - } - Walk(x.Word, f) - if x.Hdoc != nil { - Walk(x.Hdoc, f) - } + walkNilable(node.N, f) + Walk(node.Word, f) + walkNilable(node.Hdoc, f) case *CallExpr: - for _, a := range x.Assigns { - Walk(a, f) - } - walkWords(x.Args, f) + walkList(node.Assigns, f) + walkList(node.Args, f) case *Subshell: - walkStmts(x.Stmts, x.Last, f) + walkList(node.Stmts, f) + walkComments(node.Last, f) case *Block: - walkStmts(x.Stmts, x.Last, f) + walkList(node.Stmts, f) + walkComments(node.Last, f) case *IfClause: - walkStmts(x.Cond, x.CondLast, f) - walkStmts(x.Then, x.ThenLast, f) - if x.Else != nil { - Walk(x.Else, f) - } + walkList(node.Cond, f) + walkComments(node.CondLast, f) + walkList(node.Then, f) + walkComments(node.ThenLast, f) + walkNilable(node.Else, f) case *WhileClause: - walkStmts(x.Cond, x.CondLast, f) - walkStmts(x.Do, x.DoLast, f) + walkList(node.Cond, f) + walkComments(node.CondLast, f) + walkList(node.Do, f) + walkComments(node.DoLast, f) case *ForClause: - Walk(x.Loop, f) - walkStmts(x.Do, x.DoLast, f) + Walk(node.Loop, f) + walkList(node.Do, f) + walkComments(node.DoLast, f) case *WordIter: - Walk(x.Name, f) - walkWords(x.Items, f) + Walk(node.Name, f) + walkList(node.Items, f) case *CStyleLoop: - if x.Init != nil { - Walk(x.Init, f) - } - if x.Cond != nil { - Walk(x.Cond, f) - } - if x.Post != nil { - Walk(x.Post, f) - } + walkNilable(node.Init, f) + walkNilable(node.Cond, f) + walkNilable(node.Post, f) case *BinaryCmd: - Walk(x.X, f) - Walk(x.Y, f) + Walk(node.X, f) + Walk(node.Y, f) case *FuncDecl: - Walk(x.Name, f) - Walk(x.Body, f) + Walk(node.Name, f) + Walk(node.Body, f) case *Word: - for _, wp := range x.Parts { - Walk(wp, f) - } + walkList(node.Parts, f) case *Lit: case *SglQuoted: case *DblQuoted: - for _, wp := range x.Parts { - Walk(wp, f) - } + walkList(node.Parts, f) case *CmdSubst: - walkStmts(x.Stmts, x.Last, f) + walkList(node.Stmts, f) + walkComments(node.Last, f) case *ParamExp: - Walk(x.Param, f) - if x.Index != nil { - Walk(x.Index, f) - } - if x.Repl != nil { - if x.Repl.Orig != nil { - Walk(x.Repl.Orig, f) - } - if x.Repl.With != nil { - Walk(x.Repl.With, f) - } + Walk(node.Param, f) + walkNilable(node.Index, f) + if node.Repl != nil { + walkNilable(node.Repl.Orig, f) + walkNilable(node.Repl.With, f) } - if x.Exp != nil && x.Exp.Word != nil { - Walk(x.Exp.Word, f) + if node.Exp != nil { + walkNilable(node.Exp.Word, f) } case *ArithmExp: - Walk(x.X, f) + Walk(node.X, f) case *ArithmCmd: - Walk(x.X, f) + Walk(node.X, f) case *BinaryArithm: - Walk(x.X, f) - Walk(x.Y, f) + Walk(node.X, f) + Walk(node.Y, f) case *BinaryTest: - Walk(x.X, f) - Walk(x.Y, f) + Walk(node.X, f) + Walk(node.Y, f) case *UnaryArithm: - Walk(x.X, f) + Walk(node.X, f) case *UnaryTest: - Walk(x.X, f) + Walk(node.X, f) case *ParenArithm: - Walk(x.X, f) + Walk(node.X, f) case *ParenTest: - Walk(x.X, f) + Walk(node.X, f) case *CaseClause: - Walk(x.Word, f) - for _, ci := range x.Items { - Walk(ci, f) - } - for _, c := range x.Last { - Walk(&c, f) - } + Walk(node.Word, f) + walkList(node.Items, f) + walkComments(node.Last, f) case *CaseItem: - for _, c := range x.Comments { - if c.Pos().After(x.Pos()) { + for _, c := range node.Comments { + if c.Pos().After(node.Pos()) { defer Walk(&c, f) break } Walk(&c, f) } - walkWords(x.Patterns, f) - walkStmts(x.Stmts, x.Last, f) + walkList(node.Patterns, f) + walkList(node.Stmts, f) + walkComments(node.Last, f) case *TestClause: - Walk(x.X, f) + Walk(node.X, f) case *DeclClause: - for _, a := range x.Args { - Walk(a, f) - } + walkList(node.Args, f) case *ArrayExpr: - for _, el := range x.Elems { - Walk(el, f) - } - for _, c := range x.Last { - Walk(&c, f) - } + walkList(node.Elems, f) + walkComments(node.Last, f) case *ArrayElem: - for _, c := range x.Comments { - if c.Pos().After(x.Pos()) { + for _, c := range node.Comments { + if c.Pos().After(node.Pos()) { defer Walk(&c, f) break } Walk(&c, f) } - if x.Index != nil { - Walk(x.Index, f) - } - if x.Value != nil { - Walk(x.Value, f) - } + walkNilable(node.Index, f) + walkNilable(node.Value, f) case *ExtGlob: - Walk(x.Pattern, f) + Walk(node.Pattern, f) case *ProcSubst: - walkStmts(x.Stmts, x.Last, f) + walkList(node.Stmts, f) + walkComments(node.Last, f) case *TimeClause: - if x.Stmt != nil { - Walk(x.Stmt, f) - } + walkNilable(node.Stmt, f) case *CoprocClause: - if x.Name != nil { - Walk(x.Name, f) - } - Walk(x.Stmt, f) + walkNilable(node.Name, f) + Walk(node.Stmt, f) case *LetClause: - for _, expr := range x.Exprs { - Walk(expr, f) - } + walkList(node.Exprs, f) case *TestDecl: - Walk(x.Description, f) - Walk(x.Body, f) + Walk(node.Description, f) + Walk(node.Body, f) default: - panic(fmt.Sprintf("syntax.Walk: unexpected node type %T", x)) + panic(fmt.Sprintf("syntax.Walk: unexpected node type %T", node)) } f(nil) } +type nilableNode interface { + Node + comparable // pointer nodes, which can be compared to nil +} + +func walkNilable[N nilableNode](node N, f func(Node) bool) { + var zero N // nil + if node != zero { + Walk(node, f) + } +} + +func walkList[N Node](list []N, f func(Node) bool) { + for _, node := range list { + Walk(node, f) + } +} + +func walkComments(list []Comment, f func(Node) bool) { + // Note that []Comment does not satisfy the generic constraint []Node. + for i := range list { + Walk(&list[i], f) + } +} + // DebugPrint prints the provided syntax tree, spanning multiple lines and with // indentation. Can be useful to investigate the content of a syntax tree. func DebugPrint(w io.Writer, node Node) error { p := debugPrinter{out: w} p.print(reflect.ValueOf(node)) + p.printf("\n") return p.err } @@ -244,7 +212,7 @@ type debugPrinter struct { err error } -func (p *debugPrinter) printf(format string, args ...interface{}) { +func (p *debugPrinter) printf(format string, args ...any) { _, err := fmt.Fprintf(p.out, format, args...) if err != nil && p.err == nil { p.err = err @@ -253,7 +221,7 @@ func (p *debugPrinter) printf(format string, args ...interface{}) { func (p *debugPrinter) newline() { p.printf("\n") - for i := 0; i < p.level; i++ { + for range p.level { p.printf(". ") } } @@ -278,7 +246,7 @@ func (p *debugPrinter) print(x reflect.Value) { if x.Len() > 0 { p.level++ p.newline() - for i := 0; i < x.Len(); i++ { + for i := range x.Len() { p.printf("%d: ", i) p.print(x.Index(i)) if i == x.Len()-1 { @@ -291,6 +259,10 @@ func (p *debugPrinter) print(x reflect.Value) { case reflect.Struct: if v, ok := x.Interface().(Pos); ok { + if v.IsRecovered() { + p.printf("") + return + } p.printf("%v:%v", v.Line(), v.Col()) return } @@ -298,7 +270,7 @@ func (p *debugPrinter) print(x reflect.Value) { p.printf("%s {", t) p.level++ p.newline() - for i := 0; i < t.NumField(); i++ { + for i := range t.NumField() { p.printf("%s: ", t.Field(i).Name) p.print(x.Field(i)) if i == x.NumField()-1 { @@ -308,6 +280,10 @@ func (p *debugPrinter) print(x reflect.Value) { } p.printf("}") default: - p.printf("%#v", x.Interface()) + if s, ok := x.Interface().(fmt.Stringer); ok && !x.IsZero() { + p.printf("%#v (%s)", x.Interface(), s) + } else { + p.printf("%#v", x.Interface()) + } } }