diff --git a/go.mod b/go.mod index 428ba7f7a0..7c4a32cd21 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 @@ -66,7 +66,7 @@ require ( k8s.io/klog v1.0.0 k8s.io/klog/v2 v2.130.1 k8s.io/kubectl v0.35.0 - mvdan.cc/sh/v3 v3.5.1 + mvdan.cc/sh/v3 v3.13.1 sigs.k8s.io/yaml v1.6.0 ) @@ -199,8 +199,8 @@ require ( golang.org/x/mod v0.29.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/term v0.41.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect diff --git a/go.sum b/go.sum index 0705c5405e..a717fcc136 100644 --- a/go.sum +++ b/go.sum @@ -106,8 +106,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= @@ -162,8 +162,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= @@ -201,6 +199,8 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -697,15 +697,15 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -832,8 +832,8 @@ k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/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.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= +mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= 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/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s new file mode 100644 index 0000000000..e07fa75eb5 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s @@ -0,0 +1,12 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +#include "textflag.h" + +TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctlbyname(SB) +GLOBL ·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8 +DATA ·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB) diff --git a/vendor/golang.org/x/sys/cpu/cpu.go b/vendor/golang.org/x/sys/cpu/cpu.go index 34c9ae76ef..63541994ef 100644 --- a/vendor/golang.org/x/sys/cpu/cpu.go +++ b/vendor/golang.org/x/sys/cpu/cpu.go @@ -92,9 +92,6 @@ var ARM64 struct { HasSHA2 bool // SHA2 hardware implementation HasCRC32 bool // CRC32 hardware implementation HasATOMICS bool // Atomic memory operation instruction set - HasHPDS bool // Hierarchical permission disables in translations tables - HasLOR bool // Limited ordering regions - HasPAN bool // Privileged access never HasFPHP bool // Half precision floating-point instruction set HasASIMDHP bool // Advanced SIMD half precision instruction set HasCPUID bool // CPUID identification scheme registers diff --git a/vendor/golang.org/x/sys/cpu/cpu_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_arm64.go index f449c679fe..5fc09e2935 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_arm64.go @@ -44,14 +44,11 @@ func initOptions() { } func archInit() { - switch runtime.GOOS { - case "freebsd": + if runtime.GOOS == "freebsd" { readARM64Registers() - case "linux", "netbsd", "openbsd": + } else { + // Most platforms don't seem to allow directly reading these registers. doinit() - default: - // Many platforms don't seem to allow reading these registers. - setMinimalFeatures() } } @@ -65,10 +62,10 @@ func setMinimalFeatures() { func readARM64Registers() { Initialized = true - parseARM64SystemRegisters(getisar0(), getisar1(), getmmfr1(), getpfr0()) + parseARM64SystemRegisters(getisar0(), getisar1(), getpfr0()) } -func parseARM64SystemRegisters(isar0, isar1, mmfr1, pfr0 uint64) { +func parseARM64SystemRegisters(isar0, isar1, pfr0 uint64) { // ID_AA64ISAR0_EL1 switch extractBits(isar0, 4, 7) { case 1: @@ -152,22 +149,6 @@ func parseARM64SystemRegisters(isar0, isar1, mmfr1, pfr0 uint64) { ARM64.HasI8MM = true } - // ID_AA64MMFR1_EL1 - switch extractBits(mmfr1, 12, 15) { - case 1, 2: - ARM64.HasHPDS = true - } - - switch extractBits(mmfr1, 16, 19) { - case 1: - ARM64.HasLOR = true - } - - switch extractBits(mmfr1, 20, 23) { - case 1, 2, 3: - ARM64.HasPAN = true - } - // ID_AA64PFR0_EL1 switch extractBits(pfr0, 16, 19) { case 0: diff --git a/vendor/golang.org/x/sys/cpu/cpu_arm64.s b/vendor/golang.org/x/sys/cpu/cpu_arm64.s index a4f24b3b0c..3b0450a06a 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_arm64.s +++ b/vendor/golang.org/x/sys/cpu/cpu_arm64.s @@ -20,13 +20,6 @@ TEXT ·getisar1(SB),NOSPLIT,$0-8 MOVD R0, ret+0(FP) RET -// func getmmfr1() uint64 -TEXT ·getmmfr1(SB),NOSPLIT,$0-8 - // get Memory Model Feature Register 1 into x0 - MRS ID_AA64MMFR1_EL1, R0 - MOVD R0, ret+0(FP) - RET - // func getpfr0() uint64 TEXT ·getpfr0(SB),NOSPLIT,$0-8 // get Processor Feature Register 0 into x0 diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go new file mode 100644 index 0000000000..0b470744a0 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go @@ -0,0 +1,67 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +package cpu + +func doinit() { + setMinimalFeatures() + + // The feature flags are explained in [Instruction Set Detection]. + // There are some differences between MacOS versions: + // + // MacOS 11 and 12 do not have "hw.optional" sysctl values for some of the features. + // + // MacOS 13 changed some of the naming conventions to align with ARM Architecture Reference Manual. + // For example "hw.optional.armv8_2_sha512" became "hw.optional.arm.FEAT_SHA512". + // It currently checks both to stay compatible with MacOS 11 and 12. + // The old names also work with MacOS 13, however it's not clear whether + // they will continue working with future OS releases. + // + // Once MacOS 12 is no longer supported the old names can be removed. + // + // [Instruction Set Detection]: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics + + // Encryption, hashing and checksum capabilities + + // For the following flags there are no MacOS 11 sysctl flags. + ARM64.HasAES = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_AES\x00")) + ARM64.HasPMULL = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_PMULL\x00")) + ARM64.HasSHA1 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA1\x00")) + ARM64.HasSHA2 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA256\x00")) + + ARM64.HasSHA3 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha3\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA3\x00")) + ARM64.HasSHA512 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha512\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA512\x00")) + + ARM64.HasCRC32 = darwinSysctlEnabled([]byte("hw.optional.armv8_crc32\x00")) + + // Atomic and memory ordering + ARM64.HasATOMICS = darwinSysctlEnabled([]byte("hw.optional.armv8_1_atomics\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LSE\x00")) + ARM64.HasLRCPC = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LRCPC\x00")) + + // SIMD and floating point capabilities + ARM64.HasFPHP = darwinSysctlEnabled([]byte("hw.optional.neon_fp16\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FP16\x00")) + ARM64.HasASIMDHP = darwinSysctlEnabled([]byte("hw.optional.neon_hpfp\x00")) || darwinSysctlEnabled([]byte("hw.optional.AdvSIMD_HPFPCvt\x00")) + ARM64.HasASIMDRDM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_RDM\x00")) + ARM64.HasASIMDDP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DotProd\x00")) + ARM64.HasASIMDFHM = darwinSysctlEnabled([]byte("hw.optional.armv8_2_fhm\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FHM\x00")) + ARM64.HasI8MM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_I8MM\x00")) + + ARM64.HasJSCVT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_JSCVT\x00")) + ARM64.HasFCMA = darwinSysctlEnabled([]byte("hw.optional.armv8_3_compnum\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FCMA\x00")) + + // Miscellaneous + ARM64.HasDCPOP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DPB\x00")) + ARM64.HasEVTSTRM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_ECV\x00")) + ARM64.HasDIT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DIT\x00")) + + // Not supported, but added for completeness + ARM64.HasCPUID = false + + ARM64.HasSM3 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM3\x00")) + ARM64.HasSM4 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM4\x00")) + ARM64.HasSVE = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE\x00")) + ARM64.HasSVE2 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE2\x00")) +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go new file mode 100644 index 0000000000..4ee68e38d9 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && !gc + +package cpu + +func doinit() { + setMinimalFeatures() + + ARM64.HasASIMD = true + ARM64.HasFP = true + + // Go already assumes these to be available because they were on the M1 + // and these are supported on all Apple arm64 chips. + ARM64.HasAES = true + ARM64.HasPMULL = true + ARM64.HasSHA1 = true + ARM64.HasSHA2 = true + + if runtime.GOOS != "ios" { + // Apple A7 processors do not support these, however + // M-series SoCs are at least armv8.4-a + ARM64.HasCRC32 = true // armv8.1 + ARM64.HasATOMICS = true // armv8.2 + ARM64.HasJSCVT = true // armv8.3, if HasFP + } +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_gc_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_gc_arm64.go index e3fc5a8d31..6ac6e1efb2 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_gc_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_gc_arm64.go @@ -8,6 +8,5 @@ package cpu func getisar0() uint64 func getisar1() uint64 -func getmmfr1() uint64 func getpfr0() uint64 func getzfr0() uint64 diff --git a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go index 8df2079e15..05913081ec 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go @@ -8,5 +8,5 @@ package cpu func getisar0() uint64 { return 0 } func getisar1() uint64 { return 0 } -func getmmfr1() uint64 { return 0 } func getpfr0() uint64 { return 0 } +func getzfr0() uint64 { return 0 } diff --git a/vendor/golang.org/x/sys/cpu/cpu_netbsd_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_netbsd_arm64.go index 19aea0633e..ebfb3fc8e7 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_netbsd_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_netbsd_arm64.go @@ -167,7 +167,7 @@ func doinit() { setMinimalFeatures() return } - parseARM64SystemRegisters(cpuid.aa64isar0, cpuid.aa64isar1, cpuid.aa64mmfr1, cpuid.aa64pfr0) + parseARM64SystemRegisters(cpuid.aa64isar0, cpuid.aa64isar1, cpuid.aa64pfr0) Initialized = true } diff --git a/vendor/golang.org/x/sys/cpu/cpu_openbsd_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_openbsd_arm64.go index 87fd3a7780..85b64d5ccb 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_openbsd_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_openbsd_arm64.go @@ -59,7 +59,7 @@ func doinit() { if !ok { return } - parseARM64SystemRegisters(isar0, isar1, 0, 0) + parseARM64SystemRegisters(isar0, isar1, 0) Initialized = true } diff --git a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go index 5341e7f88d..6c7c5bfd53 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !linux && !netbsd && !openbsd && arm64 +//go:build !darwin && !linux && !netbsd && !openbsd && !windows && arm64 package cpu -func doinit() {} +func doinit() { + setMinimalFeatures() +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go new file mode 100644 index 0000000000..d09e85a361 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go @@ -0,0 +1,42 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cpu + +import ( + "golang.org/x/sys/windows" +) + +func doinit() { + // set HasASIMD and HasFP to true as per + // https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170#base-requirements + // + // The ARM64 version of Windows always presupposes that it's running on an ARMv8 or later architecture. + // Both floating-point and NEON support are presumed to be present in hardware. + // + ARM64.HasASIMD = true + ARM64.HasFP = true + + if windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) { + ARM64.HasAES = true + ARM64.HasPMULL = true + ARM64.HasSHA1 = true + ARM64.HasSHA2 = true + } + ARM64.HasSHA3 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE) + ARM64.HasCRC32 = windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE) + ARM64.HasSHA512 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE) + ARM64.HasATOMICS = windows.IsProcessorFeaturePresent(windows.PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE) + if windows.IsProcessorFeaturePresent(windows.PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) { + ARM64.HasASIMDDP = true + ARM64.HasASIMDRDM = true + } + if windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) { + ARM64.HasLRCPC = true + ARM64.HasSM3 = true + } + ARM64.HasSVE = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE_INSTRUCTIONS_AVAILABLE) + ARM64.HasSVE2 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE) + ARM64.HasJSCVT = windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE) +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_x86.go b/vendor/golang.org/x/sys/cpu/cpu_x86.go index 1e642f3304..f5723d4f7e 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_x86.go +++ b/vendor/golang.org/x/sys/cpu/cpu_x86.go @@ -64,6 +64,80 @@ func initOptions() { func archInit() { + // From internal/cpu + const ( + // eax bits + cpuid_AVXVNNI = 1 << 4 + + // ecx bits + cpuid_SSE3 = 1 << 0 + cpuid_PCLMULQDQ = 1 << 1 + cpuid_AVX512VBMI = 1 << 1 + cpuid_AVX512VBMI2 = 1 << 6 + cpuid_SSSE3 = 1 << 9 + cpuid_AVX512GFNI = 1 << 8 + cpuid_AVX512VAES = 1 << 9 + cpuid_AVX512VNNI = 1 << 11 + cpuid_AVX512BITALG = 1 << 12 + cpuid_FMA = 1 << 12 + cpuid_AVX512VPOPCNTDQ = 1 << 14 + cpuid_SSE41 = 1 << 19 + cpuid_SSE42 = 1 << 20 + cpuid_POPCNT = 1 << 23 + cpuid_AES = 1 << 25 + cpuid_OSXSAVE = 1 << 27 + cpuid_AVX = 1 << 28 + + // "Extended Feature Flag" bits returned in EBX for CPUID EAX=0x7 ECX=0x0 + cpuid_BMI1 = 1 << 3 + cpuid_AVX2 = 1 << 5 + cpuid_BMI2 = 1 << 8 + cpuid_ERMS = 1 << 9 + cpuid_AVX512F = 1 << 16 + cpuid_AVX512DQ = 1 << 17 + cpuid_ADX = 1 << 19 + cpuid_AVX512CD = 1 << 28 + cpuid_SHA = 1 << 29 + cpuid_AVX512BW = 1 << 30 + cpuid_AVX512VL = 1 << 31 + + // "Extended Feature Flag" bits returned in ECX for CPUID EAX=0x7 ECX=0x0 + cpuid_AVX512_VBMI = 1 << 1 + cpuid_AVX512_VBMI2 = 1 << 6 + cpuid_GFNI = 1 << 8 + cpuid_AVX512VPCLMULQDQ = 1 << 10 + cpuid_AVX512_BITALG = 1 << 12 + + // edx bits + cpuid_FSRM = 1 << 4 + // edx bits for CPUID 0x80000001 + cpuid_RDTSCP = 1 << 27 + ) + // Additional constants not in internal/cpu + const ( + // eax=1: edx + cpuid_SSE2 = 1 << 26 + // eax=1: ecx + cpuid_CX16 = 1 << 13 + cpuid_RDRAND = 1 << 30 + // eax=7,ecx=0: ebx + cpuid_RDSEED = 1 << 18 + cpuid_AVX512IFMA = 1 << 21 + cpuid_AVX512PF = 1 << 26 + cpuid_AVX512ER = 1 << 27 + // eax=7,ecx=0: edx + cpuid_AVX5124VNNIW = 1 << 2 + cpuid_AVX5124FMAPS = 1 << 3 + cpuid_AMXBF16 = 1 << 22 + cpuid_AMXTile = 1 << 24 + cpuid_AMXInt8 = 1 << 25 + // eax=7,ecx=1: eax + cpuid_AVX512BF16 = 1 << 5 + cpuid_AVXIFMA = 1 << 23 + // eax=7,ecx=1: edx + cpuid_AVXVNNIInt8 = 1 << 4 + ) + Initialized = true maxID, _, _, _ := cpuid(0, 0) @@ -73,90 +147,90 @@ func archInit() { } _, _, ecx1, edx1 := cpuid(1, 0) - X86.HasSSE2 = isSet(26, edx1) - - X86.HasSSE3 = isSet(0, ecx1) - X86.HasPCLMULQDQ = isSet(1, ecx1) - X86.HasSSSE3 = isSet(9, ecx1) - X86.HasFMA = isSet(12, ecx1) - X86.HasCX16 = isSet(13, ecx1) - X86.HasSSE41 = isSet(19, ecx1) - X86.HasSSE42 = isSet(20, ecx1) - X86.HasPOPCNT = isSet(23, ecx1) - X86.HasAES = isSet(25, ecx1) - X86.HasOSXSAVE = isSet(27, ecx1) - X86.HasRDRAND = isSet(30, ecx1) + X86.HasSSE2 = isSet(edx1, cpuid_SSE2) + + X86.HasSSE3 = isSet(ecx1, cpuid_SSE3) + X86.HasPCLMULQDQ = isSet(ecx1, cpuid_PCLMULQDQ) + X86.HasSSSE3 = isSet(ecx1, cpuid_SSSE3) + X86.HasFMA = isSet(ecx1, cpuid_FMA) + X86.HasCX16 = isSet(ecx1, cpuid_CX16) + X86.HasSSE41 = isSet(ecx1, cpuid_SSE41) + X86.HasSSE42 = isSet(ecx1, cpuid_SSE42) + X86.HasPOPCNT = isSet(ecx1, cpuid_POPCNT) + X86.HasAES = isSet(ecx1, cpuid_AES) + X86.HasOSXSAVE = isSet(ecx1, cpuid_OSXSAVE) + X86.HasRDRAND = isSet(ecx1, cpuid_RDRAND) var osSupportsAVX, osSupportsAVX512 bool // For XGETBV, OSXSAVE bit is required and sufficient. if X86.HasOSXSAVE { eax, _ := xgetbv() // Check if XMM and YMM registers have OS support. - osSupportsAVX = isSet(1, eax) && isSet(2, eax) + osSupportsAVX = isSet(eax, 1<<1) && isSet(eax, 1<<2) if runtime.GOOS == "darwin" { // Darwin requires special AVX512 checks, see cpu_darwin_x86.go osSupportsAVX512 = osSupportsAVX && darwinSupportsAVX512() } else { // Check if OPMASK and ZMM registers have OS support. - osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax) + osSupportsAVX512 = osSupportsAVX && isSet(eax, 1<<5) && isSet(eax, 1<<6) && isSet(eax, 1<<7) } } - X86.HasAVX = isSet(28, ecx1) && osSupportsAVX + X86.HasAVX = isSet(ecx1, cpuid_AVX) && osSupportsAVX if maxID < 7 { return } eax7, ebx7, ecx7, edx7 := cpuid(7, 0) - X86.HasBMI1 = isSet(3, ebx7) - X86.HasAVX2 = isSet(5, ebx7) && osSupportsAVX - X86.HasBMI2 = isSet(8, ebx7) - X86.HasERMS = isSet(9, ebx7) - X86.HasRDSEED = isSet(18, ebx7) - X86.HasADX = isSet(19, ebx7) - - X86.HasAVX512 = isSet(16, ebx7) && osSupportsAVX512 // Because avx-512 foundation is the core required extension + X86.HasBMI1 = isSet(ebx7, cpuid_BMI1) + X86.HasAVX2 = isSet(ebx7, cpuid_AVX2) && osSupportsAVX + X86.HasBMI2 = isSet(ebx7, cpuid_BMI2) + X86.HasERMS = isSet(ebx7, cpuid_ERMS) + X86.HasRDSEED = isSet(ebx7, cpuid_RDSEED) + X86.HasADX = isSet(ebx7, cpuid_ADX) + + X86.HasAVX512 = isSet(ebx7, cpuid_AVX512F) && osSupportsAVX512 // Because avx-512 foundation is the core required extension if X86.HasAVX512 { X86.HasAVX512F = true - X86.HasAVX512CD = isSet(28, ebx7) - X86.HasAVX512ER = isSet(27, ebx7) - X86.HasAVX512PF = isSet(26, ebx7) - X86.HasAVX512VL = isSet(31, ebx7) - X86.HasAVX512BW = isSet(30, ebx7) - X86.HasAVX512DQ = isSet(17, ebx7) - X86.HasAVX512IFMA = isSet(21, ebx7) - X86.HasAVX512VBMI = isSet(1, ecx7) - X86.HasAVX5124VNNIW = isSet(2, edx7) - X86.HasAVX5124FMAPS = isSet(3, edx7) - X86.HasAVX512VPOPCNTDQ = isSet(14, ecx7) - X86.HasAVX512VPCLMULQDQ = isSet(10, ecx7) - X86.HasAVX512VNNI = isSet(11, ecx7) - X86.HasAVX512GFNI = isSet(8, ecx7) - X86.HasAVX512VAES = isSet(9, ecx7) - X86.HasAVX512VBMI2 = isSet(6, ecx7) - X86.HasAVX512BITALG = isSet(12, ecx7) + X86.HasAVX512CD = isSet(ebx7, cpuid_AVX512CD) + X86.HasAVX512ER = isSet(ebx7, cpuid_AVX512ER) + X86.HasAVX512PF = isSet(ebx7, cpuid_AVX512PF) + X86.HasAVX512VL = isSet(ebx7, cpuid_AVX512VL) + X86.HasAVX512BW = isSet(ebx7, cpuid_AVX512BW) + X86.HasAVX512DQ = isSet(ebx7, cpuid_AVX512DQ) + X86.HasAVX512IFMA = isSet(ebx7, cpuid_AVX512IFMA) + X86.HasAVX512VBMI = isSet(ecx7, cpuid_AVX512_VBMI) + X86.HasAVX5124VNNIW = isSet(edx7, cpuid_AVX5124VNNIW) + X86.HasAVX5124FMAPS = isSet(edx7, cpuid_AVX5124FMAPS) + X86.HasAVX512VPOPCNTDQ = isSet(ecx7, cpuid_AVX512VPOPCNTDQ) + X86.HasAVX512VPCLMULQDQ = isSet(ecx7, cpuid_AVX512VPCLMULQDQ) + X86.HasAVX512VNNI = isSet(ecx7, cpuid_AVX512VNNI) + X86.HasAVX512GFNI = isSet(ecx7, cpuid_AVX512GFNI) + X86.HasAVX512VAES = isSet(ecx7, cpuid_AVX512VAES) + X86.HasAVX512VBMI2 = isSet(ecx7, cpuid_AVX512VBMI2) + X86.HasAVX512BITALG = isSet(ecx7, cpuid_AVX512BITALG) } - X86.HasAMXTile = isSet(24, edx7) - X86.HasAMXInt8 = isSet(25, edx7) - X86.HasAMXBF16 = isSet(22, edx7) + X86.HasAMXTile = isSet(edx7, cpuid_AMXTile) + X86.HasAMXInt8 = isSet(edx7, cpuid_AMXInt8) + X86.HasAMXBF16 = isSet(edx7, cpuid_AMXBF16) // These features depend on the second level of extended features. if eax7 >= 1 { eax71, _, _, edx71 := cpuid(7, 1) if X86.HasAVX512 { - X86.HasAVX512BF16 = isSet(5, eax71) + X86.HasAVX512BF16 = isSet(eax71, cpuid_AVX512BF16) } if X86.HasAVX { - X86.HasAVXIFMA = isSet(23, eax71) - X86.HasAVXVNNI = isSet(4, eax71) - X86.HasAVXVNNIInt8 = isSet(4, edx71) + X86.HasAVXIFMA = isSet(eax71, cpuid_AVXIFMA) + X86.HasAVXVNNI = isSet(eax71, cpuid_AVXVNNI) + X86.HasAVXVNNIInt8 = isSet(edx71, cpuid_AVXVNNIInt8) } } } -func isSet(bitpos uint, value uint32) bool { - return value&(1< 0 +} + +//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib" + +var libc_sysctlbyname_trampoline_addr uintptr + +// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix +func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error { + if _, _, err := syscall_syscall6( + libc_sysctlbyname_trampoline_addr, + uintptr(unsafe.Pointer(name)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen), + 0, + ); err != 0 { + return err + } + + return nil +} + +//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib" + +// Implemented in the runtime package (runtime/sys_darwin.go) +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) + +//go:linkname syscall_syscall6 syscall.syscall6 diff --git a/vendor/golang.org/x/sys/plan9/syscall_plan9.go b/vendor/golang.org/x/sys/plan9/syscall_plan9.go index d079d8116e..761912237f 100644 --- a/vendor/golang.org/x/sys/plan9/syscall_plan9.go +++ b/vendor/golang.org/x/sys/plan9/syscall_plan9.go @@ -19,13 +19,7 @@ import ( // A Note is a string describing a process note. // It implements the os.Signal interface. -type Note string - -func (n Note) Signal() {} - -func (n Note) String() string { - return string(n) -} +type Note = syscall.Note var ( Stdin = 0 diff --git a/vendor/golang.org/x/sys/unix/ioctl_signed.go b/vendor/golang.org/x/sys/unix/ioctl_signed.go index 5b0759bd86..be0f3fba65 100644 --- a/vendor/golang.org/x/sys/unix/ioctl_signed.go +++ b/vendor/golang.org/x/sys/unix/ioctl_signed.go @@ -6,9 +6,7 @@ package unix -import ( - "unsafe" -) +import "unsafe" // ioctl itself should not be exposed directly, but additional get/set // functions for specific types are permissible. @@ -28,6 +26,13 @@ func IoctlSetPointerInt(fd int, req int, value int) error { return ioctlPtr(fd, req, unsafe.Pointer(&v)) } +// IoctlSetString performs an ioctl operation which sets a string value +// on fd, using the specified request number. +func IoctlSetString(fd int, req int, value string) error { + bs := append([]byte(value), 0) + return ioctlPtr(fd, req, unsafe.Pointer(&bs[0])) +} + // IoctlSetWinsize performs an ioctl on fd with a *Winsize argument. // // To change fd's window size, the req argument should be TIOCSWINSZ. diff --git a/vendor/golang.org/x/sys/unix/ioctl_unsigned.go b/vendor/golang.org/x/sys/unix/ioctl_unsigned.go index 20f470b9d0..f0c282136d 100644 --- a/vendor/golang.org/x/sys/unix/ioctl_unsigned.go +++ b/vendor/golang.org/x/sys/unix/ioctl_unsigned.go @@ -6,9 +6,7 @@ package unix -import ( - "unsafe" -) +import "unsafe" // ioctl itself should not be exposed directly, but additional get/set // functions for specific types are permissible. @@ -28,6 +26,13 @@ func IoctlSetPointerInt(fd int, req uint, value int) error { return ioctlPtr(fd, req, unsafe.Pointer(&v)) } +// IoctlSetString performs an ioctl operation which sets a string value +// on fd, using the specified request number. +func IoctlSetString(fd int, req uint, value string) error { + bs := append([]byte(value), 0) + return ioctlPtr(fd, req, unsafe.Pointer(&bs[0])) +} + // IoctlSetWinsize performs an ioctl on fd with a *Winsize argument. // // To change fd's window size, the req argument should be TIOCSWINSZ. diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh index 42517077c4..fd39be4efd 100644 --- a/vendor/golang.org/x/sys/unix/mkerrors.sh +++ b/vendor/golang.org/x/sys/unix/mkerrors.sh @@ -256,6 +256,7 @@ struct ltchars { #include #include #include +#include #include #include #include @@ -613,7 +614,7 @@ ccflags="$@" $2 !~ /IOC_MAGIC/ && $2 ~ /^[A-Z][A-Z0-9_]+_MAGIC2?$/ || $2 ~ /^(VM|VMADDR)_/ || - $2 ~ /^IOCTL_VM_SOCKETS_/ || + $2 ~ /^(IOCTL_VM_SOCKETS_|IOCTL_MEI_)/ || $2 ~ /^(TASKSTATS|TS)_/ || $2 ~ /^CGROUPSTATS_/ || $2 ~ /^GENL_/ || diff --git a/vendor/golang.org/x/sys/unix/syscall_solaris.go b/vendor/golang.org/x/sys/unix/syscall_solaris.go index 18a3d9bdab..a6a2ea0cc0 100644 --- a/vendor/golang.org/x/sys/unix/syscall_solaris.go +++ b/vendor/golang.org/x/sys/unix/syscall_solaris.go @@ -1052,14 +1052,6 @@ func IoctlSetIntRetInt(fd int, req int, arg int) (int, error) { return ioctlRet(fd, req, uintptr(arg)) } -func IoctlSetString(fd int, req int, val string) error { - bs := make([]byte, len(val)+1) - copy(bs[:len(bs)-1], val) - err := ioctlPtr(fd, req, unsafe.Pointer(&bs[0])) - runtime.KeepAlive(&bs[0]) - return err -} - // Lifreq Helpers func (l *Lifreq) SetName(name string) error { diff --git a/vendor/golang.org/x/sys/unix/syscall_unix.go b/vendor/golang.org/x/sys/unix/syscall_unix.go index 4e92e5aa40..de6fccf9aa 100644 --- a/vendor/golang.org/x/sys/unix/syscall_unix.go +++ b/vendor/golang.org/x/sys/unix/syscall_unix.go @@ -367,7 +367,9 @@ func Recvmsg(fd int, p, oob []byte, flags int) (n, oobn int, recvflags int, from iov[0].SetLen(len(p)) } var rsa RawSockaddrAny - n, oobn, recvflags, err = recvmsgRaw(fd, iov[:], oob, flags, &rsa) + if n, oobn, recvflags, err = recvmsgRaw(fd, iov[:], oob, flags, &rsa); err != nil { + return + } // source address is only specified if the socket is unconnected if rsa.Addr.Family != AF_UNSPEC { from, err = anyToSockaddr(fd, &rsa) @@ -389,8 +391,10 @@ func RecvmsgBuffers(fd int, buffers [][]byte, oob []byte, flags int) (n, oobn in } } var rsa RawSockaddrAny - n, oobn, recvflags, err = recvmsgRaw(fd, iov, oob, flags, &rsa) - if err == nil && rsa.Addr.Family != AF_UNSPEC { + if n, oobn, recvflags, err = recvmsgRaw(fd, iov, oob, flags, &rsa); err != nil { + return + } + if rsa.Addr.Family != AF_UNSPEC { from, err = anyToSockaddr(fd, &rsa) } return diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index d0a75da572..120a7b35d1 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -1615,6 +1615,8 @@ const ( IN_OPEN = 0x20 IN_Q_OVERFLOW = 0x4000 IN_UNMOUNT = 0x2000 + IOCTL_MEI_CONNECT_CLIENT = 0xc0104801 + IOCTL_MEI_CONNECT_CLIENT_VTAG = 0xc0144804 IPPROTO_AH = 0x33 IPPROTO_BEETPH = 0x5e IPPROTO_COMP = 0x6c diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go index 1c37f9fbc4..97a61fc5b8 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go @@ -116,6 +116,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x80044803 + IOCTL_MEI_NOTIFY_SET = 0x40044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go index 6f54d34aef..a0d6d498c4 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go @@ -116,6 +116,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x80044803 + IOCTL_MEI_NOTIFY_SET = 0x40044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go index 783ec5c126..dd9c903f9a 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x80044803 + IOCTL_MEI_NOTIFY_SET = 0x40044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go index ca83d3ba16..384c61ca3a 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -120,6 +120,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x80044803 + IOCTL_MEI_NOTIFY_SET = 0x40044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go index 607e611c0c..6384c9831f 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go @@ -116,6 +116,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x80044803 + IOCTL_MEI_NOTIFY_SET = 0x40044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go index b9cb5bd3c0..553c1c6f15 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x100 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xfffffff IPV6_FLOWLABEL_MASK = 0xfffff diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go index 65b078a638..b3339f2099 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x100 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xfffffff IPV6_FLOWLABEL_MASK = 0xfffff diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go index 5298a3033d..177091d2bc 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x100 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go index 7bc557c876..c5abf156d0 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x100 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x80 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go index 152399bb04..f1f3fadf57 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x400 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xfffffff IPV6_FLOWLABEL_MASK = 0xfffff diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go index 1a1ce2409c..203ad9c54a 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x400 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xfffffff IPV6_FLOWLABEL_MASK = 0xfffff diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go index 4231a1fb57..4b9abcb21a 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x400 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go index 21c0e95266..f87983037d 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x80044803 + IOCTL_MEI_NOTIFY_SET = 0x40044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 IPV6_FLOWINFO_MASK = 0xffffff0f IPV6_FLOWLABEL_MASK = 0xffff0f00 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go index f00d1cd7cf..64347eb354 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go @@ -115,6 +115,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x80000 IN_NONBLOCK = 0x800 + IOCTL_MEI_NOTIFY_GET = 0x80044803 + IOCTL_MEI_NOTIFY_SET = 0x40044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x7b9 IPV6_FLOWINFO_MASK = 0xfffffff IPV6_FLOWLABEL_MASK = 0xfffff diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go index bc8d539e6a..7d71911718 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go @@ -119,6 +119,8 @@ const ( IEXTEN = 0x8000 IN_CLOEXEC = 0x400000 IN_NONBLOCK = 0x4000 + IOCTL_MEI_NOTIFY_GET = 0x40044803 + IOCTL_MEI_NOTIFY_SET = 0x80044802 IOCTL_VM_SOCKETS_GET_LOCAL_CID = 0x200007b9 IPV6_FLOWINFO_MASK = 0xfffffff IPV6_FLOWLABEL_MASK = 0xfffff diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index c1a4670171..45476a73c6 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -593,110 +593,115 @@ const ( ) const ( - NDA_UNSPEC = 0x0 - NDA_DST = 0x1 - NDA_LLADDR = 0x2 - NDA_CACHEINFO = 0x3 - NDA_PROBES = 0x4 - NDA_VLAN = 0x5 - NDA_PORT = 0x6 - NDA_VNI = 0x7 - NDA_IFINDEX = 0x8 - NDA_MASTER = 0x9 - NDA_LINK_NETNSID = 0xa - NDA_SRC_VNI = 0xb - NTF_USE = 0x1 - NTF_SELF = 0x2 - NTF_MASTER = 0x4 - NTF_PROXY = 0x8 - NTF_EXT_LEARNED = 0x10 - NTF_OFFLOADED = 0x20 - NTF_ROUTER = 0x80 - NUD_INCOMPLETE = 0x1 - NUD_REACHABLE = 0x2 - NUD_STALE = 0x4 - NUD_DELAY = 0x8 - NUD_PROBE = 0x10 - NUD_FAILED = 0x20 - NUD_NOARP = 0x40 - NUD_PERMANENT = 0x80 - NUD_NONE = 0x0 - IFA_UNSPEC = 0x0 - IFA_ADDRESS = 0x1 - IFA_LOCAL = 0x2 - IFA_LABEL = 0x3 - IFA_BROADCAST = 0x4 - IFA_ANYCAST = 0x5 - IFA_CACHEINFO = 0x6 - IFA_MULTICAST = 0x7 - IFA_FLAGS = 0x8 - IFA_RT_PRIORITY = 0x9 - IFA_TARGET_NETNSID = 0xa - IFAL_LABEL = 0x2 - IFAL_ADDRESS = 0x1 - RT_SCOPE_UNIVERSE = 0x0 - RT_SCOPE_SITE = 0xc8 - RT_SCOPE_LINK = 0xfd - RT_SCOPE_HOST = 0xfe - RT_SCOPE_NOWHERE = 0xff - RT_TABLE_UNSPEC = 0x0 - RT_TABLE_COMPAT = 0xfc - RT_TABLE_DEFAULT = 0xfd - RT_TABLE_MAIN = 0xfe - RT_TABLE_LOCAL = 0xff - RT_TABLE_MAX = 0xffffffff - RTA_UNSPEC = 0x0 - RTA_DST = 0x1 - RTA_SRC = 0x2 - RTA_IIF = 0x3 - RTA_OIF = 0x4 - RTA_GATEWAY = 0x5 - RTA_PRIORITY = 0x6 - RTA_PREFSRC = 0x7 - RTA_METRICS = 0x8 - RTA_MULTIPATH = 0x9 - RTA_FLOW = 0xb - RTA_CACHEINFO = 0xc - RTA_TABLE = 0xf - RTA_MARK = 0x10 - RTA_MFC_STATS = 0x11 - RTA_VIA = 0x12 - RTA_NEWDST = 0x13 - RTA_PREF = 0x14 - RTA_ENCAP_TYPE = 0x15 - RTA_ENCAP = 0x16 - RTA_EXPIRES = 0x17 - RTA_PAD = 0x18 - RTA_UID = 0x19 - RTA_TTL_PROPAGATE = 0x1a - RTA_IP_PROTO = 0x1b - RTA_SPORT = 0x1c - RTA_DPORT = 0x1d - RTN_UNSPEC = 0x0 - RTN_UNICAST = 0x1 - RTN_LOCAL = 0x2 - RTN_BROADCAST = 0x3 - RTN_ANYCAST = 0x4 - RTN_MULTICAST = 0x5 - RTN_BLACKHOLE = 0x6 - RTN_UNREACHABLE = 0x7 - RTN_PROHIBIT = 0x8 - RTN_THROW = 0x9 - RTN_NAT = 0xa - RTN_XRESOLVE = 0xb - SizeofNlMsghdr = 0x10 - SizeofNlMsgerr = 0x14 - SizeofRtGenmsg = 0x1 - SizeofNlAttr = 0x4 - SizeofRtAttr = 0x4 - SizeofIfInfomsg = 0x10 - SizeofIfAddrmsg = 0x8 - SizeofIfAddrlblmsg = 0xc - SizeofIfaCacheinfo = 0x10 - SizeofRtMsg = 0xc - SizeofRtNexthop = 0x8 - SizeofNdUseroptmsg = 0x10 - SizeofNdMsg = 0xc + NDA_UNSPEC = 0x0 + NDA_DST = 0x1 + NDA_LLADDR = 0x2 + NDA_CACHEINFO = 0x3 + NDA_PROBES = 0x4 + NDA_VLAN = 0x5 + NDA_PORT = 0x6 + NDA_VNI = 0x7 + NDA_IFINDEX = 0x8 + NDA_MASTER = 0x9 + NDA_LINK_NETNSID = 0xa + NDA_SRC_VNI = 0xb + NTF_USE = 0x1 + NTF_SELF = 0x2 + NTF_MASTER = 0x4 + NTF_PROXY = 0x8 + NTF_EXT_LEARNED = 0x10 + NTF_OFFLOADED = 0x20 + NTF_ROUTER = 0x80 + NUD_INCOMPLETE = 0x1 + NUD_REACHABLE = 0x2 + NUD_STALE = 0x4 + NUD_DELAY = 0x8 + NUD_PROBE = 0x10 + NUD_FAILED = 0x20 + NUD_NOARP = 0x40 + NUD_PERMANENT = 0x80 + NUD_NONE = 0x0 + IFA_UNSPEC = 0x0 + IFA_ADDRESS = 0x1 + IFA_LOCAL = 0x2 + IFA_LABEL = 0x3 + IFA_BROADCAST = 0x4 + IFA_ANYCAST = 0x5 + IFA_CACHEINFO = 0x6 + IFA_MULTICAST = 0x7 + IFA_FLAGS = 0x8 + IFA_RT_PRIORITY = 0x9 + IFA_TARGET_NETNSID = 0xa + IFAL_LABEL = 0x2 + IFAL_ADDRESS = 0x1 + RT_SCOPE_UNIVERSE = 0x0 + RT_SCOPE_SITE = 0xc8 + RT_SCOPE_LINK = 0xfd + RT_SCOPE_HOST = 0xfe + RT_SCOPE_NOWHERE = 0xff + RT_TABLE_UNSPEC = 0x0 + RT_TABLE_COMPAT = 0xfc + RT_TABLE_DEFAULT = 0xfd + RT_TABLE_MAIN = 0xfe + RT_TABLE_LOCAL = 0xff + RT_TABLE_MAX = 0xffffffff + RTA_UNSPEC = 0x0 + RTA_DST = 0x1 + RTA_SRC = 0x2 + RTA_IIF = 0x3 + RTA_OIF = 0x4 + RTA_GATEWAY = 0x5 + RTA_PRIORITY = 0x6 + RTA_PREFSRC = 0x7 + RTA_METRICS = 0x8 + RTA_MULTIPATH = 0x9 + RTA_FLOW = 0xb + RTA_CACHEINFO = 0xc + RTA_TABLE = 0xf + RTA_MARK = 0x10 + RTA_MFC_STATS = 0x11 + RTA_VIA = 0x12 + RTA_NEWDST = 0x13 + RTA_PREF = 0x14 + RTA_ENCAP_TYPE = 0x15 + RTA_ENCAP = 0x16 + RTA_EXPIRES = 0x17 + RTA_PAD = 0x18 + RTA_UID = 0x19 + RTA_TTL_PROPAGATE = 0x1a + RTA_IP_PROTO = 0x1b + RTA_SPORT = 0x1c + RTA_DPORT = 0x1d + RTN_UNSPEC = 0x0 + RTN_UNICAST = 0x1 + RTN_LOCAL = 0x2 + RTN_BROADCAST = 0x3 + RTN_ANYCAST = 0x4 + RTN_MULTICAST = 0x5 + RTN_BLACKHOLE = 0x6 + RTN_UNREACHABLE = 0x7 + RTN_PROHIBIT = 0x8 + RTN_THROW = 0x9 + RTN_NAT = 0xa + RTN_XRESOLVE = 0xb + PREFIX_UNSPEC = 0x0 + PREFIX_ADDRESS = 0x1 + PREFIX_CACHEINFO = 0x2 + SizeofNlMsghdr = 0x10 + SizeofNlMsgerr = 0x14 + SizeofRtGenmsg = 0x1 + SizeofNlAttr = 0x4 + SizeofRtAttr = 0x4 + SizeofIfInfomsg = 0x10 + SizeofPrefixmsg = 0xc + SizeofPrefixCacheinfo = 0x8 + SizeofIfAddrmsg = 0x8 + SizeofIfAddrlblmsg = 0xc + SizeofIfaCacheinfo = 0x10 + SizeofRtMsg = 0xc + SizeofRtNexthop = 0x8 + SizeofNdUseroptmsg = 0x10 + SizeofNdMsg = 0xc ) type NlMsghdr struct { @@ -735,6 +740,22 @@ type IfInfomsg struct { Change uint32 } +type Prefixmsg struct { + Family uint8 + Pad1 uint8 + Pad2 uint16 + Ifindex int32 + Type uint8 + Len uint8 + Flags uint8 + Pad3 uint8 +} + +type PrefixCacheinfo struct { + Preferred_time uint32 + Valid_time uint32 +} + type IfAddrmsg struct { Family uint8 Prefixlen uint8 diff --git a/vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go b/vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go index 439548ec9a..50e8e64497 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go +++ b/vendor/golang.org/x/sys/unix/ztypes_netbsd_arm.go @@ -104,7 +104,7 @@ type Statvfs_t struct { Fsid uint32 Namemax uint32 Owner uint32 - Spare [4]uint32 + Spare [4]uint64 Fstypename [32]byte Mntonname [1024]byte Mntfromname [1024]byte diff --git a/vendor/golang.org/x/sys/windows/aliases.go b/vendor/golang.org/x/sys/windows/aliases.go index 16f90560a2..96317966e5 100644 --- a/vendor/golang.org/x/sys/windows/aliases.go +++ b/vendor/golang.org/x/sys/windows/aliases.go @@ -8,5 +8,6 @@ package windows import "syscall" +type Signal = syscall.Signal type Errno = syscall.Errno type SysProcAttr = syscall.SysProcAttr diff --git a/vendor/golang.org/x/sys/windows/registry/key.go b/vendor/golang.org/x/sys/windows/registry/key.go index 39aeeb644f..7cc6ff3afa 100644 --- a/vendor/golang.org/x/sys/windows/registry/key.go +++ b/vendor/golang.org/x/sys/windows/registry/key.go @@ -198,7 +198,20 @@ type KeyInfo struct { // ModTime returns the key's last write time. func (ki *KeyInfo) ModTime() time.Time { - return time.Unix(0, ki.lastWriteTime.Nanoseconds()) + lastHigh, lastLow := ki.lastWriteTime.HighDateTime, ki.lastWriteTime.LowDateTime + // 100-nanosecond intervals since January 1, 1601 + hsec := uint64(lastHigh)<<32 + uint64(lastLow) + // Convert _before_ gauging; the nanosecond difference between Epoch (00:00:00 + // UTC, January 1, 1970) and Filetime's zero offset (January 1, 1601) is out + // of bounds for int64: -11644473600*1e7*1e2 < math.MinInt64 + sec := int64(hsec/1e7) - 11644473600 + nsec := int64(hsec%1e7) * 100 + return time.Unix(sec, nsec) +} + +// modTimeZero reports whether the key's last write time is zero. +func (ki *KeyInfo) modTimeZero() bool { + return ki.lastWriteTime.LowDateTime == 0 && ki.lastWriteTime.HighDateTime == 0 } // Stat retrieves information about the open key k. diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 69439df2a4..d766436587 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -900,6 +900,7 @@ const socket_error = uintptr(^uint32(0)) //sys NotifyRouteChange2(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyRouteChange2 //sys NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyUnicastIpAddressChange //sys CancelMibChangeNotify2(notificationHandle Handle) (errcode error) = iphlpapi.CancelMibChangeNotify2 +//sys IsProcessorFeaturePresent(ProcessorFeature uint32) (ret bool) = kernel32.IsProcessorFeaturePresent // For testing: clients can set this flag to force // creation of IPv6 sockets to return EAFNOSUPPORT. @@ -1489,20 +1490,6 @@ func Getgid() (gid int) { return -1 } func Getegid() (egid int) { return -1 } func Getgroups() (gids []int, err error) { return nil, syscall.EWINDOWS } -type Signal int - -func (s Signal) Signal() {} - -func (s Signal) String() string { - if 0 <= s && int(s) < len(signals) { - str := signals[s] - if str != "" { - return str - } - } - return "signal " + itoa(int(s)) -} - func LoadCreateSymbolicLink() error { return procCreateSymbolicLinkW.Find() } diff --git a/vendor/golang.org/x/sys/windows/types_windows.go b/vendor/golang.org/x/sys/windows/types_windows.go index 6e4f50eb48..d5658a138c 100644 --- a/vendor/golang.org/x/sys/windows/types_windows.go +++ b/vendor/golang.org/x/sys/windows/types_windows.go @@ -3938,3 +3938,88 @@ const ( MOUSE_EVENT = 0x0002 WINDOW_BUFFER_SIZE_EVENT = 0x0004 ) + +// The processor features to be tested for IsProcessorFeaturePresent, see +// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent +const ( + PF_ARM_64BIT_LOADSTORE_ATOMIC = 25 + PF_ARM_DIVIDE_INSTRUCTION_AVAILABLE = 24 + PF_ARM_EXTERNAL_CACHE_AVAILABLE = 26 + PF_ARM_FMAC_INSTRUCTIONS_AVAILABLE = 27 + PF_ARM_VFP_32_REGISTERS_AVAILABLE = 18 + PF_3DNOW_INSTRUCTIONS_AVAILABLE = 7 + PF_CHANNELS_ENABLED = 16 + PF_COMPARE_EXCHANGE_DOUBLE = 2 + PF_COMPARE_EXCHANGE128 = 14 + PF_COMPARE64_EXCHANGE128 = 15 + PF_FASTFAIL_AVAILABLE = 23 + PF_FLOATING_POINT_EMULATED = 1 + PF_FLOATING_POINT_PRECISION_ERRATA = 0 + PF_MMX_INSTRUCTIONS_AVAILABLE = 3 + PF_NX_ENABLED = 12 + PF_PAE_ENABLED = 9 + PF_RDTSC_INSTRUCTION_AVAILABLE = 8 + PF_RDWRFSGSBASE_AVAILABLE = 22 + PF_SECOND_LEVEL_ADDRESS_TRANSLATION = 20 + PF_SSE3_INSTRUCTIONS_AVAILABLE = 13 + PF_SSSE3_INSTRUCTIONS_AVAILABLE = 36 + PF_SSE4_1_INSTRUCTIONS_AVAILABLE = 37 + PF_SSE4_2_INSTRUCTIONS_AVAILABLE = 38 + PF_AVX_INSTRUCTIONS_AVAILABLE = 39 + PF_AVX2_INSTRUCTIONS_AVAILABLE = 40 + PF_AVX512F_INSTRUCTIONS_AVAILABLE = 41 + PF_VIRT_FIRMWARE_ENABLED = 21 + PF_XMMI_INSTRUCTIONS_AVAILABLE = 6 + PF_XMMI64_INSTRUCTIONS_AVAILABLE = 10 + PF_XSAVE_ENABLED = 17 + PF_ARM_V8_INSTRUCTIONS_AVAILABLE = 29 + PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE = 30 + PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE = 31 + PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE = 34 + PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE = 43 + PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE = 44 + PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE = 45 + PF_ARM_SVE_INSTRUCTIONS_AVAILABLE = 46 + PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE = 47 + PF_ARM_SVE2_1_INSTRUCTIONS_AVAILABLE = 48 + PF_ARM_SVE_AES_INSTRUCTIONS_AVAILABLE = 49 + PF_ARM_SVE_PMULL128_INSTRUCTIONS_AVAILABLE = 50 + PF_ARM_SVE_BITPERM_INSTRUCTIONS_AVAILABLE = 51 + PF_ARM_SVE_BF16_INSTRUCTIONS_AVAILABLE = 52 + PF_ARM_SVE_EBF16_INSTRUCTIONS_AVAILABLE = 53 + PF_ARM_SVE_B16B16_INSTRUCTIONS_AVAILABLE = 54 + PF_ARM_SVE_SHA3_INSTRUCTIONS_AVAILABLE = 55 + PF_ARM_SVE_SM4_INSTRUCTIONS_AVAILABLE = 56 + PF_ARM_SVE_I8MM_INSTRUCTIONS_AVAILABLE = 57 + PF_ARM_SVE_F32MM_INSTRUCTIONS_AVAILABLE = 58 + PF_ARM_SVE_F64MM_INSTRUCTIONS_AVAILABLE = 59 + PF_BMI2_INSTRUCTIONS_AVAILABLE = 60 + PF_MOVDIR64B_INSTRUCTION_AVAILABLE = 61 + PF_ARM_LSE2_AVAILABLE = 62 + PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE = 64 + PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE = 65 + PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE = 66 + PF_ARM_V82_FP16_INSTRUCTIONS_AVAILABLE = 67 + PF_ARM_V86_BF16_INSTRUCTIONS_AVAILABLE = 68 + PF_ARM_V86_EBF16_INSTRUCTIONS_AVAILABLE = 69 + PF_ARM_SME_INSTRUCTIONS_AVAILABLE = 70 + PF_ARM_SME2_INSTRUCTIONS_AVAILABLE = 71 + PF_ARM_SME2_1_INSTRUCTIONS_AVAILABLE = 72 + PF_ARM_SME2_2_INSTRUCTIONS_AVAILABLE = 73 + PF_ARM_SME_AES_INSTRUCTIONS_AVAILABLE = 74 + PF_ARM_SME_SBITPERM_INSTRUCTIONS_AVAILABLE = 75 + PF_ARM_SME_SF8MM4_INSTRUCTIONS_AVAILABLE = 76 + PF_ARM_SME_SF8MM8_INSTRUCTIONS_AVAILABLE = 77 + PF_ARM_SME_SF8DP2_INSTRUCTIONS_AVAILABLE = 78 + PF_ARM_SME_SF8DP4_INSTRUCTIONS_AVAILABLE = 79 + PF_ARM_SME_SF8FMA_INSTRUCTIONS_AVAILABLE = 80 + PF_ARM_SME_F8F32_INSTRUCTIONS_AVAILABLE = 81 + PF_ARM_SME_F8F16_INSTRUCTIONS_AVAILABLE = 82 + PF_ARM_SME_F16F16_INSTRUCTIONS_AVAILABLE = 83 + PF_ARM_SME_B16B16_INSTRUCTIONS_AVAILABLE = 84 + PF_ARM_SME_F64F64_INSTRUCTIONS_AVAILABLE = 85 + PF_ARM_SME_I16I64_INSTRUCTIONS_AVAILABLE = 86 + PF_ARM_SME_LUTv2_INSTRUCTIONS_AVAILABLE = 87 + PF_ARM_SME_FA64_INSTRUCTIONS_AVAILABLE = 88 + PF_UMONITOR_INSTRUCTION_AVAILABLE = 89 +) diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go index f25b7308a1..fe7a4ea124 100644 --- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go +++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go @@ -320,6 +320,7 @@ var ( procGetVolumePathNamesForVolumeNameW = modkernel32.NewProc("GetVolumePathNamesForVolumeNameW") procGetWindowsDirectoryW = modkernel32.NewProc("GetWindowsDirectoryW") procInitializeProcThreadAttributeList = modkernel32.NewProc("InitializeProcThreadAttributeList") + procIsProcessorFeaturePresent = modkernel32.NewProc("IsProcessorFeaturePresent") procIsWow64Process = modkernel32.NewProc("IsWow64Process") procIsWow64Process2 = modkernel32.NewProc("IsWow64Process2") procLoadLibraryExW = modkernel32.NewProc("LoadLibraryExW") @@ -2786,6 +2787,12 @@ func initializeProcThreadAttributeList(attrlist *ProcThreadAttributeList, attrco return } +func IsProcessorFeaturePresent(ProcessorFeature uint32) (ret bool) { + r0, _, _ := syscall.SyscallN(procIsProcessorFeaturePresent.Addr(), uintptr(ProcessorFeature)) + ret = r0 != 0 + return +} + func IsWow64Process(handle Handle, isWow64 *bool) (err error) { var _p0 uint32 if *isWow64 { diff --git a/vendor/golang.org/x/term/terminal.go b/vendor/golang.org/x/term/terminal.go index 9255449b9b..6ec537cdc1 100644 --- a/vendor/golang.org/x/term/terminal.go +++ b/vendor/golang.org/x/term/terminal.go @@ -160,7 +160,9 @@ const ( keyEnd keyDeleteWord keyDeleteLine + keyDelete keyClearScreen + keyTranspose keyPasteStart keyPasteEnd ) @@ -194,6 +196,8 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { return keyDeleteLine, b[1:] case 12: // ^L return keyClearScreen, b[1:] + case 20: // ^T + return keyTranspose, b[1:] case 23: // ^W return keyDeleteWord, b[1:] case 14: // ^N @@ -228,6 +232,10 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) { } } + if !pasteActive && len(b) >= 4 && b[0] == keyEscape && b[1] == '[' && b[2] == '3' && b[3] == '~' { + return keyDelete, b[4:] + } + if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' { switch b[5] { case 'C': @@ -590,7 +598,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { } t.line = t.line[:t.pos] t.moveCursorToPos(t.pos) - case keyCtrlD: + case keyCtrlD, keyDelete: // Erase the character under the current position. // The EOF case when the line is empty is handled in // readLine(). @@ -600,6 +608,24 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) { } case keyCtrlU: t.eraseNPreviousChars(t.pos) + case keyTranspose: + // This transposes the two characters around the cursor and advances the cursor. Best-effort. + if len(t.line) < 2 || t.pos < 1 { + return + } + swap := t.pos + if swap == len(t.line) { + swap-- // special: at end of line, swap previous two chars + } + t.line[swap-1], t.line[swap] = t.line[swap], t.line[swap-1] + if t.pos < len(t.line) { + t.pos++ + } + if t.echo { + t.moveCursorToPos(swap - 1) + t.writeLine(t.line[swap-1:]) + t.moveCursorToPos(t.pos) + } case keyClearScreen: // Erases the screen and moves the cursor to the home position. t.queue([]rune("\x1b[2J\x1b[H")) diff --git a/vendor/modules.txt b/vendor/modules.txt index 58c43b679f..ed8efc4093 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -115,8 +115,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 @@ -840,15 +840,15 @@ golang.org/x/oauth2/internal ## explicit; go 1.24.0 golang.org/x/sync/errgroup golang.org/x/sync/semaphore -# golang.org/x/sys v0.38.0 -## explicit; go 1.24.0 +# golang.org/x/sys v0.42.0 +## explicit; go 1.25.0 golang.org/x/sys/cpu golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows golang.org/x/sys/windows/registry -# golang.org/x/term v0.37.0 -## explicit; go 1.24.0 +# golang.org/x/term v0.41.0 +## explicit; go 1.25.0 golang.org/x/term # golang.org/x/text v0.31.0 ## explicit; go 1.24.0 @@ -1492,10 +1492,11 @@ k8s.io/utils/exec k8s.io/utils/internal/third_party/forked/golang/net k8s.io/utils/net k8s.io/utils/ptr -# mvdan.cc/sh/v3 v3.5.1 -## explicit; go 1.17 +# mvdan.cc/sh/v3 v3.13.1 +## explicit; go 1.25.0 mvdan.cc/sh/v3/expand mvdan.cc/sh/v3/fileutil +mvdan.cc/sh/v3/internal mvdan.cc/sh/v3/interp mvdan.cc/sh/v3/pattern mvdan.cc/sh/v3/syntax diff --git a/vendor/mvdan.cc/sh/v3/expand/arith.go b/vendor/mvdan.cc/sh/v3/expand/arith.go index 1e48a709bc..6865a88408 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,71 +34,73 @@ 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: return ^val, nil case syntax.Plus: return val, nil - default: // syntax.Minus + case syntax.Minus: return -val, nil + default: + return 0, fmt.Errorf("unsupported unary arithmetic operator: %q", expr.Op) } 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 +111,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 +136,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 +156,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 +174,56 @@ 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) - default: // syntax.Comma + return oneIf(x != 0 || y != 0), nil + case syntax.Comma: // x is executed but its result discarded - return y + return y, nil + default: + return 0, fmt.Errorf("unsupported binary arithmetic operator: %q", op) } } 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..51fea7354f 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 go tool 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,38 @@ 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 +} + +// Flags returns the variable's attribute flags in the order used by bash's +// declare builtin and ${var@a}: type (a/A/n), readonly (r), exported (x). +func (v Variable) Flags() string { + var flags []byte + switch v.Kind { + case Indexed: + flags = append(flags, 'a') + case Associative: + flags = append(flags, 'A') + case NameRef: + flags = append(flags, 'n') + } + if v.ReadOnly { + flags = append(flags, 'r') + } + if v.Exported { + flags = append(flags, 'x') + } + return string(flags) } // String returns the variable's value as a string. In general, this only makes @@ -108,7 +162,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 +173,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,38 +188,29 @@ 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. // // On Windows, where environment variable names are case-insensitive, the // resulting variable names will all be uppercase. func ListEnviron(pairs ...string) Environ { - return listEnvironWithUpper(runtime.GOOS == "windows", pairs...) + return listEnviron_(runtime.GOOS == "windows", pairs...) } -// listEnvironWithUpper implements ListEnviron, but letting the tests specify +// listEnviron_ 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...) - 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:] - } - } - } - - sort.SliceStable(list, func(i, j int) bool { - isep := strings.IndexByte(list[i], '=') - jsep := strings.IndexByte(list[j], '=') +func listEnviron_(caseInsensitive bool, pairs ...string) Environ { + list := slices.Clone(pairs) + env := listEnviron{caseInsensitive: caseInsensitive} + slices.SortStableFunc(list, func(a, b string) int { + isep := strings.IndexByte(a, '=') + jsep := strings.IndexByte(b, '=') if isep < 0 { isep = 0 } else { @@ -176,51 +221,75 @@ func listEnvironWithUpper(upper bool, pairs ...string) Environ { } else { jsep += 1 } - return list[i][:isep] < list[j][:jsep] + return env.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 { + if env.compare(last, name) == 0 { // duplicate; the last one wins - list = append(list[:i-1], list[i:]...) + list = slices.Delete(list, i-1, i) continue } last = name i++ } - return listEnviron(list) + env.pairs = list + return env } // listEnviron is a sorted list of "name=value" strings. -type listEnviron []string +type listEnviron struct { + caseInsensitive bool + pairs []string +} + +func (l listEnviron) compare(a, b string) int { + if l.caseInsensitive { + // This is not particularly efficient, but it does the job. + // If we had a cmp-compatible version of [strings.EqualFold], we'd use it. + a = strings.ToUpper(a) + b = strings.ToUpper(b) + } + return strings.Compare(a, b) +} 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.pairs, name, func(pair, name string) int { + if len(pair) < endpos { + // Too short; see if we are before or after the name. + return l.compare(pair, name) + } + // Compare the name prefix, then the equal character. + c := l.compare(pair[:eqpos], name) + eq := pair[eqpos] + if c == 0 { + return cmp.Compare(eq, '=') + } + return c + }) + if ok { + return Variable{Set: true, Exported: true, Kind: String, Str: l.pairs[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 { + for _, pair := range l.pairs { + 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..d0677e1c70 100644 --- a/vendor/mvdan.cc/sh/v3/expand/expand.go +++ b/vendor/mvdan.cc/sh/v3/expand/expand.go @@ -4,20 +4,23 @@ 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/internal" "mvdan.cc/sh/v3/pattern" "mvdan.cc/sh/v3/syntax" ) @@ -40,39 +43,51 @@ 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. CmdSubst func(io.Writer, *syntax.CmdSubst) error // ProcSubst expands a process substitution node. - // - // Note that this feature is a work in progress, and the signature of - // 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 the older form of [ReadDir2], before io/fs. + // + // Deprecated: use ReadDir2 instead. + ReadDir func(string) ([]fs.FileInfo, error) - // 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) + // 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 corresponds to the shell option which allows globbing with "**". GlobStar bool - // NullGlob corresponds to the shell option that allows globbing + // DotGlob corresponds to the shell option which allows filenames beginning + // with a dot to be matched by a pattern which does not begin with a dot. + DotGlob bool + + // NoCaseGlob corresponds to the shell option which causes case-insensitive + // pattern matching in pathname expansion. + NoCaseGlob bool + + // NullGlob corresponds to the shell option which allows globbing // patterns which match nothing to result in zero fields. NullGlob bool - // NoUnset corresponds to the shell option that treats unset variables + // NoUnset corresponds to the shell option which treats unset variables // as errors. NoUnset bool - bufferAlloc bytes.Buffer // TODO: use strings.Builder + // ExtGlob corresponds to the shell option which allows using extended + // pattern matching features when performing pathname expansion (globbing). + ExtGlob bool + + bufferAlloc strings.Builder fieldAlloc [4]fieldPart fieldsAlloc [4][]fieldPart @@ -83,7 +98,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 +109,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 +154,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 +169,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 +190,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,58 +201,71 @@ 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 // shell's format specifications. These include printf(1), among others. // // The resulting string is returned, along with the number of arguments used. +// Note that the resulting string may contain null bytes, for example +// if the format string used `\x00`. The caller should terminate the string +// at the first null byte if needed, such as when expanding for `$'foo\x00bar'`. // // The config specifies shell expansion options; nil behaves the same as an // 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) -formatLoop: for i := 0; i < len(format); i++ { // readDigits reads from 0 to max digits, either octal or // hexadecimal. readDigits := func(max int, hex bool) string { j := 0 - for ; j < max; j++ { + for ; j < max && i+j < len(format); j++ { c := format[i+j] if (c >= '0' && c <= '9') || (hex && c >= 'a' && c <= 'f') || @@ -242,64 +283,64 @@ formatLoop: switch { case c == '\\': // escaped i++ + if i >= len(format) { + sb.WriteByte('\\') + break + } 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) if len(digits) > 0 { // can't error n, _ := strconv.ParseUint(digits, 16, 32) - if n == 0 { - // If we're about to print \x00, - // stop the entire loop, like bash. - break 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 +351,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 +384,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 +416,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 -// command. This includes brace expansion, tilde expansion, parameter expansion, -// command substitution, arithmetic expansion, and quote removal. +// 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, quote removal, and globbing. +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 +513,54 @@ 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") // TODO: why is this needed? 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) + fp.val, _, _ = strings.Cut(fp.val, "\x00") // cut the string if format included \x00 } 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 +569,36 @@ 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: + // Like how [Config.wordFields] deals with [syntax.ExtGlob], + // except that we allow these through even when [Config.ExtGlob] + // is false, as it only applies to pathname expansion. + field = append(field, fieldPart{val: wp.Op.String() + wp.Pattern.Value + ")"}) default: - panic(fmt.Sprintf("unhandled word part: %T", x)) + panic(fmt.Sprintf("unhandled word part: %T", wp)) } } return field, nil @@ -523,14 +608,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 +629,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,30 +660,32 @@ 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 == '\\' { if i++; i >= len(s) { + sb.WriteByte(b) break } 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) + fp.val, _, _ = strings.Cut(fp.val, "\x00") // cut the string if format included \x00 } 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 +700,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 +709,43 @@ 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: + if !cfg.ExtGlob { + return nil, fmt.Errorf("extended globbing operator used without the \"extglob\" option set") + } + // We don't translate or interpret the pattern here in any way; + // that's done later when globbing takes place via [pattern.Regexp]. + // Here, all we do is keep the extended globbing expression in string form. + // + // TODO(v4): perhaps the syntax parser should keep extended globbing expressions + // as plain literal strings, because a custom node is not particularly helpful. + // It's not like other globbing operators like `*` or `**` get their own nodes. + curField = append(curField, fieldPart{val: wp.Op.String() + wp.Pattern.Value + ")"}) default: - panic(fmt.Sprintf("unhandled word part: %T", x)) + panic(fmt.Sprintf("unhandled word part: %T", wp)) } } flush() @@ -648,51 +756,125 @@ 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 { + if pe == nil || pe.Length || pe.Width || pe.IsSet { 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 "*": - return []string{cfg.ifsJoin(cfg.Env.Get(name).List)} - case "@": - return cfg.Env.Get(name).List + case "*": // "${*}" or "${*:offset:length}" + return []string{cfg.ifsJoin(cfg.sliceElems(pe, cfg.Env.Get(name).List, true))} + case "@": // "${@}" or "${@:offset:length}" + return cfg.sliceElems(pe, cfg.Env.Get(name).List, true) } switch nodeLit(pe.Index) { - case "@": - if vr := cfg.Env.Get(name); vr.Kind == Indexed { - return vr.List + case "@": // "${name[@]}" + vr := cfg.Env.Get(name) + switch vr.Kind { + case Indexed: + return cfg.sliceElems(pe, vr.List, false) + case Associative: + return slices.Collect(maps.Values(vr.Map)) + case Unknown: + if !vr.IsSet() { + // An unset variable expanded as "${name[@]}" produces + // zero fields, just like an empty array. + return []string{} + } } - case "*": + case "*": // "${name[*]}" if vr := cfg.Env.Get(name); vr.Kind == Indexed { - return []string{cfg.ifsJoin(vr.List)} + return []string{cfg.ifsJoin(cfg.sliceElems(pe, vr.List, false))} } } return nil } -func (cfg *Config) expandUser(field string) (prefix, rest string) { - if len(field) == 0 || field[0] != '~' { +// sliceElems applies ${var:offset:length} slicing to a list of elements. +// When positional is true, $0 is prepended to the list before slicing. +// In bash, positional parameter offsets ($@ and $*) are 1-based and +// offset 0 includes $0 (the shell or script name). Negative offsets +// count from $# + 1, so $0 is reachable via large enough negative values. +func (cfg *Config) sliceElems(pe *syntax.ParamExp, elems []string, positional bool) []string { + if pe.Slice == nil { + return elems + } + if positional { + elems = append([]string{cfg.Env.Get("0").Str}, elems...) + } + slicePos := func(n int) int { + if n < 0 { + n = len(elems) + n + if n < 0 { + n = len(elems) + } + } else if n > len(elems) { + n = len(elems) + } + return n + } + if pe.Slice.Offset != nil { + offset, err := Arithm(cfg, pe.Slice.Offset) + if err != nil { + return elems + } + elems = elems[slicePos(offset):] + } + if pe.Slice.Length != nil { + length, err := Arithm(cfg, pe.Slice.Length) + if err != nil { + return elems + } + elems = elems[:slicePos(length)] + } + return elems +} + +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() { @@ -730,9 +912,12 @@ func findAllIndex(pat, name string, n int) [][]int { return rx.FindAllStringIndex(name, n) } -var rxGlobStar = regexp.MustCompile(".*") +var ( + rxGlobStar = regexp.MustCompile(`^[^/.][^/]*$`) + rxGlobStarDotGlob = 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 +930,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 +954,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 +971,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 +979,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,54 +1010,70 @@ 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], "")) + // note how the zero-match case there has a trailing separator. + stack = append(stack, pathJoin2(match, "")) } matches = matches[:0] var newMatches []string // to reuse its capacity for len(stack) > 0 { dir := stack[len(stack)-1] stack = stack[:len(stack)-1] - - // Don't include the original "" match as it's not a valid path. - if dir != "" { - matches = append(matches, dir) - } + matches = append(matches, dir) // 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]) + rx := rxGlobStar.MatchString + if cfg.DotGlob { + rx = rxGlobStarDotGlob.MatchString + } + newMatches, _ = cfg.globDir(base, dir, rx, wantDir, newMatches) + 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 + } + if cfg.DotGlob { + mode |= pattern.GlobLeadingDot + } + if cfg.ExtGlob { + mode |= pattern.ExtendedOperators + } + matcher, err := internal.ExtendedPatternMatcher(part, mode) if err != nil { return nil, err } - rx := regexp.MustCompile("^" + expr + "$") var newMatches []string for _, dir := range matches { - newMatches, err = cfg.globDir(base, dir, rx, wantDir, newMatches) + newMatches, err = cfg.globDir(base, dir, matcher, wantDir, newMatches) if err != nil { return nil, err } } matches = newMatches } + // Note that the results need to be sorted. + // TODO: above we do a BFS; if we did a DFS, the matches would already be sorted. + slices.Sort(matches) + // Remove any empty matches left behind from "**". + if len(matches) > 0 && matches[0] == "" { + matches = matches[1:] + } return matches, nil } -func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, matches []string) ([]string, error) { +func (cfg *Config) globDir(base, dir string, matcher func(string) bool, wantDir bool, matches []string) ([]string, error) { fullDir := dir 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,30 +1082,28 @@ 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) { + if matcher(name) { matches = append(matches, pathJoin2(dir, name)) } } 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..55afd45b40 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" @@ -23,6 +24,8 @@ func nodeLit(node syntax.Node) string { return "" } +// UnsetParameterError is returned when a parameter expansion encounters an +// unset variable and [Config.NoUnset] has been set. type UnsetParameterError struct { Node *syntax.ParamExp Message string @@ -65,13 +68,15 @@ 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 n, v := vr.Resolve(cfg.Env); n != "" { + name, vr = n, v + } + if cfg.NoUnset && !vr.IsSet() && !overridingUnset(pe) { return "", UnsetParameterError{ Node: pe, Message: "unbound variable", @@ -106,30 +111,13 @@ 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: indexAllElements = true callVarInd = false - elems = vr.List - slicePos := func(n int) int { - if n < 0 { - n = len(elems) + n - if n < 0 { - n = len(elems) - } - } else if n > len(elems) { - n = len(elems) - } - return n - } - if pe.Slice != nil && pe.Slice.Offset != nil { - elems = elems[slicePos(sliceOffset):] - } - if pe.Slice != nil && pe.Slice.Length != nil { - elems = elems[:slicePos(sliceLen)] - } + elems = cfg.sliceElems(pe, vr.List, name == "@" || name == "*") str = strings.Join(elems, " ") } } @@ -160,24 +148,28 @@ 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.Width: + return "", fmt.Errorf("unsupported") + case pe.IsSet: + return "", fmt.Errorf("unsupported") case pe.Slice != nil: if callVarInd { slicePos := func(n int) int { @@ -197,13 +189,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 +207,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 { @@ -325,8 +319,24 @@ func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { rns = append(rns, rn) } str = string(rns) - case "P", "A", "a": - panic(fmt.Sprintf("unhandled @%s param expansion", arg)) + case "a": + // ${var@a} returns variable attribute flags. + // We use orig (before nameref resolve) for the attributes. + str = orig.Flags() + case "A": + // ${var@A} returns a declare statement that recreates the variable. + flags := orig.Flags() + quoted, err := syntax.Quote(str, syntax.LangBash) + if err != nil { + return "", err + } + if flags == "" { + str = fmt.Sprintf("%s=%s", name, quoted) + } else { + str = fmt.Sprintf("declare -%s %s=%s", flags, name, quoted) + } + case "P": + // TODO: implement prompt expansion (\u, \h, \w, etc.). default: panic(fmt.Sprintf("unexpected @%s param expansion", arg)) } @@ -395,11 +405,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 +422,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..f5a6d1095a --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/expand/valuekind_string.go @@ -0,0 +1,29 @@ +// 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 { + idx := int(i) - 0 + if i < 0 || idx >= len(_ValueKind_index)-1 { + return "ValueKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ValueKind_name[_ValueKind_index[idx]:_ValueKind_index[idx+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/internal/pattern.go b/vendor/mvdan.cc/sh/v3/internal/pattern.go new file mode 100644 index 0000000000..d257967534 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/internal/pattern.go @@ -0,0 +1,77 @@ +// Copyright (c) 2026, Daniel Martí +// See LICENSE for licensing information + +package internal + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "mvdan.cc/sh/v3/pattern" +) + +// ExtendedPatternMatcher returns a [regexp.Regexp.MatchString]-like function +// to support !(pattern-list) extended patterns where possible. +// It can be used instead of [pattern.Regexp] for narrow use cases. +func ExtendedPatternMatcher(pat string, mode pattern.Mode) (func(string) bool, error) { + if mode&pattern.ExtendedOperators != 0 && mode&pattern.EntireString == 0 { + // In the future we could try to support !(pattern) without matching + // the entire input, ensuring we add enough test cases. + panic("ExtendedOperators is only supported with EntireString") + } + + // Extended pattern matching operators are always on outside of pathname expansion. + expr, err := pattern.Regexp(pat, mode) + if err != nil { + // Handle !(pattern-list) negation: when Regexp returns NegExtglobError, + // match the inner pattern and negate the result. + var negErr *pattern.NegExtGlobError + if !errors.As(err, &negErr) { + return nil, err + } + return extNegatedMatcher(pat, negErr.Groups) + } + rx := regexp.MustCompile(expr) + return rx.MatchString, nil +} + +// extNegatedMatcher handles !(pattern-list) extglob negation. +// Only a single !(...) group with fixed-string prefix and suffix is supported. +func extNegatedMatcher(pat string, groups []pattern.NegExtGlobGroup) (func(string) bool, error) { + if len(groups) != 1 { + return nil, fmt.Errorf("multiple extglob !(...) groups are not supported yet") + } + g := groups[0] + prefix := pat[:g.Start] + suffix := pat[g.End:] + + if pattern.HasMeta(prefix, 0) || pattern.HasMeta(suffix, 0) { + return nil, fmt.Errorf("extglob !(...) is only supported with a fixed prefix and suffix") + } + + // Use @(inner) to compile the pattern list, then negate the match. + inner := pat[g.Start+len("!(") : g.End-len(")")] + expr, err := pattern.Regexp("@("+inner+")", pattern.EntireString|pattern.ExtendedOperators) + if err != nil { + return nil, err + } + rx := regexp.MustCompile(expr) + + return func(name string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if !strings.HasSuffix(name, suffix) { + return false + } + end := len(name) - len(suffix) + if end < len(prefix) { + return false // prefix and suffix overlap in name + } + middle := name[len(prefix):end] + + return !rx.MatchString(middle) + }, nil +} diff --git a/vendor/mvdan.cc/sh/v3/internal/testing.go b/vendor/mvdan.cc/sh/v3/internal/testing.go new file mode 100644 index 0000000000..d9907c8092 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/internal/testing.go @@ -0,0 +1,52 @@ +// Copyright (c) 2026, Daniel Martí +// See LICENSE for licensing information + +package internal + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +// TestMainSetup is used by the integration tests running shell scripts +// either via our interpreter or via real shells, +// to ensure a reasonably clean and consistent environment. +func TestMainSetup() { + // Set the locale to computer-friendly English and UTF-8. + // Some systems like macOS miss C.UTF8, so fall back to the US English locale. + if out, _ := exec.Command("locale", "-a").Output(); strings.Contains( + strings.ToLower(string(out)), "c.utf", + ) { + os.Setenv("LANGUAGE", "C.UTF-8") + os.Setenv("LC_ALL", "C.UTF-8") + } else { + os.Setenv("LANGUAGE", "en_US.UTF-8") + os.Setenv("LC_ALL", "en_US.UTF-8") + } + + // Bash prints the pwd after changing directories when CDPATH is set. + os.Unsetenv("CDPATH") + + pathDir, err := os.MkdirTemp("", "interp-bin-") + if err != nil { + panic(err) + } + + // These short names are commonly used as variables. + // Ensure they are unset as env vars. + // We can't easily remove names from $PATH, + // so do the next best thing: override each name with a failing script. + for _, s := range []string{ + "a", "b", "c", "d", "e", "f", "foo", "bar", + } { + os.Unsetenv(s) + pathFile := filepath.Join(pathDir, s) + if err := os.WriteFile(pathFile, []byte("#!/bin/sh\necho NO_SUCH_COMMAND; exit 1"), 0o777); err != nil { + panic(err) + } + } + + os.Setenv("PATH", pathDir+string(os.PathListSeparator)+os.Getenv("PATH")) +} diff --git a/vendor/mvdan.cc/sh/v3/interp/api.go b/vendor/mvdan.cc/sh/v3/interp/api.go index 0648bd9db6..e785d48bf5 100644 --- a/vendor/mvdan.cc/sh/v3/interp/api.go +++ b/vendor/mvdan.cc/sh/v3/interp/api.go @@ -1,9 +1,13 @@ // Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information -// 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. +// Package interp implements an interpreter to execute shell programs +// parsed by the [syntax] package as either [syntax.LangBash] +// or [syntax.LangPOSIX], behaving like Bash as a result. +// +// 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 +15,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 +77,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 +109,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 + + lastExpandExit exitStatus // used to surface exit statuses while expanding fields - bgShells errgroup.Group + // 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,18 +168,90 @@ type Runner struct { callbackExit string } -type alias struct { - args []*syntax.Word - blank bool +// 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. + // When code is zero, err must be nil. + 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 holds the error information for a non-zero exit status code or fatal error. + // 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 (r *Runner) optByFlag(flag byte) *bool { - for i, opt := range &shellOptsTable { - if opt.flag == flag { - return &r.opts[i] - } +// clear sets the exit status code and error to zero, as long as the exit status +// was not set by `return`, `exit`, or a fatal error. +func (e *exitStatus) clear() { + if e.returning || e.exiting || e.fatalExit { + return + } + e.code = 0 + e.err = nil +} + +func (e *exitStatus) ok() bool { return e.code == 0 } + +// oneIf sets the exit status code to 1 if b is true. +// Note that it assumes the exit status hasn't been set yet, +// meaning that [exitStatus.code] and [exitStatus.err] are zero values. +func (e *exitStatus) oneIf(b bool) { + if b { + e.code = 1 + } +} + +func (e *exitStatus) fatal(err error) { + if e.fatalExit || err == nil { + return + } + 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 { + return + } + 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 } - return nil +} + +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 } // New creates a new Runner, applying a number of options. If applying any of @@ -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(posixOptsTable)+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. @@ -264,7 +371,7 @@ func Params(args ...string) RunnerOption { } enable := flag[0] == '-' if flag[1] != 'o' { - opt := r.optByFlag(flag[1]) + opt := r.posixOptByFlag(flag[1]) if opt == nil { return fmt.Errorf("invalid option: %q", flag) } @@ -273,13 +380,13 @@ func Params(args ...string) RunnerOption { } value := fp.value() if value == "" && enable { - for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) + for i, opt := range &posixOptsTable { + r.printOptLine(opt.name, r.opts[i], true) } continue } if value == "" && !enable { - for i, opt := range &shellOptsTable { + for i, opt := range &posixOptsTable { setFlag := "+o" if r.opts[i] { setFlag = "-o" @@ -288,7 +395,7 @@ func Params(args ...string) RunnerOption { } continue } - opt := r.optByName(value, false) + opt := r.posixOptByName(value) 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,30 +559,50 @@ func StdIO(in io.Reader, out, err io.Writer) RunnerOption { } } -func (r *Runner) optByName(name string, bash bool) *bool { - if bash { - for i, optName := range bashOptsTable { - if optName == name { - return &r.opts[len(shellOptsTable)+i] - } +func (r *Runner) posixOptByName(name string) *bool { + for i, opt := range &posixOptsTable { + if opt.name == name { + return &r.opts[i] } } - for i, opt := range &shellOptsTable { - if opt.name == name { + return nil +} + +func (r *Runner) posixOptByFlag(flag byte) *bool { + for i, opt := range &posixOptsTable { + if opt.flag == flag { return &r.opts[i] } } return nil } -type runnerOpts [len(shellOptsTable) + len(bashOptsTable)]bool +func (r *Runner) bashOptByName(name string) (status *bool, supported bool) { + for i, opt := range bashOptsTable { + if opt.name == name { + index := len(posixOptsTable) + i + return &r.opts[index], opt.supported + } + } + return nil, false +} + +// runnerOpts contains all POSIX Shell and Bash options as one contiguous table. +type runnerOpts [len(posixOptsTable) + len(bashOptsTable)]bool + +type posixOpt struct { + flag byte // one-character flag form for this option; a space if none exists + name string // full name of the option +} + +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 = [...]struct { - flag byte - name string -}{ - // sorted alphabetically by name; use a space for the options - // that have no flag form +var posixOptsTable = [...]posixOpt{ + // sorted alphabetically by name {'a', "allexport"}, {'e', "errexit"}, {'n', "noexec"}, @@ -399,17 +612,127 @@ 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: "dotglob", + defaultState: false, + supported: true, + }, + { + name: "expand_aliases", + defaultState: false, + supported: true, + }, + { + name: "extglob", + 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: "execfail"}, + {name: "extdebug"}, + { + 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 +741,13 @@ const ( optXTrace optPipeFail + // These correspond to indexes (offset by the above seven items) of + // supported options in [bashOptsTable] + optDotGlob optExpandAliases + optExtGlob optGlobStar + optNoCaseGlob optNullGlob ) @@ -441,17 +769,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 +818,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 +839,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 +867,81 @@ 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) - } - if r.exit != 0 { - r.setErr(NewExitStatus(uint8(r.exit))) + return fmt.Errorf("node can only be File, Stmt, or Command: %T", node) + } + 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 { + if r.exit.code == 0 { + // This should never happen; too much code relies on checking [exitStatus.code] + // to see if the last command succeeded or failed. [exitStatus.err] should only be + // additional information, so fail loudly if the invariant is broken. + panic("ended up with a non-nil exitStatus.err but a zero exitStatus.code") + } + 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 +950,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 +996,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..8c4cfd3533 100644 --- a/vendor/mvdan.cc/sh/v3/interp/builtin.go +++ b/vendor/mvdan.cc/sh/v3/interp/builtin.go @@ -4,74 +4,174 @@ package interp import ( + "bufio" "bytes" "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 { +// TODO: given the categories below, perhaps this should be more like: +// +// func IsBuiltin(lang syntax.LangVariant, name string) bool +// +// or perhaps some API that also lets the user iterate through the builtins? +// +// Also, should we move this to the syntax package too? +// It's not a syntactical property strictly speaking, +// but it's also odd to require importing the interp package for it. + +// IsBuiltin returns true if the given word is a POSIX Shell +// or Bash builtin. +func IsBuiltin(name string) bool { switch name { - 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": + case + // POSIX Shell builtins, from section 1.d obtained in September 2025 from: + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01_01 + "alias", + "bg", + "cd", + "command", + "false", + "fc", + "fg", + "getopts", + "hash", + "jobs", + "kill", + "newgrp", + "pwd", + "read", + "true", + "umask", + "unalias", + "wait", + + // POSIX Shell special built-ins, obtained in September 2025 from: + // https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_14 + "break", + ":", + "continue", + ".", + "eval", + "exec", + "exit", + "export", // NOTE: our parser treats this as a keyword + "readonly", // NOTE: our parser treats this as a keyword + "return", + "set", + "shift", + "times", + "trap", + "unset", + + // Bash built-ins which are not present in POSIX, obtained in September 2025 from: + // https://man.archlinux.org/man/bash.1.en#SHELL_BUILTIN_COMMANDS + "source", + "bind", + "builtin", + "caller", + "compgen", + "complete", + "compopt", + "declare", // NOTE: our parser treats this as a keyword + "typeset", // NOTE: our parser treats this as a keyword + "dirs", + "disown", + "echo", // TODO: surely this is POSIX? but why is it not in the main POSIX spec page? + "enable", + "history", + "help", + "let", // NOTE: our parser treats this as a keyword + "local", + "logout", + "mapfile", + "readarray", + "popd", + "printf", // TODO: surely this is POSIX? but why is it not in the main POSIX spec page? + "pushd", + "shopt", + "suspend", + "test", + "[", // NOTE: an alias for "test", not explicitly listed + "type", + "ulimit": 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 +185,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 +244,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 +260,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 +276,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 +287,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 +296,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,45 +316,62 @@ 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, "cd", 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 := "" fp := flagParser{remaining: args} for fp.more() { switch flag := fp.flag(); flag { - case "-a", "-f", "-P", "--help": - r.errf("command: NOT IMPLEMENTED\n") - return 3 - case "-p", "-t": + case "-a", "-f", "--help": + return failf(3, "command: NOT IMPLEMENTED\n") + case "-p", "-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() for _, arg := range args { - if mode == "-p" { + if mode == "-p" || mode == "-P" { if path, err := LookPathDir(r.Dir, r.writeEnv, arg); err == nil { r.outf("%s\n", path) } else { @@ -302,7 +413,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 +435,22 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a anyNotFound = true } if anyNotFound { - return 1 + exit.code = 1 } + case "hash": + // TODO: implement. for now, having this as a no-op is better than nothing. 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 +462,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 +497,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 +517,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 +530,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 +541,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 +549,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 +567,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 +595,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 + if code := r.changeDir(ctx, "pushd", newtop); code != 0 { + 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 + if code := r.changeDir(ctx, "pushd", args[0]); code != 0 { + 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 +627,67 @@ 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 + if code := r.changeDir(ctx, "popd", newtop); code != 0 { + 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 + readArray := false fp := flagParser{remaining: args} for fp.more() { switch flag := fp.flag(); flag { + case "-s": + silent = true case "-r": raw = true + case "-a": + readArray = 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,29 +695,52 @@ 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 - } - if len(args) == 0 { - args = append(args, "REPLY") + 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 readArray { + // read -a arrayname: split line into fields and assign to indexed array. + arrayName := shellReplyVar + if len(args) > 0 { + arrayName = args[0] + } + // Use -1 as max to get all fields without joining the last ones. + values := expand.ReadFields(r.ecfg, string(line), -1, raw) + r.setVar(arrayName, expand.Variable{ + Set: true, + Kind: expand.Indexed, + List: values, + }) + } else { + if len(args) == 0 { + args = append(args, shellReplyVar) + } - values := expand.ReadFields(r.ecfg, string(line), len(args), raw) - for i, name := range args { - val := "" - if i < len(values) { - val = values[i] + values := expand.ReadFields(r.ecfg, string(line), len(args), raw) + for i, name := range args { + val := "" + if i < len(values) { + val = values[i] + } + r.setVarString(name, val) } - 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 +752,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 +778,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 := "" @@ -660,36 +791,43 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a case "-o": posixOpts = true case "-p", "-q": - panic(fmt.Sprintf("unhandled shopt flag: %s", flag)) + return failf(2, "shopt: unsupported option %q\n", flag) default: - r.errf("shopt: invalid option %q\n", flag) - return 2 + return failf(2, "shopt: invalid option %q\n", flag) } } args := fp.args() if len(args) == 0 { - if !posixOpts { - for i, name := range bashOptsTable { - r.printOptLine(name, r.opts[len(shellOptsTable)+i]) + if posixOpts { + for i, opt := range &posixOptsTable { + r.printOptLine(opt.name, r.opts[i], true) + } + } else { + for i, opt := range bashOptsTable { + r.printOptLine(opt.name, r.opts[len(posixOptsTable)+i], opt.supported) } - break - } - for i, opt := range &shellOptsTable { - r.printOptLine(opt.name, r.opts[i]) } break } for _, arg := range args { - opt := r.optByName(arg, !posixOpts) + opt, supported := (*bool)(nil), true + if posixOpts { + opt = r.posixOptByName(arg) + } else { + opt, supported = r.bashOptByName(arg) + } if opt == nil { - r.errf("shopt: invalid option name %q\n", arg) - return 1 + return failf(1, "shopt: invalid option name %q\n", arg) } + switch mode { case "-s", "-u": + if !supported { + return failf(1, "shopt: unsupported option %q\n", arg) + } *opt = mode == "-s" default: // "" - r.printOptLine(arg, *opt) + r.printOptLine(arg, *opt, supported) } } r.updateExpandOpts() @@ -714,9 +852,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 +868,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 +895,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 +933,100 @@ 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: unsupported builtin\n", name) } - return 0 + return exit } -func (r *Runner) printOptLine(name string, enabled bool) { - status := "off" - if enabled { - status = "on" +// 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 + } +} + +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 +1034,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,30 +1067,30 @@ 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 { +func (r *Runner) changeDir(ctx context.Context, cmd, path string) uint8 { if path == "" { - path = "." + r.errf("%s: empty directory path\n", cmd) + return 1 } - path = r.absPath(path) - info, err := r.stat(ctx, path) + apath := r.absPath(path) + info, err := r.stat(ctx, apath) if err != nil || !info.IsDir() { + r.errf("%s: no such file or directory: %q\n", cmd, path) return 1 } - if !hasPermissionToDir(info) { + if r.access(ctx, apath, access_X_OK) != nil { + r.errf("%s: permission denied: %q\n", cmd, path) return 1 } - r.Dir = path + r.Dir = apath r.setVarString("OLDPWD", r.envGet("PWD")) - r.setVarString("PWD", path) + r.setVarString("PWD", apath) return 0 } @@ -877,7 +1101,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 +1211,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..a60f6fd8f2 100644 --- a/vendor/mvdan.cc/sh/v3/interp/handler.go +++ b/vendor/mvdan.cc/sh/v3/interp/handler.go @@ -7,19 +7,21 @@ 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. +// HandlerCtx returns the [HandlerContext] value stored in ctx, +// which is used when calling handler functions. // It panics if ctx has no HandlerContext stored. func HandlerCtx(ctx context.Context) HandlerContext { hc, ok := ctx.Value(handlerCtxKey{}).(HandlerContext) @@ -31,9 +33,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 +59,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 +76,16 @@ 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 context includes a [HandlerContext] value. +// // The call's arguments are replaced by what the handler returns, // and then the call is executed by the Runner as usual. +// The args slice is never empty. // 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 +93,26 @@ 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. +// The args slice is never empty. +// The context includes a [HandlerContext] value. // // 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 +120,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 +141,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 +230,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 +244,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 +280,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) @@ -268,7 +295,7 @@ func pathExts(env expand.Environ) []string { return []string{".com", ".exe", ".bat", ".cmd"} } var exts []string - for _, e := range strings.Split(strings.ToLower(pathext), `;`) { + for e := range strings.SplitSeq(strings.ToLower(pathext), `;`) { if e == "" { continue } @@ -280,49 +307,82 @@ 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. +// The context includes a [HandlerContext] value. +// 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. +// Any other error will halt the [Runner] and will be returned via the API. // -// 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. +// 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" + // Note that even though https://go.dev/issue/71752 was resolved for Windows, + // the workaround here seems to still be required for Wine as of 10.14. + // TODO(mvdan): Why? Is this Wine's fault? + 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. +// The context includes a [HandlerContext] value. +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. +// The context includes a [HandlerContext] value. +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..e7ca682a42 --- /dev/null +++ b/vendor/mvdan.cc/sh/v3/interp/os_notunix.go @@ -0,0 +1,58 @@ +// 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 { + r.errf("unsupported unary test op: %v\n", op) + return false +} + +// 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..f0096b1e8a 100644 --- a/vendor/mvdan.cc/sh/v3/interp/runner.go +++ b/vendor/mvdan.cc/sh/v3/interp/runner.go @@ -6,22 +6,41 @@ package interp import ( "bytes" "context" + "errors" "fmt" "io" + "io/fs" + "iter" "math" - "math/rand" + mathrand "math/rand/v2" "os" - "regexp" + "path/filepath" "runtime" + "slices" + "strconv" "strings" "sync" "time" "mvdan.cc/sh/v3/expand" + "mvdan.cc/sh/v3/internal" "mvdan.cc/sh/v3/pattern" "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 + case 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 @@ -115,8 +142,12 @@ func (r *Runner) fillExpandConfig(ctx context.Context) { f.Close() os.Remove(path) }() + default: + // Should only happen if we forgot a case above. + panic(fmt.Sprintf("unexpected process substitution operator: %q", ps.Op)) } r2.stmts(ctx, ps.Stmts) + r2.exit.exiting = false // subshells don't exit the parent shell }() return path, nil }, @@ -127,7 +158,7 @@ func (r *Runner) fillExpandConfig(ctx context.Context) { // catShortcutArg checks if a statement is of the form "$(, 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 +373,46 @@ 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 { + name := as.Name.Value + + prev := r.lookupVar(name) + // 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 + + name, vr := r.assignVal(name, prev, as, "") + r.setVarWithIndex(prev, name, as.Index, vr) if !tracingEnabled { continue @@ -359,14 +428,14 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { if err != nil { // should never happen panic(err) } - trace.stringf("%s=%s", as.Name.Value, val) + trace.stringf("%s=%s", name, val) } trace.newLineFlush() } // 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,94 +447,99 @@ 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) + // Resolve any nameref so we can restore the original final value later on. + if n, v := prev.Resolve(r.writeEnv); n != "" { + name, prev = n, v + } - vr := r.assignVal(as, "") + name, vr := r.assignVal(name, 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 } r.stdin = pr var wg sync.WaitGroup - wg.Add(1) - go func() { - r2.stmt(ctx, x.X) + wg.Go(func() { + 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.clear() + 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.clear() + 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 +549,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 +602,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 +611,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 +620,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 +664,16 @@ 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 { + declQuery := "" // "-f" or "-p" for query mode + switch cm.Variant.Value { case "declare": // When used in a function, "declare" acts as "local" // unless the "-g" option is used. @@ -565,7 +681,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 +692,129 @@ 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 + case "-f", "-p": + declQuery = flag + 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) + continue assignLoop + } + name := as.Name.Value + if !syntax.ValidName(name) { + r.errf("declare: invalid name %q\n", name) + r.exit.code = 1 + return + } + if declQuery == "-f" { + // declare -f name: print function definition. + // Bash silently returns exit 1 for missing functions. + if body := r.Funcs[name]; body != nil { + r.outf("%s()\n", name) + printer := syntax.NewPrinter() + var buf bytes.Buffer + printer.Print(&buf, body) + r.outf("%s\n", buf.String()) + } else { + r.exit.code = 1 + } + continue + } + if declQuery == "-p" { + // declare -p name: print variable with attributes. + vr := r.lookupVar(name) + if !vr.Declared() { + r.errf("declare: %s: not found\n", name) + r.exit.code = 1 + continue } - if global { - vr.Local = false - } else if local { - vr.Local = true + flags := vr.Flags() + if flags == "" { + flags = "-" } - for _, mode := range modes { - switch mode { - case "-x": - vr.Exported = true - case "-r": - vr.ReadOnly = true + switch vr.Kind { + case expand.Indexed: + r.outf("declare -%s %s=(", flags, name) + for i, v := range vr.List { + if i > 0 { + r.out(" ") + } + r.outf("[%d]=%q", i, v) } - } - if as.Naked { - if vr.Exported || vr.Local || vr.ReadOnly { - r.setVarInternal(name, vr) + r.out(")\n") + case expand.Associative: + r.outf("declare -%s %s=(", flags, name) + first := true + for k, v := range vr.Map { + if !first { + r.out(" ") + } + r.outf("[%s]=%q", k, v) + first = false } + r.out(")\n") + default: + r.outf("declare -%s %s=%q\n", flags, name, vr.Str) + } + continue + } + vr := r.lookupVar(name) + if as.Naked { + if valType == "-A" { + vr.Kind = expand.Associative } else { - r.setVar(name, as.Index, vr) + vr.Kind = expand.KeepValue } + } else { + name, vr = r.assignVal(name, 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)) + // Should only happen if we forgot a case above. + r.errf("unhandled command node: %T\n", cm) + r.exit.code = 1 } } @@ -663,54 +835,48 @@ 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) - if err != nil { - return false - } - rx := regexp.MustCompile("(?m)^" + expr + "$") - return rx.MatchString(name) + matcher, err := internal.ExtendedPatternMatcher(pat, pattern.EntireString|pattern.ExtendedOperators) + _ = err // TODO: report these errors + return matcher != nil && matcher(name) } func elapsedString(d time.Duration, posix bool) string { @@ -728,10 +894,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 @@ -748,51 +926,90 @@ func (r *Runner) hdocReader(rd *syntax.Redirect) io.Reader { cur = append(cur, wp) continue } - for i, part := range strings.Split(lit.Value, "\n") { - if i > 0 { + first := true + for part := range strings.SplitSeq(lit.Value, "\n") { + if !first { flushLine() cur = cur[:0] } + first = false part = strings.TrimLeft(part, "\t") cur = append(cur, &syntax.Lit{Value: part}) } } 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: + return nil, fmt.Errorf("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: + return nil, fmt.Errorf("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: + return nil, fmt.Errorf("unhandled %v arg: %q", rd.Op, arg) + } + return nil, nil default: - panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op)) + return nil, fmt.Errorf("unhandled redirect op: %v", rd.Op) } mode := os.O_RDONLY switch rd.Op { @@ -807,14 +1024,18 @@ 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: r.stdout = f r.stderr = f default: - panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op)) + return nil, fmt.Errorf("unhandled redirect op: %v", rd.Op) } return f, nil } @@ -837,20 +1058,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 +1080,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 +1090,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..23101cb901 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) @@ -107,8 +116,11 @@ func (r *Runner) binTest(ctx context.Context, op syntax.BinTestOperator, x, y st return x != "" || y != "" case syntax.TsBefore: return x < y - default: // syntax.TsAfter + case syntax.TsAfter: return x > y + default: + // Should only happen if we forgot a case above. + panic(fmt.Sprintf("unexpected binary test operator: %q", op)) } } @@ -117,6 +129,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: @@ -146,30 +165,21 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) return r.statMode(ctx, x, os.ModeSetuid) case syntax.TsGIDSet: return r.statMode(ctx, x, os.ModeSetgid) - // case syntax.TsGrpOwn: - // case syntax.TsUsrOwn: - // case syntax.TsModif: + case syntax.TsModif: + r.errf("unsupported unary test op: %v\n", op) + return false 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 +189,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 +200,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.posixOptByName(x); opt != nil { return *opt } return false @@ -200,7 +210,10 @@ 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)) + // Should only happen if we forgot a case above. + panic(fmt.Sprintf("unexpected 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..0037933078 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,13 +51,13 @@ 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 op := testBinaryOp(p.val) if op == illegalTok { - p.errf("not a valid test operator: %s", p.val) + p.errf("not a valid test operator: %#q", p.val) } b := &syntax.BinaryTest{ Op: op, @@ -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..773fd07f51 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,81 +19,108 @@ 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. + for name, vr := range parent.Each { + oenv.Set(name, vr) + } + } + 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 + + // values maps normalized variable names, per [overlayEnviron.normalize]. + values map[string]namedVariable // 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 } +// namedVariable records the original name of a variable for platforms +// where variable names are matched in a case-insensitive way. +type namedVariable struct { + // TODO(v4): consider adding this field to [expand.Variable], + // as a general way for a variable to report its original name. + // This can be useful for GOOS=windows with case insensitive env vars, + // as otherwise it's not possible to Environ.Get a var + // and know what was its original name without looping over Environ.Each. + Name string + expand.Variable +} + +func (o *overlayEnviron) normalize(name string) string { + if runtime.GOOS == "windows" { + return strings.ToUpper(name) + } + return name +} + func (o *overlayEnviron) Get(name string) expand.Variable { - if vr, ok := o.values[name]; ok { - return vr + normalized := o.normalize(name) + if vr, ok := o.values[normalized]; ok { + return vr.Variable + } + if o.parent != nil { + return o.parent.Get(name) } - 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 + normalized := o.normalize(name) + prev, inOverlay := o.values[normalized] + // 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.Variable = 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 { + o.values = make(map[string]namedVariable) + } + 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 if prev.Local { vr.Local = true - o.values[name] = vr - return nil - } - delete(o.values, name) - if writeEnv, _ := o.parent.(expand.WriteEnviron); writeEnv != nil { - writeEnv.Set(name, vr) + o.values[normalized] = namedVariable{name, vr} return nil } - } else if prev.Exported { - // variable is set and was marked as exported - vr.Exported = true + delete(o.values, normalized) } // modifying the entire variable vr.Local = prev.Local || vr.Local - o.values[name] = vr + o.values[normalized] = namedVariable{name, vr} return nil } func (o *overlayEnviron) Each(f func(name string, vr expand.Variable) bool) { - o.parent.Each(f) - for name, vr := range o.values { - if !f(name, vr) { + if o.parent != nil { + o.parent.Each(f) + } + for _, vr := range o.values { + if !f(vr.Name, vr.Variable) { return } } @@ -96,7 +128,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 +144,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 +164,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,26 +192,18 @@ 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() { - return vr - } - } return expand.Variable{} } @@ -179,37 +214,31 @@ 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 != "" { - name = name2 - cur = var2 - } - +func (r *Runner) setVarWithIndex(prev expand.Variable, name string, index syntax.ArithmExpr, vr expand.Variable) { 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 +250,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 +259,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 +273,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 +288,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,22 +312,26 @@ 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(name string, prev expand.Variable, as *syntax.Assign, valType string) (string, expand.Variable) { + if n, v := prev.Resolve(r.writeEnv); n != "" { + name, prev = n, v + } + 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 } prev.Str = s - return prev + return name, 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 { @@ -301,7 +341,7 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { case expand.Associative: // TODO } - return prev + return name, prev } if as.Array == nil { // don't return the zero value, as that's an unset variable @@ -310,7 +350,7 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { prev.Kind = expand.NameRef } prev.Str = "" - return prev + return name, prev } // Array assignment. elems := as.Array.Elems @@ -329,10 +369,10 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { if !as.Append { prev.Kind = expand.Associative prev.Map = amap - return prev + return name, prev } // TODO - return prev + return name, prev } // Evaluate values for each array element. elemValues := make([]struct { @@ -351,9 +391,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) @@ -365,10 +403,10 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { if !as.Append { prev.Kind = expand.Indexed prev.List = strs - return prev + return name, prev } switch prev.Kind { - case expand.Unset: + case expand.Unknown: prev.Kind = expand.Indexed prev.List = strs case expand.String: @@ -379,7 +417,8 @@ func (r *Runner) assignVal(as *syntax.Assign, valType string) expand.Variable { case expand.Associative: // TODO default: - panic(fmt.Sprintf("unhandled conversion of kind %d", prev.Kind)) + // Should only happen if we forgot a case above. + panic(fmt.Sprintf("unexpected conversion of kind %d", prev.Kind)) } - return prev + return name, prev } diff --git a/vendor/mvdan.cc/sh/v3/pattern/pattern.go b/vendor/mvdan.cc/sh/v3/pattern/pattern.go index fd80f71721..f4128b1bc1 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,322 @@ func (e SyntaxError) Error() string { return e.msg } func (e SyntaxError) Unwrap() error { return e.err } +// NegExtGlobGroup represents the byte offset range of a single !(expr) group +// within a pattern string. Start is the offset of '!', End is one past ')'. +type NegExtGlobGroup struct { + Start, End int +} + +// NegExtGlobError is returned by [Regexp] when an extglob negation operator +// !(pattern-list) is encountered, as Go's [regexp] package does not support +// negative lookahead. Callers can handle this by negating the result of +// matching the inner pattern. +type NegExtGlobError struct { + Groups []NegExtGlobGroup +} + +func (e *NegExtGlobError) Error() string { + return "extglob !(...) is not supported in this scenario" +} + +// 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; only makes sense with EntireString too + EntireString // match the entire string using ^$ delimiters + NoGlobCase // do case-insensitive match (that is, use (?i) in the regexp); shopt "nocaseglob" + NoGlobStar // do not support "**"; negated shopt "globstar" + GlobLeadingDot // let wildcards match leading dots in filenames; shopt "dotglob" + ExtendedOperators // support extended pattern matching operators; shopt "extglob" for pathname expansion ) -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-- - } - } else { - buf.WriteString("[^/]*") - i-- - } - } 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 mode&Shortest != 0 { + sb.WriteString(`U`) + } + sb.WriteString(`)`) + if mode&EntireString != 0 { + sb.WriteString(`^`) + } + sl := stringLexer{s: pat} + var negGroups []NegExtGlobGroup + for { + if err := regexpNext(&sb, &sl, mode); err == io.EOF { + break + } else if err != nil { + negErr, ok := err.(*NegExtGlobError) + if !ok { + return "", err } - if name != "" { - buf.WriteString(name) - i += len(name) - 1 + negGroups = append(negGroups, negErr.Groups...) + } + } + if len(negGroups) > 0 { + return "", &NegExtGlobError{Groups: negGroups} + } + 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. +type stringLexer struct { + s string + i int +} + +func (sl *stringLexer) next() rune { + if sl.i >= len(sl.s) { + return '\x00' + } + c, size := utf8.DecodeRuneInString(sl.s[sl.i:]) + sl.i += size + return c +} + +func (sl *stringLexer) last() rune { + if sl.i < 2 { + return '\x00' + } + c, _ := utf8.DecodeLastRuneInString(sl.s[:sl.i-1]) + return c +} + +func (sl *stringLexer) peekNext() rune { + if sl.i >= len(sl.s) { + return '\x00' + } + c, _ := utf8.DecodeRuneInString(sl.s[sl.i:]) + return c +} + +func (sl *stringLexer) peekRest() string { + return sl.s[sl.i:] +} + +func regexpNext(sb *strings.Builder, sl *stringLexer, mode Mode) error { + c := sl.next() + if mode&ExtendedOperators != 0 { + // Handle extended pattern matching operators separately, + // given that they can be one of many two-character prefixes. + // Note that we recurse into the same function in a loop, + // as each of the patterns in the list separated by '|' is a regular pattern. + switch op := c; op { + case '!', '?', '*', '+', '@': + if sl.peekNext() != '(' { 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]) - } + start := sl.i - 1 // position of the operator + sb.WriteRune(sl.next()) // ( + nestedLoop: + for { + switch sl.peekNext() { + case ')': + break nestedLoop + case '|': + // extended operators support a list of "or" separated expressions + sb.WriteRune(sl.next()) 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 + if err := regexpNext(sb, sl, mode); err == io.EOF { + break + } else if err != nil { + return err } } - if i >= len(pat) { - return "", &SyntaxError{msg: "[ was not matched with a closing ]"} + sb.WriteRune(sl.next()) // ) + if op == '!' { + return &NegExtGlobError{Groups: []NegExtGlobGroup{{Start: start, End: sl.i}}} } - case '{': - if mode&Braces == 0 { - buf.WriteString(regexp.QuoteMeta(string(c))) - break + if op != '@' { + // @( is [syntax.GlobOne] for matching once; no suffix needed + sb.WriteRune(op) } - 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 + return nil + } + } + switch 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 { + // ** - match any number of slashes or "*" path elements + slashSuffix := sl.peekNext() == '/' + if slashSuffix { + // **/ - like "**" but requiring a trailing slash when matching + sl.i++ + // wrap the expression to ensure that any match has a slash suffix + sb.WriteString(`(`) } - } - 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])} + if mode&GlobLeadingDot == 0 { + sb.WriteString(`(/|[^/.][^/]*)*`) + } else { + // with GlobLeadingDot (dotglob), match anything at all + sb.WriteString(`.*`) } - // TODO: can we do better here? - buf.WriteString("(?:") - for n := start; n <= end; n++ { - if n > start { - buf.WriteByte('|') - } - fmt.Fprintf(&buf, "%d", n) + if slashSuffix { + sb.WriteString(`/)?`) } - 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('|') + // foo**, **bar, or NoGlobStar - behaves like "*" below + } + // * - matches anything except slashes and leading dots + if singleBefore && mode&GlobLeadingDot == 0 { + sb.WriteString(`([^/.][^/]*)?`) + } else { + // with GlobLeadingDot (dotglob), match anything except slashes + 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 i, c := range sl.peekRest() { + if i > 0 && c == ']' { + break + } else if c == '/' { + sb.WriteString(`\[`) + return nil + } + } + } + sb.WriteRune(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 ]"} } - case '}': - if len(closingBraces) > 0 && closingBraces[len(closingBraces)-1] == i { - buf.WriteByte(')') - closingBraces = closingBraces[:len(closingBraces)-1] - } else { - buf.WriteString(regexp.QuoteMeta(string(c))) + } + if c == ']' { + sb.WriteByte(']') + if c = sl.next(); c == '\x00' { + return &SyntaxError{msg: "[ was not matched with a closing ]"} } - default: - if c > 128 { - buf.WriteByte(c) - } else { - buf.WriteString(regexp.QuoteMeta(string(c))) + } + for { + sb.WriteRune(c) + switch c { + case '\x00': + return &SyntaxError{msg: "[ was not matched with a closing ]"} + case '\\': + if c = sl.next(); c != '0' { + sb.WriteRune(c) + } + 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)} + } + case ']': + return nil } + c = sl.next() + } + default: + if c > utf8.RuneSelf { + sb.WriteRune(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 +354,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 +366,6 @@ func HasMeta(pat string, mode Mode) bool { i++ case '*', '?', '[': return true - case '{': - if mode&Braces != 0 { - return true - } } } return false @@ -283,35 +375,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..743b188d16 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/braces.go +++ b/vendor/mvdan.cc/sh/v3/syntax/braces.go @@ -3,7 +3,11 @@ package syntax -import "strconv" +import ( + "slices" + "strconv" + "strings" +) var ( litLeftBrace = &Lit{Value: "{"} @@ -12,10 +16,10 @@ var ( litRightBrace = &Lit{Value: "}"} ) -// SplitBraces parses brace expansions within a word's literal parts. If any -// valid brace expansions are found, they are replaced with BraceExp nodes, and -// the function returns true. Otherwise, the word is left untouched and the -// function returns false. +// SplitBraces parses brace expansions within a word's literal parts. +// If any valid brace expansions are found, they are replaced with BraceExp nodes, +// and the function returns true. +// Otherwise, the word is left untouched and the function returns false. // // For example, a literal word "foo{bar,baz}" will result in a word containing // the literal "foo", and a brace expansion with the elements "bar" and "baz". @@ -23,7 +27,13 @@ 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 !slices.ContainsFunc(word.Parts, func(part WordPart) bool { + lit, ok := part.(*Lit) + return ok && strings.Contains(lit.Value, "{") + }) { + // In the common case where a word has no braces, skip any allocs. + return false + } top := &Word{} acc := top var cur *BraceExp @@ -90,7 +100,6 @@ func SplitBraces(word *Word) bool { if cur == nil { continue } - any = true addlitidx() br := pop() if len(br.Elems) == 1 { @@ -109,8 +118,7 @@ func SplitBraces(word *Word) bool { for i, elem := range br.Elems[:2] { val := elem.Lit() if _, err := strconv.Atoi(val); err == nil { - } else if len(val) == 1 && - 'a' <= val[0] && val[0] <= 'z' { + } else if len(val) == 1 && asciiLetter(val[0]) { chars[i] = true } else { broken = true @@ -154,9 +162,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..7747501d08 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/lexer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/lexer.go @@ -9,6 +9,14 @@ import ( "unicode/utf8" ) +func asciiLetter[T rune | byte](r T) bool { + return ('a' <= r && r <= 'z') || ('A' <= r && r <= 'Z') +} + +func asciiDigit[T rune | byte](r T) bool { + return r >= '0' && r <= '9' +} + // bytes that form or start a token func regOps(r rune) bool { switch r { @@ -28,20 +36,11 @@ func paramOps(r rune) bool { return false } -// these start a parameter expansion name -func paramNameOp(r rune) bool { - switch r { - case '}', ':', '+', '=', '%', '[', ']', '/', '^', ',': - return false - } - return true -} - // tokenize these inside arithmetic expansions func arithmOps(r rune) bool { switch r { case '+', '-', '!', '~', '*', '/', '%', '(', ')', '^', '<', '>', ':', '=', - ',', '?', '|', '&', '[', ']', '#': + ',', '?', '|', '&', '[', ']', '#', '.': return true } return false @@ -61,89 +60,108 @@ 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 b := p.bs[p.bsp]; b < utf8.RuneSelf { - p.bsp++ - if b == '\x00' { - // Ignore null bytes while parsing, like bash. + if p.bsp >= uint(len(p.bs)) && p.fill() == 0 { + if len(p.bs) == 0 { + // Necessary for the last position to be correct. + // TODO: this is not exactly intuitive; figure out a better way. + p.bsp = 1 + } + p.r = utf8.RuneSelf + p.w = 1 + return p.r + } + if b := p.bs[p.bsp]; b < utf8.RuneSelf { + p.bsp++ + switch b { + case '\x00': + // Ignore null bytes while parsing, like bash. + p.col++ + goto retry + case '\r': + if p.peek() == '\n' { // \r\n turns into \n + p.col++ goto retry } - if b == '\\' { - if p.r == '\\' { - } else if p.peekByte('\n') { - p.bsp++ - p.w, p.r = 1, escNewl - return escNewl - } else if p.peekBytes("\r\n") { - 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]) { - bquotes++ - goto retry - } - } - if b == '`' { - p.lastBquoteEsc = bquotes + case '\\': + if p.r == '\\' { + } else if p.peek() == '\n' { + p.bsp++ + p.w, p.r = 1, escNewl + return escNewl + } else if p1, p2 := p.peekTwo(); p1 == '\r' && p2 == '\n' { // \\\r\n turns into \\\n + p.col++ + p.bsp += 2 + p.w, p.r = 2, escNewl + return escNewl } - if p.litBs != nil { - p.litBs = append(p.litBs, b) + // TODO: why is this necessary to ensure correct position info? + p.readEOF = false + if p.openBquotes > 0 && bquotes < p.openBquotes && + 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 } - p.w, p.r = 1, rune(b) - return p.r } - if !utf8.FullRune(p.bs[p.bsp:]) { - // we need more bytes to read a full non-ascii rune - p.fill() + if b == '`' { + p.lastBquoteEsc = bquotes } - 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, b) } - p.bsp += w - if p.r == utf8.RuneError && w == 1 { - p.posErr(p.nextPos(), "invalid UTF-8 encoding") - } - p.w = w - } else { - if p.r == utf8.RuneSelf { - } else if p.fill(); p.bs == nil { - p.bsp++ - p.r = utf8.RuneSelf - p.w = 1 - } else { - goto retry + p.w, p.r = 1, rune(b) + return p.r + } +decodeRune: + var w int + p.r, w = utf8.DecodeRune(p.bs[p.bsp:]) + if p.r == utf8.RuneError && !utf8.FullRune(p.bs[p.bsp:]) { + // we need more bytes to read a full non-ascii rune + if p.fill() > 0 { + goto decodeRune } } + if p.litBs != nil { + p.litBs = append(p.litBs, p.bs[p.bsp:p.bsp+uint(w)]...) + } + p.bsp += uint(w) + if p.r == utf8.RuneError && w == 1 { + p.posErr(p.nextPos(), "invalid UTF-8 encoding") + } + p.w = w return p.r } -// fill reads more bytes from the input src into readBuf. Any bytes that -// 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 +// fill reads more bytes from the input src into readBuf. +// Any bytes that had not yet been used at the end of the buffer +// are slid into the beginning of the buffer. +// The number of read bytes is returned, which is at least one +// unless a read error occurred, such as [io.EOF]. +func (p *Parser) fill() (n int) { + if p.readEOF || p.r == utf8.RuneSelf { + // If the reader already gave us [io.EOF], do not try again. + // If we decided to stop for any reason, do not bother reading either. + return 0 + } + 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 if err == nil { n, err = p.src.Read(p.readBuf[left:]) p.readErr = err + if err == io.EOF { + p.readEOF = true + } } if n == 0 { if err == nil { @@ -162,28 +180,21 @@ readAgain: p.bs = p.readBuf[:left+n] } p.bsp = 0 + return n } func (p *Parser) nextKeepSpaces() { r := p.r if p.quote != hdocBody && p.quote != hdocBodyTabs { - // Heredocs handle escaped newlines in a special way, but others - // do not. + // Heredocs handle escaped newlines in a special way, but others do not. for r == escNewl { r = p.rune() } } p.pos = p.nextPos() switch p.quote { - case paramExpRepl: - switch r { - case '}', '/': - p.tok = p.paramToken(r) - case '`', '"', '$', '\'': - p.tok = p.regToken(r) - default: - p.advanceLitOther(r) - } + case runeByRune: + p.tok = illegalTok case dblQuotes: switch r { case '`', '"', '$': @@ -198,7 +209,14 @@ func (p *Parser) nextKeepSpaces() { default: p.advanceLitHdoc(r) } - default: // paramExpExp: + case paramExpRepl: + if r == '/' { + p.rune() + p.tok = slash + break + } + fallthrough + case paramExpExp: switch r { case '}': p.tok = p.paramToken(r) @@ -208,7 +226,7 @@ func (p *Parser) nextKeepSpaces() { p.advanceLitOther(r) } } - if p.err != nil && p.tok != _EOF { + if p.err != nil { p.tok = _EOF } } @@ -256,7 +274,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 @@ -268,8 +286,18 @@ skipSpace: case p.quote&allRegTokens != 0: switch r { case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`': + if r == '<' && p.lang.in(LangZsh) && p.zshNumRange() { + p.advanceLitNone(r) + return + } 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. + if p.quote == unquotedWordCont && !p.spaced { + p.advanceLitNone(r) + return + } r = p.rune() p.newLit(r) runeLoop: @@ -296,14 +324,26 @@ skipSpace: p.litBs = nil } p.next() - case '[', '=': + case '[': if p.quote == arrayElems { - p.tok = p.paramToken(r) + p.rune() + p.tok = leftBrack + } else { + p.advanceLitNone(r) + } + case '=': + if p.peek() == '(' { + p.rune() + p.rune() + p.tok = assgnParen + } else if p.quote == arrayElems { + p.rune() + p.tok = assgn } else { p.advanceLitNone(r) } case '?', '*', '+', '@', '!': - if p.tokenizeGlob() { + if p.extendedGlob() { switch r { case '?': p.tok = globQuest @@ -313,7 +353,7 @@ skipSpace: p.tok = globPlus case '@': p.tok = globAt - default: // '!' + case '!': p.tok = globExcl } p.rune() @@ -330,7 +370,7 @@ skipSpace: p.tok = p.paramToken(r) case p.quote == testExprRegexp: if !p.rxFirstPart && p.spaced { - p.quote = noState + p.quote = testExpr goto skipSpace } p.rxFirstPart = false @@ -343,7 +383,7 @@ skipSpace: p.advanceLitRe(r) } else { p.tok = rightParen - p.quote = noState + p.quote = testExpr p.rune() // we are tokenizing manually } default: // including '(', '|' @@ -354,48 +394,66 @@ skipSpace: default: p.advanceLitOther(r) } - if p.err != nil && p.tok != _EOF { + if p.err != nil { p.tok = _EOF } } -// tokenizeGlob determines whether the expression should be tokenized as a glob literal -func (p *Parser) tokenizeGlob() bool { - if p.val == "function" { +// 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.lang.in(LangZsh) { + // Zsh supports Bash extended globs via the KSH_GLOB option. + // In Bash we would parse extended globs as [ExtGlob] nodes, + // but trying to do that in Zsh would cause ambiguity with glob qualifiers. + // Just like glob qualifiers, parse extended globs as literals in Zsh. 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("()") { + if p.val == "function" { + // We don't support e.g. `function @() { ... }` at the moment, but we could. return false } - return p.peekByte('(') + if p.peek() == '(' { + // 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. + _, p2 := p.peekTwo() + return p2 != ')' + } + return false } -func (p *Parser) peekBytes(s string) bool { - for p.bsp+(len(p.bs)-1) >= len(p.bs) { +func (p *Parser) peek() byte { + if int(p.bsp) >= len(p.bs) { p.fill() } - bw := p.bsp + len(s) - return bw <= len(p.bs) && bytes.HasPrefix(p.bs[p.bsp:bw], []byte(s)) + if int(p.bsp) >= len(p.bs) { + return utf8.RuneSelf + } + return p.bs[p.bsp] } -func (p *Parser) peekByte(b byte) bool { - if p.bsp == len(p.bs) { +func (p *Parser) peekTwo() (byte, byte) { + // 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 int(p.bsp+1) >= len(p.bs) { p.fill() } - return p.bsp < len(p.bs) && p.bs[p.bsp] == b + if int(p.bsp) >= len(p.bs) { + return utf8.RuneSelf, utf8.RuneSelf + } + if int(p.bsp+1) >= len(p.bs) { + return p.bs[p.bsp], utf8.RuneSelf + } + return p.bs[p.bsp], p.bs[p.bsp+1] } 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,14 +469,39 @@ func (p *Parser) regToken(r rune) token { p.rune() return andAnd case '>': - if p.lang == LangPOSIX { - break - } - if p.rune() == '>' { + switch p.rune() { + case '|': p.rune() + return rdrAllClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return rdrAllClob + } + case '>': + switch p.rune() { + case '|': + p.rune() + return appAllClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return appAllClob + } + } return appAll } return rdrAll + case '|': + if p.lang.in(LangZsh) { + p.rune() + return andPipe + } + case '!': + if p.lang.in(LangZsh) { + p.rune() + return andBang + } } return and case '|': @@ -427,7 +510,7 @@ func (p *Parser) regToken(r rune) token { p.rune() return orOr case '&': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { break } p.rune() @@ -437,13 +520,13 @@ func (p *Parser) regToken(r rune) token { case '$': switch p.rune() { case '\'': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { break } p.rune() return dollSglQuote case '"': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn) { break } p.rune() @@ -452,7 +535,7 @@ func (p *Parser) regToken(r rune) token { p.rune() return dollBrace case '[': - if !p.lang.isBash() || p.quote == paramExpName { + if !p.lang.in(langBashLike) { // latter to not tokenise ${$[@]} as $[ break } @@ -467,7 +550,7 @@ func (p *Parser) regToken(r rune) token { } return dollar case '(': - if p.rune() == '(' && p.lang != LangPOSIX && p.quote != testExpr { + if p.rune() == '(' && p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.quote != testExpr { p.rune() return dblLeftParen } @@ -478,19 +561,19 @@ func (p *Parser) regToken(r rune) token { case ';': switch p.rune() { case ';': - if p.rune() == '&' && p.lang.isBash() { + if p.rune() == '&' && p.lang.in(langBashLike) { p.rune() return dblSemiAnd } return dblSemicolon case '&': - if p.lang == LangPOSIX { + if !p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { break } p.rune() return semiAnd case '|': - if p.lang != LangMirBSDKorn { + if !p.lang.in(LangMirBSDKorn) { break } p.rune() @@ -500,10 +583,11 @@ func (p *Parser) regToken(r rune) token { case '<': switch p.rune() { case '<': - if r = p.rune(); r == '-' { + switch p.rune() { + case '-': p.rune() return dashHdoc - } else if r == '<' && p.lang != LangPOSIX { + case '<': p.rune() return wordHdoc } @@ -515,26 +599,63 @@ func (p *Parser) regToken(r rune) token { p.rune() return dplIn case '(': - if !p.lang.isBash() { + if !p.lang.in(langBashLike | LangZsh) { break } p.rune() return cmdIn } return rdrIn - default: // '>' + case '>': switch p.rune() { case '>': - p.rune() + switch p.rune() { + case '|': + p.rune() + return appClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return appClob + } + case '&': + if !p.lang.in(LangZsh) { + break + } + switch p.rune() { + case '|': + p.rune() + return appAllClob // >>&| is an alias for &>>| + case '!': + p.rune() + return appAllClob // >>&! is an alias for &>>| + } + return appAll // >>& is an alias for &>> + } return appOut case '&': - p.rune() + r = p.rune() + if p.lang.in(LangZsh) { + switch r { + case '|': + p.rune() + return rdrAllClob // >&| is an alias for &>| + case '!': + p.rune() + return rdrAllClob // >&! is an alias for &>| + } + } return dplOut case '|': p.rune() - return clbOut + return rdrClob + case '!': + if p.lang.in(LangZsh) { + p.rune() + return rdrClob + } case '(': - if !p.lang.isBash() { + if !p.lang.in(langBashLike | LangZsh) { break } p.rune() @@ -542,6 +663,7 @@ func (p *Parser) regToken(r rune) token { } return rdrOut } + panic("unreachable") } func (p *Parser) dqToken(r rune) token { @@ -553,13 +675,13 @@ func (p *Parser) dqToken(r rune) token { // Don't call p.rune, as we need to work out p.openBquotes to // properly handle backslashes in the lexer. return bckQuote - default: // '$' + case '$': switch p.rune() { case '{': p.rune() return dollBrace case '[': - if !p.lang.isBash() { + if !p.lang.in(langBashLike) { break } p.rune() @@ -573,6 +695,7 @@ func (p *Parser) dqToken(r rune) token { } return dollar } + panic("unreachable") } func (p *Parser) paramToken(r rune) token { @@ -594,6 +717,15 @@ func (p *Parser) paramToken(r rune) token { case '=': p.rune() return colAssgn + case '#': + p.rune() + return colHash + case '|': + p.rune() + return colPipe + case '*': + p.rune() + return colStar } return colon case '+': @@ -623,14 +755,11 @@ func (p *Parser) paramToken(r rune) token { case '!': p.rune() return exclMark - case '[': - p.rune() - return leftBrack case ']': p.rune() return rightBrack case '/': - if p.rune() == '/' && p.quote != paramExpRepl { + if p.rune() == '/' { p.rune() return dblSlash } @@ -650,9 +779,16 @@ func (p *Parser) paramToken(r rune) token { case '@': p.rune() return at - default: // '*' + case '*': p.rune() return star + + // This func gets called by the parser in [runeByRune] mode; + // we need to handle EOF and unexpected runes. + case utf8.RuneSelf: + return _EOF + default: + return illegalTok } } @@ -682,7 +818,10 @@ func (p *Parser) arithmToken(r rune) token { case '&': switch p.rune() { case '&': - p.rune() + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return andBoolAssgn + } return andAnd case '=': p.rune() @@ -692,7 +831,10 @@ func (p *Parser) arithmToken(r rune) token { case '|': switch p.rune() { case '|': - p.rune() + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return orBoolAssgn + } return orOr case '=': p.rune() @@ -754,7 +896,10 @@ func (p *Parser) arithmToken(r rune) token { case '*': switch p.rune() { case '*': - p.rune() + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return powAssgn + } return power case '=': p.rune() @@ -768,7 +913,14 @@ func (p *Parser) arithmToken(r rune) token { } return slash case '^': - if p.rune() == '=' { + switch p.rune() { + case '^': + if p.rune() == '=' && p.lang.in(LangZsh) { + p.rune() + return xorBoolAssgn + } + return dblCaret + case '=': p.rune() return xorAssgn } @@ -788,10 +940,14 @@ func (p *Parser) arithmToken(r rune) token { case ':': p.rune() return colon - default: // '#' + case '#': p.rune() return hash + case '.': + p.rune() + return period } + panic("unreachable") } func (p *Parser) newLit(r rune) { @@ -801,9 +957,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,10 +969,10 @@ 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 + return s } func (p *Parser) isLitRedir() bool { @@ -824,31 +980,27 @@ func (p *Parser) isLitRedir() bool { if lit[0] == '{' && lit[len(lit)-1] == '}' { return ValidName(string(lit[1 : len(lit)-1])) } - for _, b := range lit { - switch b { - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - default: - return false - } + return numberLiteral(lit) +} + +func positionalRuneParam[T rune | byte](r T) bool { + switch r { + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + return true } - return true + return false } -func (p *Parser) advanceNameCont(r rune) { - // we know that r is a letter or underscore -loop: - for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { - switch { - case 'a' <= r && r <= 'z': - case 'A' <= r && r <= 'Z': - case r == '_': - case '0' <= r && r <= '9': - case r == escNewl: - default: - break loop - } +func singleRuneParam[T rune | byte](r T) bool { + switch r { + case '@', '*', '#', '$', '?', '!', '-': + return true } - p.tok, p.val = _LitWord, p.endLit() + return positionalRuneParam(r) +} + +func paramNameRune[T rune | byte](r T) bool { + return asciiLetter(r) || asciiDigit(r) || r == '_' } func (p *Parser) advanceLitOther(r rune) { @@ -870,18 +1022,18 @@ loop: break loop } case ':', '=', '%', '^', ',', '?', '!', '~', '*': - if p.quote&allArithmExpr != 0 || p.quote == paramExpName { + if p.quote&allArithmExpr != 0 { break loop } - case '[', ']': - if p.lang != LangPOSIX && p.quote&allArithmExpr != 0 { + case '.': + if !p.lang.in(LangZsh) && p.quote&allArithmExpr != 0 { break loop } - fallthrough - case '#', '@': - if p.quote&allParamReg != 0 { + case '[', ']': + if p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.quote&allArithmExpr != 0 { break loop } + fallthrough case '+', '-', ' ', '\t', ';', '&', '>', '<', '|', '(', ')', '\n', '\r': if p.quote&allKeepSpaces == 0 { break loop @@ -891,18 +1043,52 @@ loop: p.tok, p.val = tok, p.endLit() } +// zshNumRange peeks at the bytes after '<' to check for a zsh numeric +// range glob pattern like <->, <5->, <-10>, or <5-10>. +func (p *Parser) zshNumRange() bool { + // Peeking a handful of bytes here should be enough. + // 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 int(p.bsp) >= len(p.bs) { + p.fill() + } + rest := p.bs[p.bsp:] + for len(rest) > 0 && rest[0] >= '0' && rest[0] <= '9' { + rest = rest[1:] + } + if len(rest) == 0 || rest[0] != '-' { + return false + } + rest = rest[1:] + for len(rest) > 0 && rest[0] >= '0' && rest[0] <= '9' { + rest = rest[1:] + } + return len(rest) > 0 && rest[0] == '>' +} + func (p *Parser) advanceLitNone(r rune) { p.eqlOffs = -1 tok := _LitWord loop: for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { switch r { - case ' ', '\t', '\n', '\r', '&', '|', ';', '(', ')': + case ' ', '\t', '\n', '\r', '&', '|', ';', ')': + break loop + case '(': break loop case '\\': // escaped byte follows p.rune() case '>', '<': - if p.peekByte('(') { + if r == '<' && p.lang.in(LangZsh) && p.zshNumRange() { + // Zsh numeric range glob like <-> or <1-100>; consume until '>'. + for { + if r = p.rune(); r == '>' || r == utf8.RuneSelf { + break + } + } + continue + } + if p.peek() == '(' { tok = _Lit } else if p.isLitRedir() { tok = _LitRedir @@ -917,7 +1103,7 @@ loop: tok = _Lit break loop case '?', '*', '+', '@', '!': - if p.tokenizeGlob() { + if p.extendedGlob() { tok = _Lit break loop } @@ -926,7 +1112,7 @@ loop: p.eqlOffs = len(p.litBs) - 1 } case '[': - if p.lang != LangPOSIX && len(p.litBs) > 1 && p.litBs[0] != '[' { + if p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && len(p.litBs) > 1 && p.litBs[0] != '[' { tok = _Lit break loop } @@ -965,10 +1151,8 @@ func (p *Parser) advanceLitHdoc(r rune) { p.tok = _Lit p.newLit(r) - if p.quote == hdocBodyTabs { - for r == '\t' { - r = p.rune() - } + for p.quote == hdocBodyTabs && r == '\t' { + r = p.rune() } lStart := len(p.litBs) - 1 stop := p.hdocStops[len(p.hdocStops)-1] @@ -1014,10 +1198,8 @@ func (p *Parser) advanceLitHdoc(r rune) { if r != '\n' { return // hit an unexpected EOF or closing backquote } - if p.quote == hdocBodyTabs { - for p.peekByte('\t') { - p.rune() - } + for p.quote == hdocBodyTabs && p.peek() == '\t' { + p.rune() } lStart = len(p.litBs) } @@ -1033,10 +1215,8 @@ func (p *Parser) quotedHdocWord() *Word { if r == utf8.RuneSelf { return nil } - if p.quote == hdocBodyTabs { - for r == '\t' { - r = p.rune() - } + for p.quote == hdocBodyTabs && r == '\t' { + r = p.rune() } lStart := len(p.litBs) - 1 runeLoop: @@ -1068,7 +1248,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)) } } } @@ -1083,13 +1263,13 @@ func (p *Parser) advanceLitRe(r rune) { case ')': if p.rxOpenParens--; p.rxOpenParens < 0 { p.tok, p.val = _LitWord, p.endLit() - p.quote = noState + p.quote = testExpr return } case ' ', '\t', '\r', '\n', ';', '&', '>', '<': if p.rxOpenParens <= 0 { p.tok, p.val = _LitWord, p.endLit() - p.quote = noState + p.quote = testExpr return } case '"', '\'', '$', '`': diff --git a/vendor/mvdan.cc/sh/v3/syntax/nodes.go b/vendor/mvdan.cc/sh/v3/syntax/nodes.go index 32518ec877..1f0e46a814 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) @@ -180,6 +219,7 @@ type Stmt struct { Negated bool // ! stmt Background bool // stmt & Coprocess bool // mksh's |& + Disown bool // zsh's &| or &! Redirs []*Redirect // stmt >a w.lastLine { - if w.Incomplete() { + // by [Parser.Stmts]. + if (w.p.r == '\n' || w.p.r == escNewl) && w.p.line > w.lastLine { + if w.p.Incomplete() { // Incomplete statement; call back to print "> ". - if !w.fn(w.accumulated) { + if !w.yield(w.accumulated, w.p.err) { return 0, io.EOF } } else if len(w.accumulated) == 0 { // Nothing was parsed; call back to print another "$ ". - if !w.fn(nil) { + if !w.yield(nil, w.p.err) { return 0, io.EOF } } - w.lastLine = w.line + w.lastLine = w.p.line + } + return w.rd.Read(p) +} + +// Interactive is a pre-iterators API which now wraps [Parser.InteractiveSeq]. +// +// Deprecated: use [Parser.InteractiveSeq]. +func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { + for stmts, err := range p.InteractiveSeq(r) { + if err != nil { + return err + } + if !fn(stmts) { + break + } } - return w.Reader.Read(p) + return nil } -// Interactive implements what is necessary to parse statements in an +// InteractiveSeq implements what is necessary to parse statements in an // interactive shell. The parser will call the given function under two // circumstances outlined below. // @@ -230,48 +345,69 @@ 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. -func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { - w := wrappedReader{Parser: p, Reader: r, fn: fn} - return p.Stmts(&w, func(stmt *Stmt) bool { - w.accumulated = append(w.accumulated, stmt) - // We finished parsing a statement and we're at a newline token, - // so we finished fully parsing a number of statements. Call - // back to run the statements and print "$ ". - if p.tok == _Newl { - if !fn(w.accumulated) { - return false +func (p *Parser) InteractiveSeq(r io.Reader) iter.Seq2[[]*Stmt, error] { + return func(yield func([]*Stmt, error) bool) { + w := wrappedReader{p: p, rd: r, yield: yield} + for stmts, err := range p.StmtsSeq(&w) { + w.accumulated = append(w.accumulated, stmts) + if err != nil { + if !yield(w.accumulated, err) { + break + } + // If the caller wishes, they can continue in the presence of parse errors. + // TODO: does this even work? Write tests for it. This only came up + continue + } + // We finished parsing a statement and we're at a newline token, + // so we finished fully parsing a number of statements. Call + // back to run the statements and print "$ ". + if p.tok == _Newl { + if !yield(w.accumulated, nil) { + break + } + w.accumulated = w.accumulated[:0] + // The callback above would already print "$ ", so we + // don't want the subsequent wrappedReader.Read to cause + // another "$ " print thinking that nothing was parsed. + w.lastLine = w.p.line + 1 } - w.accumulated = w.accumulated[:0] - // The callback above would already print "$ ", so we - // don't want the subsequent wrappedReader.Read to cause - // another "$ " print thinking that nothing was parsed. - w.lastLine = w.line + 1 } - return true - }) + } } -// 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]. +// +// Deprecated: use [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 +416,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("%#q 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,38 +480,37 @@ 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 - r rune // next rune - w int // width of r + bsp uint // offset within [Parser.bs] for the rune after [Parser.r] + r rune // next rune; [utf8.RuneSelf] when it went past EOF, or we stopped + w int // width of [Parser.r] f *File - spaced bool // whether tok has whitespace on its left + spaced bool // whether [Parser.tok] has whitespace on its left err error // lexer/parser error readErr error // got a read error, but bytes left + readEOF bool // [Parser.src] already gave us an [io.EOF] error tok token // current token val string // current value (valid if tok is _Lit*) - // position of r, to be converted to Parser.pos later - offs, line, col int + // position of [Parser.r], to be converted to [Parser.pos] later + 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) + eqlOffs int // position of '=' in [Parser.val] when [Parser.tok].isLit is true keepComments bool lang LangVariant stopAt []byte + recoveredErrors int + recoverErrorsMax int + forbidNested bool // list of pending heredoc bodies @@ -379,20 +519,17 @@ type Parser struct { hdocStops [][]byte // stack of end words for open heredocs - parsingDoc bool // true if using Parser.Document + 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 +537,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 @@ -433,30 +564,39 @@ func (p *Parser) reset() { p.bs, p.bsp = nil, 0 p.offs, p.line, p.col = 0, 1, 1 p.r, p.w = 0, 0 - p.err, p.readErr = nil, nil + p.err, p.readErr, p.readEOF = nil, nil, false 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 } +// nextPos returns the position of the next rune, [Parser.r]. 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,68 +606,59 @@ 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 return ce } -//go:generate stringer -type=quoteState - type quoteState uint32 const ( + // The initial state of the parser. noState quoteState = 1 << iota + + // Used when parsing parameter expansions; use with [Parser.rune], + // [Parser.next] always returns [illegalTok]. + runeByRune + + // unquotedWordCont exists purely so that the '#' in $foo#bar does not + // get parsed as a comment; it's a tiny variation on [noState]. + unquotedWordCont + subCmd subCmdBckquo dblQuotes @@ -537,24 +668,20 @@ const ( arithmExpr arithmExprLet arithmExprCmd - arithmExprBrack testExpr testExprRegexp switchCase - paramExpName - paramExpSlice + paramExpArithm paramExpRepl paramExpExp arrayElems - allKeepSpaces = paramExpRepl | dblQuotes | hdocBody | - hdocBodyTabs | paramExpExp - allRegTokens = noState | subCmd | subCmdBckquo | hdocWord | + allKeepSpaces = runeByRune | paramExpRepl | dblQuotes | hdocBody | + hdocBodyTabs | paramExpRepl | paramExpExp + allRegTokens = noState | unquotedWordCont | subCmd | subCmdBckquo | hdocWord | switchCase | arrayElems | testExpr - allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | - arithmExprBrack | paramExpSlice - allParamReg = paramExpName | paramExpSlice - allParamExp = allParamReg | paramExpRepl | paramExpExp | arithmExprBrack + allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | paramExpArithm + allParamExp = paramExpArithm | paramExpRepl | paramExpExp ) type saveState struct { @@ -565,7 +692,7 @@ type saveState struct { func (p *Parser) preNested(quote quoteState) (s saveState) { s.quote, s.buriedHdocs = p.quote, p.buriedHdocs p.buriedHdocs, p.quote = len(p.heredocs), quote - return + return s } func (p *Parser) postNested(s saveState) { @@ -573,39 +700,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() { @@ -630,28 +755,14 @@ func (p *Parser) doHeredocs() { if i > 0 && p.r == '\n' { p.rune() } - lastLine := p.line if quoted { r.Hdoc = p.quotedHdocWord() } else { p.next() r.Hdoc = p.getWord() } - if r.Hdoc != nil { - lastLine = int(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)) - } else { - r.Hdoc.Parts = append(r.Hdoc.Parts, l) - } - } if stop := p.hdocStops[len(p.hdocStops)-1]; stop != nil { - p.posErr(r.Pos(), "unclosed here-document '%s'", stop) + p.posErr(r.Pos(), "unclosed here-document %#q", stop) } p.hdocStops = p.hdocStops[:len(p.hdocStops)-1] } @@ -675,45 +786,80 @@ func (p *Parser) gotRsrv(val string) (Pos, bool) { return pos, false } -func readableStr(s string) string { - // don't quote tokens like & or } - if s != "" && s[0] >= 'a' && s[0] <= 'z' { - return strconv.Quote(s) +func (p *Parser) recoverError() bool { + if p.recoveredErrors < p.recoverErrorsMax { + p.recoveredErrors++ + return true } - return s + return false } -func (p *Parser) followErr(pos Pos, left, right string) { - leftStr := readableStr(left) - p.posErr(pos, "%s must be followed by %s", leftStr, right) +type noQuote string + +func (s noQuote) Format(f fmt.State, verb rune) { + f.Write([]byte(s)) } -func (p *Parser) followErrExp(pos Pos, left string) { - p.followErr(pos, left, "an expression") +func (t token) Format(f fmt.State, verb rune) { + if t < _realTokenBoundary && verb == 'q' { + // EOF, Lit and the others should not be quoted in error messages + // as they are not real shell syntax like `if` or `{`. + f.Write([]byte(t.String())) + } else { + fmt.Fprintf(f, fmt.FormatString(f, verb), t.String()) + } +} + +func (p *Parser) followErr(pos Pos, left, right any) { + p.posErr(pos, "%#q must be followed by %#q", left, right) +} + +func (p *Parser) followErrExp(pos Pos, left any) { + p.followErr(pos, left, noQuote("an expression")) } func (p *Parser) follow(lpos Pos, left string, tok token) { if !p.got(tok) { - p.followErr(lpos, left, tok.String()) + p.followErr(lpos, left, tok) } } func (p *Parser) followRsrv(lpos Pos, left, val string) Pos { pos, ok := p.gotRsrv(val) if !ok { - p.followErr(lpos, left, fmt.Sprintf("%q", val)) + if p.recoverError() { + return recoveredPos + } + p.followErr(lpos, left, val) } return pos } func (p *Parser) followStmts(left string, lpos Pos, stops ...string) ([]*Stmt, []Comment) { + // Language variants disallowing empty command lists: + // * [LangPOSIX]: "A list is a sequence of one or more AND-OR lists...". + // * [LangBash]: "A list is a sequence of one or more pipelines..." + // + // Language variants allowing empty command lists: + // * [LangZsh]: "A list is a sequence of zero or more sublists...". + // * [LangMirBSDKorn]: "Lists of commands can be created by separating pipelines..."; + // note that the man page is not explicit, but the shell clearly allows e.g. `{ }`. if p.got(semicolon) { + if p.lang.in(LangZsh | LangMirBSDKorn) { + return nil, nil // allow an empty list + } + p.followErr(lpos, left, noQuote("a statement list")) return nil, nil } - newLine := p.got(_Newl) stmts, last := p.stmtList(stops...) - if len(stmts) < 1 && !newLine { - p.followErr(lpos, left, "a statement list") + if len(stmts) < 1 { + if p.lang.in(LangZsh | LangMirBSDKorn) { + return nil, nil // allow an empty list + } + if p.recoverError() { + return []*Stmt{{Position: recoveredPos}}, nil + } + p.followErr(lpos, left, noQuote("a statement list")) } return stmts, last } @@ -721,7 +867,10 @@ 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 { - p.followErr(pos, tok.String(), "a word") + if p.recoverError() { + return p.wordOne(&Lit{ValuePos: recoveredPos}) + } + p.followErr(pos, tok, noQuote("a word")) } return w } @@ -729,24 +878,28 @@ 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 { - p.posErr(n.Pos(), "%s statement must end with %q", start, end) + if p.recoverError() { + return recoveredPos + } + p.posErr(n.Pos(), "%#q statement must end with %#q", start, end) } return pos } func (p *Parser) quoteErr(lpos Pos, quote token) { - p.posErr(lpos, "reached %s without closing quote %s", - p.tok.String(), quote) + p.posErr(lpos, "reached %#q without closing quote %#q", p.tok, quote) } -func (p *Parser) matchingErr(lpos Pos, left, right interface{}) { - p.posErr(lpos, "reached %s without matching %s with %s", - p.tok.String(), left, right) +func (p *Parser) matchingErr(lpos Pos, left, right token) { + p.posErr(lpos, "reached %#q without matching %#q with %#q", p.tok, left, right) } 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 +908,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,16 +916,21 @@ 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) return ok && perr.Incomplete } -// IsKeyword returns true if the given word is part of the language keywords. +// TODO: probably redo with a [LangVariant] argument. +// Perhaps offer an iterator version as well. + +// IsKeyword returns true if the given word is a language keyword +// in POSIX Shell or Bash. func IsKeyword(word string) bool { // This list has been copied from the bash 5.1 source code, file y.tab.c +4460 + // TODO: should we include entries for zsh here? e.g. "{}", "repeat", "always", ... switch word { case "!", @@ -805,17 +963,17 @@ func IsKeyword(word string) bool { // the parser cannot recover. type ParseError struct { Filename string - Pos - Text string + Pos Pos + Text string Incomplete bool } func (e ParseError) Error() string { if e.Filename == "" { - return fmt.Sprintf("%s: %s", e.Pos.String(), e.Text) + return fmt.Sprintf("%s: %s", e.Pos, e.Text) } - return fmt.Sprintf("%s:%s: %s", e.Filename, e.Pos.String(), e.Text) + return fmt.Sprintf("%s:%s: %s", e.Filename, e.Pos, e.Text) } // LangError is returned when the parser encounters code that is only valid in @@ -823,56 +981,80 @@ func (e ParseError) Error() string { // in the current language variant, and what languages support it. type LangError struct { Filename string - Pos + Pos Pos + + // TODO: consider replacing the Langs slice with a bitset. + + // 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) + sb.WriteString(":") } - buf.WriteString(e.Pos.String() + ": ") - buf.WriteString(e.Feature) + sb.WriteString(e.Pos.String()) + sb.WriteString(": ") + 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, args ...any) { + // for i, arg := range args { + // if arg, ok := arg.(fmt.Stringer); ok && arg != _EOF { + // args[i] = quotedToken(arg) + // } + // } p.errPass(ParseError{ Filename: p.f.Name, Pos: pos, - Text: fmt.Sprintf(format, a...), + Text: fmt.Sprintf(format, args...), Incomplete: p.tok == _EOF && p.Incomplete(), }) } -func (p *Parser) curErr(format string, a ...interface{}) { - p.posErr(p.pos, format, a...) +func (p *Parser) curErr(format string, args ...any) { + p.posErr(p.pos, format, args...) } -func (p *Parser) langErr(pos Pos, feature string, langs ...LangVariant) { +func (p *Parser) checkLang(pos Pos, langSet LangVariant, format string, a ...any) { + if p.lang.in(langSet) { + return + } + if langBashLike.in(langSet) { + // If we're reporting an error because a feature is for bash-like funcs, + // just mention "bash" rather than "bash/bats" for the sake of clarity. + langSet &^= LangBats + } p.errPass(LangError{ Filename: p.f.Name, Pos: pos, - Feature: feature, - Langs: langs, + Feature: fmt.Sprintf(format, a...), + Langs: slices.Collect(langSet.bits()), + LangUsed: p.lang, }) } -func (p *Parser) stmts(fn func(*Stmt) bool, stops ...string) { +func (p *Parser) stmts(yield func(*Stmt, error) bool, stops ...string) { gotEnd := true loop: for p.tok != _EOF { @@ -884,6 +1066,9 @@ loop: break loop } } + if p.val == "}" { + p.curErr(`%#q can only be used to close a block`, rightBrace) + } case rightParen: if p.quote == subCmd { break loop @@ -896,7 +1081,7 @@ loop: if p.quote == switchCase { break loop } - p.curErr("%s can only be used in a case clause", p.tok) + p.curErr("%#q can only be used in a case clause", p.tok) } if !newLine && !gotEnd { p.curErr("statements must be separated by &, ; or a newline") @@ -904,15 +1089,15 @@ 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 } gotEnd = s.Semicolon.IsValid() - if !fn(s) { + if !yield(s, p.err) { break } } @@ -921,10 +1106,7 @@ loop: 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() - } + fn := func(s *Stmt, err error) bool { stmts = append(stmts, s) return true } @@ -941,8 +1123,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 } @@ -958,25 +1139,24 @@ func (p *Parser) stmtList(stops ...string) ([]*Stmt, []Comment) { func (p *Parser) invalidStmtStart() { switch p.tok { - case semicolon, and, or, andAnd, orOr: - p.curErr("%s can only immediately follow a statement", p.tok) + case semicolon, and, or, andAnd, orOr, andPipe, andBang: + p.curErr("%#q can only immediately follow a statement", p.tok) case rightParen: - p.curErr("%s can only be used to close a subshell", p.tok) + p.curErr("%#q can only be used to close a subshell", p.tok) default: - p.curErr("%s is not a valid start for a statement", p.tok) + p.curErr("%#q is not a valid start for a statement", p.tok) } } 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 } func (p *Parser) getLit() *Lit { - switch p.tok { - case _Lit, _LitWord, _LitRedir: + if p.tok.isLit() { l := p.lit(p.pos, p.val) p.next() return l @@ -984,47 +1164,48 @@ func (p *Parser) getLit() *Lit { return nil } -func (p *Parser) wordParts() (wps []WordPart) { +func (p *Parser) wordParts(wps []WordPart) []WordPart { + if p.quote == noState { + p.quote = unquotedWordCont + defer func() { p.quote = noState }() + } 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 } } } -func (p *Parser) ensureNoNested() { +func (p *Parser) ensureNoNested(pos Pos) { if p.forbidNested { - p.curErr("expansions not allowed in heredoc words") + p.posErr(pos, "expansions not allowed in heredoc words") } } 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 case dollBrace: - p.ensureNoNested() + p.ensureNoNested(p.pos) switch p.r { case '|': - if p.lang != LangMirBSDKorn { - p.curErr(`"${|stmts;}" is a mksh feature`) - } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "`${|stmts;}`") fallthrough case ' ', '\t', '\n': - if p.lang != LangMirBSDKorn { - p.curErr(`"${ stmts;}" is a mksh feature`) - } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "`${ stmts;}`") cs := &CmdSubst{ Left: p.pos, TempFile: p.r != '|', @@ -1037,7 +1218,7 @@ func (p *Parser) wordPart() WordPart { p.postNested(old) pos, ok := p.gotRsrv("}") if !ok { - p.matchingErr(cs.Left, "${", "}") + p.matchingErr(cs.Left, dollBrace, rightBrace) } cs.Right = pos return cs @@ -1045,20 +1226,13 @@ func (p *Parser) wordPart() WordPart { return p.paramExp() } case dollDblParen, dollBrack: - p.ensureNoNested() + p.ensureNoNested(p.pos) left := p.tok ar := &ArithmExp{Left: p.pos, Bracket: left == dollBrack} - var old saveState - if ar.Bracket { - old = p.preNested(arithmExprBrack) - } else { - old = p.preNested(arithmExpr) - } + old := p.preNested(arithmExpr) p.next() if p.got(hash) { - if p.lang != LangMirBSDKorn { - p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn) - } + p.checkLang(ar.Pos(), LangMirBSDKorn, "unsigned expressions") ar.Unsigned = true } ar.X = p.followArithm(left, ar.Left) @@ -1074,42 +1248,22 @@ func (p *Parser) wordPart() WordPart { } return ar case dollParen: - p.ensureNoNested() - cs := &CmdSubst{Left: p.pos} - old := p.preNested(subCmd) - p.next() - cs.Stmts, cs.Last = p.stmtList() - p.postNested(old) - cs.Right = p.matched(cs.Left, leftParen, rightParen) - return cs + p.ensureNoNested(p.pos) + return p.cmdSubst() case dollar: - r := p.r - switch { - case singleRuneParam(r): - p.tok, p.val = _LitWord, string(r) - p.rune() - case 'a' <= r && r <= 'z', 'A' <= r && r <= 'Z', - '0' <= r && r <= '9', r == '_', r == '\\': - p.advanceNameCont(r) - default: + pe := p.paramExp() + if pe == nil { // was not actually a parameter expansion, like: "foo$" l := p.lit(p.pos, "$") p.next() return l } - p.ensureNoNested() - pe := &ParamExp{Dollar: p.pos, Short: true} - p.pos = posAddCol(p.pos, 1) - pe.Param = p.getLit() - if pe.Param != nil && pe.Param.Value == "" { - l := p.lit(pe.Dollar, "$") - // e.g. "$\\\"" within double quotes, so we must - // keep the rest of the literal characters. - l.ValueEnd = posAddCol(l.ValuePos, 1) - return l - } + p.ensureNoNested(pe.Dollar) return pe + case assgnParen: + p.checkLang(p.pos, LangZsh, `%#q process substitutions`, p.tok) + fallthrough case cmdIn, cmdOut: - p.ensureNoNested() + p.ensureNoNested(p.pos) ps := &ProcSubst{Op: ProcOperator(p.tok), OpPos: p.pos} old := p.preNested(subCmd) p.next() @@ -1130,10 +1284,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 +1291,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 } @@ -1155,7 +1309,7 @@ func (p *Parser) wordPart() WordPart { if p.backquoteEnd() { return nil } - p.ensureNoNested() + p.ensureNoNested(p.pos) cs := &CmdSubst{Left: p.pos, Backquotes: true} old := p.preNested(subCmdBckquo) p.openBquotes++ @@ -1178,13 +1332,35 @@ 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: - if p.lang == LangPOSIX { - p.langErr(p.pos, "extended globs", LangBash, LangMirBSDKorn) + case leftParen: + if p.lang.in(LangZsh) && p.r != ')' { + // Zsh glob qualifier like *(N) or .(:a); the only case where + // ( immediately after a word is not a glob qualifier is () + // for a function declaration, which the parser handles earlier. + pos := p.pos + p.pos = p.nextPos() + for p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() { + } + if p.r != ')' { + p.tok = _EOF // we can only get here due to EOF + p.matchingErr(pos, leftParen, rightParen) + } + p.rune() + p.val = p.endLit() + l := p.lit(pos, "("+p.val) + p.next() + return l } + return nil + case globQuest, globStar, globPlus, globAt, globExcl: + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "extended globs") eg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos} lparens := 1 r := p.r @@ -1205,7 +1381,7 @@ func (p *Parser) wordPart() WordPart { p.rune() p.next() if lparens != 0 { - p.matchingErr(eg.OpPos, eg.Op, rightParen) + p.matchingErr(eg.OpPos, token(eg.Op), rightParen) } return eg default: @@ -1213,118 +1389,140 @@ func (p *Parser) wordPart() WordPart { } } +func (p *Parser) cmdSubst() *CmdSubst { + cs := &CmdSubst{Left: p.pos} + old := p.preNested(subCmd) + p.next() + cs.Stmts, cs.Last = p.stmtList() + p.postNested(old) + cs.Right = p.matched(cs.Left, dollParen, rightParen) + return cs +} + 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 } -func singleRuneParam(r rune) bool { - switch r { - case '@', '*', '#', '$', '?', '!', '-', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - return true - } - return false -} - +// paramExp parses a short or full parameter expansion, depending on whether +// [Parser.tok] is [dollar] or [dollBrace]. It returns nil if a [dollar] token +// does not form a valid parameter expansion, in which case it should be parsed +// as a literal. func (p *Parser) paramExp() *ParamExp { - pe := &ParamExp{Dollar: p.pos} old := p.quote - p.quote = paramExpName - if p.r == '#' { - p.tok = hash - p.pos = p.nextPos() + p.quote = runeByRune + // [ParamExp.Short] means we are parsing $exp rather than ${exp}. + pe := &ParamExp{ + Dollar: p.pos, + Short: p.tok == dollar, + } + if !pe.Short && p.r == '(' { + p.checkLang(pe.Pos(), LangZsh, `parameter expansion flags`) + // For now, for simplicity, we parse flags as just a literal. + // In the future, parsing as a word is better for cases like + // `${(ps.$sep.)val}`. + lparen := p.nextPos() p.rune() - } else { - p.next() - } - switch p.tok { - case hash: - if paramNameOp(p.r) { - pe.Length = true - p.next() - } - case perc: - if p.lang != LangMirBSDKorn { - p.posErr(pe.Pos(), `"${%%foo}" is a mksh feature`) + p.pos = p.nextPos() + for p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() { } - if paramNameOp(p.r) { - pe.Width = true - p.next() + p.val = p.endLit() + if p.r != ')' { + p.tok = _EOF // we can only get here due to EOF + p.matchingErr(lparen, leftParen, rightParen) } - case exclMark: - if paramNameOp(p.r) { - if p.lang == LangPOSIX { - p.langErr(p.pos, "${!foo}", LangBash, LangMirBSDKorn) + pe.Flags = p.lit(p.pos, p.val) + p.rune() + } + if !pe.Short || p.lang.in(LangZsh) { + // Prefixes, like ${#name} to get the length of a variable. + // Note that in Zsh, the short form like $#name is allowed too. + switch p.r { + case '#': + if p.paramNameStart() { + pe.Length = true + } + case '%': + if p.paramNameStart() { + p.checkLang(pe.Pos(), LangMirBSDKorn, "`${%%foo}`") + pe.Width = true + } + case '!': + // Unlike the others, zsh has no $!foo prefix. + if !pe.Short && p.paramNameStart() { + p.checkLang(pe.Pos(), langBashLike|LangMirBSDKorn, "`${!foo}`") + pe.Excl = true + } + case '+': + if p.paramNameStart() { + p.checkLang(pe.Pos(), LangZsh, "`${+foo}`") + pe.IsSet = true } - pe.Excl = true - p.next() } } - op := p.tok - switch p.tok { - case _Lit, _LitWord: - if !numberLiteral(p.val) && !ValidName(p.val) { - p.curErr("invalid parameter name") - } - pe.Param = p.lit(p.pos, p.val) - p.next() - case quest, minus: - if pe.Length && p.r != '}' { - // actually ${#-default}, not ${#-}; fix the ambiguity - pe.Length = false - pe.Param = p.lit(posAddCol(p.pos, -1), "#") - pe.Param.ValueEnd = p.pos - break - } - fallthrough - case at, star, hash, exclMark, dollar: - pe.Param = p.lit(p.pos, p.tok.String()) - p.next() - default: - p.curErr("parameter expansion requires a literal") + if pe = p.paramExpParameter(pe); pe == nil { + p.quote = old + return nil // just "$" } - switch p.tok { - case _Lit, _LitWord: - p.curErr("%s cannot be followed by a word", op) - case rightBrace: - pe.Rbrace = p.pos + // In short mode, any indexing or suffixes is not allowed, and we don't require '}'. + // Zsh is an exception: $foo[1] and $foo[1,3] are valid. Note that $1[x] does not qualify. + if pe.Short { + if p.lang.in(LangZsh) && p.r == '[' && (len(p.val) != 1 || !positionalRuneParam(p.val[0])) { + p.pos = p.nextPos() + p.rune() + pe.Index = p.eitherIndex() + } p.quote = old p.next() return pe - case leftBrack: - if p.lang == LangPOSIX { - p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn) - } - if !ValidName(pe.Param.Value) { - p.curErr("cannot index a special parameter name") + } + // Index expressions like ${foo[1]}. Note that expansion suffixes can be combined, + // like ${foo[@]//replace/with}. + if p.r == '[' { + p.checkLang(p.nextPos(), langBashLike|LangMirBSDKorn|LangZsh, "arrays") + // In zsh some of these like ${@[-1]} or ${*[1,3]} work, + // so we don't do this sort of check at all. + if !p.lang.in(LangZsh) && pe.Param != nil && !ValidName(pe.Param.Value) { + p.posErr(p.nextPos(), "cannot index a special parameter name") } + p.pos = p.nextPos() + p.rune() pe.Index = p.eitherIndex() } + tokRune := p.r + p.pos = p.nextPos() + p.tok = p.paramToken(p.r) if p.tok == rightBrace { pe.Rbrace = p.pos p.quote = old p.next() return pe } - if p.tok != _EOF && (pe.Length || pe.Width) { + if p.tok != _EOF && (pe.Length || pe.Width || pe.IsSet) { p.curErr("cannot combine multiple parameter expansion operators") } switch p.tok { - case slash, dblSlash: - // pattern search and replace - if p.lang == LangPOSIX { - p.langErr(p.pos, "search and replace", LangBash, LangMirBSDKorn) - } + case slash, dblSlash: // pattern search and replace + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "search and replace") pe.Repl = &Replace{All: p.tok == dblSlash} p.quote = paramExpRepl p.next() @@ -1333,14 +1531,36 @@ func (p *Parser) paramExp() *ParamExp { if p.got(slash) { pe.Repl.With = p.getWord() } - case colon: - // slicing - if p.lang == LangPOSIX { - p.langErr(p.pos, "slicing", LangBash, LangMirBSDKorn) + case colon: // slicing + if p.lang.in(LangZsh) && (p.r == '&' || asciiLetter(p.r)) { + pos := p.pos + loop: + for p.newLit(p.r); ; p.rune() { + switch p.r { + case utf8.RuneSelf: + p.tok = _EOF + p.matchingErr(pe.Dollar, dollBrace, rightBrace) + break loop + case '}': + pe.Modifiers = append(pe.Modifiers, p.lit(pos, p.endLit())) + pe.Rbrace = p.nextPos() + p.rune() + break loop + case ':': + pe.Modifiers = append(pe.Modifiers, p.lit(pos, p.endLit())) + p.rune() + pos = p.nextPos() + p.newLit(p.r) + } + } + p.quote = old + p.next() + return pe } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "slicing") pe.Slice = &Slice{} colonPos := p.pos - p.quote = paramExpSlice + p.quote = paramExpArithm if p.next(); p.tok != colon { pe.Slice.Offset = p.followArithm(colon, colonPos) } @@ -1354,59 +1574,186 @@ func (p *Parser) paramExp() *ParamExp { pe.Rbrace = p.pos p.matchedArithm(pe.Dollar, dollBrace, rightBrace) return pe - case caret, dblCaret, comma, dblComma: - // upper/lower case - if !p.lang.isBash() { - p.langErr(p.pos, "this expansion operator", LangBash) - } + case caret, dblCaret, comma, dblComma: // upper/lower case + p.checkLang(p.pos, langBashLike, "this expansion operator") pe.Exp = p.paramExpExp() case at, star: switch { - case p.tok == at && p.lang == LangPOSIX: - p.langErr(p.pos, "this expansion operator", LangBash, LangMirBSDKorn) case p.tok == star && !pe.Excl: - p.curErr("not a valid parameter expansion operator: %v", p.tok) + p.curErr("not a valid parameter expansion operator: %#q", p.tok) case pe.Excl && p.r == '}': + p.checkLang(pe.Pos(), langBashLike, "`${!foo%s}`", p.tok) pe.Names = ParNamesOperator(p.tok) p.next() + case p.tok == at: + p.checkLang(p.pos, langBashLike|LangMirBSDKorn, "this expansion operator") + fallthrough default: pe.Exp = p.paramExpExp() } case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn, - perc, dblPerc, hash, dblHash: + perc, dblPerc, hash, dblHash, colHash, colPipe, colStar: pe.Exp = p.paramExpExp() case _EOF: default: - p.curErr("not a valid parameter expansion operator: %v", p.tok) + if paramNameRune(tokRune) { + if pe.Param != nil { + p.curErr("%#q cannot be followed by a word", pe.Param.Value) + } else { + p.curErr("nested parameter expansion cannot be followed by a word") + } + } else { + p.curErr("not a valid parameter expansion operator: %#q", string(tokRune)) + } + } + if p.tok != _EOF && p.tok != rightBrace { + p.tok = p.paramToken(p.r) } p.quote = old - pe.Rbrace = p.pos - p.matched(pe.Dollar, dollBrace, rightBrace) + pe.Rbrace = p.matched(pe.Dollar, dollBrace, rightBrace) + return pe +} + +func (p *Parser) paramNameStart() bool { + r := p.peek() + if r == utf8.RuneSelf || singleRuneParam(r) || paramNameRune(r) || r == '"' { + p.rune() + return true + } + return false +} + +func (p *Parser) nestedParameterStart(pe *ParamExp) (left token, quotePos Pos) { + if pe.Short { + return illegalTok, Pos{} + } + if p.r == '"' { + quotePos = p.nextPos() + p.rune() + } + if p.r != '$' { + if quotePos.IsValid() { + return dollar, quotePos + } + return illegalTok, Pos{} + } + switch p1 := p.peek(); p1 { + case '{', '(': + p.pos = p.nextPos() + p.checkLang(p.pos, LangZsh, "nested parameter expansions") + if p.err != nil { + return illegalTok, Pos{} // xxx given that we overwrite p.tok below + } + p.rune() + p.rune() + if p1 == '{' { + left = dollBrace + } else { // '(' + left = dollParen + } + } + return left, quotePos +} + +func (p *Parser) paramExpParameter(pe *ParamExp) *ParamExp { + // Check for Zsh nested parameter expressions like ${(f)"$(foo)"}. + if left, quotePos := p.nestedParameterStart(pe); left != illegalTok { + var wp WordPart + switch p.tok = left; p.tok { + case dollBrace: // ${#${nested parameter}} + p.tok = dollBrace + wp = p.paramExp() + case dollParen: // ${#$(nested command)} + wp = p.cmdSubst() + default: // dollar + p.posErr(pe.Pos(), "invalid nested parameter expansion") + } + if quotePos.IsValid() { + if p.r != '"' { + p.tok = p.paramToken(p.r) + if p.tok == illegalTok { + p.posErr(pe.Pos(), "invalid nested parameter expansion") + } else { + p.quoteErr(quotePos, dblQuote) + } + } + pe.NestedParam = &DblQuoted{ + Left: quotePos, + Right: p.nextPos(), + Parts: []WordPart{wp}, + } + p.rune() + } else { + pe.NestedParam = wp + } + return pe + } + // The parameter name itself, like $foo or $?. + switch p.r { + case '?', '-': + if pe.Length && p.peek() != '}' { + // actually ${#-default}, not ${#-}; fix the ambiguity + pe.Length = false + pos := p.nextPos() + pe.Param = p.lit(posAddCol(pos, -1), "#") + pe.Param.ValueEnd = pos + break + } + fallthrough + case '@', '*', '#', '!', '$': + r, pos := p.r, p.nextPos() + p.rune() + pe.Param = p.lit(pos, string(r)) + default: + // Note that $1a is equivalent to ${1}a, but ${1a} is not. + // POSIX Shell says the latter is unspecified behavior, so match Bash's behavior. + pos := p.nextPos() + if pe.Short && singleRuneParam(p.r) { + p.val = string(p.r) + p.rune() + } else { + for p.newLit(p.r); p.r != utf8.RuneSelf; p.rune() { + if !paramNameRune(p.r) && p.r != escNewl { + break + } + } + p.val = p.endLit() + if !numberLiteral(p.val) && !ValidName(p.val) { + if pe.Short { + return nil // just "$" + } + if p.lang.in(LangZsh) && p.val == "" { + // Zsh allows omitting the parameter name, e.g. ${:-word}. + return pe + } + p.posErr(pos, "invalid parameter name") + } + } + pe.Param = p.lit(pos, p.val) + } return pe } func (p *Parser) paramExpExp() *Expansion { op := ParExpOperator(p.tok) + switch op { + case MatchEmpty, ArrayExclude, ArrayIntersect: + p.checkLang(p.pos, LangZsh, "${name%sarg}", op) + } p.quote = paramExpExp p.next() if op == OtherParamOps { - switch p.tok { - case _Lit, _LitWord: - default: + if !p.tok.isLit() { p.curErr("@ expansion operator requires a literal") } switch p.val { - case "a", "u", "A", "E", "K", "L", "P", "U": - if !p.lang.isBash() { - p.langErr(p.pos, "this expansion operator", LangBash) - } + case "a", "k", "u", "A", "E", "K", "L", "P", "U": + p.checkLang(p.pos, langBashLike, "this expansion operator") case "#": - if p.lang != LangMirBSDKorn { - p.langErr(p.pos, "this expansion operator", LangMirBSDKorn) - } + p.checkLang(p.pos, LangMirBSDKorn, "this expansion operator") case "Q": default: - p.curErr("invalid @ expansion operator") + p.curErr("invalid @ expansion operator %#q", p.val) } } return &Expansion{Op: op, Word: p.getWord()} @@ -1415,7 +1762,7 @@ func (p *Parser) paramExpExp() *Expansion { func (p *Parser) eitherIndex() ArithmExpr { old := p.quote lpos := p.pos - p.quote = arithmExprBrack + p.quote = paramExpArithm p.next() if p.tok == star || p.tok == at { p.tok, p.val = _LitWord, p.tok.String() @@ -1426,10 +1773,36 @@ func (p *Parser) eitherIndex() ArithmExpr { return expr } +func (p *Parser) zshSubFlags() *FlagsArithm { + zf := &FlagsArithm{} + // Lex flags as raw text, like paramExp does for ${(flags)...}. + lparen := p.pos + old := p.quote + p.quote = runeByRune + p.pos = p.nextPos() + for p.newLit(p.r); p.r != utf8.RuneSelf && p.r != ')'; p.rune() { + } + p.val = p.endLit() + if p.r != ')' { + p.tok = _EOF + p.matchingErr(lparen, leftParen, rightParen) + } + zf.Flags = p.lit(p.pos, p.val) + p.rune() + p.quote = old + // Parse the expression; use arithmExprAssign so commas are left for ranges. + p.next() + if p.tok == star || p.tok == at { + p.tok, p.val = _LitWord, p.tok.String() + } + zf.X = p.arithmExprAssign(false) + return zf +} + func (p *Parser) stopToken() bool { switch p.tok { - case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, dblSemicolon, - semiAnd, dblSemiAnd, semiOr, rightParen: + case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, andPipe, andBang, + dblSemicolon, semiAnd, dblSemiAnd, semiOr, rightParen: return true case bckQuote: return p.backquoteEnd() @@ -1448,10 +1821,8 @@ func ValidName(val string) bool { } for i, r := range val { switch { - case 'a' <= r && r <= 'z': - case 'A' <= r && r <= 'Z': - case r == '_': - case i > 0 && '0' <= r && r <= '9': + case asciiLetter(r), r == '_': + case i > 0 && asciiDigit(r): default: return false } @@ -1459,9 +1830,12 @@ func ValidName(val string) bool { return true } -func numberLiteral(val string) bool { - for _, r := range val { - if '0' > r || r > '9' { +func numberLiteral[T string | []byte](val T) bool { + if len(val) == 0 { + return false + } + for _, r := range string(val) { + if !asciiDigit(r) { return false } } @@ -1469,11 +1843,11 @@ func numberLiteral(val string) bool { } func (p *Parser) hasValidIdent() bool { - if p.tok != _Lit && p.tok != _LitWord { + if !p.tok.isLit() { return false } if end := p.eqlOffs; end > 0 { - if p.val[end-1] == '+' && p.lang != LangPOSIX { + if p.val[end-1] == '+' && p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) { end-- // a+=x } if ValidName(p.val[:end]) { @@ -1489,7 +1863,7 @@ func (p *Parser) getAssign(needEqual bool) *Assign { as := &Assign{} if p.eqlOffs > 0 { // foo=bar nameEnd := p.eqlOffs - if p.lang != LangPOSIX && p.val[p.eqlOffs-1] == '+' { + if p.lang.in(langBashLike|LangMirBSDKorn|LangZsh) && p.val[p.eqlOffs-1] == '+' { // a+=b as.Append = true nameEnd-- @@ -1500,7 +1874,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 @@ -1511,44 +1885,51 @@ func (p *Parser) getAssign(needEqual bool) *Assign { as.Index = p.eitherIndex() if p.spaced || p.stopToken() { if needEqual { - p.followErr(as.Pos(), "a[b]", "=") + p.followErr(as.Pos(), "a[b]", assgn) } else { as.Naked = true return as } } - if len(p.val) > 0 && p.val[0] == '+' { - as.Append = true - p.val = p.val[1:] + if p.tok == assgnParen { + if !p.lang.in(LangZsh) { + p.curErr("arrays cannot be nested") + return nil + } + // zsh allows a[i]=(values...). + // assgnParen consumed both '=' and '(', + // so rewrite as leftParen for array parsing below. + p.tok = leftParen p.pos = posAddCol(p.pos, 1) - } - if len(p.val) < 1 || p.val[0] != '=' { - if as.Append { - p.followErr(as.Pos(), "a[b]+", "=") - } else { - p.followErr(as.Pos(), "a[b]", "=") + } else { + if len(p.val) > 0 && p.val[0] == '+' { + as.Append = true + p.val = p.val[1:] + p.pos = posAddCol(p.pos, 1) + } + if len(p.val) < 1 || p.val[0] != '=' { + if as.Append { + p.followErr(as.Pos(), "a[b]+", assgn) + } else { + p.followErr(as.Pos(), "a[b]", assgn) + } + return nil + } + p.pos = posAddCol(p.pos, 1) + p.val = p.val[1:] + if p.val == "" { + p.next() } - return nil - } - p.pos = posAddCol(p.pos, 1) - p.val = p.val[1:] - if p.val == "" { - p.next() } } if p.spaced || p.stopToken() { return as } if as.Value == nil && p.tok == leftParen { - if p.lang == LangPOSIX { - p.langErr(p.pos, "arrays", LangBash, LangMirBSDKorn) - } - if as.Index != nil { - p.curErr("arrays cannot be nested") - } + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "arrays") as.Array = &ArrayExpr{Lparen: p.pos} newQuote := p.quote - if p.lang.isBash() { + if p.lang.in(langBashLike | LangZsh) { newQuote = arrayElems } old := p.preNested(newQuote) @@ -1560,13 +1941,14 @@ func (p *Parser) getAssign(needEqual bool) *Assign { if p.tok == leftBrack { left := p.pos ae.Index = p.eitherIndex() - p.follow(left, `"[x]"`, assgn) + if p.tok == assgnParen { + p.curErr("arrays cannot be nested") + return nil + } + p.follow(left, `[x]`, assgn) } if ae.Value = p.getWord(); ae.Value == nil { switch p.tok { - case leftParen: - p.curErr("arrays cannot be nested") - return nil case _Newl, rightParen, leftBrack: // TODO: support [index]=[ default: @@ -1599,8 +1981,9 @@ func (p *Parser) getAssign(needEqual bool) *Assign { func (p *Parser) peekRedir() bool { switch p.tok { - case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, - hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: + case _LitRedir, rdrOut, appOut, rdrIn, rdrInOut, dplIn, dplOut, + rdrClob, appClob, hdoc, dashHdoc, wordHdoc, + rdrAll, rdrAllClob, appAll, appAllClob: return true } return false @@ -1621,10 +2004,16 @@ func (p *Parser) doRedirect(s *Stmt) { s.Redirs = append(s.Redirs, r) } r.N = p.getLit() - if !p.lang.isBash() && r.N != nil && r.N.Value[0] == '{' { - p.langErr(r.N.Pos(), "{varname} redirects", LangBash) + if r.N != nil && r.N.Value[0] == '{' { + p.checkLang(r.N.Pos(), langBashLike, "`{varname}` redirects") } r.Op, r.OpPos = RedirOperator(p.tok), p.pos + switch r.Op { + case RdrAll, AppAll: + p.checkLang(p.pos, langBashLike|LangMirBSDKorn|LangZsh, "%#q redirects", r.Op) + case AppClob, RdrAllClob, AppAllClob: + p.checkLang(p.pos, LangZsh, "%#q redirects", r.Op) + } p.next() switch r.Op { case Hdoc, DashHdoc: @@ -1643,6 +2032,9 @@ func (p *Parser) doRedirect(s *Stmt) { } p.doHeredocs() } + case WordHdoc: + p.checkLang(r.OpPos, langBashLike|LangMirBSDKorn|LangZsh, "herestrings") + fallthrough default: r.Word = p.followWordTok(token(r.Op), r.OpPos) } @@ -1650,11 +2042,11 @@ 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() { - p.posErr(s.Pos(), `"!" cannot form a statement alone`) + p.posErr(s.Pos(), `%#q cannot form a statement alone`, exclMark) } if _, ok := p.gotRsrv("!"); ok { p.posErr(s.Pos(), `cannot negate a command multiple times`) @@ -1679,10 +2071,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, noQuote("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 } @@ -1699,6 +2095,10 @@ func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { s.Semicolon = p.pos p.next() s.Coprocess = true + case andPipe, andBang: + s.Semicolon = p.pos + p.next() + s.Disown = true } } if len(p.accComs) > 0 && !binCmd && !fnBody { @@ -1713,77 +2113,86 @@ func (p *Parser) getStmt(readEnd, binCmd, fnBody bool) *Stmt { func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { s.Comments, p.accComs = p.accComs, nil + for p.peekRedir() { + p.doRedirect(s) + } + redirsStart := len(s.Redirs) switch p.tok { case _LitWord: switch p.val { case "{": p.block(s) + case "{}": + // Zsh treats closing braces in a special way, allowing this. + if p.lang.in(LangZsh) { + s.Cmd = &Block{Lbrace: p.pos, Rbrace: posAddCol(p.pos, 1)} + p.next() + } case "if": p.ifClause(s) case "while", "until": + // TODO(zsh): "repeat" p.whileClause(s, p.val == "until") case "for": p.forClause(s) case "case": p.caseClause(s) + // TODO(zsh): { try-list } "always" { always-list } case "}": - p.curErr(`%q can only be used to close a block`, p.val) - case "then": - p.curErr(`%q can only be used in an if`, p.val) - case "elif": - p.curErr(`%q can only be used in an if`, p.val) + p.curErr(`%#q can only be used to close a block`, rightBrace) + case "then", "elif": + p.curErr("%#q can only be used in an `if`", p.val) case "fi": - p.curErr(`%q can only be used to end an if`, p.val) + p.curErr("%#q can only be used to end an `if`", p.val) case "do": - p.curErr(`%q can only be used in a loop`, p.val) + p.curErr(`%#q can only be used in a loop`, p.val) case "done": - p.curErr(`%q can only be used to end a loop`, p.val) + p.curErr(`%#q can only be used to end a loop`, p.val) case "esac": - p.curErr(`%q can only be used to end a case`, p.val) + p.curErr("%#q can only be used to end a `case`", p.val) case "!": if !s.Negated { - p.curErr(`"!" can only be used in full statements`) + p.curErr(`%#q can only be used in full statements`, exclMark) break } case "[[": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.testClause(s) } case "]]": - if p.lang != LangPOSIX { - p.curErr(`%q can only be used to close a test`, - p.val) + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { + p.curErr(`%#q can only be used to close a test`, dblRightBrack) } case "let": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.letClause(s) } case "function": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.bashFuncDecl(s) } case "declare": - if p.lang.isBash() { + if p.lang.in(langBashLike | LangZsh) { // Note that mksh lacks this one. p.declClause(s) } case "local", "export", "readonly", "typeset", "nameref": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.declClause(s) } case "time": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.timeClause(s) } case "coproc": - if p.lang.isBash() { + if p.lang.in(langBashLike) { // Note that mksh lacks this one. p.coprocClause(s) } case "select": - if p.lang != LangPOSIX { + if p.lang.in(langBashLike | LangMirBSDKorn | LangZsh) { p.selectClause(s) } case "@test": - if p.lang == LangBats { + if p.lang.in(LangBats) { p.testDecl(s) } } @@ -1795,43 +2204,61 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { break } name := p.lit(p.pos, p.val) - if p.next(); p.got(leftParen) { + p.next() + // In zsh, ( after a word is a glob qualifier unless followed + // immediately by ), which is the func declaration syntax. + if p.tok == leftParen && (!p.lang.in(LangZsh) || p.r == ')') { + p.next() p.follow(name.ValuePos, "foo(", rightParen) - if p.lang == LangPOSIX && !ValidName(name.Value) { + if p.lang.in(LangPOSIX) && !ValidName(name.Value) { p.posErr(name.Pos(), "invalid func name") } - p.funcDecl(s, name, name.ValuePos, true) + p.funcDecl(s, name.ValuePos, false, true, name) } else { - p.callExpr(s, p.word(p.wps(name)), false) + w := p.wordOne(name) + if p.lang.in(LangZsh) && !p.spaced { + w.Parts = append(w.Parts, p.wordParts(nil)...) + } + p.callExpr(s, w, false) } - case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, - hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: - p.doRedirect(s) - p.callExpr(s, nil, false) case bckQuote: if p.backquoteEnd() { - return nil + break } fallthrough - case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, + case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, assgnParen, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: if p.hasValidIdent() { 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") } p.callExpr(s, w, false) case leftParen: + if p.r == ')' { + p.rune() + fpos := p.pos + p.next() + if p.tok == _LitWord && p.val == "{" { + p.checkLang(fpos, LangZsh, "anonymous functions") + } + p.funcDecl(s, fpos, false, true) + break + } p.subshell(s) case dblLeftParen: p.arithmExpCmd(s) - default: - if len(s.Redirs) == 0 { - return nil + } + if s.Cmd == nil && len(s.Redirs) == 0 { + return nil // no statement found + } + if redirsStart > 0 && s.Cmd != nil { + if _, ok := s.Cmd.(*CallExpr); !ok { + p.checkLang(s.Pos(), LangZsh, "redirects before compound commands") } } for p.peekRedir() { @@ -1844,7 +2271,7 @@ func (p *Parser) gotStmtPipe(s *Stmt, binCmd bool) *Stmt { // right recursion should only read a single element return s } - if p.tok == orAnd && p.lang == LangMirBSDKorn { + if p.tok == orAnd && p.lang.in(LangMirBSDKorn) { // No need to check for LangPOSIX, as on that language // we parse |& as two tokens. break @@ -1852,11 +2279,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, noQuote("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 @@ -1870,7 +2301,7 @@ func (p *Parser) subshell(s *Stmt) { sub := &Subshell{Lparen: p.pos} old := p.preNested(subCmd) p.next() - sub.Stmts, sub.Last = p.stmtList() + sub.Stmts, sub.Last = p.followStmts("(", sub.Lparen) p.postNested(old) sub.Rparen = p.matched(sub.Lparen, leftParen, rightParen) s.Cmd = sub @@ -1881,9 +2312,7 @@ func (p *Parser) arithmExpCmd(s *Stmt) { old := p.preNested(arithmExprCmd) p.next() if p.got(hash) { - if p.lang != LangMirBSDKorn { - p.langErr(ar.Pos(), "unsigned expressions", LangMirBSDKorn) - } + p.checkLang(ar.Pos(), LangMirBSDKorn, "unsigned expressions") ar.Unsigned = true } ar.X = p.followArithm(dblLeftParen, ar.Left) @@ -1894,11 +2323,13 @@ func (p *Parser) arithmExpCmd(s *Stmt) { 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 { - p.matchingErr(b.Lbrace, "{", "}") + b.Stmts, b.Last = p.followStmts("{", b.Lbrace, "}") + if pos, ok := p.gotRsrv("}"); ok { + b.Rbrace = pos + } else if p.recoverError() { + b.Rbrace = recoveredPos + } else { + p.matchingErr(b.Lbrace, leftBrace, rightBrace) } s.Cmd = b } @@ -1962,9 +2393,7 @@ func (p *Parser) forClause(s *Stmt) { start, end := "do", "done" if pos, ok := p.gotRsrv("{"); ok { - if p.lang == LangPOSIX { - p.langErr(pos, "for loops with braces", LangBash, LangMirBSDKorn) - } + p.checkLang(pos, langBashLike|LangMirBSDKorn, "for loops with braces") fc.DoPos = pos fc.Braces = true start, end = "{", "}" @@ -1980,11 +2409,9 @@ func (p *Parser) forClause(s *Stmt) { } func (p *Parser) loop(fpos Pos) Loop { - if !p.lang.isBash() { - switch p.tok { - case leftParen, dblLeftParen: - p.langErr(p.pos, "c-style fors", LangBash) - } + switch p.tok { + case leftParen, dblLeftParen: + p.checkLang(p.pos, langBashLike|LangZsh, "c-style fors") } if p.tok == dblLeftParen { cl := &CStyleLoop{Lparen: p.pos} @@ -2008,7 +2435,7 @@ func (p *Parser) loop(fpos Pos) Loop { func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter { wi := &WordIter{} if wi.Name = p.getLit(); wi.Name == nil { - p.followErr(fpos, ftok, "a literal") + p.followErr(fpos, ftok, noQuote("a literal")) } if p.got(semicolon) { p.got(_Newl) @@ -2028,7 +2455,7 @@ func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter { p.got(_Newl) } else if p.tok == _LitWord && p.val == "do" { } else { - p.followErr(fpos, ftok+" foo", `"in", "do", ;, or a newline`) + p.followErr(fpos, ftok+" foo", noQuote("`in`, `do`, `;`, or a newline")) } return wi } @@ -2048,16 +2475,14 @@ func (p *Parser) caseClause(s *Stmt) { p.next() cc.Word = p.getWord() if cc.Word == nil { - p.followErr(cc.Case, "case", "a word") + p.followErr(cc.Case, "case", noQuote("a word")) } end := "esac" p.got(_Newl) if pos, ok := p.gotRsrv("{"); ok { cc.In = pos cc.Braces = true - if p.lang != LangMirBSDKorn { - p.posErr(cc.Pos(), `"case i {" is a mksh feature`) - } + p.checkLang(cc.Pos(), LangMirBSDKorn, "`case i {`") end = "}" } else { cc.In = p.followRsrv(cc.Case, "case x", "in") @@ -2070,7 +2495,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) @@ -2084,7 +2509,7 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) { break } if !p.got(or) { - p.curErr("case patterns must be separated with |") + p.curErr("case patterns must be separated with %#q", or) } } old := p.preNested(switchCase) @@ -2096,7 +2521,7 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) { default: ci.Op = Break items = append(items, ci) - return + return items } ci.Last = append(ci.Last, p.accComs...) p.accComs = nil @@ -2104,49 +2529,54 @@ 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 + return items } func (p *Parser) testClause(s *Stmt) { tc := &TestClause{Left: p.pos} old := p.preNested(testExpr) p.next() - 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) - if tc.X == nil { - p.followErrExp(tc.Left, "[[") + if tc.X = p.testExprBinary(false); tc.X == nil { + p.followErrExp(tc.Left, dblLeftBrack) } tc.Right = p.pos if _, ok := p.gotRsrv("]]"); !ok { - p.matchingErr(tc.Left, "[[", "]]") + p.matchingErr(tc.Left, dblLeftBrack, dblRightBrack) } p.postNested(old) s.Cmd = tc } -func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { +func (p *Parser) testExprBinary(pastAndOr bool) TestExpr { p.got(_Newl) var left TestExpr if pastAndOr { - left = p.testExprBase() + left = p.testExprUnary() } else { - left = p.testExpr(ftok, fpos, true) + left = p.testExprBinary(true) } if left == nil { return left @@ -2159,7 +2589,7 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { return left } if p.tok = token(testBinaryOp(p.val)); p.tok == illegalTok { - p.curErr("not a valid test operator: %s", p.val) + p.curErr("not a valid test operator: %#q", p.val) } case rdrIn, rdrOut: case _EOF, rightParen: @@ -2167,26 +2597,21 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { case _Lit: p.curErr("test operator words must consist of a single literal") default: - p.curErr("not a valid test operator: %v", p.tok) + p.curErr("not a valid test operator: %#q", p.tok) } b := &BinaryTest{ OpPos: p.pos, Op: BinTestOperator(p.tok), X: left, } - // Save the previous quoteState, since we change it in TsReMatch. - oldQuote := p.quote - switch b.Op { case AndTest, OrTest: p.next() - if b.Y = p.testExpr(token(b.Op), b.OpPos, false); b.Y == nil { - p.followErrExp(b.OpPos, b.Op.String()) + if b.Y = p.testExprBinary(false); b.Y == nil { + p.followErrExp(b.OpPos, b.Op) } case TsReMatch: - if !p.lang.isBash() { - p.langErr(p.pos, "regex tests", LangBash) - } + p.checkLang(p.pos, langBashLike|LangZsh, "regex tests") p.rxOpenParens = 0 p.rxFirstPart = true // TODO(mvdan): Using nested states within a regex will break in @@ -2196,17 +2621,16 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { fallthrough default: if _, ok := b.X.(*Word); !ok { - p.posErr(b.OpPos, "expected %s, %s or %s after complex expr", - AndTest, OrTest, "]]") + p.posErr(b.OpPos, "expected %#q, %#q or %#q after complex expr", + AndTest, OrTest, dblRightBrack) } p.next() b.Y = p.followWordTok(token(b.Op), b.OpPos) } - p.quote = oldQuote return b } -func (p *Parser) testExprBase() TestExpr { +func (p *Parser) testExprUnary() TestExpr { switch p.tok { case _EOF, rightParen: return nil @@ -2215,7 +2639,7 @@ func (p *Parser) testExprBase() TestExpr { switch op { case illegalTok: case tsRefVar, tsModif: // not available in mksh - if p.lang.isBash() { + if p.lang.in(langBashLike) { p.tok = op } default: @@ -2226,8 +2650,8 @@ 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 { - p.followErrExp(u.OpPos, u.Op.String()) + if u.X = p.testExprBinary(false); u.X == nil { + p.followErrExp(u.OpPos, u.Op) } return u case tsExists, tsRegFile, tsDirect, tsCharSp, tsBlckSp, tsNmPipe, @@ -2241,8 +2665,8 @@ 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 { - p.followErrExp(pe.Lparen, "(") + if pe.X = p.testExprBinary(false); pe.X == nil { + p.followErrExp(pe.Lparen, leftParen) } pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) return pe @@ -2266,7 +2690,7 @@ func (p *Parser) declClause(s *Stmt) { for !p.stopToken() && !p.peekRedir() { if p.hasValidIdent() { ds.Args = append(ds.Args, p.getAssign(false)) - } else if p.eqlOffs > 0 { + } else if p.tok.isLit() && p.eqlOffs > 0 && !strings.Contains(p.val[:p.eqlOffs], "{") { p.curErr("invalid var name") } else if p.tok == _LitWord && ValidName(p.val) { ds.Args = append(ds.Args, &Assign{ @@ -2279,7 +2703,7 @@ func (p *Parser) declClause(s *Stmt) { Value: w, }) } else { - p.followErr(p.pos, ds.Variant.Value, "names or assignments") + p.followErr(p.pos, ds.Variant.Value, noQuote("names or assignments")) } } s.Cmd = ds @@ -2306,7 +2730,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 +2738,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 { @@ -2359,26 +2783,40 @@ func (p *Parser) letClause(s *Stmt) { func (p *Parser) bashFuncDecl(s *Stmt) { fpos := p.pos - if p.next(); p.tok != _LitWord { - p.followErr(fpos, "function", "a name") + p.next() + names := make([]*Lit, 0, 1) + for p.tok == _LitWord && p.val != "{" { + names = append(names, p.lit(p.pos, p.val)) + p.next() } - name := p.lit(p.pos, p.val) - hasParens := false - if p.next(); p.got(leftParen) { - hasParens = true - p.follow(name.ValuePos, "foo(", rightParen) + hasParens := p.got(leftParen) + switch len(names) { + case 0: + if hasParens || (p.tok == _LitWord && p.val == "{") { + p.checkLang(fpos, LangZsh, "anonymous functions") + } else if !p.lang.in(LangZsh) { + p.followErr(fpos, "function", noQuote("a name")) + } + names = nil // avoid non-nil zero-length slices + case 1: + // allowed in all variants + default: + p.checkLang(fpos, LangZsh, "multi-name functions") } - p.funcDecl(s, name, fpos, hasParens) + if hasParens { + p.follow(fpos, "function foo(", rightParen) + } + p.funcDecl(s, fpos, true, hasParens, names...) } func (p *Parser) testDecl(s *Stmt) { td := &TestDecl{Position: p.pos} p.next() if td.Description = p.getWord(); td.Description == nil { - p.followErr(td.Position, "@test", "a description word") + p.followErr(td.Position, "@test", noQuote("a description word")) } if td.Body = p.getStmt(false, false, true); td.Body == nil { - p.followErr(td.Position, `@test "desc"`, "a statement") + p.followErr(td.Position, `@test "desc"`, noQuote("a statement")) } s.Cmd = td } @@ -2394,7 +2832,7 @@ func (p *Parser) callExpr(s *Stmt, w *Word, assign bool) { loop: for { switch p.tok { - case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, + case _EOF, _Newl, semicolon, and, or, andAnd, orOr, orAnd, andPipe, andBang, dblSemicolon, semiAnd, dblSemiAnd, semiOr: break loop case _LitWord: @@ -2402,42 +2840,56 @@ 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.val == "{" && w != nil && w.Lit() == "function" { + p.checkLang(p.pos, langBashLike, `the "function" builtin`) + } + // Zsh does not require a semicolon to close a block. + if p.lang.in(LangZsh) && p.val == "}" { + break loop + } + w := p.wordOne(p.lit(p.pos, p.val)) p.next() + if p.lang.in(LangZsh) && !p.spaced { + w.Parts = append(w.Parts, p.wordParts(nil)...) + } + ce.Args = append(ce.Args, w) 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 } fallthrough - case dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, + case dollBrace, dollDblParen, dollParen, dollar, cmdIn, assgnParen, cmdOut, sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, globQuest, globStar, globPlus, globAt, globExcl: - ce.Args = append(ce.Args, p.word(p.wordParts())) - case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, - hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: - p.doRedirect(s) + ce.Args = append(ce.Args, p.wordAnyNumber()) case dblLeftParen: - p.curErr("%s can only be used to open an arithmetic cmd", p.tok) + p.curErr("%#q can only be used to open an arithmetic cmd", p.tok) case rightParen: if p.quote == subCmd { break loop } fallthrough default: - p.curErr("a command can only contain words and redirects; encountered %s", p.tok) + if p.peekRedir() { + p.doRedirect(s) + continue + } + // Note that we'll only keep the first error that happens. + if len(ce.Args) > 0 { + if cmd := ce.Args[0].Lit(); isBashCompoundCommand(_LitWord, cmd) { + p.checkLang(p.pos, langBashLike, "the %#q builtin", cmd) + } + } + p.curErr("a command can only contain words and redirects; encountered %#q", p.tok) } } - if len(ce.Assigns) == 0 && len(ce.Args) == 0 { - return - } if len(ce.Args) == 0 { ce.Args = nil } else { @@ -2450,16 +2902,21 @@ loop: s.Cmd = ce } -func (p *Parser) funcDecl(s *Stmt, name *Lit, pos Pos, withParens bool) { +func (p *Parser) funcDecl(s *Stmt, pos Pos, long, withParens bool, names ...*Lit) { fd := &FuncDecl{ Position: pos, - RsrvWord: pos != name.ValuePos, + RsrvWord: long, Parens: withParens, - Name: name, + } + if len(names) == 1 { + fd.Name = names[0] + } else { + fd.Names = names } p.got(_Newl) + // TODO: reject any body which isn't a compound command, like a quoted word if fd.Body = p.getStmt(false, false, true); fd.Body == nil { - p.followErr(fd.Pos(), "foo()", "a statement") + p.followErr(fd.Pos(), "foo()", noQuote("a statement")) } s.Cmd = fd } diff --git a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go index 0021c62d7c..6062dac051 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go +++ b/vendor/mvdan.cc/sh/v3/syntax/parser_arithm.go @@ -18,19 +18,20 @@ func (p *Parser) arithmExprAssign(compact bool) ArithmExpr { value := p.arithmExprTernary(compact) switch BinAritOperator(p.tok) { case AddAssgn, SubAssgn, MulAssgn, QuoAssgn, RemAssgn, AndAssgn, - OrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn: + OrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn, + AndBoolAssgn, OrBoolAssgn, XorBoolAssgn, PowAssgn: if compact && p.spaced { return value } if !isArithName(value) { - p.posErr(p.pos, "%s must follow a name", p.tok.String()) + p.posErr(p.pos, "%#q must follow a name", p.tok) } pos := p.pos tok := p.tok p.nextArithOp(compact) y := p.arithmExprAssign(compact) if y == nil { - p.followErrExp(pos, tok.String()) + p.followErrExp(pos, tok) } return &BinaryArithm{ OpPos: pos, @@ -49,25 +50,25 @@ func (p *Parser) arithmExprTernary(compact bool) ArithmExpr { } if value == nil { - p.curErr("%s must follow an expression", p.tok.String()) + p.curErr("%#q must follow an expression", p.tok) } questPos := p.pos p.nextArithOp(compact) if BinAritOperator(p.tok) == TernColon { - p.followErrExp(questPos, TernQuest.String()) + p.followErrExp(questPos, TernQuest) } trueExpr := p.arithmExpr(compact) if trueExpr == nil { - p.followErrExp(questPos, TernQuest.String()) + p.followErrExp(questPos, TernQuest) } if BinAritOperator(p.tok) != TernColon { - p.posErr(questPos, "ternary operator missing : after ?") + p.posErr(questPos, "ternary operator missing %#q after %#q", colon, quest) } colonPos := p.pos p.nextArithOp(compact) falseExpr := p.arithmExprTernary(compact) if falseExpr == nil { - p.followErrExp(colonPos, TernColon.String()) + p.followErrExp(colonPos, TernColon) } return &BinaryArithm{ OpPos: questPos, @@ -83,7 +84,7 @@ func (p *Parser) arithmExprTernary(compact bool) ArithmExpr { } func (p *Parser) arithmExprLor(compact bool) ArithmExpr { - return p.arithmExprBinary(compact, p.arithmExprLand, OrArit) + return p.arithmExprBinary(compact, p.arithmExprLand, OrArit, XorBool) } func (p *Parser) arithmExprLand(compact bool) ArithmExpr { @@ -130,7 +131,7 @@ func (p *Parser) arithmExprPower(compact bool) ArithmExpr { } if value == nil { - p.curErr("%s must follow an expression", p.tok.String()) + p.curErr("%#q must follow an expression", p.tok) } op := p.tok @@ -138,7 +139,7 @@ func (p *Parser) arithmExprPower(compact bool) ArithmExpr { p.nextArithOp(compact) y := p.arithmExprPower(compact) if y == nil { - p.followErrExp(pos, op.String()) + p.followErrExp(pos, op) } return &BinaryArithm{ OpPos: pos, @@ -158,7 +159,7 @@ func (p *Parser) arithmExprUnary(compact bool) ArithmExpr { ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} p.nextArithOp(compact) if ue.X = p.arithmExprUnary(compact); ue.X == nil { - p.followErrExp(ue.OpPos, ue.Op.String()) + p.followErrExp(ue.OpPos, ue.Op) } return ue } @@ -172,29 +173,36 @@ func (p *Parser) arithmExprValue(compact bool) ArithmExpr { ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} p.nextArith(compact) if p.tok != _LitWord { - p.followErr(ue.OpPos, token(ue.Op).String(), "a literal") + p.followErr(ue.OpPos, ue.Op, noQuote("a literal")) } ue.X = p.arithmExprValue(compact) return ue case leftParen: + if p.quote == paramExpArithm && p.lang.in(LangZsh) { + x = p.zshSubFlags() + break + } pe := &ParenArithm{Lparen: p.pos} p.nextArithOp(compact) pe.X = p.followArithm(leftParen, pe.Lparen) pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) + if p.quote == paramExpArithm && p.tok == _LitWord { + p.checkLang(pe.Lparen, LangZsh, "subscript flags") + } x = pe case leftBrack: - p.curErr("[ must follow a name") + p.curErr("%#q must follow a name like a[i]", p.tok) case colon: - p.curErr("ternary operator missing ? before :") + p.curErr("ternary operator missing %#q before %#q", quest, colon) 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 := &ParamExp{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 @@ -219,7 +227,7 @@ func (p *Parser) arithmExprValue(compact bool) ArithmExpr { // sets the type to non-nil and then x != nil if p.tok == addAdd || p.tok == subSub { if !isArithName(x) { - p.curErr("%s must follow a name", p.tok.String()) + p.curErr("%#q must follow a name", p.tok) } u := &UnaryArithm{ Post: true, @@ -250,7 +258,7 @@ func (p *Parser) nextArithOp(compact bool) { pos := p.pos tok := p.tok if p.nextArith(compact) { - p.followErrExp(pos, tok.String()) + p.followErrExp(pos, tok) } } @@ -271,14 +279,14 @@ func (p *Parser) arithmExprBinary(compact bool, nextOp func(bool) ArithmExpr, op } if value == nil { - p.curErr("%s must follow an expression", p.tok.String()) + p.curErr("%#q must follow an expression", p.tok) } pos := p.pos p.nextArithOp(compact) y := nextOp(compact) if y == nil { - p.followErrExp(pos, foundOp.String()) + p.followErrExp(pos, foundOp) } value = &BinaryArithm{ @@ -295,11 +303,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 } @@ -308,7 +316,7 @@ func isArithName(left ArithmExpr) bool { func (p *Parser) followArithm(ftok token, fpos Pos) ArithmExpr { x := p.arithmExpr(false) if x == nil { - p.followErrExp(fpos, ftok.String()) + p.followErrExp(fpos, ftok) } return x } @@ -320,16 +328,18 @@ func (p *Parser) peekArithmEnd() bool { func (p *Parser) arithmMatchingErr(pos Pos, left, right token) { switch p.tok { case _Lit, _LitWord: - p.curErr("not a valid arithmetic operator: %s", p.val) + p.curErr("not a valid arithmetic operator: %#q", p.val) case leftBrack: - p.curErr("[ must follow a name") + p.curErr("%#q must follow a name like a[i]", leftBrack) case colon: - p.curErr("ternary operator missing ? before :") + p.curErr("ternary operator missing %#q before %#q", quest, colon) case rightParen, _EOF: p.matchingErr(pos, left, right) + case period: + p.checkLang(p.pos, LangZsh, `floating point arithmetic`) default: - if p.quote == arithmExpr { - p.curErr("not a valid arithmetic operator: %v", p.tok) + if p.quote&allArithmExpr != 0 { + p.curErr("not a valid arithmetic operator: %#q", p.tok) } p.matchingErr(pos, left, right) } @@ -343,6 +353,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..9015231235 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/printer.go +++ b/vendor/mvdan.cc/sh/v3/syntax/printer.go @@ -55,18 +55,23 @@ 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 { // Enable the flag, and set up the writer wrapper. p.keepPadding = true - p.cols.Writer = p.bufWriter.(*bufio.Writer) - p.bufWriter = &p.cols + p.cols.Writer = p.w.(*bufio.Writer) + p.w = &p.cols } else if !enabled && p.keepPadding { // Ensure we reset the state to that of NewPrinter. p.keepPadding = false - p.bufWriter = p.cols.Writer + p.w = p.cols.Writer p.cols = colCounter{} } } @@ -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 } } @@ -97,7 +102,7 @@ func FunctionNextLine(enabled bool) PrinterOption { // NewPrinter allocates a new Printer and applies any number of options. func NewPrinter(opts ...PrinterOption) *Printer { p := &Printer{ - bufWriter: bufio.NewWriter(nil), + w: bufio.NewWriter(nil), tabWriter: new(tabwriter.Writer), } for _, opt := range opts { @@ -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() @@ -133,32 +138,32 @@ func (p *Printer) Print(w io.Writer, node Node) error { p.tabWriter.Init(w, 0, tabwidth, 1, ' ', twmode) w = p.tabWriter - p.bufWriter.Reset(w) - switch x := node.(type) { + p.w.Reset(w) + 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() // flush the writers - if err := p.bufWriter.Flush(); err != nil { + if err := p.w.Flush(); err != nil { return err } if tw, _ := w.(*tabwriter.Writer); tw != nil { @@ -216,9 +221,9 @@ func (c *colCounter) Reset(w io.Writer) { // Printer holds the internal state of the printing mechanism of a // program. type Printer struct { - bufWriter + w bufWriter tabWriter *tabwriter.Writer - cols colCounter + cols colCounter // used for [KeepPadding] indentSpaces uint binNextLine bool @@ -280,13 +285,13 @@ func (p *Printer) reset() { } func (p *Printer) spaces(n uint) { - for i := uint(0); i < n; i++ { - p.WriteByte(' ') + for range n { + p.w.WriteByte(' ') } } func (p *Printer) space() { - p.WriteByte(' ') + p.w.WriteByte(' ') p.wantSpace = spaceWritten } @@ -297,27 +302,32 @@ func (p *Printer) spacePad(pos Pos) { return } if p.wantSpace == spaceRequired { - p.WriteByte(' ') + p.w.WriteByte(' ') p.wantSpace = spaceWritten } for p.cols.column > 0 && p.cols.column < int(pos.Col()) { - p.WriteByte(' ') + p.w.WriteByte(' ') } } // 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 } @@ -326,42 +336,42 @@ func (p *Printer) bslashNewl() { if p.wantSpace == spaceRequired { p.space() } - p.WriteString("\\\n") + p.w.WriteString("\\\n") p.line++ p.indent() } func (p *Printer) spacedString(s string, pos Pos) { p.spacePad(pos) - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } func (p *Printer) spacedToken(s string, pos Pos) { if p.minify { - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceNotRequired return } p.spacePad(pos) - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } func (p *Printer) semiOrNewl(s string, pos Pos) { - if p.wantsNewline(Pos{}) { + if p.wantsNewline(Pos{}, false) { p.newline(pos) p.indent() } else { if !p.wroteSemi { - p.WriteByte(';') + p.w.WriteByte(';') } if !p.minify { p.space() } - p.line = pos.Line() + p.advanceLine(pos.Line()) } - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } @@ -370,10 +380,10 @@ func (p *Printer) writeLit(s string) { // <<- heredoc bodies, so the parent printer will add the escape bytes // later. if p.tabWriter != nil && strings.Contains(s, "\t") { - p.WriteByte(tabwriter.Escape) - defer p.WriteByte(tabwriter.Escape) + p.w.WriteByte(tabwriter.Escape) + defer p.w.WriteByte(tabwriter.Escape) } - p.WriteString(s) + p.w.WriteString(s) } func (p *Printer) incLevel() { @@ -403,11 +413,11 @@ func (p *Printer) indent() { switch { case p.level == 0: case p.indentSpaces == 0: - p.WriteByte(tabwriter.Escape) + p.w.WriteByte(tabwriter.Escape) for i := uint(0); i < p.level; i++ { - p.WriteByte('\t') + p.w.WriteByte('\t') } - p.WriteByte(tabwriter.Escape) + p.w.WriteByte(tabwriter.Escape) default: p.spaces(p.indentSpaces * p.level) } @@ -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.w.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 } } @@ -452,18 +467,18 @@ func (p *Printer) flushHeredocs() { for _, r := range hdocs { p.line++ - p.WriteByte('\n') + p.w.WriteByte('\n') p.wantSpace = spaceWritten p.wantNewline, p.wantNewline = false, false if r.Op == DashHdoc && p.indentSpaces == 0 && !p.minify { if r.Hdoc != nil { extra := extraIndenter{ - bufWriter: p.bufWriter, + bufWriter: p.w, baseIndent: int(p.level + 1), firstIndent: -1, } p.tabsPrinter = &Printer{ - bufWriter: &extra, + w: &extra, // The options need to persist. indentSpaces: p.indentSpaces, @@ -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,45 +510,50 @@ 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.w.WriteByte('\n') + p.wantSpace = spaceWritten + p.wantNewline, p.mustNewline = false, false + + l := pos.Line() + if l > p.line+1 && !p.minify { + p.w.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(')') + p.w.WriteByte(')') p.wantSpace = spaceRequired } func (p *Printer) semiRsrv(s string, pos Pos) { - if p.wantsNewline(pos) { + if p.wantsNewline(pos, false) { p.newlines(pos) } else { if !p.wroteSemi { - p.WriteByte(';') + p.w.WriteByte(';') } if !p.minify { p.spacePad(pos) } } - p.WriteString(s) + p.w.WriteString(s) p.wantSpace = spaceRequired } @@ -550,9 +570,9 @@ func (p *Printer) flushComments() { cline := c.Hash.Line() switch { case p.mustNewline, i > 0, cline > p.line && p.line > 0: - p.WriteByte('\n') + p.w.WriteByte('\n') if cline > p.line+1 { - p.WriteByte('\n') + p.w.WriteByte('\n') } p.indent() p.wantSpace = spaceWritten @@ -561,16 +581,14 @@ func (p *Printer) flushComments() { if p.keepPadding { p.spacePad(c.Pos()) } else { - p.WriteByte('\t') + p.w.WriteByte('\t') } case p.wantSpace != spaceWritten: p.space() } // don't go back one line, which may happen in some edge cases - if p.line < cline { - p.line = cline - } - p.WriteByte('#') + p.advanceLine(cline) + p.w.WriteByte('#') p.writeLit(strings.TrimRightFunc(c.Text, unicode.IsSpace)) p.wantNewline = true p.mustNewline = true @@ -582,8 +600,8 @@ func (p *Printer) comments(comments ...Comment) { if p.minify { for _, c := range comments { if fileutil.Shebang([]byte("#"+c.Text)) != "" && c.Hash.Col() == 1 && c.Hash.Line() == 1 { - p.WriteString(strings.TrimRightFunc("#"+c.Text, unicode.IsSpace)) - p.WriteString("\n") + p.w.WriteString(strings.TrimRightFunc("#"+c.Text, unicode.IsSpace)) + p.w.WriteString("\n") p.line++ } } @@ -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) { @@ -605,123 +631,98 @@ func (p *Printer) wordParts(wps []WordPart, quoted bool) { // Can't use p.wantsNewline here, since this is only about // escaped newlines. for quoted && !p.singleLine && wp.Pos().Line() > p.line { - p.WriteString("\\\n") + p.w.WriteString("\\\n") 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 { - p.WriteByte('$') + if wp.Dollar { + p.w.WriteByte('$') } - p.WriteByte('\'') - p.writeLit(x.Value) - p.WriteByte('\'') - p.line = x.End().Line() + p.w.WriteByte('\'') + p.writeLit(wp.Value) + p.w.WriteByte('\'') + p.advanceLine(wp.End().Line()) case *DblQuoted: - p.dblQuoted(x) + p.dblQuoted(wp) case *CmdSubst: - p.line = x.Pos().Line() - switch { - case x.TempFile: - p.WriteString("${") - p.wantSpace = spaceRequired - p.nestedStmts(x.Stmts, x.Last, x.Right) - p.wantSpace = spaceNotRequired - p.semiRsrv("}", x.Right) - case x.ReplyVar: - p.WriteString("${|") - p.nestedStmts(x.Stmts, x.Last, x.Right) - p.wantSpace = spaceNotRequired - p.semiRsrv("}", x.Right) - // Special case: `# inline comment` - case x.Backquotes && len(x.Stmts) == 0 && - len(x.Last) == 1 && x.Right.Line() == p.line: - p.WriteString("`#") - p.WriteString(x.Last[0].Text) - p.WriteString("`") - default: - p.WriteString("$(") - if len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) { - p.wantSpace = spaceRequired - } else { - p.wantSpace = spaceNotRequired - } - p.nestedStmts(x.Stmts, x.Last, x.Right) - p.rightParen(x.Right) - } + p.advanceLine(wp.Pos().Line()) + p.cmdSubst(wp) case *ParamExp: litCont := ";" if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" { litCont = nextLit.Value[:1] } - name := x.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 len(name) > 1 && !ValidName(name): // ${10} - case ValidName(name + litCont): // ${var}cont - default: - x2 := *x - x2.Short = true - p.paramExp(&x2) - return + if p.minify && !wp.Short && wp.simple() { + name := wp.Param.Value + switch { + case len(name) > 1 && !ValidName(name): // ${10} + case ValidName(name + litCont): // ${var}cont + default: + x2 := *wp + x2.Short = true + p.paramExp(&x2) + return + } } - p.paramExp(x) + p.paramExp(wp) case *ArithmExp: - p.WriteString("$((") - if x.Unsigned { - p.WriteString("# ") + p.w.WriteString("$((") + if wp.Unsigned { + p.w.WriteString("# ") } - p.arithmExpr(x.X, false, false) - p.WriteString("))") + p.arithmExpr(wp.X, false, false) + p.w.WriteString("))") case *ExtGlob: - p.WriteString(x.Op.String()) - p.writeLit(x.Pattern.Value) - p.WriteByte(')') + p.w.WriteString(wp.Op.String()) + p.writeLit(wp.Pattern.Value) + p.w.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.w.WriteString(wp.Op.String()) + p.nestedStmts(wp.Stmts, wp.Last, wp.Rparen) + p.rightParen(wp.Rparen) } } func (p *Printer) dblQuoted(dq *DblQuoted) { if dq.Dollar { - p.WriteByte('$') + p.w.WriteByte('$') } - p.WriteByte('"') + p.w.WriteByte('"') if len(dq.Parts) > 0 { p.wordParts(dq.Parts, true) } // Add any trailing escaped newlines. for p.line < dq.Right.Line() { - p.WriteString("\\\n") + p.w.WriteString("\\\n") p.line++ } - p.WriteByte('"') + p.w.WriteByte('"') } func (p *Printer) wroteIndex(index ArithmExpr) bool { if index == nil { return false } - p.WriteByte('[') - p.arithmExpr(index, false, false) - p.WriteByte(']') + p.w.WriteByte('[') + // Note that e.g. foo[1,3]=$bar in Zsh does not allow any spaces around the comma, + // as that breaks the assignment word. + binary, ok := index.(*BinaryArithm) + compact := ok && binary.Op == Comma + p.arithmExpr(index, compact, false) + p.w.WriteByte(']') return true } @@ -731,73 +732,125 @@ func (p *Printer) paramExp(pe *ParamExp) { p.wroteIndex(pe.Index) return } - if pe.Short { // $var - p.WriteByte('$') - p.writeLit(pe.Param.Value) - return + p.w.WriteByte('$') + if !pe.Short { + p.w.WriteByte('{') + } + if pe.Flags != nil { + p.w.WriteByte('(') + p.writeLit(pe.Flags.Value) + p.w.WriteByte(')') } - // ${var...} - p.WriteString("${") switch { case pe.Length: - p.WriteByte('#') + p.w.WriteByte('#') case pe.Width: - p.WriteByte('%') + p.w.WriteByte('%') + case pe.IsSet: + p.w.WriteByte('+') case pe.Excl: - p.WriteByte('!') + p.w.WriteByte('!') + } + switch { + case pe.Param != nil: + p.writeLit(pe.Param.Value) + case pe.NestedParam != nil: + // Note that Zsh supports ${${nested}} but not ${$nested}, + // so we need to avoid that simplification here. + saved := p.minify + p.minify = false + p.wordPart(pe.NestedParam, nil) + p.minify = saved } - p.writeLit(pe.Param.Value) p.wroteIndex(pe.Index) switch { + case len(pe.Modifiers) > 0: + for _, lit := range pe.Modifiers { + p.w.WriteByte(':') + p.w.WriteString(lit.Value) + } case pe.Slice != nil: - p.WriteByte(':') + p.w.WriteByte(':') p.arithmExpr(pe.Slice.Offset, true, true) if pe.Slice.Length != nil { - p.WriteByte(':') + p.w.WriteByte(':') p.arithmExpr(pe.Slice.Length, true, false) } case pe.Repl != nil: if pe.Repl.All { - p.WriteByte('/') + p.w.WriteByte('/') } - p.WriteByte('/') + p.w.WriteByte('/') if pe.Repl.Orig != nil { p.word(pe.Repl.Orig) } - p.WriteByte('/') + p.w.WriteByte('/') if pe.Repl.With != nil { p.word(pe.Repl.With) } case pe.Names != 0: p.writeLit(pe.Names.String()) case pe.Exp != nil: - p.WriteString(pe.Exp.Op.String()) + p.w.WriteString(pe.Exp.Op.String()) if pe.Exp.Word != nil { p.word(pe.Exp.Word) } } - p.WriteByte('}') + if !pe.Short { + p.w.WriteByte('}') + } +} + +func (p *Printer) cmdSubst(cs *CmdSubst) { + switch { + case cs.TempFile: + p.w.WriteString("${") + p.wantSpace = spaceRequired + p.nestedStmts(cs.Stmts, cs.Last, cs.Right) + p.wantSpace = spaceNotRequired + p.semiRsrv("}", cs.Right) + case cs.ReplyVar: + p.w.WriteString("${|") + p.nestedStmts(cs.Stmts, cs.Last, cs.Right) + p.wantSpace = spaceNotRequired + p.semiRsrv("}", cs.Right) + // Special case: `# inline comment` + case cs.Backquotes && len(cs.Stmts) == 0 && + len(cs.Last) == 1 && cs.Right.Line() == p.line: + p.w.WriteString("`#") + p.w.WriteString(cs.Last[0].Text) + p.w.WriteString("`") + default: + p.w.WriteString("$(") + if len(cs.Stmts) > 0 && startsWithLparen(cs.Stmts[0]) { + p.wantSpace = spaceRequired + } else { + p.wantSpace = spaceNotRequired + } + p.nestedStmts(cs.Stmts, cs.Last, cs.Right) + p.rightParen(cs.Right) + } } 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 { + p.w.WriteString("((") + if loop.Init == nil { p.space() } - p.arithmExpr(x.Init, false, false) - p.WriteString("; ") - p.arithmExpr(x.Cond, false, false) - p.WriteString("; ") - p.arithmExpr(x.Post, false, false) - p.WriteString("))") + p.arithmExpr(loop.Init, false, false) + p.w.WriteString("; ") + p.arithmExpr(loop.Cond, false, false) + p.w.WriteString("; ") + p.arithmExpr(loop.Post, false, false) + p.w.WriteString("))") } } @@ -805,41 +858,52 @@ func (p *Printer) arithmExpr(expr ArithmExpr, compact, spacePlusMinus bool) { if p.minify { compact = true } - switch x := expr.(type) { + p.arithmExprRecurse(expr, compact, spacePlusMinus) +} + +func (p *Printer) arithmExprRecurse(expr ArithmExpr, compact, spacePlusMinus bool) { + 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.arithmExprRecurse(expr.X, compact, spacePlusMinus) + p.w.WriteString(expr.Op.String()) + p.arithmExprRecurse(expr.Y, compact, false) } else { - p.arithmExpr(x.X, compact, spacePlusMinus) - if x.Op != Comma { + p.arithmExprRecurse(expr.X, compact, spacePlusMinus) + if expr.Op != Comma { p.space() } - p.WriteString(x.Op.String()) + p.w.WriteString(expr.Op.String()) p.space() - p.arithmExpr(x.Y, compact, false) + p.arithmExprRecurse(expr.Y, compact, false) } case *UnaryArithm: - if x.Post { - p.arithmExpr(x.X, compact, spacePlusMinus) - p.WriteString(x.Op.String()) + if expr.Post { + p.arithmExprRecurse(expr.X, compact, spacePlusMinus) + p.w.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.w.WriteString(expr.Op.String()) + p.arithmExprRecurse(expr.X, compact, false) } case *ParenArithm: - p.WriteByte('(') - p.arithmExpr(x.X, false, false) - p.WriteByte(')') + p.w.WriteByte('(') + p.arithmExprRecurse(expr.X, false, false) + p.w.WriteByte(')') + case *FlagsArithm: + p.w.WriteByte('(') + p.w.WriteString(expr.Flags.Value) + p.w.WriteByte(')') + if expr.X != nil { + p.arithmExprRecurse(expr.X, compact, false) + } } } @@ -855,35 +919,35 @@ 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.w.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.w.WriteString(expr.Op.String()) p.space() - p.testExprSameLine(x.X) + p.testExprSameLine(expr.X) case *ParenTest: - p.WriteByte('(') - if startsWithLparen(x.X) { + p.w.WriteByte('(') + if startsWithLparen(expr.X) { p.wantSpace = spaceRequired } else { p.wantSpace = spaceNotRequired } - p.testExpr(x.X) - p.WriteByte(')') + p.testExpr(expr.X) + p.w.WriteByte(')') } } @@ -894,19 +958,19 @@ 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.w.WriteByte(wp.Value[i]) } } else { - p.WriteByte(b) + p.w.WriteByte(b) } } } @@ -934,10 +998,14 @@ func (p *Printer) wordJoin(ws []*Word) { func (p *Printer) casePatternJoin(pats []*Word) { anyNewline := false for i, w := range pats { + // Only valid situation for a literal 'esac' here is with a preceding left paran. + if i == 0 && w.Lit() == "esac" { + p.w.WriteString("(") + } if i > 0 { p.spacedToken("|", Pos{}) } - if p.wantsNewline(w.Pos()) { + if p.wantsNewline(w.Pos(), true) { if !anyNewline { p.incLevel() anyNewline = true @@ -972,7 +1040,7 @@ func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) { p.space() } if p.wroteIndex(el.Index) { - p.WriteByte('=') + p.w.WriteByte('=') } if el.Value != nil { p.word(el.Value) @@ -997,7 +1065,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 { @@ -1006,7 +1074,7 @@ func (p *Printer) stmt(s *Stmt) { if r.N != nil { p.writeLit(r.N.Value) } - p.WriteString(r.Op.String()) + p.w.WriteString(r.Op.String()) if p.spaceRedirects && (r.Op != DplIn && r.Op != DplOut) { p.space() } else { @@ -1018,18 +1086,20 @@ func (p *Printer) stmt(s *Stmt) { } } sep := s.Semicolon.IsValid() && s.Semicolon.Line() > p.line && !p.singleLine - if sep || s.Background || s.Coprocess { + if sep || s.Background || s.Coprocess || s.Disown { if sep { p.bslashNewl() } else if !p.minify { p.space() } if s.Background { - p.WriteString("&") + p.w.WriteString("&") } else if s.Coprocess { - p.WriteString("|&") + p.w.WriteString("|&") + } else if s.Disown { + p.w.WriteString("&|") } else { - p.WriteString(";") + p.w.WriteString(";") } p.wroteSemi = true p.wantSpace = spaceRequired @@ -1037,85 +1107,110 @@ 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.w.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()) + } + if len(cmd.Args) <= 1 { + p.wordJoin(cmd.Args) + return startRedirs } - p.wordJoin(x.Args[1:]) + p.wordJoin(cmd.Args[:1]) + startRedirs = p.printRedirsUntil(redirs, startRedirs, cmd.Args[1].Pos()) + p.wordJoin(cmd.Args[1:]) case *Block: - p.WriteByte('{') + p.w.WriteByte('{') + // avoid ; in an empty block + p.wroteSemi = true 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]) { + p.w.WriteByte('(') + 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 if len(stmts) == 0 { + // Zsh allows empty subshells, but prevent `()` + // from looking like `() { anon-func; }`. p.wantSpace = spaceRequired } 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 { - p.WriteString("select ") + if cmd.Select { + p.w.WriteString("select ") } else { - p.WriteString("for ") + p.w.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 +1221,67 @@ 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 { - p.WriteString("function ") + if cmd.RsrvWord { + p.spacedString("function", Pos{}) } - p.writeLit(x.Name.Value) - if !x.RsrvWord || x.Parens { - p.WriteString("()") + if cmd.Name != nil { + p.spacedString(cmd.Name.Value, Pos{}) + } else { + for _, name := range cmd.Names { + p.spacedString(name.Value, Pos{}) + } + } + if !cmd.RsrvWord || cmd.Parens { + p.w.WriteString("()") + p.wantSpace = spaceNotRequired } 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.WriteString(" in") + p.w.WriteString("case ") + p.word(cmd.Word) + p.w.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()) { @@ -1190,25 +1293,22 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.newlines(ci.Pos()) p.spacePad(ci.Pos()) p.casePatternJoin(ci.Patterns) - p.WriteByte(')') + p.w.WriteByte(')') if !p.minify { p.wantSpace = spaceRequired } else { p.wantSpace = spaceNotRequired } - bodyPos := stmtsPos(ci.Stmts, ci.Last) - bodyEnd := stmtsEnd(ci.Stmts, ci.Last) - sep := len(ci.Stmts) > 1 || bodyPos.Line() > p.line || - (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 sep { + if !p.minify || i != len(cmd.Items)-1 { + if p.wantsNewline(ci.OpPos, false) { 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 +1316,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 { - p.WriteString("# ") + p.w.WriteString("((") + if cmd.Unsigned { + p.w.WriteString("# ") } - p.arithmExpr(x.X, false, false) - p.WriteString("))") + p.arithmExpr(cmd.X, false, false) + p.w.WriteString("))") case *TestClause: - p.WriteString("[[ ") + p.w.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 } @@ -1316,7 +1416,7 @@ func (p *Printer) stmtList(stmts []*Stmt, last []Comment) { if i > 0 && p.singleLine && p.wantNewline && !p.wroteSemi { // In singleLine mode, ensure we use semicolons between // statements. - p.WriteByte(';') + p.w.WriteByte(';') p.wantSpace = spaceRequired } pos := s.Pos() @@ -1338,10 +1438,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 +1482,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()) @@ -1391,24 +1491,21 @@ func (p *Printer) assigns(assigns []*Assign) { p.writeLit(a.Name.Value) p.wroteIndex(a.Index) if a.Append { - p.WriteByte('+') + p.w.WriteByte('+') } if !a.Naked { - p.WriteByte('=') + p.w.WriteByte('=') } } if a.Value != nil { // 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('(') + p.w.WriteByte('(') p.elemJoin(a.Array.Elems, a.Array.Last) p.rightParen(a.Array.Rparen) } @@ -1468,7 +1565,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 +1575,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/quote.go b/vendor/mvdan.cc/sh/v3/syntax/quote.go index 6f27eba12d..8f7f57eac8 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/quote.go +++ b/vendor/mvdan.cc/sh/v3/syntax/quote.go @@ -78,7 +78,7 @@ func Quote(s string, lang LangVariant) (string, error) { return "", &QuoteError{ByteOffset: offs, Message: quoteErrNull} } if r == utf8.RuneError || !unicode.IsPrint(r) { - if lang == LangPOSIX { + if lang.in(LangPOSIX) { return "", &QuoteError{ByteOffset: offs, Message: quoteErrPOSIX} } nonPrintable = true @@ -132,13 +132,13 @@ func Quote(s string, lang LangVariant) (string, error) { fmt.Fprintf(&b, "\\x%02x", rem[0]) // Unfortunately, mksh allows \x to consume more hex characters. // Ensure that we don't allow it to read more than two. - if lang == LangMirBSDKorn { + if lang.in(LangMirBSDKorn) { nextRequoteIfHex = true } case r > utf8.MaxRune: // Not a valid Unicode code point? return "", &QuoteError{ByteOffset: offs, Message: quoteErrRange} - case lang == LangMirBSDKorn && r > 0xFFFD: + case lang.in(LangMirBSDKorn) && r > 0xFFFD: // From the CAVEATS section in R59's man page: // // mksh currently uses OPTU-16 internally, which is the same as @@ -180,6 +180,5 @@ func Quote(s string, lang LangVariant) (string, error) { func isHex(r rune) bool { return (r >= '0' && r <= '9') || - (r >= 'a' && r <= 'f') || - (r >= 'A' && r <= 'F') + (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') } diff --git a/vendor/mvdan.cc/sh/v3/syntax/quotestate_string.go b/vendor/mvdan.cc/sh/v3/syntax/quotestate_string.go deleted file mode 100644 index d43466f8fc..0000000000 --- a/vendor/mvdan.cc/sh/v3/syntax/quotestate_string.go +++ /dev/null @@ -1,61 +0,0 @@ -// Code generated by "stringer -type=quoteState"; DO NOT EDIT. - -package syntax - -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[noState-1] - _ = x[subCmd-2] - _ = x[subCmdBckquo-4] - _ = x[dblQuotes-8] - _ = x[hdocWord-16] - _ = x[hdocBody-32] - _ = x[hdocBodyTabs-64] - _ = x[arithmExpr-128] - _ = x[arithmExprLet-256] - _ = x[arithmExprCmd-512] - _ = x[arithmExprBrack-1024] - _ = x[testExpr-2048] - _ = x[testExprRegexp-4096] - _ = x[switchCase-8192] - _ = x[paramExpName-16384] - _ = x[paramExpSlice-32768] - _ = x[paramExpRepl-65536] - _ = x[paramExpExp-131072] - _ = x[arrayElems-262144] -} - -const _quoteState_name = "noStatesubCmdsubCmdBckquodblQuoteshdocWordhdocBodyhdocBodyTabsarithmExprarithmExprLetarithmExprCmdarithmExprBracktestExprtestExprRegexpswitchCaseparamExpNameparamExpSliceparamExpReplparamExpExparrayElems" - -var _quoteState_map = map[quoteState]string{ - 1: _quoteState_name[0:7], - 2: _quoteState_name[7:13], - 4: _quoteState_name[13:25], - 8: _quoteState_name[25:34], - 16: _quoteState_name[34:42], - 32: _quoteState_name[42:50], - 64: _quoteState_name[50:62], - 128: _quoteState_name[62:72], - 256: _quoteState_name[72:85], - 512: _quoteState_name[85:98], - 1024: _quoteState_name[98:113], - 2048: _quoteState_name[113:121], - 4096: _quoteState_name[121:135], - 8192: _quoteState_name[135:145], - 16384: _quoteState_name[145:157], - 32768: _quoteState_name[157:170], - 65536: _quoteState_name[170:182], - 131072: _quoteState_name[182:193], - 262144: _quoteState_name[193:203], -} - -func (i quoteState) String() string { - if str, ok := _quoteState_map[i]; ok { - return str - } - return "quoteState(" + strconv.FormatInt(int64(i), 10) + ")" -} diff --git a/vendor/mvdan.cc/sh/v3/syntax/simplify.go b/vendor/mvdan.cc/sh/v3/syntax/simplify.go index 6f245918e9..1f809db6e8 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 } @@ -152,12 +152,11 @@ func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr { return x } pe, _ := w.Parts[0].(*ParamExp) - if pe == nil || !ValidName(pe.Param.Value) { - // Not a parameter expansion, or not a valid name, like $3. + if pe == nil || pe.Param == nil || !ValidName(pe.Param.Value) { + // Not a POSIX-like parameter expansion, or not a valid name without `$`, like $3. return x } - if pe.Excl || pe.Length || pe.Width || pe.Slice != nil || - pe.Repl != nil || pe.Exp != nil || pe.Index != nil { + if !pe.simple() { // A complex parameter expansion can't be simplified. // // Note that index expressions can't generally be simplified @@ -172,7 +171,7 @@ func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr { func (s *simplifier) inlineSubshell(stmts []*Stmt) []*Stmt { for len(stmts) == 1 { st := stmts[0] - if st.Negated || st.Background || st.Coprocess || + if st.Negated || st.Background || st.Coprocess || st.Disown || len(st.Redirs) > 0 { break } diff --git a/vendor/mvdan.cc/sh/v3/syntax/token_string.go b/vendor/mvdan.cc/sh/v3/syntax/token_string.go index ab5c83aca0..ef5c8c6f39 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/token_string.go +++ b/vendor/mvdan.cc/sh/v3/syntax/token_string.go @@ -14,136 +14,154 @@ func _() { _ = x[_Lit-3] _ = x[_LitWord-4] _ = x[_LitRedir-5] - _ = x[sglQuote-6] - _ = x[dblQuote-7] - _ = x[bckQuote-8] - _ = x[and-9] - _ = x[andAnd-10] - _ = x[orOr-11] - _ = x[or-12] - _ = x[orAnd-13] - _ = x[dollar-14] - _ = x[dollSglQuote-15] - _ = x[dollDblQuote-16] - _ = x[dollBrace-17] - _ = x[dollBrack-18] - _ = x[dollParen-19] - _ = x[dollDblParen-20] - _ = x[leftBrack-21] - _ = x[dblLeftBrack-22] - _ = x[leftParen-23] - _ = x[dblLeftParen-24] - _ = x[rightBrace-25] - _ = x[rightBrack-26] - _ = x[rightParen-27] - _ = x[dblRightParen-28] - _ = x[semicolon-29] - _ = x[dblSemicolon-30] - _ = x[semiAnd-31] - _ = x[dblSemiAnd-32] - _ = x[semiOr-33] - _ = x[exclMark-34] - _ = x[tilde-35] - _ = x[addAdd-36] - _ = x[subSub-37] - _ = x[star-38] - _ = x[power-39] - _ = x[equal-40] - _ = x[nequal-41] - _ = x[lequal-42] - _ = x[gequal-43] - _ = x[addAssgn-44] - _ = x[subAssgn-45] - _ = x[mulAssgn-46] - _ = x[quoAssgn-47] - _ = x[remAssgn-48] - _ = x[andAssgn-49] - _ = x[orAssgn-50] - _ = x[xorAssgn-51] - _ = x[shlAssgn-52] - _ = x[shrAssgn-53] - _ = x[rdrOut-54] - _ = x[appOut-55] - _ = x[rdrIn-56] - _ = x[rdrInOut-57] - _ = x[dplIn-58] - _ = x[dplOut-59] - _ = x[clbOut-60] - _ = x[hdoc-61] - _ = x[dashHdoc-62] - _ = x[wordHdoc-63] - _ = x[rdrAll-64] - _ = x[appAll-65] - _ = x[cmdIn-66] - _ = x[cmdOut-67] - _ = x[plus-68] - _ = x[colPlus-69] - _ = x[minus-70] - _ = x[colMinus-71] - _ = x[quest-72] - _ = x[colQuest-73] - _ = x[assgn-74] - _ = x[colAssgn-75] - _ = x[perc-76] - _ = x[dblPerc-77] - _ = x[hash-78] - _ = x[dblHash-79] - _ = x[caret-80] - _ = x[dblCaret-81] - _ = x[comma-82] - _ = x[dblComma-83] - _ = x[at-84] - _ = x[slash-85] - _ = x[dblSlash-86] - _ = x[colon-87] - _ = x[tsExists-88] - _ = x[tsRegFile-89] - _ = x[tsDirect-90] - _ = x[tsCharSp-91] - _ = x[tsBlckSp-92] - _ = x[tsNmPipe-93] - _ = x[tsSocket-94] - _ = x[tsSmbLink-95] - _ = x[tsSticky-96] - _ = x[tsGIDSet-97] - _ = x[tsUIDSet-98] - _ = x[tsGrpOwn-99] - _ = x[tsUsrOwn-100] - _ = x[tsModif-101] - _ = x[tsRead-102] - _ = x[tsWrite-103] - _ = x[tsExec-104] - _ = x[tsNoEmpty-105] - _ = x[tsFdTerm-106] - _ = x[tsEmpStr-107] - _ = x[tsNempStr-108] - _ = x[tsOptSet-109] - _ = x[tsVarSet-110] - _ = x[tsRefVar-111] - _ = x[tsReMatch-112] - _ = x[tsNewer-113] - _ = x[tsOlder-114] - _ = x[tsDevIno-115] - _ = x[tsEql-116] - _ = x[tsNeq-117] - _ = x[tsLeq-118] - _ = x[tsGeq-119] - _ = x[tsLss-120] - _ = x[tsGtr-121] - _ = x[globQuest-122] - _ = x[globStar-123] - _ = x[globPlus-124] - _ = x[globAt-125] - _ = x[globExcl-126] + _ = x[_realTokenBoundary-6] + _ = x[sglQuote-7] + _ = x[dblQuote-8] + _ = x[bckQuote-9] + _ = x[and-10] + _ = x[andAnd-11] + _ = x[orOr-12] + _ = x[or-13] + _ = x[orAnd-14] + _ = x[andPipe-15] + _ = x[andBang-16] + _ = x[dollar-17] + _ = x[dollSglQuote-18] + _ = x[dollDblQuote-19] + _ = x[dollBrace-20] + _ = x[dollBrack-21] + _ = x[dollParen-22] + _ = x[dollDblParen-23] + _ = x[leftBrace-24] + _ = x[leftBrack-25] + _ = x[dblLeftBrack-26] + _ = x[leftParen-27] + _ = x[dblLeftParen-28] + _ = x[rightBrace-29] + _ = x[rightBrack-30] + _ = x[dblRightBrack-31] + _ = x[rightParen-32] + _ = x[dblRightParen-33] + _ = x[semicolon-34] + _ = x[dblSemicolon-35] + _ = x[semiAnd-36] + _ = x[dblSemiAnd-37] + _ = x[semiOr-38] + _ = x[exclMark-39] + _ = x[tilde-40] + _ = x[addAdd-41] + _ = x[subSub-42] + _ = x[star-43] + _ = x[power-44] + _ = x[equal-45] + _ = x[nequal-46] + _ = x[lequal-47] + _ = x[gequal-48] + _ = x[addAssgn-49] + _ = x[subAssgn-50] + _ = x[mulAssgn-51] + _ = x[quoAssgn-52] + _ = x[remAssgn-53] + _ = x[andAssgn-54] + _ = x[orAssgn-55] + _ = x[xorAssgn-56] + _ = x[shlAssgn-57] + _ = x[shrAssgn-58] + _ = x[andBoolAssgn-59] + _ = x[orBoolAssgn-60] + _ = x[xorBoolAssgn-61] + _ = x[powAssgn-62] + _ = x[rdrOut-63] + _ = x[appOut-64] + _ = x[rdrIn-65] + _ = x[rdrInOut-66] + _ = x[dplIn-67] + _ = x[dplOut-68] + _ = x[rdrClob-69] + _ = x[appClob-70] + _ = x[hdoc-71] + _ = x[dashHdoc-72] + _ = x[wordHdoc-73] + _ = x[rdrAll-74] + _ = x[rdrAllClob-75] + _ = x[appAll-76] + _ = x[appAllClob-77] + _ = x[cmdIn-78] + _ = x[assgnParen-79] + _ = x[cmdOut-80] + _ = x[plus-81] + _ = x[colPlus-82] + _ = x[minus-83] + _ = x[colMinus-84] + _ = x[quest-85] + _ = x[colQuest-86] + _ = x[assgn-87] + _ = x[colAssgn-88] + _ = x[perc-89] + _ = x[dblPerc-90] + _ = x[hash-91] + _ = x[dblHash-92] + _ = x[colHash-93] + _ = x[colPipe-94] + _ = x[colStar-95] + _ = x[caret-96] + _ = x[dblCaret-97] + _ = x[comma-98] + _ = x[dblComma-99] + _ = x[at-100] + _ = x[slash-101] + _ = x[dblSlash-102] + _ = x[period-103] + _ = x[colon-104] + _ = x[tsExists-105] + _ = x[tsRegFile-106] + _ = x[tsDirect-107] + _ = x[tsCharSp-108] + _ = x[tsBlckSp-109] + _ = x[tsNmPipe-110] + _ = x[tsSocket-111] + _ = x[tsSmbLink-112] + _ = x[tsSticky-113] + _ = x[tsGIDSet-114] + _ = x[tsUIDSet-115] + _ = x[tsGrpOwn-116] + _ = x[tsUsrOwn-117] + _ = x[tsModif-118] + _ = x[tsRead-119] + _ = x[tsWrite-120] + _ = x[tsExec-121] + _ = x[tsNoEmpty-122] + _ = x[tsFdTerm-123] + _ = x[tsEmpStr-124] + _ = x[tsNempStr-125] + _ = x[tsOptSet-126] + _ = x[tsVarSet-127] + _ = x[tsRefVar-128] + _ = x[tsReMatch-129] + _ = x[tsNewer-130] + _ = x[tsOlder-131] + _ = x[tsDevIno-132] + _ = x[tsEql-133] + _ = x[tsNeq-134] + _ = x[tsLeq-135] + _ = x[tsGeq-136] + _ = x[tsLss-137] + _ = x[tsGtr-138] + _ = x[globQuest-139] + _ = x[globStar-140] + _ = x[globPlus-141] + _ = x[globAt-142] + _ = x[globExcl-143] } -const _token_name = "illegalTokEOFNewlLitLitWordLitRedir'\"`&&&||||&$$'$\"${$[$($(([[[(((}])));;;;&;;&;|!~++--***==!=<=>=+=-=*=/=%=&=|=^=<<=>>=>>><<><&>&>|<<<<-<<<&>&>><(>(+:+-:-?:?=:=%%%###^^^,,,@///:-e-f-d-c-b-p-S-L-k-g-u-G-O-N-r-w-x-s-t-z-n-o-v-R=~-nt-ot-ef-eq-ne-le-ge-lt-gt?(*(+(@(!(" +const _token_name = "illegalTokEOFNewlLitLitWordLitRedirrealTokenBoundary'\"`&&&||||&&|&!$$'$\"${$[$($(({[[[(((}]]])));;;;&;;&;|!~++--***==!=<=>=+=-=*=/=%=&=|=^=<<=>>=&&=||=^^=**=>>><<><&>&>|>>|<<<<-<<<&>&>|&>>&>>|<(=(>(+:+-:-?:?=:=%%%###:#:|:*^^^,,,@///.:-e-f-d-c-b-p-S-L-k-g-u-G-O-N-r-w-x-s-t-z-n-o-v-R=~-nt-ot-ef-eq-ne-le-ge-lt-gt?(*(+(@(!(" -var _token_index = [...]uint16{0, 10, 13, 17, 20, 27, 35, 36, 37, 38, 39, 41, 43, 44, 46, 47, 49, 51, 53, 55, 57, 60, 61, 63, 64, 66, 67, 68, 69, 71, 72, 74, 76, 79, 81, 82, 83, 85, 87, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 117, 120, 121, 123, 124, 126, 128, 130, 132, 134, 137, 140, 142, 145, 147, 149, 150, 152, 153, 155, 156, 158, 159, 161, 162, 164, 165, 167, 168, 170, 171, 173, 174, 175, 177, 178, 180, 182, 184, 186, 188, 190, 192, 194, 196, 198, 200, 202, 204, 206, 208, 210, 212, 214, 216, 218, 220, 222, 224, 226, 228, 231, 234, 237, 240, 243, 246, 249, 252, 255, 257, 259, 261, 263, 265} +var _token_index = [...]uint16{0, 10, 13, 17, 20, 27, 35, 52, 53, 54, 55, 56, 58, 60, 61, 63, 65, 67, 68, 70, 72, 74, 76, 78, 81, 82, 83, 85, 86, 88, 89, 90, 92, 93, 95, 96, 98, 100, 103, 105, 106, 107, 109, 111, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 141, 144, 147, 150, 153, 156, 157, 159, 160, 162, 164, 166, 168, 171, 173, 176, 179, 181, 184, 187, 191, 193, 195, 197, 198, 200, 201, 203, 204, 206, 207, 209, 210, 212, 213, 215, 217, 219, 221, 222, 224, 225, 227, 228, 229, 231, 232, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, 255, 257, 259, 261, 263, 265, 267, 269, 271, 273, 275, 277, 279, 281, 283, 286, 289, 292, 295, 298, 301, 304, 307, 310, 312, 314, 316, 318, 320} func (i token) String() string { - if i >= token(len(_token_index)-1) { + idx := int(i) - 0 + if i < 0 || idx >= len(_token_index)-1 { return "token(" + strconv.FormatInt(int64(i), 10) + ")" } - return _token_name[_token_index[i]:_token_index[i+1]] + return _token_name[_token_index[idx]:_token_index[idx+1]] } diff --git a/vendor/mvdan.cc/sh/v3/syntax/tokens.go b/vendor/mvdan.cc/sh/v3/syntax/tokens.go index 6a64b21378..d6adf3650a 100644 --- a/vendor/mvdan.cc/sh/v3/syntax/tokens.go +++ b/vendor/mvdan.cc/sh/v3/syntax/tokens.go @@ -3,7 +3,7 @@ package syntax -//go:generate stringer -type token -linecomment -trimprefix _ +//go:generate go tool stringer -type token -linecomment -trimprefix _ type token uint32 @@ -17,15 +17,20 @@ const ( _LitWord _LitRedir + // Token values beyond this point stringify as exact source. + _realTokenBoundary + sglQuote // ' dblQuote // " bckQuote // ` - and // & - andAnd // && - orOr // || - or // | - orAnd // |& + and // & + andAnd // && + orOr // || + or // | + orAnd // |& + andPipe // &| + andBang // &! dollar // $ dollSglQuote // $' @@ -34,6 +39,7 @@ const ( dollBrack // $[ dollParen // $( dollDblParen // $(( + leftBrace // { leftBrack // [ dblLeftBrack // [[ leftParen // ( @@ -41,6 +47,7 @@ const ( rightBrace // } rightBrack // ] + dblRightBrack // ]] rightParen // ) dblRightParen // )) semicolon // ; @@ -61,32 +68,40 @@ const ( lequal // <= gequal // >= - addAssgn // += - subAssgn // -= - mulAssgn // *= - quoAssgn // /= - remAssgn // %= - andAssgn // &= - orAssgn // |= - xorAssgn // ^= - shlAssgn // <<= - shrAssgn // >>= - - rdrOut // > - appOut // >> - rdrIn // < - rdrInOut // <> - dplIn // <& - dplOut // >& - clbOut // >| - hdoc // << - dashHdoc // <<- - wordHdoc // <<< - rdrAll // &> - appAll // &>> - - cmdIn // <( - cmdOut // >( + addAssgn // += + subAssgn // -= + mulAssgn // *= + quoAssgn // /= + remAssgn // %= + andAssgn // &= + orAssgn // |= + xorAssgn // ^= + shlAssgn // <<= + shrAssgn // >>= + andBoolAssgn // &&= + orBoolAssgn // ||= + xorBoolAssgn // ^^= + powAssgn // **= + + rdrOut // > + appOut // >> + rdrIn // < + rdrInOut // <> + dplIn // <& + dplOut // >& + rdrClob // >| + appClob // >>| + hdoc // << + dashHdoc // <<- + wordHdoc // <<< + rdrAll // &> + rdrAllClob // &>| + appAll // &>> + appAllClob // &>>| + + cmdIn // <( + assgnParen // =( + cmdOut // >( plus // + colPlus // :+ @@ -100,6 +115,9 @@ const ( dblPerc // %% hash // # dblHash // ## + colHash // :# + colPipe // :| + colStar // :* caret // ^ dblCaret // ^^ comma // , @@ -107,6 +125,7 @@ const ( at // @ slash // / dblSlash // // + period // . colon // : tsExists // -e @@ -152,28 +171,41 @@ const ( globExcl // !( ) +func (t token) isLit() bool { + return t == _Lit || t == _LitWord || t == _LitRedir +} + type RedirOperator token const ( - RdrOut = RedirOperator(rdrOut) + iota // > - AppOut // >> - RdrIn // < - RdrInOut // <> - DplIn // <& - DplOut // >& - ClbOut // >| - Hdoc // << - DashHdoc // <<- - WordHdoc // <<< - RdrAll // &> - AppAll // &>> + RdrOut = RedirOperator(rdrOut) + iota // > + AppOut // >> + RdrIn // < + RdrInOut // <> + DplIn // <& + DplOut // >& + RdrClob // >| + AppClob // >>| with [LangZsh] + Hdoc // << + DashHdoc // <<- + WordHdoc // <<< + RdrAll // &> + RdrAllClob // &>| with [LangZsh] + AppAll // &>> + AppAllClob // &>>| with [LangZsh] + + // Deprecated: use [RdrClob] + // + //go:fix inline + ClbOut = RdrClob ) type ProcOperator token const ( - CmdIn = ProcOperator(cmdIn) + iota // <( - CmdOut // >( + CmdIn = ProcOperator(cmdIn) + iota // <( + CmdInTemp // =( + CmdOut // >( ) type GlobOperator token @@ -226,6 +258,9 @@ const ( RemLargeSuffix // %% RemSmallPrefix // # RemLargePrefix // ## + MatchEmpty // :# with [LangZsh] + ArrayExclude // :| with [LangZsh] + ArrayIntersect // :* with [LangZsh] UpperFirst // ^ UpperAll // ^^ LowerFirst // , @@ -265,23 +300,30 @@ const ( Shr = BinAritOperator(appOut) // >> Shl = BinAritOperator(hdoc) // << - AndArit = BinAritOperator(andAnd) // && - OrArit = BinAritOperator(orOr) // || - Comma = BinAritOperator(comma) // , - TernQuest = BinAritOperator(quest) // ? - TernColon = BinAritOperator(colon) // : - - Assgn = BinAritOperator(assgn) // = - AddAssgn = BinAritOperator(addAssgn) // += - SubAssgn = BinAritOperator(subAssgn) // -= - MulAssgn = BinAritOperator(mulAssgn) // *= - QuoAssgn = BinAritOperator(quoAssgn) // /= - RemAssgn = BinAritOperator(remAssgn) // %= - AndAssgn = BinAritOperator(andAssgn) // &= - OrAssgn = BinAritOperator(orAssgn) // |= - XorAssgn = BinAritOperator(xorAssgn) // ^= - ShlAssgn = BinAritOperator(shlAssgn) // <<= - ShrAssgn = BinAritOperator(shrAssgn) // >>= + // TODO: use "Bool" consistently for logical operators like AndArit and OrArit; use //go:fix inline? + + AndArit = BinAritOperator(andAnd) // && + OrArit = BinAritOperator(orOr) // || + XorBool = BinAritOperator(dblCaret) // ^^ + Comma = BinAritOperator(comma) // , + TernQuest = BinAritOperator(quest) // ? + TernColon = BinAritOperator(colon) // : + + Assgn = BinAritOperator(assgn) // = + AddAssgn = BinAritOperator(addAssgn) // += + SubAssgn = BinAritOperator(subAssgn) // -= + MulAssgn = BinAritOperator(mulAssgn) // *= + QuoAssgn = BinAritOperator(quoAssgn) // /= + RemAssgn = BinAritOperator(remAssgn) // %= + AndAssgn = BinAritOperator(andAssgn) // &= + OrAssgn = BinAritOperator(orAssgn) // |= + XorAssgn = BinAritOperator(xorAssgn) // ^= + ShlAssgn = BinAritOperator(shlAssgn) // <<= + ShrAssgn = BinAritOperator(shrAssgn) // >>= + AndBoolAssgn = BinAritOperator(andBoolAssgn) // &&= + OrBoolAssgn = BinAritOperator(orBoolAssgn) // ||= + XorBoolAssgn = BinAritOperator(xorBoolAssgn) // ^^= + PowAssgn = BinAritOperator(powAssgn) // **= ) type UnTestOperator token @@ -312,6 +354,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..8a67dc55c1 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,203 @@ 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) + walkNilable(node.Name, f) + walkList(node.Names, 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) + walkNilable(node.Flags, f) + walkNilable(node.Param, f) + walkNilable(node.NestedParam, f) + walkNilable(node.Index, f) + if node.Slice != nil { + walkNilable(node.Slice.Offset, f) + walkNilable(node.Slice.Length, 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) - } + 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 *FlagsArithm: + Walk(node.Flags, f) + if node.X != nil { + 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 +224,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 +233,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(". ") } } @@ -266,7 +246,7 @@ func (p *debugPrinter) print(x reflect.Value) { return } p.print(x.Elem()) - case reflect.Ptr: + case reflect.Pointer: if x.IsNil() { p.printf("nil") return @@ -278,7 +258,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 +271,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 +282,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 +292,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()) + } } }